نصائح وحيل Kubernetes: ميزات إغلاق رشيقة في NGINX و PHP-FPM

شرط نموذجي لتنفيذ CI / CD في Kubernetes: يجب أن يكون التطبيق قادرًا على عدم قبول طلبات العملاء الجديدة قبل التوقف الكامل ، والأهم من ذلك ، إكمال الطلبات الموجودة بنجاح.

نصائح وحيل Kubernetes: ميزات إغلاق رشيقة في NGINX و PHP-FPM

يتيح لك الامتثال لهذا الشرط تحقيق صفر توقف أثناء النشر. ومع ذلك ، حتى عند استخدام حزم شائعة جدًا (مثل NGINX و PHP-FPM) ، قد تواجه صعوبات تؤدي إلى سلسلة من الأخطاء مع كل عملية نشر ...

نظرية. كيف يعيش الكبسولة

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

ضع في اعتبارك أيضًا أن فترة السماح الافتراضية هي 30 ثانية: بعد ذلك ، سيتم إنهاء الكبسولة ويجب أن يكون لدى التطبيق الوقت لمعالجة جميع الطلبات قبل هذه الفترة. لاحظ: على الرغم من أن أي طلب يتم تشغيله لأكثر من 5-10 ثوانٍ يمثل مشكلة بالفعل ، إلا أن الإغلاق اللطيف لن يساعده بعد الآن ...

لفهم ما يحدث عند انتهاء الحجرة بشكل أفضل ، ما عليك سوى إلقاء نظرة على الرسم التخطيطي التالي:

نصائح وحيل Kubernetes: ميزات إغلاق رشيقة في NGINX و PHP-FPM

A1 ، B1 - إجراء تغييرات حول حالة الكبسولة
A2 - إرسال SIGTERM
B2 - إزالة جراب من نقاط النهاية
B3 - الحصول على التغييرات (تغيرت قائمة نقاط النهاية)
B4 - تحديث قواعد iptables

يرجى ملاحظة أن حذف حجرة نقطة نهاية وإرسال SIGTERM لا يحدث بالتتابع ، ولكن بالتوازي. ونظرًا لحقيقة أن Ingress لا يتلقى على الفور قائمة محدثة بنقاط النهاية ، سيتم إرسال الطلبات الجديدة من العملاء إلى الكبسولة ، مما سيؤدي إلى حدوث 500 خطأ أثناء إنهاء الكبسولة (مواد أكثر تفصيلا حول هذه المسألة نحن مترجم). تحتاج إلى حل هذه المشكلة بالطرق التالية:

  • Send in Connection: رؤوس استجابة قريبة (إذا كانت تتعلق بتطبيق HTTP).
  • إذا لم يكن من الممكن إجراء تغييرات على الكود ، فإن المقالة التالية تصف حلاً يسمح لك بمعالجة الطلبات حتى نهاية فترة السماح.

نظرية. كيف ينهي NGINX و PHP-FPM عملياتهما

NGINX

لنبدأ بـ NGINX ، لأن كل شيء أكثر أو أقل وضوحا معها. بالبحث في النظرية ، نتعلم أن NGINX لديها عملية رئيسية واحدة والعديد من "العاملين" - هذه عمليات فرعية تعالج طلبات العميل. هناك احتمال مناسب: باستخدام الأمر nginx -s <SIGNAL> إنهاء العمليات إما في وضع الإغلاق السريع أو الإغلاق الرشيق. من الواضح أننا مهتمون بالخيار الأخير.

ثم كل شيء بسيط: تحتاج إلى إضافة إلى خطاف preStop أمر يرسل إشارة إيقاف رشيقة. يمكن القيام بذلك في النشر ، في كتلة الحاوية:

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

الآن ، في لحظة إغلاق الكبسولة ، سنرى ما يلي في سجلات حاوية NGINX:

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

وهذا يعني ما نحتاجه: تنتظر NGINX الطلبات حتى تكتمل ، وبعد ذلك تقضي على العملية. ومع ذلك ، سيتم النظر في مشكلة شائعة أدناه ، بسبب ذلك ، حتى لو كان هناك أمر nginx -s quit تنتهي العملية بشكل غير صحيح.

وفي هذه المرحلة ، انتهينا من NGINX: على الأقل من السجلات ، يمكنك أن تفهم أن كل شيء يعمل كما ينبغي.

كيف تسير الأمور مع PHP-FPM؟ كيف تتعامل مع إغلاق رشيق؟ دعونا نفهم ذلك.

PHP-FPM

في حالة PHP-FPM ، هناك القليل من المعلومات. إذا كنت تركز على دليل رسمي PHP-FPM ، سيخبرك بقبول إشارات POSIX التالية:

  1. SIGINT, SIGTERM - اغلاق سريع
  2. SIGQUIT - إغلاق رشيق (ما نحتاجه).

الإشارات المتبقية غير مطلوبة في هذه المشكلة ، لذلك سنحذف تحليلها. لإنهاء العملية بشكل صحيح ، ستحتاج إلى كتابة خطاف preStop التالي:

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

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

يمارس. المشكلات المحتملة مع الإغلاق السلس

NGINX

بادئ ذي بدء ، من المفيد تذكر: بالإضافة إلى تنفيذ الأمر nginx -s quit هناك خطوة أخرى يجب الانتباه إليها. لقد واجهنا مشكلة حيث لا تزال NGINX ترسل SIGTERM بدلاً من إرسال إشارة SIGQUIT ، مما تسبب في عدم اكتمال الطلبات بشكل صحيح. يمكن العثور على حالات مماثلة ، على سبيل المثال ، هنا. لسوء الحظ ، لم نتمكن من تحديد السبب المحدد لهذا السلوك: كان هناك شك في إصدار NGINX ، لكن لم يتم تأكيده. كانت الأعراض أن الرسائل لوحظت في سجلات حاوية NGINX "فتح المقبس رقم 10 متبقي في اتصال 5"، وبعد ذلك توقف الكبسولة.

يمكننا أن نلاحظ مثل هذه المشكلة ، على سبيل المثال ، من الإجابات إلى المؤتمر الذي نحتاجه:

نصائح وحيل Kubernetes: ميزات إغلاق رشيقة في NGINX و PHP-FPM
مؤشرات رموز الحالة وقت النشر

في هذه الحالة ، نحصل فقط على رمز خطأ 503 من Ingress نفسه: لا يمكن الوصول إلى حاوية NGINX ، لأنها لم تعد متوفرة. إذا نظرت إلى سجلات الحاوية مع NGINX ، فإنها تحتوي على ما يلي:

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

بعد تغيير إشارة التوقف ، تبدأ الحاوية في التوقف بشكل صحيح: وهذا ما تؤكده حقيقة أن الخطأ 503 لم يعد يُلاحظ.

إذا واجهت مشكلة مماثلة ، فمن المنطقي معرفة نوع إشارة التوقف المستخدمة في الحاوية وما هو شكل خطاف preStop بالضبط. من الممكن أن السبب يكمن بالتحديد في هذا.

PHP-FPM ... والمزيد

يتم وصف مشكلة PHP-FPM بشكل تافه: فهي لا تنتظر حتى تنتهي العمليات الفرعية ، بل تنهيها ، مما يتسبب في حدوث أخطاء 502 أثناء النشر والعمليات الأخرى. هناك العديد من تقارير الأخطاء على bugs.php.net من عام 2005 (على سبيل المثال ، هنا и هنا) التي تصف المشكلة. لكن في السجلات ، من المرجح ألا ترى أي شيء: سيعلن PHP-FPM عن انتهاء عمليته دون أي أخطاء أو إشعارات من طرف ثالث.

تجدر الإشارة إلى أن المشكلة نفسها قد تعتمد بدرجة أقل أو أكبر على التطبيق نفسه وقد لا تظهر، على سبيل المثال، في المراقبة. إذا واجهت ذلك، يتبادر إلى ذهنك أولاً حل بديل بسيط: قم بإضافة خطاف preStop باستخدام sleep(30). سيسمح لك بإكمال جميع الطلبات التي كانت من قبل (ونحن لا نقبل الطلبات الجديدة ، منذ pod قد قادر على تنتهي) ، وبعد 30 ثانية ، ستنتهي الكبسولة نفسها بإشارة SIGTERM.

تبين أن lifecycle لأن الحاوية ستبدو كما يلي:

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

ومع ذلك ، نظرًا لمواصفات 30 ثانية sleep نحن بقوة زيادة وقت النشر ، حيث سيتم إنهاء كل جراب الحد الأدنى 30 ثانية ، وهو أمر سيء. ما الذي يمكن عمله حيال ذلك؟

دعنا ننتقل إلى الجهة المسؤولة عن التنفيذ المباشر للتطبيق. في حالتنا ، هذا PHP-FPMالتي بشكل افتراضي لا تراقب تنفيذ العمليات التابعة لها: يتم إنهاء العملية الرئيسية على الفور. يمكنك تغيير هذا السلوك مع التوجيه process_control_timeout، والتي تحدد الحدود الزمنية للعمليات الفرعية لانتظار الإشارات من المعلم. إذا تم التعيين على 20 ثانية ، فسيغطي هذا معظم الطلبات التي تعمل في الحاوية ويوقف العملية الرئيسية عند اكتمالها.

بهذه المعرفة ، دعونا نعود إلى مشكلتنا الأخيرة. كما ذكرنا سابقًا ، Kubernetes ليست منصة متجانسة: فهي تستغرق بعض الوقت للتفاعل بين مكوناتها المختلفة. هذا صحيح بشكل خاص عندما نفكر في تشغيل الدخول والمكونات الأخرى ذات الصلة ، لأنه نظرًا لمثل هذا التأخير في وقت النشر ، فمن السهل الحصول على زيادة قدرها 500 خطأ. على سبيل المثال ، قد يحدث خطأ في مرحلة إرسال طلب إلى المنبع ، لكن "الفاصل الزمني" للتفاعل بين المكونات قصير جدًا - أقل من ثانية.

وبالتالي، في المجموع مع التوجيه المذكور بالفعل process_control_timeout يمكنك استخدام البناء التالي لـ lifecycle:

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

في هذه الحالة ، نعوض التأخير بالأمر sleep ونحن لا نزيد وقت النشر بشكل كبير: بعد كل شيء ، هل هناك فرق ملحوظ بين 30 ثانية وواحدة؟ .. في الواقع ، هو بالضبط process_control_timeoutو lifecycle تستخدم فقط "كشبكة أمان" في حالة التأخر.

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

يمارس. اختبار الحمل للتحقق من تشغيل الكبسولة

يعد اختبار الحمل إحدى الطرق للتحقق من كيفية عمل الحاوية ، حيث أن هذا الإجراء يقربها من ظروف القتال الحقيقية عند دخول المستخدمين إلى الموقع. لاختبار التوصيات المذكورة أعلاه ، يمكنك استخدام Yandex.Tankom: إنه يغطي جميع احتياجاتنا بشكل مثالي. فيما يلي بعض النصائح والحيل للاختبار بشكل واضح - بفضل مخططات Grafana و Yandex.Tank نفسها - مثال من تجربتنا.

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

فارق بسيط آخر هو مراقبة سجلات الحاوية أثناء إنهائها. هل تم تسجيل معلومات حول الإغلاق الرشيق هناك؟ هل توجد أي أخطاء في السجلات عند الوصول إلى موارد أخرى (على سبيل المثال ، حاوية PHP-FPM المجاورة)؟ أخطاء التطبيق نفسه (كما في حالة NGINX الموصوفة أعلاه)؟ آمل أن تساعدك المعلومات التمهيدية من هذه المقالة على فهم أفضل لما يحدث للحاوية أثناء إنهائها.

لذلك ، تم إجراء أول اختبار تشغيل بدون lifecycle وبدون توجيهات إضافية لخادم التطبيق (process_control_timeout في PHP-FPM). كان الغرض من هذا الاختبار هو تحديد العدد التقريبي للأخطاء (وما إذا كان هناك أي خطأ على الإطلاق). أيضًا ، من المعلومات الإضافية ، يجب أن تعلم أن متوسط ​​وقت النشر لكل جراب كان حوالي 5-10 ثوانٍ إلى حالة الاستعداد الكامل. النتائج هي:

نصائح وحيل Kubernetes: ميزات إغلاق رشيقة في NGINX و PHP-FPM

تُظهر لوحة معلومات Yandex.Tank ارتفاعًا في 502 خطأ حدث وقت النشر واستمر حتى 5 ثوانٍ في المتوسط. من المفترض أن هذا أدى إلى قطع الطلبات الحالية إلى الكبسولة القديمة عندما تم إنهاؤها. بعد ذلك ، ظهر 503 أخطاء ، والتي كانت نتيجة إيقاف حاوية NGINX ، والتي أسقطت أيضًا الاتصالات بسبب الواجهة الخلفية (بسبب عدم تمكن Ingress من الاتصال بها).

دعونا نرى كيف process_control_timeout في PHP-FPM سيساعدنا على انتظار انتهاء العمليات الفرعية ، أي تصحيح مثل هذه الأخطاء. أعد النشر بالفعل باستخدام هذا التوجيه:

نصائح وحيل Kubernetes: ميزات إغلاق رشيقة في NGINX و PHP-FPM

لا يوجد أكثر من 500 خطأ أثناء النشر! النشر ناجح ، يعمل إيقاف تشغيل رشيقة.

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

اختتام

لإنهاء العملية بأمان ، نتوقع السلوك التالي من التطبيق:

  1. انتظر بضع ثوان ، ثم توقف عن قبول الاتصالات الجديدة.
  2. انتظر حتى تكتمل جميع الطلبات وتغلق جميع اتصالات الاحتفاظ المستمرة التي لا تلبي الطلبات.
  3. قم بإنهاء العملية الخاصة بك.

ومع ذلك ، لا يمكن لجميع التطبيقات العمل بهذه الطريقة. أحد الحلول للمشكلة في واقع Kubernetes هو:

  • إضافة خطاف ما قبل التوقف الذي سينتظر بضع ثوان ؛
  • فحص ملف التكوين للخلفية الخاصة بنا للمعلمات المناسبة.

يوضح مثال NGINX أنه حتى التطبيق الذي يجب أن يعالج إشارات الإنهاء بشكل صحيح في البداية قد لا يقوم بذلك ، لذلك من الأهمية بمكان التحقق من أخطاء 500 أثناء نشر التطبيق. كما يسمح لك بالنظر إلى المشكلة على نطاق أوسع وعدم التركيز على حجرة أو حاوية منفصلة ، ولكن انظر إلى البنية التحتية بأكملها ككل.

كأداة اختبار ، يمكنك استخدام Yandex.Tank جنبًا إلى جنب مع أي نظام مراقبة (في حالتنا ، أخذنا بيانات من Grafana مع واجهة خلفية في شكل Prometheus للاختبار). تظهر مشاكل الإغلاق السلس بوضوح في ظل الأحمال الثقيلة التي يمكن أن يولدها المعيار ، وتساعد المراقبة في تحليل الموقف بمزيد من التفصيل أثناء الاختبار أو بعده.

الرد على التعليقات على المقال: من الجدير بالذكر أن المشاكل وطرق حلها موصوفة هنا فيما يتعلق بـ NGINX Ingress. بالنسبة للحالات الأخرى ، هناك حلول أخرى ، والتي ، ربما ، سننظر فيها في المواد التالية من الدورة.

PS

آخرون من سلسلة النصائح والحيل K8s:

المصدر: www.habr.com

إضافة تعليق