3 คะแนน โดย GN⁺ 2025-05-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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+C
    • SIGHUP: เดิมใช้สำหรับแจ้งการตัดการเชื่อมต่อเทอร์มินัล และปัจจุบันมักใช้เป็นสัญญาณให้ 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 ได้
  • สามารถเรียก 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
  • 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 จะได้ผลเมื่อฟังก์ชันต่าง ๆ เคารพการยกเลิก 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 ความคิดเห็น

 
GN⁺ 2025-05-06
ความเห็นจาก Hacker News
  • เคยเจอกรณีที่ Kubernetes ใช้เวลานานกว่าที่คิดในการอัปเดต target IP ของ load balancer ในบางคอนฟิก สำหรับผม 90% ของ graceful shutdown คือการทำให้แน่ใจว่าทราฟฟิกถูก drain ออกจริงก่อนที่พ็อดจะปิดตัว
    พอใส่ sleep 15 วินาที ไว้ใน global preStop hook อัตรา HTTP 503 ก็ลดลงมาก และยังช่วยถ่วงเวลาไว้ระหว่างที่เริ่ม deregister ออกจาก load balancer กับตอนที่ SIGTERM ถูกส่งมาถึงแอป ทำให้ฝั่งแอปจัดการได้ง่ายขึ้นเยอะ

    • เห็นด้วย preStop sleep เป็นเหมือนทางแก้มหัศจรรย์ในการรักษา SLO ระหว่าง rolling deployment คุณภาพสูง
      ผมคิดว่า Kubernetes น่าจะปรับปรุงได้ 2 จุด คือควรถอดพ็อดออกจาก Endpoints ก่อนเริ่มลำดับการปิด และควรมีตัวเลือก termination delay คล้าย termination grace อีกอย่าง PDB ก็ควรมีตัวเลือกให้ยอมสร้างใหม่ก่อน eviction
  • ถ้าใช้ Prometheus /metrics endpoint แบบทั่วไปที่ถูก scrape ทุก N วินาที จะมีช่วงหนึ่งที่ metric ที่ถูกบันทึกไว้ระหว่างการ scrape ครั้งสุดท้ายกับตอนที่โปรเซสปิดจริงไม่ถูกส่งต่อออกไป ทำให้เกิด ภาพลวงตา ว่าระหว่างลำดับการปิดไม่มีข้อผิดพลาด
    ถ้าไม่ระวัง คุณอาจทำ log ช่วงไม่กี่วินาทีก่อนบริการหยุดหายไปด้วย เช่น ถ้ามี sidecar อย่าง Promtail หรือ Vector คอยเฝ้าไฟล์ log แล้ว service ไป truncate path เดิมตอนเริ่มต้นก่อนเขียนใหม่ ก็จะเกิด race condition ที่ทำให้ log ระหว่างการปิดหายไป

    • observability stack ดูเกินจริงนิด ๆ log, metrics, traces ต่างก็มีฐานข้อมูลของตัวเอง sidecar ของตัวเอง visualization stack ของตัวเอง ส่วนไลบรารีรวมตามภาษาแต่ละภาษาก็ไม่เหมือนกัน แถม ค่าใช้จ่ายบนคลาวด์ ก็สูงมาก
      ทั้งที่ลงแรงไปขนาดนั้น ข้อมูลส่วนใหญ่กลับไม่เคยถูกใช้จริง และข้อมูลเชิงธุรกิจก็แทบไม่ได้ดีกว่าการ ssh เข้าเครื่องไป grep ไฟล์ log แบบบ้าน ๆ มากนัก ผมก็ไม่แน่ใจว่าความพยายามที่ทุ่มให้ ecosystem นี้ช่วยให้อัปไทม์ ประสิทธิภาพ หรือ usability ดีขึ้นอย่างมีนัยสำคัญจริงไหม
    • ตลอด 8 ปีที่ผ่านมาในการดูแลแอป Go ที่โหลดสูง ปัญหาที่เจอถูกจัดการในลักษณะนี้พอดีใน platform library ของเรา ทุกบริษัทเหมือนมีงานอดิเรกคือพัฒนาและปรับปรุงแพลตฟอร์มกับ rolling deployment
      มีแผนจะจัดการเรื่องอย่าง “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 แบบทั่วไป
    • ผมไม่เคยเข้าใจเลยว่าทำไม Prometheus กับเครื่องมือที่เกี่ยวข้องถึงใช้ pull model ทั้งที่ส่วนใหญ่ใช้ push model กัน
    • สงสัยว่ามีใครเคยเห็นทางออกที่สะดวกสำหรับปัญหานี้ไหม ถ้า scrape interval คือ 15 วินาที ก็ไม่สามารถรอ 30 วินาทีเพื่อบันทึก metric สองรอบได้
      เพราะพฤติกรรมแบบนี้ บริการของเราจึงยังใช้ statsd อยู่ เพราะโมเดลแบบ push ไม่มีปัญหานี้
  • มีกับดักเล็ก ๆ ที่เจอบ่อย คือบางคนคิดว่าเรียก log.Fatal แล้ว defer จะยังทำงาน ซึ่งจริง ๆ ไม่ทำ
    log.Fatal("fatal") เรียก os.Exit ภายใน จึงจบทันทีและไม่รัน defer ส่วน panic("fatal") จะทำให้เห็นทั้ง fatal และ in defer

  • ถ้าระบบกระจายศูนย์จะทำงานได้ถูกต้องโดยต้องอาศัยสมมติฐานว่า client จะ ปิดตัวอย่างนุ่มนวล ได้เสมอ สักวันมันต้องพังหนักแน่

    • ผมเชื่อเรื่องนี้มากจนแทบไม่คิดเรื่อง graceful shutdown ตอนออกแบบเลย แต่ละองค์ประกอบควรพังแบบ hard crash ได้อย่างปลอดภัย แม้จะเกิดขึ้นบ่อยก็ได้ และตราบใดที่สัดส่วนสำคัญของระบบยังทำงานตามที่ตั้งใจ ระบบโดยรวมก็ไม่ควรได้รับผลกระทบอย่างมีนัยสำคัญ
      วิธีเดียวที่จะตรวจสอบได้ว่าระบบทนต่อ hard crash ขององค์ประกอบได้จริง คือทำให้ hard crash กลายเป็นเรื่องปกติที่เกิดขึ้นตลอด ขอสดุดีให้ chaos monkey
    • graceful shutdown ที่มีไว้เพื่อให้เป็นมิตรกับ client หรือ workflow กับการที่ระบบจะทำงานได้ก็ต่อเมื่อ client ต้องพึ่งพามัน เป็นคนละเรื่องกันมาก
    • สมัยก่อนที่ยังเป็นเซิร์ฟเวอร์จริง เราใช้ STONITH เพื่อการนั้น: https://smcleod.net/2015/07/delayed-serial-stonith/
    • แม้ในสถานการณ์ที่กู้คืนได้ ก็ยังมีเหตุผลที่ดีที่จะไม่ทำให้การปิดตามปกติดูเหมือนการปิดแบบหายนะ
      แอปที่ลงด้วย 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 กับคำสั่งปิดบางอย่างได้ไม่เรียบร้อย มันคือ โปรแกรมที่เขียนมาไม่ดี

    • Ctrl-C ถูกจองไว้สำหรับคัดลอกไปคลิปบอร์ด การเอามาใช้เป็นคำสั่งหยุดโปรแกรมนั้นขัดกับสัญชาตญาณมาก และจะทำให้ผู้ใช้หงุดหงิด
  • คิดว่า Elixir จัดการเรื่องแบบนี้ได้ฉลาดมาก แม้จะยังมีประสบการณ์ไม่มากนัก แต่เพราะมันถูกออกแบบมาให้โปรเซส VM ขนาดเล็กเกิด panic, จบการทำงาน แล้วถูกสร้างขึ้นใหม่ได้ จึงดูเหมือนว่าความจำเป็นในการทำ graceful shutdown routine แบบตั้งใจจะลดลง
    เพราะคุณสมบัติแบบนี้ถูกฝังอยู่ในสถาปัตยกรรมของแอปพลิเคชันอยู่แล้ว

    • สงสัยว่านั่นทำให้ความจำเป็นของ graceful shutdown ที่ผู้เขียนพูดถึงหายไปได้อย่างไร
  • ผมทำไลบรารีเล็ก ๆ ขึ้นมาเพื่อจัดการ graceful shutdown ในโปรเจกต์ของตัวเอง: https://github.com/eberkund/graceful
    ปกติมักจะมีหลาย service ที่ต้องเริ่มทำงาน และแต่ละตัวก็มักมีวิธีเริ่มกับวิธีปิดที่ต่างกัน บางครั้งต้องสร้าง object ก่อน บางครั้งมี context ที่อยาก cancel และบางครั้งก็มีเมธอด Stop ที่ต้องเรียก มันถูกออกแบบมาเพื่อรวบทั้งหมดนี้ไว้ในที่เดียวด้วย API แบบเดียวกัน

    • ผมก็มีไอเดียเดียวกันเป๊ะ ๆ เหมือนกัน เพียงแต่ API ของผมดูจะไม่ elegant เท่าไหร่ น่าจะเพราะเปิดให้ผู้เรียกกำหนดหลาย signal กับวิธีจัดการได้
      https://pkg.go.dev/git.sr.ht/~mariusor/wrapper#example-Regis...
    • ผมก็ทำอะไรคล้าย ๆ กันไว้เหมือนกัน: https://github.com/pseidemann/finish
  • 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