أداء PHP
سرعة PHP تُضبَط، ولا تُشترى.
يعمل فريقنا بلغة PHP منذ خمسة وعشرين عامًا، ويعرف أين يضيع وقت الطلب. نحدّد ذلك على خادمك ونستعيده، طبقةً بعد طبقة.
تشريح طلب واحد
الصفحة سلسلة من الانتظارات: العثور على الخادم، وفتح الاتصال، والوقوف في طابور PHP، والترجمة، والتنفيذ، والاستعلام من قاعدة البيانات، والإرسال. بدّل بين الحالات لترى أي الحلقات نختصرها.
-
استعلام DNS يُجيب عنه محلِّل أسماء النطاقات لدى الزائر. لا يتغير في أي حالة: هذا الجزء يخص شبكة الزائر. لا يتغير في أي حالة: هذا الجزء يخص شبكة الزائر.
-
الاتصال وTLS إعداد TLS قديم، واتصال جديد لكل ملف. TLS 1.3 واتصال HTTP/2 واحد يُعاد استخدامه لكل ملف. TLS 1.3 واتصال HTTP/2 واحد يُعاد استخدامه لكل ملف.
-
انتظار عامل PHP كل العمّال مشغولون، فينتظر الطلب في الطابور. مجمّع محسوب حجمه من الذاكرة: عامل متاح جاهز. لا حاجة إلى عامل PHP.
-
ترجمة السكربتات تقرأ PHP كل ملف وتترجمه مع كل طلب. يقدّم OPcache السكربتات المترجَمة من الذاكرة. لا شيء يحتاج إلى ترجمة.
-
العثور على الملفات والأصناف يفتّش المحمِّل التلقائي المجلدات عن كل صنف. محمِّل تلقائي بخريطة أصناف (classmap) وذاكرة مسارات مؤقتة ممتلئة. لا شيء يحتاج إلى تحميل.
-
تنفيذ كودك عمل التطبيق نفسه. يكاد يبقى كما هو: نادرًا ما يغيّر JIT هذا الجزء في موقع معتاد. لا يُنفَّذ كودك في هذه الزيارة.
-
استعلامات قاعدة البيانات فهارس ناقصة، والاستعلام نفسه في كل صفحة. فهارس مضافة، وإجابات متكررة محفوظة في Redis. لا يُرسَل أي استعلام.
-
إرسال الصفحة تُرسَل دون ضغط. تُضغط قبل أن تغادر الخادم. الصفحة الجاهزة، مباشرة من ذاكرة الصفحات المؤقتة.
الأشرطة أعلاه رسم توضيحي، أما هذا الرقم فقياس فعلي.
بقية الوقت هي المسافة بين خوادمنا في ألمانيا وبينك. هذا الجزء يعتمد على مكانك، فقِسه من اتصالك أنت.
قِسه من اتصالكالإعدادات التي تزيل معظم الانتظار
تجري الترجمة والبحث عن الملفات وتحميل الأصناف قبل أن ينفّذ كودك سطرًا واحدًا. وهذه الإعدادات تجعلها تحدث مرة واحدة بدلًا من تكرارها مع كل طلب.
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.preload=/srv/app/preload.php
opcache.preload_user=www-data
opcache.jit=tracing
opcache.jit_buffer_size=64M
realpath_cache_size=4096K
realpath_cache_ttl=600
$ composer install --no-dev --classmap-authoritative
-
OPcache
تترجم PHP كل ملف قبل تنفيذه. ويحتفظ OPcache بالناتج المترجَم في ذاكرة مشتركة، فيتخطى الطلب التالي هذا العمل. نحدّد حجم ذاكرته وعدد ملفاته بما يناسب الموقع، ونقرأ نسبة إصاباته لنتأكد من أن شيئًا لا يُطرَد منه.
في بيئة الإنتاج تتوقف PHP عن فحص كل ملف بحثًا عن تغييرات؛ وكل عملية نشر تمسح الذاكرة المؤقتة بنفسها.
-
التحميل المسبق
في حالة إطار العمل، يحمّل التحميل المسبق ملفاته الأساسية مرة واحدة عند بدء PHP ويُبقيها جاهزة لكل طلب. وأي تغيير في هذه الملفات يستلزم إعادة التشغيل، ولذلك تكون إعادة التشغيل جزءًا من النشر.
-
JIT
يحوّل JIT كود PHP الأكثر تنفيذًا إلى كود آلة. وهو يفيد العمل الذي يُشغل المعالج — معالجة الصور والحسابات والحلقات الطويلة. أما الموقع المعتاد فيقضي وقته في انتظار قاعدة البيانات والشبكة، وهناك لا يغيّر JIT إلا القليل. لذلك نفعّله حيث يُظهر القياس فرقًا، لا في كل مكان.
-
ذاكرة المسارات المؤقتة (realpath)
كل تضمين لملف يسأل القرص عن موقعه الحقيقي. وذاكرة مسارات مؤقتة أكبر تحفظ هذه الإجابات في الذاكرة، وهو ما يُحدث فرقًا في أطر العمل التي تحمّل مئات الملفات في كل طلب.
-
المحمِّل التلقائي في Composer
خريطة أصناف موثوقة (classmap) تعثر على كل صنف ببحث واحد بدلًا من التفتيش في المجلدات، وتبقى حزم التطوير خارج الخادم.
مفتاح الضبط: PHP-FPM
يحتفظ PHP-FPM بمجمّع من عمّال PHP. إن قلّ عددهم اصطفت الطلبات في طابور، وإن زاد نفدت ذاكرة الخادم فتباطأ على الجميع. ولذلك يُضبط المجمّع وفق ما يملكه الخادم فعلًا.
pm = static
pm.max_children = 64
عدد ثابت من العمّال يعمل دائمًا
لخادم مخصّص لموقع واحد كثيف الزيارات. لا يضيع وقت في بدء العمليات، والذاكرة المستهلكة معروفة مسبقًا.
pm = dynamic
pm.max_children = 64
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
بعض العمّال جاهزون، ويزيدون مع الضغط
يُبقي بضعة عمّال خاملين في الانتظار ويضيف غيرهم كلما ارتفعت الزيارات، حتى الحد الأقصى. وهو الخيار المعتاد لموقع له ذروات يومية.
pm = ondemand
pm.max_children = 64
pm.process_idle_timeout = 10s
عمّال عند وصول الطلب فقط
لمواقع صغيرة كثيرة على خادم واحد: تذهب الذاكرة إلى المواقع التي تُزار، ويتحمّل أول طلب بعد فترة هدوء زمن بدء قصيرًا.
كيف نحدّد pm.max_children
pm.max_children = ذاكرة PHP ÷ ذاكرة العامل الواحد
$ ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1} END {print s/NR/1024 " MB"}'
مثال محسوب لا توصية: الأرقام الحقيقية تأتي من خادمك، ونترك هامشًا لأوقات الذروة.
- pm.max_requests
- يُعيد تدوير العامل بعد عدد محدد من الطلبات، فلا يكبر تسرّب بطيء في الذاكرة حتى يصير انقطاعًا.
- request_slowlog_timeout
- الطلب الذي يتجاوز هذه المدة يكتب تتبّع المكدّس الخاص به في سجل الطلبات البطيئة، فيسمّي الدالة التي ينبغي فحصها.
- request_terminate_timeout
- يوقف الطلب الخارج عن السيطرة قبل أن يحتجز عاملًا إلى الأبد.
- pm.status_path
- يعرض الطابور وعدد المرات التي بلغ فيها المجمّع حدّه — وهو أول ما ننظر إليه حين يتباطأ موقع وقت الذروة.
طريقان من خادم الويب إلى PHP
نشغّل الاثنين، ونختار بحسب ما يحتاجه الموقع. وفي الحالتين، أسرع طلب هو الذي لا تحتاج PHP إلى الرد عليه أصلًا.
LiteSpeed + LSAPI
- الزائر
- LiteSpeed
- LSCacheذاكرة الصفحات المؤقتة
- LSAPI
- lsphp
يتخاطب LiteSpeed مع PHP عبر بروتوكوله الخاص LSAPI، ويحفظ الصفحات كاملة في LSCache، التي يستطيع WordPress وغيره من التطبيقات مسحها في اللحظة التي يتغير فيها المحتوى.
Nginx + PHP-FPM
- الزائر
- Nginx
- fastcgi_cacheذاكرة الصفحات المؤقتة
- FastCGI
- PHP-FPM
يقدّم Nginx الملفات الثابتة بنفسه، ويمرّر PHP إلى مجمّع PHP-FPM عبر مقبس محلي، ويمكنه حفظ الصفحات الجاهزة في ذاكرة FastCGI المؤقتة للزوار غير المسجّلين دخولهم.
-
Redis للكائنات والجلسات
تُحفَظ في الذاكرة الإجابات التي تطلبها الصفحة من قاعدة البيانات مرة بعد مرة، وتكفّ الجلسات عن الكتابة على القرص.
-
تخزين مؤقت للصفحة كاملة
يحصل الزائر غير المسجّل دخوله على صفحة جاهزة دون الرجوع إلى PHP أو قاعدة البيانات إطلاقًا. وتبقى سلال الشراء والحسابات وصفحات الدفع خارجه.
-
HTTP/2 والضغط
اتصال مشفّر واحد ينقل كل ملفات الصفحة، ويُضغط النص قبل خروجه، وتحمل الملفات الثابتة مدد تخزين مؤقت طويلة مع أسماء تتضمن رقم الإصدار.
-
وقت قاعدة البيانات
يكشف سجل الاستعلامات البطيئة وEXPLAIN أي استعلام ينتظر ولماذا؛ وغالبًا ما يكون فهرس واحد أو استعلام واحد متكرر هو معظم المشكلة.
كيف نضبط قواعد البيانات
نقيس أولًا، ثم نغيّر شيئًا واحدًا
كل تغيير يُجرى مقابل قراءة أُخذت قبله، لنريك أثره بوضوح. وهذه هي القراءات.
opcache_get_status()نسبة الإصابات، والذاكرة الحرة، وما إذا كانت السكربتات تُطرَد.pm.status_pathطابور الاستماع، وعدد المرات التي بلغ فيها المجمّع حدّه.slowlogتتبّع المكدّس لكل طلب استغرق وقتًا أطول من اللازم.slow_query_log + EXPLAINأي الاستعلامات ينتظر، وكم صفًا يقرأ، وأي فهرس ينقصه.redis-cli INFO statsالإصابات مقابل الإخفاقات في الذاكرة المؤقتة، وما إذا كانت المفاتيح تُطرَد.curl -w %{time_starttransfer}الزمن حتى أول بايت، مقيسًا بالطريقة نفسها قبل التغيير وبعده.
أتريد النصف الآخر من الرقم — المسافة بين خوادمنا وبينك؟ شغّل اختبار السرعة

أخبرنا أي صفحة بطيئة
أرسل إلينا العنوان وأين يعمل الموقع اليوم. نقرأ الخادم أولًا، ثم نخبرك بما سنغيّره وما ينبغي أن يحققه — ونسعّر العمل بعد هذه المحادثة.