خلفية المشكلة:
في يوم 1 من الشهر 9، نحو الساعة 17:12، تلقّينا شكوى من فريق التنفيذ بشأن مشكلات منها تعذّر ترقية بعض الأجهزة بصورة طبيعية، وعدم تلقّيها الأوامر المرسلة من وحدة التحكم. راجع أحد الزملاء سجلات مشروع control(ويُشار إليه لاحقًا باسم control)، فوجد أن control لم يتلقَّ معلومات ظل الجهاز التي أرسلتها الأجهزة، ولذلك لم يرسل الأوامر. يتصل مشروع control مباشرة بالأجهزة، ويرسل إليها بعض الأوامر ومعلومات الإعداد وفقًا للمعلومات التي تبلّغ عنها، بما يشمل الترقية ورفع السجلات وغيرها. قبل إطلاق IoT، كان control يعتمد على تقارير نبضات الحياة للحصول على المعلومات الحالية للأجهزة، وبعد إطلاق IoT أصبح يعتمد على معلومات ظل الجهاز
للحصول على المعلومات الحالية للجهاز، يشترك control في معلومات ظل الجهاز، لكن خدمة الظل(المختصرة باسم IoT)هي التي تعيد توجيه تلك المعلومات إليه، وهو لا يتلقى مباشرة تقارير ظل الجهاز، ولمعرفة تفاصيل انتقالها، يُرجى الاطلاع على الشكلين التاليين:
حصول التطبيق(control)على حالة الجهاز

يرسل التطبيق (control) الأوامر إلى الأجهزة

بعد أن علمت أن control لا يتلقى رسائل الظل، ذهبت فورًا إلى لوحة تحكم rabbitmq للتحقق من وجود رسائل والتأكد من أمرين:1.هل أرسل الجهاز رسائل 2.هل يعمل rabbitmq بصورة طبيعية، والصورتان 1 و2 أدناه لقطتان من لوحة تحكم rabbitmq التُقطتا آنذاك، ومن الشكل 1 يمكن التأكد بوضوح من أن الجهاز يرسل رسائل بالفعل، لكن كثيرًا من الرسائل في حالة unacked(هذا يعني أنها سُلّمت بالفعل إلى المستهلك، لكنه لم يرسل تأكيد الاستلام ack فحسب؛ ومن الناحية النظرية، يفترض أن تعود الأمور إلى طبيعتها بعد الانتظار بعض الوقت)، ولمعرفة أي طابور تحديدًا تتراكم فيه الرسائل غير المؤكدة unacked، يرجى الاطلاع على الشكل 2،“spacebridgeiot-shadow” هو بالضبط ما نستخدمه لاستقبال معلومات الظل التي ترسلها الأجهزة. وقد تراكمت الرسائل كلها في الطابور، لذا كان من المعقول ألا تُحوَّل إلى control. وبعد المراقبة لفترة، تبيّن أن عدد unacked أصبح 0، لكن العدد الإجمالي total لم يتغيّر كثيرًا، مما أوحى بأن رسائل unacked عادت إلى طابور الرسائل في انتظار التسليم. وبالفعل، بعد بضع دقائق ظهر مجددًا عدد كبير من رسائل unacked، ثم عادت هذه الرسائل إلى الطابور بعد بضع دقائق أخرى، بينما ظل control لا يتلقى أي رسائل. وعند تفحّص سجلات IoT حينها، تبيّن على نحو مفاجئ أنه لم تصل أي رسائل ظل. وفي لوحة تحكّم rabbitmq، تفحّصت «spacebridgeiot-shadow” تبيّن، على نحو مفاجئ، أن قائمة الانتظار هذه لم يعد فيها أي مستهلك، مثل الشكل 3 كما هو موضح. وعند فحص سجلات rabbitmq حينها، وُجدت بالفعل معلومات عن أخطاء، مثل الشكل 4 كما هو موضح، أغلق rabbitmq الاتصال من تلقاء نفسه.
الشكل 1:مخطط عام لـ rabbitmq

الشكل 2: مخطط إحصاءات قوائم انتظار rabbitmq

الشكل 3:نظرة عامة على spacebridgeiot-shadow

الشكل 4:رسالة خطأ rabbitmq

حل مؤقت:
لأن عددًا كبيرًا من الشكاوى كان قد وصل آنذاك، لجأنا إلى حل عنيف نسبيًا، وهو «حذف الرسائل المتراكمة»، وبالفعل عاد كل شيء إلى طبيعته بعد حذفها(ملاحظة:يجب حل مشكلات بيئة الإنتاج بأسرع وقت، فلا وقت يسمح لنا بتحليل السجلات ثم معالجتها بروية وانتظام، لا بد من السرعة)。

إعادة إنتاج المشكلة في بيئة غير إنتاجية:
1.تكديس عدد كبير من الرسائل(عشرات الآلاف على الأقل)في طابور rabbitmq المسمى “mirrorTestQueue” في بيئة 10.200.41.166
2.أوقف مستهلكي mirrorTestQueue، ثم أعد تشغيلهم بعد اكتمال تراكم الرسائل
3.اكتمل التراكم، أعد تشغيل المستهلك
كما توقعنا، دُفعت آلاف الرسائل إلى المستهلك خلال ثوانٍ، وبعد استمرار ذلك بضع دقائق أغلق rabbitmq الاتصال بالمستهلك من تلقاء نفسه، وعندها لم يعد مستهلك الطابور ظاهرًا في لوحة التحكم. ولأننا ضبطنا المستهلك على الاستعادة التلقائية، فإنه يعاود الاتصال تلقائيًا بعد فترة،
لكن الاتصال كان ينقطع مجددًا بسرعة، على نحو يكاد يطابق المشكلة التي واجهناها في بيئة الإنتاج. فما الذي تسبب فيها بالضبط؟ بصراحة، لم تكن لدي آنذاك فكرة واضحة، وبحثت على الإنترنت دون أن أجد إجابة مُرضية حقًا(لم أدرك جوهر المشكلة آنذاك، وكانت كلمات البحث عامة أكثر من اللازم),ولاحقًا
خمّنّا أن مشكلة ما ربما حدثت على مستوى TCP، فقررنا التقاط حزم الشبكة لنرى إن كان بإمكاننا العثور على أي خيط. وبالفعل، حالفنا الحظ؛ ومن دون إطالة، إليكم الصورة مباشرة.


قبل 13:06:25.643428، ظل rabbitmq يدفع الرسائل إلى المستهلكين، حتى بدأت عند اللحظة 13:06:25.643428 حالة امتلاء نافذة tcp لدى المستهلك. واستمر ذلك نحو 30 ثانية، ثم قطع rabbitmq الاتصال من جانبه(بإرسال حزمة rst). وبعد ذلك، أعاد المستهلك الاتصال، لكن النافذة عادت إلى الامتلاء، وبعد نحو 30 ثانية قُطع الاتصال مجددًا.


بدا الأمر منتظمًا إلى حد بعيد، إذ كانت كل مرة تستمر 30 ثانية، وكأنه معامل قابل للضبط. ويمكن تلخيصه تقريبًا هكذا: «تسببت tcp full window في أن يعيد الخادم ضبط الاتصال بإشارة rst من تلقاء نفسه، وبنمط منتظم أيضًا»
هذه المرة غيّرت كلمات البحث قليلًا فعثرت عليه الإجابة، لدى rabbitmq معامل يُسمّى tcp_listen_options.send_timeout هو مُعامل للتحكم في مهلة الكتابة، وعند انقضائها يُفعَّل RST في TCP(https://github.com/rabbitmq/rabbitmq-java-client/issues/341)، لنعدّلها قليلًا ونرَ النتيجة:
1. تغيير مهلة الكتابة إلى 10 ثانية
tcp_listen_options.send_timeout = 10000
2.التقاط حزم الشبكة للتحقق مما إذا كان ذلك قد دخل حيّز التنفيذ


استغرق الأمر نحو 10 ثوانٍ من امتلاء النافذة إلى إغلاق الاتصال.
مراجعة ما حدث:
نظرًا إلى عدم ضبط prefetch لدى مستهلك rabbitmq، أرسل rabbitmq إليه عددًا مفرطًا من الرسائل دفعةً واحدة، فامتلأت نافذة tcp لدى المستهلك، مما فعّل tcp_listen_options.send_timeout في rabbitmq. وعندما تبلغ مهلة الكتابة هذه حدًا معينًا، يدفع ذلك rabbitmq إلى قطع اتصال tcp الخاص بالمستهلك.
الحل النهائي:
كان حذف الرسائل سابقًا مجرد حل اضطراري؛ ورغم أنه حل المشكلة، فإنه كان عنيفًا أكثر مما ينبغي. نحتاج إلى حل أنيق للتعامل معها. وبما أن كثرة الرسائل المدفوعة إلى المستهلك هي ما ملأ نافذة TCP، فعلينا العمل على معدل الاستقبال، ويكفي أن نخبر rabbitmq عند الاتصال به بألا يرسل إلينا الكثير.(المقطع التالي مقتبس من https://blog.csdn.net/james_searcher/article/details/70308565)لدى rabbitmq خاصية تسمى prefetch، وهي تشير إلى الحد الأقصى لعدد الرسائل غير المؤكدة الاستلام التي يمكن لمستهلك واحد استهلاكها. كيف نفهم ذلك؟ يخصص mq لكل مستهلك مخزنًا مؤقتًا حجمه هو prefetch. كلما وصلت رسالة، يدفعها MQ إلى المخزن المؤقت، ثم يدفعها إلى العميل. وعند تلقي رسالة ack (يرسل المستهلك تعليمة baseack)، يفرغ mq موضعًا في المخزن المؤقت، ثم يضيف رسالة جديدة. لكن إذا كان المخزن المؤقت ممتلئًا حينها، يدخل MQ في حالة حجب. وبصورة أدق، لنفترض أن قيمة prefetch مضبوطة على 10، وأن هناك مستهلكين اثنين. أي إن كل مستهلك يجلب مسبقًا في كل مرة 10 رسائل من قائمة الانتظار إلى ذاكرته المحلية المؤقتة لتنتظر الاستهلاك. وفي الوقت نفسه يصبح عدد الرسائل غير المؤكدة الاستلام في هذه القناة 20. أما ترتيب التسليم في Rabbit فهو إرسال 10 رسائل كاملة إلى consumer1 أولًا، ثم إرسال 10 رسائل إلى consumer2. وإذا وجدت حينها رسالة جديدة تحتاج إلى التسليم، يتحقق أولًا مما إذا كان عدد الرسائل غير المؤكدة الاستلام في القناة يساوي 20؛ فإن كان كذلك، لا يسلم الرسالة إلى أي مستهلك، وتبقى الرسالة في قائمة الانتظار. ثم عندما يؤكد أحد المستهلكين استلام رسالة، يصبح عدد الرسائل غير المؤكدة الاستلام 19، فيتحقق Rabbit من المستهلك الذي يقل عدد رسائله غير المؤكدة الاستلام عن 10، ويسلم الرسالة إليه.
وفي الشيفرة تحديدًا، يكون ذلك

كيف نقيّم هذه القيمة؟ يوجد مقال رسمي لـ RabbitMQ يشرح ذلك جيدًا، لذا لن أفصّل هنا؛ والقيمة المضبوطة حاليًا في نظامنا هي 20.
https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/
كلمة ختامية:
ما زلنا مبتدئين في استخدام rabbitmq، لذا ينبغي قبل استخدامه الإكثار من قراءة واجهات API الخاصة به، والتعرف على استخدام الواجهات الشائعة، وإجراء تجارب كثيرة في بيئة غير متصلة بالإنتاج.