แพตเทิร์นเชิงปฏิบัติสำหรับการทำ Graceful Shutdown ในภาษา Go
(victoriametrics.com)- Graceful Shutdown ของแอปพลิเคชัน Go คือขั้นตอนการปิดระบบที่หยุดรับคำขอใหม่ รอให้งานที่กำลังดำเนินอยู่เสร็จ แล้วจัดการทรัพยากร เช่น การเชื่อมต่อฐานข้อมูล file lock และ network listener
- การจัดการการปิดระบบเริ่มจากการรับ สัญญาณปิดระบบ เช่น
SIGTERMและSIGINTผ่านos/signalหรือsignal.NotifyContextใน Go 1.16 ขึ้นไป เพื่อแทนที่พฤติกรรมเริ่มต้นที่ปิดทันที - ใน Kubernetes ต้องปิดระบบให้เสร็จภายใน grace period เริ่มต้น 30 วินาที และควรเผื่อเวลาให้สถานะหยุดรับทราฟฟิกถูกส่งต่อไปถึง load balancer ภายนอกด้วยการหน่วง
preStopหรือทำให้ readiness probe ล้มเหลว http.Server.Shutdownจะหยุดรับการเชื่อมต่อใหม่และรอให้คำขอที่ยัง active เสร็จ แต่ถ้า handler ไม่ทำตาม context cancellation อาจเกิดปัญหา เช่น การเขียนข้อมูลบางส่วน ข้อมูลสูญหาย หรือ transaction ที่ยังเปิดอยู่- ทรัพยากรสำคัญควรถูกจัดการหลังคำขอจบแล้วหรือหลังหมดเวลาที่กำหนด ไม่ใช่ทันทีหลังได้รับสัญญาณปิดระบบ และการปิดตาม ลำดับย้อนกลับของการเริ่มต้น จะช่วยรักษา dependency ระหว่างคอมโพเนนต์ได้ง่ายขึ้น
เงื่อนไขขั้นต่ำของ Graceful Shutdown
- โดยทั่วไป Graceful Shutdown ควรเป็นไปตามเงื่อนไขสามข้อ
- ไม่รับคำขอหรือข้อความใหม่เพิ่มจาก จุดรับเข้า เช่น HTTP หรือ pub/sub
- รอให้คำขอที่กำลังดำเนินอยู่เสร็จ และถ้าใช้เวลานานเกินไปให้ตอบกลับด้วย graceful error
- ปลดปล่อย ทรัพยากรสำคัญ เช่น การเชื่อมต่อฐานข้อมูล file lock และ network listener พร้อมทำการเก็บกวาดขั้นสุดท้าย
- การเชื่อมต่อฐานข้อมูลหรือแคชที่ออกไปยังบริการภายนอก จะไม่ถูกตัดทันทีในขั้นตอนหยุดรับคำขอใหม่
- แม้เนื้อหาจะเน้น HTTP server และแอปพลิเคชันในคอนเทนเนอร์ แต่หลักการสำคัญสามารถใช้กับแอปพลิเคชันอื่นได้เช่นกัน
การจัดการสัญญาณปิดระบบ
- ในระบบตระกูล Unix signal คือ software interrupt ที่แจ้งให้โปรเซสทราบว่ามีสถานการณ์บางอย่างเกิดขึ้น
- โปรเซสสามารถลงทะเบียน handler สำหรับ signal เฉพาะได้ และหากไม่มี handler ก็จะทำตามพฤติกรรมเริ่มต้น
- พฤติกรรมเริ่มต้นอาจเป็นการปิด หยุด ทำงานต่อ หรือเพิกเฉย
- signal บางตัว เช่น
SIGKILLไม่สามารถดักจับหรือเพิกเฉยได้ และจะปิดโปรเซส
- Go runtime จะลงทะเบียน handler สำหรับ signal หลายตัวโดยอัตโนมัติ ตั้งแต่ก่อนเรียกใช้ฟังก์ชัน
mainเช่นSIGTERM,SIGQUIT,SIGILL,SIGTRAP - ใน Graceful Shutdown มี signal สำหรับปิดระบบที่สำคัญหลัก ๆ สามตัว
SIGTERM: วิธีมาตรฐานและสุภาพในการขอให้โปรเซสปิดระบบ และเป็น signal ที่ Kubernetes ส่งให้แอปพลิเคชันก่อนบังคับปิดSIGINT: ถูกส่งเมื่อผู้ใช้พยายามหยุดโปรเซสในเทอร์มินัลด้วยCtrl+CSIGHUP: เดิมใช้สำหรับแจ้งการตัดการเชื่อมต่อเทอร์มินัล และปัจจุบันมักใช้เป็นสัญญาณให้ reload configuration ด้วย
- หากได้รับ
SIGTERM,SIGINT,SIGHUPโดยไม่มีการจัดการเพิ่มเติม Go runtime จะปิดแอปพลิเคชัน
os/signal และ NotifyContext
signal.Notifyสั่งให้ Go runtime ส่ง signal ที่กำหนดผ่าน channel แทนพฤติกรรมเริ่มต้น- ควรสร้าง channel สำหรับ signal โดยมี buffer size 1 เพื่อความเสถียร
- ภายใน Go ใช้
selectและdefaultในการส่งเข้า channel - หากมีพื้นที่ใน buffer signal จะถูกส่งต่อ แต่ถ้า buffer เต็ม signal จะถูกทิ้ง
- หากใช้ channel ที่ไม่มี buffer และไม่มี goroutine กำลังรอรับอยู่ ก็อาจพลาด signal ได้
- ภายใน Go ใช้
- สามารถเรียก
signal.Notifyหลายครั้งสำหรับ signal เดียวกันได้ และ Go จะส่ง signal นั้นไปยัง channel ที่ลงทะเบียนไว้ทั้งหมด - แม้กด
Ctrl+Cหลายครั้ง โดยทั่วไปครั้งที่สองจะไม่ถูกเลื่อนเป็นSIGKILLโดยอัตโนมัติ- shell ส่วนใหญ่ของ bash หรือ Linux ไม่ได้เลื่อนระดับให้อัตโนมัติ
- การบังคับปิดต้องส่ง
SIGKILLโดยตรงด้วยkill -9
- หากต้องการให้การกด
Ctrl+Cครั้งที่สองในระหว่างพัฒนา local เป็นการบังคับปิด สามารถเรียกsignal.Stopทันทีหลังได้รับ signal แรก เพื่อหยุดรับ signal เพิ่มเติม - ตั้งแต่ Go 1.16 สามารถใช้
signal.NotifyContextเพื่อเชื่อมการจัดการ signal กับ context cancellation ได้- หลัง
ctx.Done()ยังต้องเรียกstop()เพื่อให้Ctrl+Cครั้งที่สองสามารถบังคับปิดแอปพลิเคชันได้
- หลัง
เวลาจำกัดในการปิดระบบและพฤติกรรมของ Kubernetes
- หลังได้รับสัญญาณปิดระบบ ต้องทราบก่อนว่าแอปพลิเคชันมีเวลาปิดระบบที่ใช้ได้จริงเท่าใด
- grace period เริ่มต้นของ Kubernetes คือ 30 วินาที หากไม่ได้กำหนด
terminationGracePeriodSecondsแยกต่างหาก - เมื่อเวลานี้ผ่านไป Kubernetes จะส่ง
SIGKILLเพื่อบังคับหยุดแอปพลิเคชันSIGKILLไม่สามารถดักจับหรือจัดการได้
- logic การปิดระบบทั้งหมด รวมทั้งการประมวลผลคำขอที่เหลือและการปลดปล่อยทรัพยากร ต้องเสร็จภายในเวลานี้
- หากยึดค่าเริ่มต้น 30 วินาทีและเผื่อ safety margin ประมาณ 20% ควรทำให้การปิดระบบทั้งหมดเสร็จภายใน 25 วินาที
การหยุดรับคำขอใหม่และการจัดการ readiness
- ใน
net/httpของ Go สามารถทำ Graceful Shutdown ด้วยhttp.Server.Shutdown- หยุดรับการเชื่อมต่อใหม่
- รอให้คำขอที่ active อยู่เสร็จ
- จากนั้นปิด idle connection
- คำขอที่กำลังดำเนินอยู่สามารถทำจนเสร็จได้ และหลังเสร็จการเชื่อมต่อนั้นจะกลายเป็น idle แล้วถูกปิด
- client ที่พยายามเชื่อมต่อใหม่ระหว่างปิดระบบมักได้รับข้อผิดพลาด
connection refusedเพราะ listener ถูกปิดไปแล้ว - ในสภาพแวดล้อมแบบคอนเทนเนอร์หรือ orchestration ที่มี load balancer ภายนอก สิ่งสำคัญคือไม่ควรหยุดรับคำขอใหม่ทันที
- หลัง pod ถูกทำเครื่องหมายเป็นเป้าหมายการปิดระบบแล้ว ก็ยังอาจได้รับทราฟฟิกอยู่ชั่วครู่
kube-proxyซึ่งเป็นคอมโพเนนต์ภายใน Kubernetes จะรับรู้ได้เร็วว่า state ของ pod เปลี่ยนเป็นTerminating- load balancer ภายนอกใช้ health check ของตัวเองโดยอิสระจาก Kubernetes จึงต้องใช้เวลาในการ propagate state
- มีสองวิธีในการรอให้การหยุดทราฟฟิกถูก propagate
- ใน hook
preStopให้sleepชั่วครู่ เพื่อให้ load balancer ภายนอกรับรู้สถานะการปิดของ pod- เวลาที่ใช้ใน
preStopนับรวมอยู่ในterminationGracePeriodSeconds
- เวลาที่ใช้ใน
- ในระดับโค้ด ให้ทำให้ readiness probe ล้มเหลวแล้วรอสักครู่
- ใช้ได้กับสภาพแวดล้อมอื่นที่ load balancer ต้องรับรู้สถานะ readiness ไม่ใช่เฉพาะ Kubernetes
- ใน hook
- readiness probe ตรวจสอบเป็นระยะว่าคอนเทนเนอร์พร้อมรับทราฟฟิกหรือไม่
- สามารถทำ health check ได้หลายแบบ เช่น HTTP request, TCP connection หรือการรันคำสั่ง
- เมื่อ probe ล้มเหลว Kubernetes จะนำ pod ออกจาก service endpoint เพื่อไม่ให้รับทราฟฟิก
- ตอนเตรียมปิดระบบ สามารถใช้
atomic.Boolเช่นisShuttingDownเพื่อให้/healthzส่ง HTTP 503 กลับมาได้ - หลังเปลี่ยน readiness state ให้ล้มเหลวแล้ว ต้องรอสักไม่กี่วินาทีเพื่อให้การเปลี่ยนแปลง propagate
- ตัวอย่างการตั้งค่าคือ
periodSeconds: 5และตัวอย่างในบทความใช้การ รอ 5 วินาที - เวลารอที่แน่นอนขึ้นอยู่กับการตั้งค่า readiness probe
- ตัวอย่างการตั้งค่าคือ
การจัดการคำขอที่กำลังดำเนินอยู่
- สร้างเวลาจำกัดด้วย
context.WithTimeoutให้สอดคล้องกับ shutdown budget แล้วส่งให้server.Shutdown(ctx) - มีสองกรณีที่
server.Shutdownจะ return- การเชื่อมต่อที่ active ทั้งหมดถูกปิด และ handler ทั้งหมดทำงานเสร็จแล้ว
- context ที่ส่งเข้าไปหมดอายุก่อน handler เสร็จ ทำให้ server เลิกรอ
- ไม่ว่ากรณีใด
Shutdownจะ return หลังจาก server หยุดประมวลผลคำขออย่างสมบูรณ์แล้ว - handler ควรทำงานเร็วและ context-aware
- ไม่เช่นนั้นเมื่อหมดเวลาจำกัด งานอาจถูกตัดกลางคัน
- อาจเกิดปัญหา เช่น การเขียนข้อมูลบางส่วน ข้อมูลสูญหาย state ไม่สอดคล้อง transaction ค้าง หรือข้อมูลเสียหาย
- วิธีหลักในการส่งสัญญาณปิดระบบไปยัง handler มีสองแบบ
- ใช้ middleware เพื่อ inject logic การยกเลิกเข้าไปใน context ของแต่ละคำขอ
- ใช้
BaseContextของhttp.Serverเพื่อให้ global context ที่แชร์กันกับทุก connection
- context ที่ปรับแต่งได้ใน HTTP server มี
BaseContextและConnContext- สำหรับ Graceful Shutdown นั้น
BaseContextเหมาะกว่า เพราะสามารถสร้าง global context แบบยกเลิกได้ที่มีผลทั้ง server
- สำหรับ Graceful Shutdown นั้น
- Graceful Shutdown จะได้ผลเมื่อฟังก์ชันต่าง ๆ เคารพการยกเลิก context
- ควรหลีกเลี่ยงการใช้ที่เพิกเฉยต่อการยกเลิก เช่น
context.Background(),time.Sleep() - สามารถแทน
time.Sleep(duration)ได้ด้วยการใช้selectเพื่อรอทั้งtime.After(duration)และctx.Done()พร้อมกัน
- ควรหลีกเลี่ยงการใช้ที่เพิกเฉยต่อการยกเลิก เช่น
- ใน Go เวอร์ชันเก่า
time.Afterอาจทำให้ memory leak จนกว่า timer จะทำงาน- ปัญหานี้แก้แล้วใน Go 1.23 ขึ้นไป
- หากไม่แน่ใจเวอร์ชัน สามารถใช้
time.NewTimerกับStopและตรวจ<-t.Cเมื่อจำเป็น - issue ที่เกี่ยวข้อง: time: stop requiring Timer/Ticker.Stop for prompt GC
ความแตกต่างระหว่าง Shutdown และ Close
- หลักการเดียวกันใช้ได้ไม่ใช่แค่กับ HTTP server แต่รวมถึง service ของ third-party ด้วย
DB.Closeของdatabase/sqlจะปิดการเชื่อมต่อฐานข้อมูล หยุดการเริ่ม query ใหม่ และรอให้ query ที่กำลังดำเนินอยู่เสร็จ- แก่นสำคัญคือไม่รับคำขอหรือข้อความใหม่เพิ่ม และให้เวลางานเดิมจบภายใน grace period ที่กำหนด
server.Close()จะปิดทันทีโดยไม่รอ connection ที่กำลังดำเนินอยู่- handler ที่กำลังใช้ network จะได้รับข้อผิดพลาดตอนอ่านหรือเขียน
- client อาจได้รับข้อผิดพลาดการเชื่อมต่อทันที เช่น
ECONNRESETหรือsocket hang up - handler ที่ทำงานนานและไม่ได้โต้ตอบกับ network อาจยังทำงานต่อใน background ได้
- หลัง
server.Shutdown()return error แล้ว อาจใช้server.Close()ได้ แต่ขึ้นอยู่กับกลยุทธ์การปิดระบบ - การ propagate สัญญาณปิดระบบผ่าน context เป็นแนวทางที่เชื่อถือได้และ graceful กว่า
ลำดับการปลดปล่อยทรัพยากรสำคัญ
- ความผิดพลาดที่พบบ่อยคือการปลดปล่อยทรัพยากรสำคัญทันทีที่ได้รับสัญญาณปิดระบบ
- ณ จุดนี้ handler และ in-flight request อาจยังใช้ทรัพยากรเหล่านั้นอยู่ ดังนั้นควรเลื่อนการเก็บกวาดทรัพยากรไปหลัง shutdown timeout ผ่านไปหรือหลังคำขอทั้งหมดเสร็จ
- ในหลายกรณี การจบโปรเซสเพียงอย่างเดียวก็ทำให้ระบบปฏิบัติการ reclaim ทรัพยากรได้
- หน่วยความจำที่ Go จัดสรรจะถูกปล่อยเมื่อโปรเซสจบ
- file descriptor จะถูกระบบปฏิบัติการปิด
- ทรัพยากรระดับ OS เช่น process handle ก็จะถูก reclaim
- มีบางกรณีที่ต้องเก็บกวาดอย่าง explicit
- การเชื่อมต่อฐานข้อมูลควรถูกปิดอย่างถูกต้อง และ transaction ที่เปิดอยู่ต้อง commit หรือ rollback
- message queue และ broker อาจต้อง flush ข้อความ commit offset และแจ้งการปิด client
- บริการภายนอกอาจตรวจไม่พบการตัดการเชื่อมต่อทันที ดังนั้นการปิด connection ด้วยตนเองช่วยให้เก็บกวาดได้เร็วกว่าการรอ TCP timeout
- กฎที่ดีคือปิดคอมโพเนนต์ตาม ลำดับย้อนกลับของการเริ่มต้น
deferของ Go เหมาะกับแพตเทิร์นนี้ เพราะฟังก์ชันที่ลงทะเบียนไว้ล่าสุดจะถูกเรียกก่อน
- คอมโพเนนต์บางตัวต้องออกแบบ shutdown routine แยกต่างหาก เช่น กรณีที่ต้องเขียนข้อมูลใน memory cache ลงดิสก์
ลำดับการทำงานของตัวอย่างทั้งหมด
- ตัวอย่างทั้งหมดสร้าง root context ที่รับ
SIGINTและSIGTERMด้วยsignal.NotifyContext - endpoint
/healthzจะส่ง HTTP 503 และShutting downกลับมาหากisShuttingDownเป็น true มิฉะนั้นจะส่งOK - handler ตัวอย่างจะส่ง
Hello, world!หลังผ่านไป 2 วินาที หรือหาก context ของคำขอถูกยกเลิก จะตอบกลับเป็น HTTP request timeout - ใน
BaseContextเชื่อมongoingCtxเพื่อไม่ให้ in-flight request ถูกยกเลิกทันทีหลังSIGTERM - เมื่อได้รับสัญญาณปิดระบบ จะดำเนินตามลำดับต่อไปนี้
- เรียก
stop()เพื่ออนุญาตให้มีการจัดการเริ่มต้นเพิ่มเติม - ใช้
isShuttingDown.Store(true)เพื่อสร้างสถานะ readiness ที่ล้มเหลว - รอการ propagate ของ readiness check เป็นเวลา 5 วินาที ซึ่งคือ
_readinessDrainDelay - เรียก
server.Shutdownด้วยเวลาจำกัด 15 วินาที ซึ่งคือ_shutdownPeriod - ใช้
stopOngoingGracefully()เพื่อยกเลิก context ที่กำลังดำเนินอยู่ - หาก
Shutdownล้มเหลว ให้มีเวลารอการบังคับยกเลิก 3 วินาที ซึ่งคือ_shutdownHardPeriod
- เรียก
1 ความคิดเห็น
ความเห็นจาก Hacker News
เคยเจอกรณีที่ Kubernetes ใช้เวลานานกว่าที่คิดในการอัปเดต target IP ของ load balancer ในบางคอนฟิก สำหรับผม 90% ของ graceful shutdown คือการทำให้แน่ใจว่าทราฟฟิกถูก drain ออกจริงก่อนที่พ็อดจะปิดตัว
พอใส่ sleep 15 วินาที ไว้ใน global
preStophook อัตรา HTTP 503 ก็ลดลงมาก และยังช่วยถ่วงเวลาไว้ระหว่างที่เริ่ม deregister ออกจาก load balancer กับตอนที่SIGTERMถูกส่งมาถึงแอป ทำให้ฝั่งแอปจัดการได้ง่ายขึ้นเยอะpreStopsleep เป็นเหมือนทางแก้มหัศจรรย์ในการรักษา SLO ระหว่าง rolling deployment คุณภาพสูงผมคิดว่า Kubernetes น่าจะปรับปรุงได้ 2 จุด คือควรถอดพ็อดออกจาก Endpoints ก่อนเริ่มลำดับการปิด และควรมีตัวเลือก termination delay คล้าย termination grace อีกอย่าง PDB ก็ควรมีตัวเลือกให้ยอมสร้างใหม่ก่อน eviction
ถ้าใช้ Prometheus
/metricsendpoint แบบทั่วไปที่ถูก scrape ทุก N วินาที จะมีช่วงหนึ่งที่ metric ที่ถูกบันทึกไว้ระหว่างการ scrape ครั้งสุดท้ายกับตอนที่โปรเซสปิดจริงไม่ถูกส่งต่อออกไป ทำให้เกิด ภาพลวงตา ว่าระหว่างลำดับการปิดไม่มีข้อผิดพลาดถ้าไม่ระวัง คุณอาจทำ log ช่วงไม่กี่วินาทีก่อนบริการหยุดหายไปด้วย เช่น ถ้ามี sidecar อย่าง Promtail หรือ Vector คอยเฝ้าไฟล์ log แล้ว service ไป truncate path เดิมตอนเริ่มต้นก่อนเขียนใหม่ ก็จะเกิด race condition ที่ทำให้ log ระหว่างการปิดหายไป
ทั้งที่ลงแรงไปขนาดนั้น ข้อมูลส่วนใหญ่กลับไม่เคยถูกใช้จริง และข้อมูลเชิงธุรกิจก็แทบไม่ได้ดีกว่าการ
sshเข้าเครื่องไปgrepไฟล์ log แบบบ้าน ๆ มากนัก ผมก็ไม่แน่ใจว่าความพยายามที่ทุ่มให้ ecosystem นี้ช่วยให้อัปไทม์ ประสิทธิภาพ หรือ usability ดีขึ้นอย่างมีนัยสำคัญจริงไหมมีแผนจะจัดการเรื่องอย่าง “log synchronization”, “รอจนกว่า ingress จะตาม liveness handler ทัน” ด้วย
https://github.com/utrack/caisson-go/blob/main/caiapp/caiapp...
https://github.com/utrack/caisson-go/tree/main/closer
เอกสารยังไม่พอและยังมีส่วนที่ขาดอยู่ แต่พอกลับจากวันหยุดก็วางแผนจะออกรีลีสแรก สุดท้ายแล้วมันจะกลายเป็น meta-platform และ reference platform library สำหรับจัดการโครงสร้างพื้นฐาน k8s/otel/grpc+http แบบทั่วไป
เพราะพฤติกรรมแบบนี้ บริการของเราจึงยังใช้ statsd อยู่ เพราะโมเดลแบบ push ไม่มีปัญหานี้
มีกับดักเล็ก ๆ ที่เจอบ่อย คือบางคนคิดว่าเรียก
log.Fatalแล้วdeferจะยังทำงาน ซึ่งจริง ๆ ไม่ทำlog.Fatal("fatal")เรียกos.Exitภายใน จึงจบทันทีและไม่รันdeferส่วนpanic("fatal")จะทำให้เห็นทั้งfatalและin deferถ้าระบบกระจายศูนย์จะทำงานได้ถูกต้องโดยต้องอาศัยสมมติฐานว่า client จะ ปิดตัวอย่างนุ่มนวล ได้เสมอ สักวันมันต้องพังหนักแน่
วิธีเดียวที่จะตรวจสอบได้ว่าระบบทนต่อ hard crash ขององค์ประกอบได้จริง คือทำให้ hard crash กลายเป็นเรื่องปกติที่เกิดขึ้นตลอด ขอสดุดีให้ chaos monkey
แอปที่ลงด้วย
sig intกับแอปที่ถูกkillตายนั้นต่างกันมาก เช่น blue-green migration ต้องอาศัยพฤติกรรมการปิดแบบนุ่มนวลคิดอีกทีก็อาจต้องทำเหมือนกัน เพราะนั่นอาจเป็นวิธีเดียวที่จะยืนยันได้ว่าสมมติฐานนั้นจริง คล้ายแนวทาง chaos monkey ของ Netflix เมื่อหลายปีก่อน
ผมนึกว่าจะพูดถึงวิธีให้ service instance ตัวใหม่รับช่วง listening socket จาก instance เดิม เพื่อรีสตาร์ตแอปโดยไม่ทำให้ connection ขาเข้าหลุดแม้แต่รายการเดียว
บน systemd ทำได้ค่อนข้างง่าย และ nginx ก็รองรับมานานกว่า 20 ปีแล้ว แต่น่าเสียดายที่ Kubernetes กับ Docker ไม่รองรับ เพราะถือว่าเรื่องนี้ให้ load balancer หรือ reverse proxy จัดการ
เพื่อนร่วมงานของผมพูดเสมอว่า ถ้าโปรแกรมจัดการ
ctrl cกับคำสั่งปิดบางอย่างได้ไม่เรียบร้อย มันคือ โปรแกรมที่เขียนมาไม่ดีคิดว่า Elixir จัดการเรื่องแบบนี้ได้ฉลาดมาก แม้จะยังมีประสบการณ์ไม่มากนัก แต่เพราะมันถูกออกแบบมาให้โปรเซส VM ขนาดเล็กเกิด panic, จบการทำงาน แล้วถูกสร้างขึ้นใหม่ได้ จึงดูเหมือนว่าความจำเป็นในการทำ graceful shutdown routine แบบตั้งใจจะลดลง
เพราะคุณสมบัติแบบนี้ถูกฝังอยู่ในสถาปัตยกรรมของแอปพลิเคชันอยู่แล้ว
ผมทำไลบรารีเล็ก ๆ ขึ้นมาเพื่อจัดการ graceful shutdown ในโปรเจกต์ของตัวเอง: https://github.com/eberkund/graceful
ปกติมักจะมีหลาย service ที่ต้องเริ่มทำงาน และแต่ละตัวก็มักมีวิธีเริ่มกับวิธีปิดที่ต่างกัน บางครั้งต้องสร้าง object ก่อน บางครั้งมี context ที่อยาก cancel และบางครั้งก็มีเมธอด
Stopที่ต้องเรียก มันถูกออกแบบมาเพื่อรวบทั้งหมดนี้ไว้ในที่เดียวด้วย API แบบเดียวกันhttps://pkg.go.dev/git.sr.ht/~mariusor/wrapper#example-Regis...
Pod ที่กำลังปิดการทำงานตามนิยามแล้วถือว่าไม่พร้อมใช้งานอยู่แล้ว และ service ก็จะแสดง endpoint ว่าอยู่ในสถานะ terminating และ not ready ด้วย เรื่องนี้เกิดขึ้นตอนเปลี่ยนไปเป็นสถานะ Terminating ดังนั้นจึงไม่จำเป็นต้องทำให้ readiness check ล้มเหลวโดยเจตนา
ผมไม่แน่ใจเรื่องลำดับระหว่าง
SIGTERMกับการอัปเดต object อย่างPod.statusหรือ endpoint slice หลังSIGTERMอาจยังมีช่วงสั้น ๆ ที่มี connection เข้ามาได้ แต่ไม่ใช่ช่วงยาวแบบ “จนกว่า readiness check จะล้มเหลว” อย่างที่บทความชวนให้เข้าใจ จากมุมมองของคนดูแลคลัสเตอร์ หน้าต่างเวลาสั้นนิดเดียวนั้นไม่ค่อยสำคัญนัก แค่ไม่รับ connection ใหม่ ปิด connection เดิมอย่างนุ่มนวล และจบการทำงานให้เร็วอย่างสมเหตุสมผลก็พอ แต่แอปที่ผมดูแลอยู่ครึ่งหนึ่งนั้นอยู่ในกลุ่มที่จัดการSIGTERMแต่ใช้เวลาปิดนาน หรือไม่ก็จัดการSIGTERMไม่ได้เลยแต่ก็ยังใช้เวลาปิดนานเรานำ Google Wire มาใช้ในบางโปรเจกต์ของ JustWatch แล้วมันเปลี่ยนเกมไปเลย ถึงจะเป็นเครื่องมือที่คนรู้จักน้อยกว่าที่คิด แต่ช่วยลด logic การปิดระบบแบบรก ๆ ใน Kubernetes ได้
Wire บังคับให้ทำ dependency injection อย่างเป็นระเบียบ ทำให้ตอนนี้ทุกอย่างปิดตัวตามลำดับที่กำหนดไว้ แทนที่จะเป็นลำดับที่คาดเดาไม่ได้
https://go.dev/blog/wire
https://github.com/google/wire