3 คะแนน โดย GN⁺ 2025-05-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การปิดระบบอย่างนุ่มนวล (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 ความคิดเห็น

 
GN⁺ 2025-05-06
ความคิดเห็นบน Hacker News
  • ใน Kubernetes บางครั้งการอัปเดต IP เป้าหมายของ load balancer ใช้เวลานาน ปัญหา 90% คือการยืนยันว่าทราฟฟิกถูก drain ออกจริงหรือไม่

    • การเพิ่มเวลารอ 15 วินาทีใน preStop hook แบบ global ช่วยปรับปรุงอัตรา HTTP 503 ได้อย่างมาก
    • ช่วยสร้างช่วงเวลาระหว่างการยกเลิกการลงทะเบียนจาก load balancer กับการส่ง SIGTERM ทำให้การจัดการฝั่งแอปพลิเคชันง่ายขึ้น
  • เมื่อใช้ log.Fatal เนื้อหาภายใน defer จะไม่ถูกรัน

    • log.Fatal เรียก os.Exit ทำให้โปรแกรมจบทันที
    • หากใช้ panic เนื้อหาใน defer จะถูกรัน
  • เมื่อ Prometheus ทำการ scrape เอ็นด์พอยต์ /metrics เป็นระยะ เมตริกที่ถูกบันทึกไว้ระหว่างการ scrape ครั้งสุดท้ายกับการปิดโปรเซสอาจไม่ถูกส่งต่อออกไป

    • อาจสูญเสียล็อกในช่วงไม่กี่วินาทีสุดท้ายตอนปิดบริการ
    • อาจเกิด race condition ได้เมื่อไฟล์ล็อกถูก sidecar process คอยเฝ้าดูอยู่
  • หากระบบแบบกระจายพึ่งพาการปิดตัวอย่างถูกต้องของไคลเอนต์ ระบบอาจพังอย่างรุนแรงได้

  • ยังอธิบายไม่พอเกี่ยวกับวิธีรีสตาร์ตแอปพลิเคชันโดยไม่ตัดการเชื่อมต่อ เมื่อ service instance ใหม่รับ socket มาจาก instance เดิม

    • ใน systemd การทำสิ่งนี้ค่อนข้างง่าย
    • nginx รองรับสิ่งนี้มานานกว่า 20 ปีแล้ว
    • Kubernetes และ Docker ไม่รองรับสิ่งนี้
  • ยังขาดการพูดถึง liveness

    • เคยเห็นหลายแอปที่ใช้เอ็นด์พอยต์เดียวกันสำหรับ liveness/readiness
  • หากโปรแกรมจัดการคำสั่งอย่าง ctrl c ได้ไม่สะอาด ก็ถือว่าเขียนมาไม่ดี

  • Elixir ออกแบบโปรเซสให้เป็นโปรเซส VM ขนาดเล็ก จึงไม่จำเป็นต้องตั้งใจสร้าง routine สำหรับ graceful shutdown

  • มีการทำไลบรารีเล็ก ๆ ขึ้นมาในโปรเจกต์เพื่อจัดการ graceful shutdown

    • มี API สำหรับรวมบริการที่มีกลไกการเริ่มต้นและการปิดตัวหลากหลายรูปแบบเข้าด้วยกัน
  • หลังอัปเดต readiness probe ควรรอสักไม่กี่วินาทีเพื่อให้ระบบหยุดส่งคำขอใหม่เข้ามา

    • pod ที่กำลังปิดตัวจะไม่อยู่ในสถานะพร้อมใช้งาน
    • service จะทำเครื่องหมายเอ็นด์พอยต์ว่าอยู่ระหว่างปิดตัว
    • แม้หลัง SIGTERM ยังอาจมีหน้าต่างเวลาสั้น ๆ อยู่ แต่ไม่ใช่ปัญหาใหญ่
    • สิ่งสำคัญคือไม่รับการเชื่อมต่อใหม่ และปิดการเชื่อมต่อเดิมอย่างถูกต้อง