PHP-FPM مقابل Laravel Octane — ما الذي يتغير فعلاً
يتبع PHP-FPM نموذج «لا شيء مشترك»: كل طلب يُحمّل الإطار من الصفر، وينفذ الكود، ويحرر الذاكرة. هذا متين بالتصميم — أي تسريب في طلب ما يموت معه. Octane يكسر هذا العقد. يُهيّأ التطبيق مرة واحدة، ثم تتتالى الطلبات في نفس العملية. المكسب في الأداء هائل، لكن مسؤولية المطوّر تزداد.
ست فروق هيكلية يجب معرفتها
- دورة حياة مشتركة — حاوية IoC الخاصة بـ Laravel والمزودين والـ singletons تُنشأ مرة واحدة وتبقى بين جميع طلبات العامل. قيمة مكتوبة في singleton في الطلب N لا تزال موجودة في الطلب N+1.
- مكسب أداء موثق — تُظهر معايير توثيق Laravel Octane الرسمية 4× إلى 6× طلبات/ثانية مقارنةً بـ PHP-FPM 8.3 على نقاط نهاية نموذجية، بفضل حذف تهيئة PHP في كل طلب.
- المتغيرات الثابتة = تسريبات مضمونة — أي خاصية
staticمكتوبة خلال طلب تلوث الطلبات التالية. واجهات Laravel Facades تخفي مراجع ثابتة؛ بعضها يُنظّفه Octane والبعض الآخر لا. - الملفات الثابتة تُخدَم بواسطة FrankenPHP — يتضمن الثنائي خادم HTTP كاملاً. على VPS، لا حاجة لـ nginx أمام PHP: يتعامل FrankenPHP مع TLS وHTTP/2 وHTTP/3 بشكل أصلي.
- ذاكرة RSS مستقرة ≠ ذاكرة مسربة — ترتفع ذاكرة العملية عند بدء التشغيل ثم تستقر. التسريب الحقيقي يظهر كنمو مستمر عبر آلاف الطلبات، ليس المستوى الأولي.
- العمال والتزامن — يبدأ Octane
--workers=Nعملية. كل عامل يحتفظ بحالته الخاصة، لكن نفس العامل يعالج طلباته بالتسلسل. - توافق Laravel 10 و11 — Octane 2.x يدعم Laravel 10+. FrankenPHP كبرنامج تشغيل متاح منذ
laravel/octane2.3. PHP 8.2 كحد أدنى، PHP 8.3 موصى به.
المتطلبات الأساسية قبل البدء
VPS مع 2 GB RAM على الأقل (4 GB موصى به لـ stack كامل مع Redis وعمال الطوابير)، وصول root، Docker 25+ وDocker Compose v2. يجب أن يعمل التطبيق على Laravel 10 أو 11 مع تثبيت laravel/octane 2.x. سيدير Traefik 3.x شهادات Let's Encrypt وتوجيه HTTPS. لا حاجة لـ nginx إضافي: FrankenPHP يستبدل طبقة HTTP بالكامل.
نشر Laravel Octane + FrankenPHP + Traefik في Docker Compose
تثبيت laravel/octane ونشر الإعداد
في مشروع Laravel:
composer require laravel/octane php artisan octane:install --server=frankenphpهذا ينشر
config/octane.phpويضيف متغيرات البيئة إلى.env.example. تحقق من تعيينOCTANE_SERVER=frankenphp.كتابة Dockerfile لـ FrankenPHP
FROM dunglas/frankenphp:1-php8.3-alpine RUN install-php-extensions \ pdo_mysql \ redis \ opcache \ pcntl \ zip \ intl WORKDIR /app COPY --chown=www-data:www-data . . RUN composer install --no-dev --optimize-autoloader --no-interaction \ && php artisan config:cache \ && php artisan route:cache \ && php artisan view:cache EXPOSE 8000 CMD ["php", "artisan", "octane:frankenphp", "--host=0.0.0.0", "--port=8000", "--workers=4", "--max-requests=500"]خيار
--max-requests=500يُعيد تشغيل العامل بعد 500 طلب: شبكة أمان ضد تسريبات الذاكرة المتبقية.كتابة docker-compose.yml الكامل
services: app: build: . restart: unless-stopped environment: APP_ENV: production APP_KEY: ${APP_KEY} DB_HOST: db REDIS_HOST: redis CACHE_DRIVER: redis SESSION_DRIVER: redis QUEUE_CONNECTION: redis OCTANE_SERVER: frankenphp networks: - internal - traefik labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=letsencrypt" - "traefik.http.services.app.loadbalancer.server.port=8000" depends_on: db: condition: service_healthy redis: condition: service_healthy worker: build: . restart: unless-stopped command: ["php", "artisan", "queue:work", "redis", "--sleep=3", "--tries=3", "--max-time=3600"] networks: - internal depends_on: db: condition: service_healthy redis: condition: service_healthy scheduler: build: . restart: unless-stopped command: ["sh", "-c", "while true; do php artisan schedule:run >> /dev/null 2>&1; sleep 60; done"] networks: - internal db: image: mysql:8.0 restart: unless-stopped volumes: - db_data:/var/lib/mysql networks: - internal healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data networks: - internal healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 volumes: db_data: redis_data: networks: internal: traefik: external: trueنشر Traefik مع شهادات تلقائية
docker network create traefik mkdir -p /opt/traefik/letsencrypt touch /opt/traefik/letsencrypt/acme.json chmod 600 /opt/traefik/letsencrypt/acme.json docker compose -f /opt/traefik/docker-compose.yml up -dتهيئة ملف .env للإنتاج
المتغيرات الأساسية التي يجب تعريفها على VPS:
APP_ENV=production APP_DEBUG=false APP_KEY=base64:... # يُنشأ بـ php artisan key:generate DOMAIN=your-domain.com DB_DATABASE=laravel DB_USERNAME=laravel DB_PASSWORD=strong_password DB_ROOT_PASSWORD=root_strong_password OCTANE_SERVER=frankenphp CACHE_DRIVER=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redisلا تُودّع .env في git أبدًا. على VPS، أدِره عبر scp أو مدير الأسرار.
تشغيل عمليات الترحيل بدون توقف
docker compose run --rm app php artisan migrate --forceللنشر بدون توقف، استخدم علامة
--stepعلى الترحيلات التي تضيف أعمدة NOT NULL.تشغيل الـ stack والتحقق من healthcheck
docker compose up -d --build docker compose psانتظر حتى يصل
dbوredisإلى حالة healthyقبل أن يبدأ app.التحقق من الحالة باستخدام octane:status
docker compose exec app php artisan octane:statusتعرض هذه الأمر الخادم النشط (frankenphp)، عدد العمال وحالتهم. استجابة
runningتؤكد أن العمال يستجيبون.إعادة تحميل العمال عند النشر
docker compose build app worker scheduler docker compose up -d --no-deps app worker schedulerلإعادة التحميل بدون قطع الاتصالات النشطة:
docker compose exec app php artisan octane:reload
قائمة التحقق من التوافق قبل التبديل إلى الإنتاج
الانتقال إلى Octane لا يتطلب إعادة كتابة التطبيق، لكن بعض الأنماط الشائعة تنكسر بصمت. راجع هذه القائمة على كودك قبل أول نشر.
8 نقاط يجب التحقق منها
- Singletons ذات حالة قابلة للتغيير — أي خدمة مسجلة بـ
$this->app->singleton()تخزن بيانات خاصة بالطلب يجب إعادة تهيئتها في OctaneServiceProvider. مراجعة: grep -r 'singleton' app/Providers. - الجلسات — مع
SESSION_DRIVER=redisتصمد الجلسات بشكل صحيح. قد يُسبب برنامج التشغيل fileحالات تنافس مع عمال متعددين. الترحيل إلى redisأو databaseموصى به. - اتصالات قاعدة البيانات — يحتفظ Octane باتصالات DB دائمة لكل عامل. تحقق من أن MySQL يقبل
max_connections ≥ workers × replicas. - المتغيرات الثابتة في النماذج أو الخدمات —
static $cache = []في نموذج Eloquent يستمر بين الطلبات. استبدله بحقن التبعية بنطاق الطلب. - مستمعو الأحداث غير الأكفاء — مستمع مسجل في
boot()يُسجَّل مرة واحدة فقط، لكنه يُنفَّذ في كل طلب يُطلق الحدث. مشكلة فقط إذا كان يجمع حالة عامة. - المهام والطوابير — لا تعمل Jobs في عملية Octane الرئيسية بل في حاوية
workerمنفصلة. لا تعديلات مطلوبة على كود Jobs. - الملفات المرفوعة والتخزين — تأكد من تركيب مجلد
/app/storageكـ volume لكي تستمر الملفات بين إعادة بناء الصورة. - الحزم التابعة لجهات خارجية — تحقق من توافق Octane لكل حزمة تستخدمها. عطّل Debugbar وTelescope في الإنتاج.
FrankenPHP مقابل Swoole مقابل RoadRunner: اختيار برنامج تشغيل Octane
مرّر الجدول أفقيًا
| المعيار | FrankenPHP | Swoole | RoadRunner |
|---|---|---|---|
| التثبيت | صورة Docker رسمية `dunglas/frankenphp` — لا حاجة لتجميع امتداد PECL | امتداد PECL `swoole` — يتطلب `pecl install swoole` أو صورة مخصصة | ثنائي Go منفصل (`roadrunner`) — يُنزَّل عبر Composer |
| HTTP/2 وHTTP/3 | أصلي — FrankenPHP مبني على Caddy الذي يدعم HTTP/3 (QUIC) تلقائياً | يدعم HTTP/2، HTTP/3 ليس أصلياً | HTTP/2 عبر مكوّن إضافي، HTTP/3 غير مدعوم أصلياً |
| توافق امتدادات PHP | واسع جداً — مبني على PHP-FPM الرسمي، معظم الامتدادات تُثبَّت بشكل طبيعي | عدم توافق معروف مع بعض الامتدادات الحاجبة | واسع، لكن بعض امتدادات PECL قد تحتاج تكيفات |
| توصية Laravel الرسمية | برنامج التشغيل الموصى به منذ Laravel 11 (2024)، مذكور أولاً في وثائق Octane | برنامج التشغيل الأولي التاريخي، لا يزال مدعوماً | مدعوم، بديل لـ Swoole، شائع في نظام Symfony |
| تعقيد التشغيل | منخفض — حاوية واحدة، Dockerfile بسيط، لا خادم منفصل | متوسط — يجب تجميع الامتداد مع نسخة PHP الصحيحة | متوسط — يجب مزامنة ثنائي RoadRunner مع نسخة حزمة Composer |
مراقبة Octane في الإنتاج باستخدام octane:status
تستعلم الأمر php artisan octane:status عن خادم FrankenPHP في الوقت الفعلي وتعرض عدد العمال النشطين والحالة الإجمالية. دمجها في المراقبة:
docker compose exec -T app php artisan octane:status | grep -q 'running' && echo OK || echo FAILلإعادة تحميل العمال بدون قطع الاتصالات:
docker compose exec app php artisan octane:reloadراقب ذاكرة RSS للعملية الرئيسية: النمو المستمر يشير إلى تسريب. الأمر docker stats app يعطي الذاكرة في الوقت الفعلي.
استكشاف الأخطاء: أخطاء الإنتاج الشائعة
تسريب ذاكرة تدريجي: ذاكرة الحاوية app تنمو باستمرار. السبب المحتمل: singleton يجمع بيانات لكل طلب. قلل --max-requests إلى 100 ريثما تحدد المصدر.
502 Bad Gateway من Traefik: تحقق من أن الحاوية على شبكة traefik، وأن التسمية traefik.http.services.app.loadbalancer.server.port=8000 تطابق المنفذ المكشوف، وأن الحاوية في حالة healthy.
عامل يُعيد التشغيل في حلقة: ابحث عن FATAL في docker compose logs app. السبب الأكثر شيوعاً: ترحيل لم يُنفَّذ يرفع QueryException.
جلسات مفقودة بين الطلبات: انتقل إلى SESSION_DRIVER=redis.
منطقة زمنية خاطئة: أضف APP_TIMEZONE في .env وRUN apk add --no-cache tzdata في Dockerfile.
لمزيد من المعلومات
لديك الآن stack عامل لـ Laravel Octane على VPS. المقالات التالية تغطي الخطوات التالية الطبيعية: [نشر Laravel على VPS](/blog/deployer-laravel-vps)، [النشر مع Traefik](/blog/deployer-avec-traefik)، [قائمة فحص Docker Compose للإنتاج](/blog/docker-compose-production-checklist)، [استضافة Redis على VPS](/blog/heberger-redis-vps)، [نهاية عمر PHP 8.2](/blog/php-82-fin-de-vie-checklist-migration).