كلاود
development

FrankenPHP مع Laravel في الإنتاج: خادم واحد بدل Nginx وPHP-FPM

كلاود|١ نوفمبر ٢٠٢٦|6 دقائق قراءة|٢١ مشاهدة
FrankenPHP مع Laravel في الإنتاج: خادم واحد بدل Nginx وPHP-FPM

FrankenPHP يدمج خادم الويب وPHP في ملف تشغيل واحد، ووضع worker يبقي Laravel محمّلاً في الذاكرة فيتضاعف عدد الطلبات في الثانية عدة مرات، بشرط أن يخلو كودك من الحالة المشتركة بين الطلبات.

الجواب المختصر

FrankenPHP خادم تطبيقات PHP حديث مبني على Caddy المكتوب بلغة Go، ويضم مفسّر PHP داخله مباشرة. بدل الثنائي التقليدي Nginx مع PHP-FPM، تشغّل ملفاً تنفيذياً واحداً يستقبل HTTP ويخدم الملفات الثابتة وينفذ PHP ويستخرج شهادات HTTPS تلقائياً ويدعم HTTP/2 وHTTP/3. هذا وحده يبسّط النشر، لكن القيمة الكبرى في وضع worker: يُحمَّل تطبيق Laravel مرة واحدة ويبقى في الذاكرة، ثم تُخدم الطلبات المتتالية دون إعادة تحميل الإطار في كل مرة.

في PHP-FPM التقليدي يمر كل طلب بمرحلة إقلاع كاملة: تحميل autoloader، وقراءة ملفات الإعداد، وتسجيل مزودي الخدمات، وهذا يستهلك عادة من 15 إلى 40 مللي ثانية قبل تنفيذ سطر واحد من منطقك، بينما يلغي وضع worker هذه المرحلة كلها، فيرتفع عدد الطلبات في الثانية من ضعفين إلى خمسة أضعاف تقريباً حسب طبيعة التطبيق. الشرط الوحيد، وهو شرط جاد، أن يكون الكود نظيفاً من الحالة المشتركة: أي متغير ساكن أو singleton يحمل بيانات طلب سابق سيتسرب للطلب التالي. الطريق الموصى به هو Laravel Octane مع مشغّل FrankenPHP، ثم خدمة systemd تبقيه حياً.

كيف يختلف عن Octane مع Swoole وRoadRunner

Laravel Octane طبقة تجعل Laravel يعمل في وضع worker فوق أحد ثلاثة خوادم: Swoole وRoadRunner وFrankenPHP. الثلاثة يحققون نفس الفكرة الأساسية، لكن الفروق في التشغيل اليومي حقيقية:

المعيارSwooleRoadRunnerFrankenPHP
طريقة التثبيتإضافة PHP تُبنى عبر PECLملف تنفيذي بلغة Goملف تنفيذي واحد يضم PHP
HTTPS تلقائيلا، يحتاج بروكسي أمامهممكن بإعداد إضافينعم، مدمج من Caddy
HTTP/3لامحدودنعم
خدمة الملفات الثابتةضعيفة، يُفضل Nginxمقبولةقوية عبر Caddy
ميزات إضافيةمهام متزامنة وجداول ذاكرةإضافات Go متعددةEarly Hints و Mercure

Swoole أقوى إن كنت تحتاج ميزاته الخاصة مثل التنفيذ المتزامن داخل الطلب، لكنه يحتاج بروكسي أمامه ويتطلب بناء إضافة PHP مع كل ترقية. FrankenPHP أبسطها تشغيلاً: لا Nginx، ولا Certbot، ولا PHP-FPM، وعملية واحدة تراقبها systemd.

أرقام تقريبية مقابل PHP-FPM

هذه أرقام تقريبية لنقطة نهاية Laravel تقرأ من قاعدة البيانات وتعيد JSON، على سيرفر بمعالجين و4GB RAM، والنتائج الفعلية تختلف حسب منطقك ووزن الاستعلامات:

الإعدادطلبات في الثانيةزمن p95ذاكرة التطبيق
Nginx مع PHP-FPM و OPcacheمن 350 إلى 500نحو 90msنحو 30MB لكل عملية FPM
FrankenPHP بالوضع الكلاسيكيمن 400 إلى 550نحو 80msمماثلة
FrankenPHP بوضع workerمن 1,500 إلى 2,500نحو 20msنحو 60MB لكل worker ثابتة

لاحظ أن الوضع الكلاسيكي لا يعطي قفزة كبيرة، فالمكسب الحقيقي من وضع worker. لاحظ أيضاً أن التحسن يتقلص كلما ثقلت الطلبات: صفحة تقضي 300ms في استعلامات قاعدة البيانات لن تصبح أسرع كثيراً، لأن الإقلاع الذي ألغيته كان جزءاً صغيراً من وقتها. أكبر المستفيدين واجهات API الخفيفة والتطبيقات ذات الطلبات الكثيرة القصيرة.

HTTPS تلقائي وHTTP/3 وEarly Hints

لأن FrankenPHP مبني على Caddy، فهو يرث إدارة الشهادات كاملة: يطلب شهادة Let's Encrypt عند أول طلب للنطاق، ويجددها قبل انتهائها، ويعيد توجيه HTTP إلى HTTPS دون سطر إعداد واحد. هذا يلغي Certbot ومهام cron الخاصة به، ويلغي أشهر سبب لانقطاع المواقع الصغيرة، وهو شهادة انتهت ولم يلاحظها أحد.

دعم HTTP/3 يعني أن المتصفح يتصل عبر QUIC فوق UDP بدل TCP، فيختصر مصافحة الاتصال من رحلتين إلى رحلة واحدة تقريباً، ويتحمل فقدان الحزم أفضل على شبكات الجوال. افتح المنفذ 443 على UDP في الجدار الناري وإلا سيرجع المتصفح بصمت إلى HTTP/2. أما Early Hints فترسل استجابة 103 مبكرة تخبر المتصفح بملفات CSS وJavaScript المطلوبة قبل أن ينتهي Laravel من بناء الصفحة، فيبدأ التنزيل بالتوازي ويتحسن زمن ظهور المحتوى بعشرات المللي ثانية في الصفحات الثقيلة.

ما يتطلبه الكود حتى يعمل في وضع worker

في PHP-FPM كل طلب يبدأ بذاكرة نظيفة وينتهي بمسحها، لذلك الأخطاء في إدارة الحالة لا تظهر. في وضع worker الذاكرة باقية بين الطلبات، وهنا تظهر ثلاث مشكلات:

الحالة المشتركة

إن سجّلت خدمة singleton تستقبل كائن Request أو المستخدم الحالي في دالة البناء، فستحتفظ بطلب أول مستخدم وتخدم به كل من بعده. هذا ليس خطأ أداء بل خطأ أمني: مستخدم قد يرى بيانات مستخدم آخر. الحل أن تحقن الحاوية نفسها أو تستدعي request() وauth() داخل الدوال وقت الحاجة لا في البناء. الشيء نفسه مع الخصائص الساكنة static التي تجمع بيانات: تنمو مع كل طلب ولا تُمسح.

تسرب الذاكرة

مصفوفة تُضاف إليها عناصر في كل طلب، أو مستمع أحداث يُسجَّل مرة مع كل طلب، سيرفع استهلاك الذاكرة ببطء حتى ينهار الـworker. الحماية العملية خيار --max-requests=500 الذي يعيد تشغيل كل worker بعد 500 طلب، فيُمسح أي تسرب قبل أن يضر. راقب ذاكرة العمليات أول أسبوع، فإن رأيتها تصعد بخط مستقيم فابحث عن المصدر بدل الاكتفاء بإعادة التشغيل.

الحزم الخارجية

معظم حزم Laravel الشائعة تدعم Octane اليوم، لكن الحزم القديمة أو المكتوبة داخلياً قد تخزن حالة. شغّل الاختبارات تحت Octane محلياً، وجرّب سيناريو مستخدمَين يسجلان الدخول بالتناوب من متصفحين مختلفين، فهذا الاختبار البسيط يكشف أخطر أنواع التسرب خلال دقائق.

خطوات النشر كخدمة systemd

  • ثبّت الحزمة: composer require laravel/octane ثم php artisan octane:install --server=frankenphp، وسيُنزَّل ملف FrankenPHP التنفيذي تلقائياً.
  • أنشئ ملف خدمة في /etc/systemd/system باسم laravel-octane.service يعمل بمستخدم www-data، مع WorkingDirectory على مجلد التطبيق.
  • اجعل أمر التشغيل: php artisan octane:start --server=frankenphp --host=example.sa --port=443 --https --workers=4 --max-requests=500، فخيار https يفعّل الشهادة التلقائية من Let's Encrypt.
  • أضف AmbientCapabilities=CAP_NET_BIND_SERVICE حتى يستمع المستخدم العادي على المنفذين 80 و443 دون صلاحيات root، وRestart=always لإعادة التشغيل عند أي انهيار.
  • في سكربت النشر بعد سحب الكود وتشغيل الترحيلات، نفّذ php artisan octane:reload لإعادة تحميل الـworkers بالكود الجديد بلا قطع للاتصالات القائمة.

عدد الـworkers يبدأ عادة بعدد الأنوية مضروباً في 2، أي 4 على سيرفر بمعالجين. كل worker يحجز ذاكرته بشكل دائم، فاحسب: 4 workers بـ60MB تقريباً مع هامش للطوابير وقاعدة البيانات. لا ترفع العدد بلا قياس، فالـworkers الزائدة تتنافس على نفس الأنوية دون مكسب.

تأكد أن المنفذين 80 و443 مفتوحان قبل الإطلاق الأول، وأن سجل DNS يشير للسيرفر، لأن Caddy يحتاجهما لإتمام تحدي Let's Encrypt. بعدها يجدد الشهادة وحده قبل انتهائها بنحو 30 يوماً.

للسجلات، اقرأ مخرجات الخدمة عبر journalctl -u laravel-octane -f، فكل طلب وكل خطأ PHP يظهر هناك بدل ملفات Nginx المتفرقة. اضبط حداً لحجم السجل في journald حتى لا يملأ القرص، فالسيرفر الذي يخدم 1,500 طلب في الثانية يكتب سجلات كثيفة خلال ساعات، ثم أرسل أخطاء Laravel نفسها إلى قناة التنبيه التي يتابعها فريقك.

أين يناسب ولا يناسب

FrankenPHP يحتاج تحكماً كاملاً في السيرفر: systemd، والمنافذ، وملف تنفيذي مخصص. لذلك مكانه الطبيعي الخادم السحابي VPS لا الاستضافة المُدارة. في الاستضافة السحابية المُدارة يعمل PHP عبر الإعداد التقليدي المضبوط مسبقاً مع cPanel، وهذا كافٍ تماماً لمعظم مواقع Laravel ذات الحمل المعتدل.

لتطبيق Laravel متوسط مع قاعدة بيانات على نفس السيرفر، باقة نمو (2 vCPU و4GB RAM و40GB SSD و500GB نقل) بـ6,900 ريال سنوياً تكفي لأربعة workers مع MySQL أو PostgreSQL. إن كانت الواجهة تخدم تطبيق جوال بآلاف الطلبات في الدقيقة، فباقة أعمال (4 vCPU و8GB RAM و1TB نقل) بـ11,500 ريال سنوياً تعطيك 8 workers وهامشاً مريحاً. السيرفرات في الرياض بنحو 8ms، وتفاصيل الموارد في صفحة الخادم السحابي.

قائمة التحويل العملية

  • ابحث في الكود عن الخصائص الساكنة وعن أي singleton يستقبل Request أو المستخدم في دالة البناء، وأصلحها أولاً.
  • شغّل التطبيق تحت Octane محلياً بخيار --max-requests=500 واختبر سيناريو المستخدمَين بالتناوب.
  • قس بأداة مثل wrk أو k6 قبل وبعد على نفس السيرفر، فإن لم يتجاوز التحسن 50% فطلباتك محكومة بقاعدة البيانات لا بالإقلاع.
  • انشر بخدمة systemd مع octane:reload في سكربت النشر، واحتفظ بإعداد Nginx القديم أسبوعاً للرجوع إن لزم.
مشاركة