K8S Zero-Downtime Rolling Update ยากกว่าที่คิด

เมื่อทำ HTTP Server เวลาเราจะ rolling update เอา version ใหม่ขึ้นไปแทนที่ version เก่า ถึงแม้จะใช้ Deployment ใน Kubernetes แล้ว อาจจะยังไม่พอ

เช่น ถ้า

  1. มี request ที่ทำงานค้างไว้อยู่ request นั้นจะโดนตัด
  2. มี request ใหม่เข้ามาพอดี request นั้นอาจจะโดน refuse หรือ timeout ได้

แล้วอยากจะทำ Zero-downtime rolling update จะทำยังไงดี ?

1. ตั้ง replicas และ strategy ใน Deployment ให้ถูก

ถ้าเรารันแค่ 1 replicas เราไม่ครบลบ pod ทันทีเมื่อ rolling update ควรจะรอให้มี pod ใหม่ ready ก่อน

replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1

ถ้าเรารันมากกว่า 1 replicas ไม่ควรลบพร้อมกันทั้งหมด อาจจะลบทีละ 1 หรือจะตั้งเป็น % ก็ได้ เช่น

replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 50%
maxSurge: 100%

หรือ

replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 100%

2. Graceful Shutdown อย่าปิดทันที

เมื่อ k8s จะปิด pod, k8s จะส่ง SIGTERM มาก่อน แล้วรอจนกว่า terminationGracePeriodSeconds ที่ตั้งไว้จะหมดเวลาถึงจะ force quit

Graceful Shutdown คือการที่

  1. หยุดรับ request ใหม่
  2. รอจนกว่า request ที่รันค้างอยู่จะทำงานเสร็จ
  3. ปิด server

เราสามารถเขียน code ง่าย ๆ ได้แบบนี้

srv := &http.Server{
Addr: ":8080",
}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()

stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM)
<-stop

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Println("server shutdown error:", err)
return
}
log.Println("server shutdown")

3. บอก k8s ว่าเราพร้อมรับ request แล้ว

เราสามารถตั้งได้ว่า ให้ k8s รู้ได้ยังไงว่า pod เรา ready แล้ว โดนการใช้ readinessProbe

readinessProbe:
httpGet:
path: /readiness
port: 18080
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 1
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 1

นอกจาก readinessProbe แล้ว ใน k8s ยังมี livenessProbe อีก เพื่อบอกว่า เมื่อไรควรจะ restart pod

livenessProbe:
httpGet:
path: /liveness
port: 18080
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 5

ตอนนี้เราก็มีทั้ง update strategy, readiness probe, graceful shutdown แล้ว แสดงว่า HTTP Server เราน่าจะ Zero-downtime update แล้วสิ…

ไม่เลย 😱

เพราะเมื่อ k8s ส่ง SIGTERM มา ไม่ได้หมายความว่าให้ปิดทันที

แต่หมายความว่า

รออีกแปป แล้วค่อยปิด

เพราะเมื่อ k8s สั่งปิด pod แล้ว ตัว load balancer (service) ยังไม่รู้ว่า pod นี้กำลังจะปิด, load balancer เลยยังส่ง request มาอยู่ ทำให้ request ที่วิ่งมาที่ pod ที่กำลังจะปิดนั้นโดนตัดได้

แสดงว่าเมื่อเราได้ SIGTERM แล้ว เราจะต้องยังไม่ปิด (เช่น sleep ไว้ก่อน)
แล้วให้ตอบ readiness probe ไปว่า error จนกระทั่ง fail (periodSeconds * failureThreshold)

หน้าตา code เลยจะได้แบบนี้


ขายของ

เนื่องจากถ้าจะเขียน code ข้างบนทุก project อาจจะเสียเวลา เลยทำเป็น lib ไว้ให้เรียกใช้ง่าย ๆ

ถ้าเอา code ข้างบนมาเขียนใหม่มาใช้ lib จะได้หน้าตาแบบนี้

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.