Kubernetes rolling update คือการเปลี่ยน pod ของ Deployment ทีละส่วน ระหว่างที่เวอร์ชันใหม่ค่อย ๆ ถูกปล่อยออกไป จะมี pod ที่รับงานอยู่เสมอ มันเป็น strategy default ของ Deployment และความเร็วของการอัปเดตถูกคุมด้วย maxSurge และ maxUnavailable แต่ rolling update อย่างเดียวยังไม่การันตี zero downtime เพราะ request ยังหลุดได้ ถ้า pod ใหม่ได้รับ traffic ก่อนพร้อม หรือ pod เก่าตายกลางคันระหว่างประมวลผล request
บทความนี้อธิบายว่า rollout ทำงานยังไง แล้วไล่สามสิ่งที่ทำให้ zero downtime เกิดขึ้นจริง ได้แก่ readiness probe, graceful shutdown และ preStop hook รวมถึง PodDisruptionBudget และการ rollback ด้วย kubectl rollout
Kubernetes rolling update ทำงานยังไง
เมื่อเราแก้ pod template ของ Deployment เช่นเปลี่ยน image tag ตัว Deployment controller จะสร้าง ReplicaSet ใหม่ขึ้นมา แล้วค่อย ๆ scale ReplicaSet ใหม่ขึ้นและ ReplicaSet เก่าลงเป็นขั้น ๆ โดยมีสองค่าคุมขอบเขตของแต่ละขั้น:
maxSurge: ระหว่างอัปเดตมี pod เกินจำนวนเป้าหมายได้กี่ตัวmaxUnavailable: ระหว่างอัปเดตมี pod ที่ไม่พร้อมใช้งาน (ต่ำกว่าจำนวนเป้าหมาย) ได้กี่ตัว
ทั้งสองค่าใส่เป็นตัวเลขหรือเปอร์เซ็นต์ก็ได้ ค่า default คือ 25% ทั้งคู่ เปอร์เซ็นต์ของ maxSurge จะปัด ขึ้น ส่วน maxUnavailable จะปัด ลง
ถ้า replicas: 4 และใช้ค่า default ทั้งสองค่าจะได้ 1 แปลว่าระหว่าง rollout มี pod ได้สูงสุด 5 ตัว และต้องมี pod พร้อมอย่างน้อย 3 ตัว ในทางปฏิบัติ controller จะลบ pod เก่าหนึ่งตัวและสร้าง pod ใหม่สองตัวทันที (4 − 1 + 2 = 5) เมื่อ pod ใหม่พร้อม มันก็ลบ pod เก่าเพิ่มและสร้าง pod ใหม่ต่อ โดยอยู่ในขอบเขตนี้ตลอด จนครบ 4 ตัวที่เป็นเวอร์ชันใหม่
ชุดค่าที่ใช้บ่อย:
| maxSurge | maxUnavailable | พฤติกรรม |
|---|---|---|
| 25% | 25% | ค่า default ความเร็วสมดุล capacity ลดลงเล็กน้อยชั่วคราว |
| 1 | 0 | capacity เต็มตลอด เพิ่ม pod ทีละตัว ช้ากว่า |
| 100% | 0 | สร้างชุดใหม่ครบก่อนแล้วค่อยลบชุดเก่า ต้องใช้ทรัพยากรสองเท่าชั่วคราว |
| 0 | 1 | ไม่มี pod ส่วนเกิน เหมาะกับคลัสเตอร์ที่ไม่มีที่ว่าง แต่ capacity ลดลง |
ทั้งสองค่าเป็น 0 พร้อมกันไม่ได้ ไม่อย่างนั้น rollout จะไม่มีวันเดินหน้า สำหรับ service ที่ capacity สำคัญ maxSurge: 1, maxUnavailable: 0 เป็นค่าเริ่มต้นที่ปลอดภัย
อีก strategy หนึ่งคือ Recreate ซึ่งหยุด pod เก่าทั้งหมดก่อนแล้วค่อยสร้างใหม่ จึงมี downtime ใช้เฉพาะกรณีที่สองเวอร์ชันรันพร้อมกันไม่ได้ เช่นแอปที่ lock volume แบบ exclusive ส่วน StatefulSet และ DaemonSet ที่อัปเดตต่างออกไป อ่านได้ใน Kubernetes workloads: Deployment, StatefulSet และ DaemonSet
rolling update อย่างเดียวยังไม่ใช่ zero downtime
maxUnavailable: 0 การันตี จำนวน pod แต่ไม่ได้การันตีว่าทุก request จะสำเร็จ ยังมีช่องโหว่อีกสามจุด
1. Readiness probe: อย่าส่ง traffic เร็วเกินไป
pod จะถูกนับว่าพร้อมใช้งาน และถูกเพิ่มเข้า endpoints ของ Service ก็ต่อเมื่อ readiness probe ผ่าน ถ้าไม่มี readiness probe pod จะถูกมองว่าพร้อมทันทีที่ container start ซึ่งอาจเร็วกว่าตอนที่แอปโหลด config, warm cache หรือต่อ database เสร็จหลายวินาที request ที่ถูกส่งเข้ามาช่วงนั้นจะ error
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
failureThreshold: 3endpoint ที่ใช้เช็ก ready ควรตรวจสิ่งที่ pod ต้องมีเพื่อรับงานได้ และต้องเบา แอปที่ start ช้าให้เพิ่ม startupProbe เพื่อไม่ให้ liveness probe ฆ่ามันระหว่าง boot รายละเอียดการออกแบบ probe อ่านได้ใน Kubernetes probe และ CrashLoopBackOff
minReadySeconds เพิ่มระยะปลอดภัยอีกชั้น pod ใหม่ต้อง ready ต่อเนื่องครบจำนวนวินาทีที่กำหนดก่อน rollout จะนับว่าพร้อมและเดินต่อ ช่วยจับ pod ที่ผ่าน probe ครั้งเดียวแล้ว crash
2. Graceful shutdown: ทำ request ที่ค้างให้เสร็จ
เมื่อ Kubernetes ลบ pod เก่า:
- pod ถูกทำเครื่องหมายเป็น
Terminatingแล้วถูกเอาออกจาก EndpointSlice ของ Service พร้อม ๆ กับที่preStophook เริ่มทำงาน (ถ้ามี) - เมื่อ
preStopเสร็จ container จะได้รับSIGTERM - ถ้า process ยังไม่จบเมื่อครบ
terminationGracePeriodSeconds(default 30) จะโดนSIGKILLเวลานี้นับตั้งแต่ขั้นที่ 1 จึงรวมเวลาของpreStopไว้ด้วย
ถ้าแอปปิดตัวทันทีที่ได้ SIGTERM ทุก request ที่ยังประมวลผลอยู่จะล้มเหลว แอปควรหยุดรับ connection ใหม่ ทำ request ที่ค้างให้เสร็จ แล้วค่อยออก ตัวอย่างใน Go:
package main
import (
"context"
"errors"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz/ready", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello\n"))
})
srv := &http.Server{Addr: ":8080", Handler: mux}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
<-ctx.Done()
log.Println("SIGTERM received, draining")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
}srv.Shutdown จะปิด listener, ปิด keep-alive connection ที่ว่างอยู่ และรอให้ request ที่กำลังทำงานเสร็จ ส่วน background worker, consumer และ database pool ก็ต้องปิดแบบเดียวกัน connection ที่อยู่ยาวอย่าง WebSocket หรือ gRPC stream ต้องสั่งปิดอย่างชัดเจน เพื่อให้ client reconnect ไปหา pod ใหม่
อีกเรื่องคือต้องแน่ใจว่าสัญญาณไปถึง process จริง ถ้า shell script เป็น PID 1 และไม่ส่งต่อสัญญาณ แอปจะไม่เคยได้รับ SIGTERM เลย ให้ใช้ exec ใน entrypoint script หรือใช้ CMD แบบ exec form ใน Dockerfile
3. preStop hook: ปิดช่อง race ของ endpoint
ขั้นที่ 1 ข้างบนเกิดขึ้นพร้อมกัน การเอา pod ออกจาก endpoints ต้องใช้เวลาสักพักกว่าจะกระจายไปถึง kube-proxy ทุก node, Ingress controller และ external load balancer ระหว่างนั้น pod อาจเริ่มปิดตัวไปแล้ว ผลคือมีช่วงสั้น ๆ ที่ traffic ยังวิ่งเข้า pod ที่เลิก listen ไปแล้ว
วิธีแก้คือหน่วงการปิดตัวไว้เล็กน้อยด้วย preStop hook ให้ routing อัปเดตเสร็จก่อน:
lifecycle:
preStop:
sleep:
seconds: 10action sleep แบบ built-in ใช้ได้ใน Kubernetes 1.30 ขึ้นไป คลัสเตอร์ที่เก่ากว่าให้ใช้ exec กับ ["sleep", "10"] ซึ่ง image ต้องมีคำสั่ง sleep และต้องตั้ง terminationGracePeriodSeconds ให้มากกว่าเวลา preStop บวกกับ timeout ของการ shutdown ในแอป
Deployment manifest สำหรับ zero downtime
รวมทุกอย่างเข้าด้วยกัน:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 4
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
terminationGracePeriodSeconds: 45
containers:
- name: api
image: registry.example.com/api:1.9.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
sleep:
seconds: 10
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
memory: 256Miงบเวลา: preStop 10 วินาที บวก Shutdown สูงสุด 30 วินาที รวม 40 วินาที ยังอยู่ใน grace period 45 วินาที
ยังมีอีกเงื่อนไขที่อยู่นอก Kubernetes คือ ระหว่าง rollout สองเวอร์ชันรันพร้อมกัน การแก้ schema ของ database ต้องใช้ได้กับทั้งโค้ดเก่าและใหม่ (ขยาย schema ก่อน ย้ายข้อมูล แล้วค่อยตัดของเก่าใน release ถัดไป) และการแก้ API ต้อง backward compatible กับ client ที่อาจโดนเวอร์ชันไหนก็ได้
เมื่อ rollout ค้าง
ถ้า pod ใหม่ไม่เคย ready เช่น image พังหรือ probe ไม่ผ่าน rollout จะหยุดเดินหน้า มันจะไม่ลบ pod เก่าเกินกว่าที่ maxUnavailable อนุญาต เวอร์ชันเก่าจึงยังรับงานต่อได้ ถ้าตั้ง maxUnavailable: 0 capacity จะเต็มเหมือนเดิม ถ้าใช้ค่า default 25% ก็จะรันด้วย capacity ที่ลดลงจนกว่าจะแก้เสร็จ
เมื่อเลย progressDeadlineSeconds (default 600) condition Progressing ของ Deployment จะเป็น False พร้อม reason ProgressDeadlineExceeded Kubernetes ไม่ rollback ให้อัตโนมัติ pipeline ของเราต้องตรวจจับและจัดการเอง:
kubectl rollout status deployment/api --timeout=10m || kubectl rollout undo deployment/apirollout status จะคืน exit code ที่ไม่ใช่ศูนย์เมื่อเกิน deadline หรือชน timeout จึงใช้เป็นด่านใน CI ได้ดี
Rollback ด้วย kubectl rollout
ทุกครั้งที่ pod template เปลี่ยนจะเกิด revision ใหม่ และ Deployment จะเก็บ ReplicaSet เก่าไว้ตามจำนวน revisionHistoryLimit:
kubectl rollout history deployment/api # list revisions
kubectl rollout history deployment/api --revision=7 # show one revision's template
kubectl rollout undo deployment/api # back to the previous revision
kubectl rollout undo deployment/api --to-revision=7 # back to a specific revision
kubectl rollout pause deployment/api # batch several changes
kubectl rollout resume deployment/api
kubectl rollout restart deployment/api # rolling restart, same specถ้าอยากบันทึกว่าแต่ละ revision มาจากอะไร ให้ตั้ง annotation kubernetes.io/change-cause ในขั้นตอน deploy ส่วน flag --record แบบเก่าถูก deprecate แล้ว
rollback ก็คือ rolling update อีกรอบหนึ่ง strategy และ probe เดิมจึงมีผลเหมือนกัน และมันย้อนแค่ pod template ไม่ได้ย้อน ConfigMap ที่แก้แยกไว้ หรือ database migration ถ้า deploy ผ่าน GitOps ให้ revert commit แทน ไม่อย่างนั้น sync รอบถัดไปจะ apply เวอร์ชันที่เพิ่ง rollback ออกไปกลับมาใหม่
PodDisruptionBudget: ป้องกันตอน drain
PodDisruptionBudget (PDB) จำกัดจำนวน pod ของแอปที่หายไปพร้อมกันได้จาก voluntary disruption เช่น kubectl drain, การอัปเกรด node และการที่ Cluster Autoscaler ลบ node
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1
selector:
matchLabels:
app: api
unhealthyPodEvictionPolicy: AlwaysAllowสิ่งที่ PDB ไม่ได้ ครอบคลุม:
- rolling update ของ Deployment ส่วนนั้นถูกคุมด้วย
maxUnavailableใน strategy ไม่ใช่ PDB - ความเสียหายที่ไม่ได้ตั้งใจ เช่น node crash หรือโดน OOM kill จาก kernel
- การสั่ง
kubectl delete podตรง ๆ
หลีกเลี่ยง PDB ที่ไม่ยอมให้ disrupt เลย เช่นตั้ง minAvailable เท่ากับจำนวน replica เพราะจะทำให้ drain node และ scale down คลัสเตอร์ไม่ได้ไปตลอด unhealthyPodEvictionPolicy: AlwaysAllow ช่วยไม่ให้ pod ที่ crash วนอยู่แล้วมาขวางการ drain ใช้ PDB คู่กับการกระจาย replica ข้าม node ตามที่อธิบายใน Kubernetes requests, limits และ scheduling เพื่อให้การ drain แต่ละครั้งไม่กระทบเกินหนึ่ง replica
คำถามที่พบบ่อย
Kubernetes rolling update เป็น zero downtime โดย default ไหม
ไม่ทั้งหมด มันทำให้มี pod รันอยู่ตลอด แต่ถ้าไม่มี readiness probe, การจัดการ SIGTERM อย่างถูกต้อง และ preStop ที่หน่วงสั้น ๆ จะมี request บางส่วนล้มเหลวทุกครั้งที่ rollout
maxSurge และ maxUnavailable ควรตั้งเท่าไร
service ที่ผู้ใช้เข้าถึงโดยตรง ใช้ maxSurge: 1 (หรือ 25%) คู่กับ maxUnavailable: 0 จะรักษา capacity เต็มไว้ได้ ถ้าคลัสเตอร์มีทรัพยากรเหลือ เพิ่ม maxSurge ได้เพื่อให้เร็วขึ้น
Kubernetes rollback deployment ที่พังให้อัตโนมัติไหม
ไม่ rollout จะค้างและถูกทำเครื่องหมาย ProgressDeadlineExceeded ให้รัน kubectl rollout undo จาก pipeline หรือใช้เครื่องมือ progressive delivery อย่าง Argo Rollouts หรือ Flagger เพื่อ rollback อัตโนมัติ
ทำไมยังเจอ 502 ระหว่าง rollout ทั้งที่มี readiness probe แล้ว
ส่วนใหญ่เป็น race ตอนเอา pod ออกจาก endpoints load balancer หรือ Ingress ยังส่ง traffic ไปหา pod ที่กำลังปิดตัว ให้เพิ่ม preStop sleep และทำให้แอป drain request ที่ค้างอยู่เมื่อได้ SIGTERM
PodDisruptionBudget มีผลกับ rolling update ไหม
ไม่มี rollout ของ Deployment ทำตาม maxUnavailable ใน strategy ส่วน PDB จำกัดเฉพาะการ evict เช่นการ drain node และการ scale down ของ autoscaler
เช็กลิสต์ rollout แบบ zero downtime
- service สำคัญตั้ง
maxSurge: 1,maxUnavailable: 0 - ทุก container ที่รับ traffic มี readiness probe และมี
startupProbeสำหรับแอปที่ start ช้า - แอปจัดการ
SIGTERMด้วยการ drain และสัญญาณไปถึง process จริง preStopsleep 5 ถึง 10 วินาที และterminationGracePeriodSecondsครอบคลุมทั้งเวลา sleep และเวลา drain- การแก้ schema และ API ใช้ได้กับทั้งสองเวอร์ชัน
- ใช้
kubectl rollout statusเป็นด่านใน CI พร้อมrollout undoหรือ GitOps revert ที่เตรียมไว้ - PDB ยอมให้ disrupt ได้อย่างน้อยหนึ่งตัว และ replica กระจายข้าม node
เมื่อทำครบตามนี้ การ deploy ในเวลาทำงานจะกลายเป็นเรื่องปกติ ถ้าอยากฝึก rollout, rollback และ drain บนคลัสเตอร์จริง คอร์ส DevOps ฟรี ของ Vectorkub ครอบคลุมเรื่องเหล่านี้
