- การปิดระบบอย่างนุ่มนวล (graceful shutdown) คือ กระบวนการที่แอปพลิเคชันเมื่อได้รับสัญญาณให้หยุดทำงานแล้วจะบล็อกคำขอใหม่ รอให้คำขอที่กำลังประมวลผลเสร็จสิ้น และจัดการล้างทรัพยากร
- ใน Go สามารถใช้แพ็กเกจ
os/signal เพื่อ จัดการสัญญาณยุติการทำงานอย่าง SIGINT, SIGTERM ได้โดยตรง และยังใช้ signal.NotifyContext เพื่อ ควบคุมการปิดระบบบนพื้นฐานของ context ได้ด้วย
- เมื่อต้องปิด HTTP server ควร กันทราฟฟิกด้วยการทำให้ readiness probe ล้มเหลวก่อน แล้วค่อยรอสักไม่กี่วินาทีก่อนเรียก
Server.Shutdown() เพื่อให้การปิดระบบมีเสถียรภาพมากขึ้น
- แฮนด์เลอร์ทั้งหมดควรตรวจจับสัญญาณการยุติจาก context และสามารถหยุดได้ โดยสามารถจัดการแบบรวมศูนย์ผ่าน
BaseContext หรือ middleware
- หลังได้รับสัญญาณยุติการทำงานแล้ว ควร จัดการทรัพยากรภายนอกอย่างฐานข้อมูล message broker แคช ฯลฯ อย่างตั้งใจ และหากลงทะเบียนด้วย
defer จะช่วยให้จัดการลำดับการปิดได้ง่าย
Graceful Shutdown คืออะไร?
- การปิดระบบอย่างนุ่มนวลคือกระบวนการที่เมื่อแอปพลิเคชันจะหยุดทำงาน จะมีการ บล็อกคำขอใหม่, รอให้คำขอที่กำลังดำเนินอยู่เสร็จสิ้น, และ ล้างทรัพยากร
- บทความนี้เน้น HTTP server และสภาพแวดล้อมแบบคอนเทนเนอร์เป็นหลัก แต่เป็น แนวคิดที่ประยุกต์ใช้ได้กับทุกแอปพลิเคชัน
1. การจัดการสัญญาณยุติการทำงาน
- ในระบบตระกูล Unix จะใช้ SIGTERM, SIGINT, SIGHUP เป็นต้น เป็นสัญญาณยุติการทำงาน
- Go runtime จะปิดแอปพลิเคชันโดยอัตโนมัติเมื่อได้รับ
SIGTERM, SIGINT แต่ก็สามารถจัดการเองได้ด้วย os/signal.Notify
- การใช้ channel แบบ buffered (ความจุ 1) ช่วยป้องกันการสูญหายของสัญญาณระหว่างการเริ่มต้นระบบได้
- ตั้งแต่ Go 1.16 เป็นต้นมา สามารถใช้
signal.NotifyContext เพื่อให้การควบคุมสัญญาณบนพื้นฐานของ context ทำได้สะดวกขึ้น
2. การรับรู้เวลาปิดระบบ
- ใน Kubernetes โดยปกติจะมี ช่วงเวลาผ่อนผันก่อนยุติการทำงาน 30 วินาที (
terminationGracePeriodSeconds)
- เพื่อให้ปิดระบบได้อย่างปลอดภัย ควร เผื่อเวลาไว้ 20% และทำงานปิดระบบให้เสร็จภายใน 25 วินาที
3. หยุดรับคำขอใหม่
http.Server.Shutdown() จะ บล็อกการเชื่อมต่อใหม่และรอจนกว่าคำขอเดิมจะเสร็จสิ้น
- ในสภาพแวดล้อม Kubernetes ควรทำให้ readiness probe ล้มเหลวก่อนเพื่อ หยุดทราฟฟิกขาเข้า แล้วรอสักครู่ก่อนทำ shutdown
- ใน readiness handler สามารถใช้ตัวแปรส่วนกลางเพื่อตรวจสอบสถานะการปิดระบบและตั้งค่าให้ คืนค่า HTTP 503 ได้
4. ปิดงานประมวลผลคำขอให้เรียบร้อย
- จำเป็นต้องตั้งค่า timeout ที่เหมาะสม ให้กับ context สำหรับการปิดระบบ (
context.WithTimeout)
- หาก shutdown context หมดเวลา การเชื่อมต่อที่เหลือจะถูกปิดแบบบังคับ
- แฮนด์เลอร์ทั้งหมดควรถูกออกแบบให้ใช้
context.Context เพื่อ ตรวจจับสัญญาณยุติการทำงานและหยุดได้
- เพื่อจุดประสงค์นี้ สามารถฉีด shutdown context ให้กับทุกคำขอผ่าน middleware หรือ
BaseContext ได้
5. การล้างทรัพยากร
- หากปิดทรัพยากรทันทีที่ได้รับสัญญาณยุติการทำงาน อาจ ทำให้เกิดปัญหากับแฮนด์เลอร์ที่กำลังประมวลผลอยู่
- ควร ล้างการเชื่อมต่อฐานข้อมูล message broker แคช ฯลฯ หลังจาก shutdown เสร็จสิ้นแล้ว
- หากใช้
defer ของ Go จะสามารถ เรียกใช้รูทีนปิดระบบในลำดับย้อนกลับจากการเริ่มต้น ทำให้จัดการ dependency ได้ง่าย
- นอกจากทรัพยากรอย่างหน่วยความจำและ file descriptor ที่ OS จัดการให้อัตโนมัติแล้ว ยังมี ทรัพยากรที่ต้องปิดอย่างชัดเจน เช่น การ flush ข้อมูล หรือการ rollback ธุรกรรม
สรุปตัวอย่างทั้งหมด
- รับสัญญาณยุติการทำงานด้วย
signal.NotifyContext
- สร้าง readiness endpoint
/healthz
- ฉีด shutdown context ให้ทุกคำขอด้วย
BaseContext
- รอ readiness 5 วินาทีก่อนทำ shutdown
- มี fallback สำหรับการปิดแบบบังคับหากเรียก
server.Shutdown ไม่สำเร็จ
เอกสารอ้างอิงและทรัพยากรที่เกี่ยวข้อง
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ใน Kubernetes บางครั้งการอัปเดต IP เป้าหมายของ load balancer ใช้เวลานาน ปัญหา 90% คือการยืนยันว่าทราฟฟิกถูก drain ออกจริงหรือไม่
เมื่อใช้
log.Fatalเนื้อหาภายในdeferจะไม่ถูกรันlog.Fatalเรียกos.Exitทำให้โปรแกรมจบทันทีpanicเนื้อหาในdeferจะถูกรันเมื่อ Prometheus ทำการ scrape เอ็นด์พอยต์
/metricsเป็นระยะ เมตริกที่ถูกบันทึกไว้ระหว่างการ scrape ครั้งสุดท้ายกับการปิดโปรเซสอาจไม่ถูกส่งต่อออกไปหากระบบแบบกระจายพึ่งพาการปิดตัวอย่างถูกต้องของไคลเอนต์ ระบบอาจพังอย่างรุนแรงได้
ยังอธิบายไม่พอเกี่ยวกับวิธีรีสตาร์ตแอปพลิเคชันโดยไม่ตัดการเชื่อมต่อ เมื่อ service instance ใหม่รับ socket มาจาก instance เดิม
ยังขาดการพูดถึง liveness
หากโปรแกรมจัดการคำสั่งอย่าง ctrl c ได้ไม่สะอาด ก็ถือว่าเขียนมาไม่ดี
Elixir ออกแบบโปรเซสให้เป็นโปรเซส VM ขนาดเล็ก จึงไม่จำเป็นต้องตั้งใจสร้าง routine สำหรับ graceful shutdown
มีการทำไลบรารีเล็ก ๆ ขึ้นมาในโปรเจกต์เพื่อจัดการ graceful shutdown
หลังอัปเดต readiness probe ควรรอสักไม่กี่วินาทีเพื่อให้ระบบหยุดส่งคำขอใหม่เข้ามา