معظم خدمات Go على ECS تتعامل مع SIGTERM بشكل خاطئ. إليك ما يحدث فعلياً أثناء النشر المتدرج وكيفية الإيقاف الآمن دون فقدان الطلبات قيد التنفيذ.
معظم حوادث الإنتاج أثناء عمليات النشر لا تنتج عن أخطاء في الكود الجديد، بل تنتج عن الإيقاف غير الصحيح للنسخة القديمة من الخدمة. عملية النشر المتدرج على ECS ترسل إشارة SIGTERM للحاويات الجارية، تنتظر عدداً من الثواني، ثم ترسل SIGKILL. كل ما يحدث في هذه النافذة الزمنية يحدد ما إذا كان مستخدموك سيواجهون أخطاء أم لا.
ما الذي يحدث فعلياً عند إيقاف المهمة في ECS
عندما يقرر ECS إيقاف مهمة، سواء كان ذلك بسبب نشر متدرج أو تحديث خدمة أو حدث توسع، فإنه يرسل إشارة SIGTERM إلى PID 1 داخل الحاوية، ثم ينتظر خروج الحاوية من تلقاء نفسها. إذا كانت الحاوية لا تزال تعمل بعد قيمة stopTimeout (الافتراضية 30 ثانية، وقابلة للضبط حتى 120 ثانية)، يرسل ECS إشارة SIGKILL.
SIGKILL لا يمكن التقاطها. تنهي العملية فوراً. أي طلبات HTTP قيد التنفيذ تموت دون استجابة. أي معاملات قاعدة بيانات قيد التنفيذ تُتخلى عنها.
هدف الإيقاف الآمن هو التقاط SIGTERM في تطبيقك والاستفادة من تلك الثواني الثلاثين.
هيكل معالج الإشارة الأساسي في Go
كل خدمة HTTP في Go يجب أن تحتوي على هذا الهيكل الأساسي:
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
go func() {
srv.ListenAndServe()
}()
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
srv.Shutdown(ctx)
http.Server.Shutdown يوقف قبول الاتصالات الجديدة وينتظر الاتصالات النشطة حتى تنتهي. مهلة 25 ثانية تمنح الطلبات قيد التنفيذ وقتاً للاكتمال مع البقاء ضمن نافذة ECS البالغة 30 ثانية.
ما الذي يتعطل في خدمات الإنتاج الفعلية
العمال في الخلفية الجارية في goroutines. إذا كان لديك goroutines تعالج عناصر قائمة الانتظار أو تشغل مهام دورية، فإن srv.Shutdown() لا تعرف عنها شيئاً. تستمر في العمل بعد إغلاق خادم HTTP، ثم تصلها SIGKILL في منتصف التنفيذ.
مجمعات اتصالات قواعد البيانات. sql.DB لا يُغلق تلقائياً عند إيقاف الخادم. إذا أغلقت المجمع قبل انتهاء الطلبات، ستفشل المعالجات التي تحاول الحصول على اتصالات.
المخازن المؤقتة والملفات المفتوحة. أنظمة التسجيل التي تكتب على دفعات تفقد آخر دفعة عند الإنهاء المفاجئ، وهي غالباً الأسطر الأكثر فائدة لفهم ما كان يحدث.
بناء منسق للإيقاف
النمط الذي نستخدمه في خلفيات SaaS بلغة Go هو منسق إيقاف يدير جميع إيقافات الأنظمة الفرعية بالترتيب الصحيح:
- أولاً: أوقف قبول طلبات HTTP الجديدة
- ثانياً: أشر للعمال في الخلفية بالانتهاء من الدفعة الحالية
- ثالثاً: أغلق مجمع قاعدة البيانات
- رابعاً: افرغ السجلات
الترتيب مهم. تُوقف طبقة HTTP أولاً حتى لا يدخل عمل جديد للنظام. تنتظر العمال في الخلفية لإنهاء وحدة عملهم الحالية. ثم تغلق الاتصالات الخارجية. ثم تفرغ IO.
ضبط ECS للإيقاف الآمن
في تعريف مهمة ECS، اضبط stopTimeout بشكل صريح. الافتراضي هو 30 ثانية، وهو في الغالب غير كافٍ للخدمات التي تعالج مهام ثقيلة. يمكنك الوصول إلى 120 ثانية:
{
"containerDefinitions": [
{
"name": "api",
"stopTimeout": 60
}
]
}
إذا كنت تستخدم AWS Application Load Balancer، اضبط تأخير إلغاء تسجيل مجموعة الهدف. عندما يلغي ECS تسجيل هدف قبل إرسال SIGTERM، يحتاج موزع الحمل إلى لحظة لوقف إرسال الطلبات الجديدة لتلك الحاوية.
اختبار سلوك الإيقاف
هذا هو الجزء الذي يتخطاه معظم الفرق. منطق الإيقاف لا يُختبر إلا أثناء النشر أو الأعطال. إيقاف معطوب يُسقط الطلبات بصمت في الإنتاج ولا أحد يحقق حتى يصبح معدل الخطأ واضحاً.
نختبره هكذا:
- تشغيل الخدمة
- إرسال تدفق من الطلبات التي تستغرق 3 إلى 5 ثواني لكل منها
- إرسال SIGTERM أثناء تنفيذ الطلبات
- التحقق من أن جميع الطلبات قيد التنفيذ تكتمل باستجابات صحيحة
- التحقق من خروج العملية ضمن النافذة المتوقعة
- التحقق من عدم تسرب اتصالات قاعدة البيانات
الدروس الرئيسية من الإنتاج
أوقف قبول حركة المرور قبل إيقاف العمل. أشر للعمال بقناة وليس بإشارة إيقاف. اضبط stopTimeout في ECS بشكل صريح. اختبر الإيقاف مع تشغيل الحمل وليس بشكل منفصل. افرغ السجلات وأغلق الاتصالات الخارجية بالترتيب الصحيح.


