تصحيح العمليات المشغولة

أحيانًا ، يمكن رؤية عمليات مشغولة عند تشغيل الأمر php start.php status ، مما يشير إلى أن العملية المقابلة تعمل على مهمة. في الحالة العادية ، بمجرد انتهاء المهمة ، ستستعيد العملية حالة "idle" ، وبشكل عام لن تكون هناك أي مشكلة. ومع ذلك ، إذا استمرت في حالة مشغولة دون استعادة حالة "idle" ، فإن ذلك يشير إلى وجود تأخر أو حلقة لا نهائية في الأعمال داخل العملية، ويمكن التعامل مع هذا بالطرق التالية.

تحديد الموقع باستخدام strace + lsof الأمر

1. تحديد مُعرف عملية مشغولة في الحالة
عند تشغيل php start.php status سيظهر الناتج التالي

حيث معرّف العملية المشغولة هو pid 11725 و 11748.

2. تتبع العملية باستخدام strace
اختر عُملية pid (مثلاً 11725) وقم بتشغيل strace -ttp 11725 ليظهر الناتج التالي

حيث يمكن ملاحظة أن العملية تقوم بحلقة لا نهائية من poll([{fd=16, events=....، وهذه تعني انتظار ظهور حدث قراءة المُعرف 16، أي أنها تنتظر عودة البيانات من هذا المُعرف.

إذا لم يظهر أي استدعاءات النظام، احتفظ بنافذة الطرفية الحالية وافتح نافذة طرفية جديدة، وقم بتشغيل kill -SIGALRM 11725 (لإرسال إشارة منبه إلى العملية)، وثم تحقق مما إذا كانت هناك استجابة في نافذة strace أم إذا كانت عالقة في استدعاء النظام. إذا لم يكن هناك أي استدعاءات للنظام، فهذا يعني على الأرجح أن البرنامج عالق في حلقة لا نهائية في الأعمال. يُمكن الرجوع إلى السبب الآخر الثاني في الجزء الأخير من الصفحة.

إذا كانت النظام معلقة في استدعاء النظام epoll_wait أو select فهذا يعني أن العملية قد استعادت حالة "idle".

3. عرض مُعرف العملية باستخدام lsof
قم بتشغيل lsof -nPp 11725 ليظهر الناتج التالي

حيث يظهر أن المُعرف 16 يُطابق السجل 16u (السطر الأخير)، ويُمكن ملاحظة أن المُعرف برقم 16 هو اتصال TCP، وعنوان الوجهة البعيد هو "101.37.136.135:80"، مما يعني أن العملية قد تقوم بالوصول إلى مورد HTTP، وحلقة poll([{fd=16, events=.... تعني انتظار خدمة HTTP لإرجاع البيانات، وهذا يفسر سبب استمرار العملية في حالة "busy".

الحل:
عند معرفة مكان عقد العملية، يُصبح الحل سهلًا، على سبيل المثال، في الحالة أعلاه وبعد التحديد، من المرجح أن الأعمال تقوم بعملية curl وأن الرابط المقابل له يستغرق وقتاً طويلاً في الاستجابة، مما يؤدي إلى استمرار العملية في الانتظار. في هذه الحالة، يُمكن الاتصال بمزود الرابط لتحديد سبب بطء الاستجابة، وفي الوقت نفسه يُفضل إضافة معامل الإنتهاء الزمني أثناء استدعاء curl، مثلاً، إذا لم يعود الاستجابة خلال 2 ثانية، فإن العملية ستنتقل إلى حالة "busy" لفترة تقدر بحوالي 2 ثانية.

الأسباب الأخرى لانشغال العمليات لفترة طويلة

بالإضافة إلى الانغماس أو التأخر في العمليات، هناك أسباب أخرى لانشغال العمليات لفترة طويلة.

1. وجود خطأ حاد في الأعمال يؤدي إلى خروج مستمر للعملية
الظاهرة: في مثل هذه الحالات، فإن الحمل على النظام مرتفع، ويكون متوسط الحمل في الحالة "busy" هو 1 أو أعلى. كما أن عدد مرات خروج العملية يكون مرتفعًا جدًا ويزيد باستمرار.
الحل: يُمكن تشغيل النظام في وضع التصحيح (بدون استخدام الخيار -d عند تشغيل php start.php start) لمراقبة أخطاء الأعمال ومعالجتها.

2. حلقة لا نهائية في الكود
الظاهرة: من خلال الأداة top يُمكن رؤية أن العمليات المشغولة تستهلك الكثير من وحدة المعالجة المركزية (CPU)، وعند تشغيل امر strace -ttp pid لا يتم طباعة أي معلومات عن استدعاءات النظام.
الحل: يُمكن الرجوع إلى مقال bird، حيث توضح الخطوات المطلوبة باستخدام gdb ومصدر الPHP. يُمكن تلخيص الخطوات الرئيسية كالتالي:

  1. php -v لمعرفة الإصدار
  2. تحميل مصدر الPHP للإصدار المحدد
  3. gdb --pid=pid العملية المشغولة
  4. source php مسار المصدر/.gdbinit
  5. zbacktrace لطباعة سلسلة الاستدعاءات
    بعد ذلك، يُمكن رؤية مكان حلقة الأعمال داخل الكود PHP الحالية.

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

3. إضافة لانهائية للموقتات
عندما يقوم الكود بإضافة موقتات بشكل مستمر دون حذفها، فإن ذلك يؤدي إلى زيادة عدد الموقتات داخل العملية وينتج عنه تشغيل لانهائي للموقتات. مثال على ذلك هو الكود التالي:

$worker = new Worker;
$worker->onConnect = function($con){
    Timer::add(10, function(){});
};
Worker::runAll();

يقوم الكود أعلاه بإضافة موقت كلما حدث توصيل عميل، ولكن لا يحتوي الكود على منطقية لحذف الموقتات. وبمرور الوقت، ستزداد الموقتات في العملية بشكل مستمر، مما يؤدي إلى تشغيل لانهائي للموقتات. الكود الصحيح يجب أن يكون كما يلي:

$worker = new Worker;
$worker->onConnect = function($con){
    $con->timer_id = Timer::add(10, function(){});
};
$worker->onClose = function($con){
    Timer::del($con->timer_id);
};
Worker::runAll();