تصحيح عمليات busy

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

استخدام أوامر strace و lsof للتحديد

1. العثور على pid للعمليات busy في حالة status
عند تشغيل php start.php status، تظهر النتيجة التالية

pid للعمليات busy هو 11725 و 11748.

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

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

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

إذا كانت العملية معلقة في استدعاءات epoll_wait أو select، فهذا أمر طبيعي، مما يعني أن العملية قد أصبحت في حالة idle.

3. استخدام lsof لرؤية وصف الملف للعملية
شغّل lsof -nPp 11725، وتظهر النتيجة التالية

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

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

أسباب أخرى لجعل العملية busy لفترة طويلة

بصرف النظر عن أي حظر أو أسباب لجعل العملية في حالة busy، هناك أسباب أخرى تؤدي لذلك.

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

2. وجود حلقة لا نهائية في الكود
الظاهرة: يمكنك رؤية أن العمليات busy تستهلك وحدة المعالجة المركزية بشكل مرتفع، وأمر strace -ttp pid لم يُظهر أي معلومات حول استدعاءات النظام.
الحل: يمكنك الرجوع إلى مقالة لاهان من خلال تحديد الموقع باستخدام gdb وphp source، والخطوات ملخصة كالتالي:

  1. قم بتشغيل php -v للتحقق من النسخة.
  2. قم بتحميل المصدر المتعلق بالنسخة من php.
  3. قم بتشغيل gdb --pid=pid للعمليات `busy
  4. ثم قم بتشغيل source php源码路径/.gdbinit،
  5. وأخيرًا، استخدم zbacktrace لطباعة مكدس الاستدعاء.
    في الخطوة الأخيرة، ستتمكن من رؤية مكان تنفيذ php في الكود، والذي يحدد مكان الحلقة الميتة.
    ملاحظة: إذا لم يظهر zbacktrace مكدس الاستدعاء، فقد يعني ذلك أن php لم يتم تجميعه مع خيار -g، وبهذا تحتاج لإعادة تجميع php، ثم إعادة تشغيل workerman لتحديد الموقع.

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

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

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

$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();