กลับไปยังบทความ

DevOps

Kubernetes Rolling Update: deploy แบบ zero downtime ด้วย probe, graceful shutdown และ PDB

ทำ Kubernetes rolling update ให้ zero downtime จริง ตั้ง maxSurge และ maxUnavailable ให้เหมาะ ใช้ readiness probe, graceful shutdown และ preStop hook ตั้ง PodDisruptionBudget และ rollback อย่างปลอดภัย

24 กันยายน 256910 นาทีในการอ่าน

kubernetes zero downtime rolling updates

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 ตัวที่เป็นเวอร์ชันใหม่

ชุดค่าที่ใช้บ่อย:

maxSurgemaxUnavailableพฤติกรรม
25%25%ค่า default ความเร็วสมดุล capacity ลดลงเล็กน้อยชั่วคราว
10capacity เต็มตลอด เพิ่ม pod ทีละตัว ช้ากว่า
100%0สร้างชุดใหม่ครบก่อนแล้วค่อยลบชุดเก่า ต้องใช้ทรัพยากรสองเท่าชั่วคราว
01ไม่มี 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

yaml
          readinessProbe:
            httpGet:
              path: /healthz/ready
              port: 8080
            periodSeconds: 5
            failureThreshold: 3

endpoint ที่ใช้เช็ก ready ควรตรวจสิ่งที่ pod ต้องมีเพื่อรับงานได้ และต้องเบา แอปที่ start ช้าให้เพิ่ม startupProbe เพื่อไม่ให้ liveness probe ฆ่ามันระหว่าง boot รายละเอียดการออกแบบ probe อ่านได้ใน Kubernetes probe และ CrashLoopBackOff

minReadySeconds เพิ่มระยะปลอดภัยอีกชั้น pod ใหม่ต้อง ready ต่อเนื่องครบจำนวนวินาทีที่กำหนดก่อน rollout จะนับว่าพร้อมและเดินต่อ ช่วยจับ pod ที่ผ่าน probe ครั้งเดียวแล้ว crash

2. Graceful shutdown: ทำ request ที่ค้างให้เสร็จ

เมื่อ Kubernetes ลบ pod เก่า:

  1. pod ถูกทำเครื่องหมายเป็น Terminating แล้วถูกเอาออกจาก EndpointSlice ของ Service พร้อม ๆ กับที่ preStop hook เริ่มทำงาน (ถ้ามี)
  2. เมื่อ preStop เสร็จ container จะได้รับ SIGTERM
  3. ถ้า process ยังไม่จบเมื่อครบ terminationGracePeriodSeconds (default 30) จะโดน SIGKILL เวลานี้นับตั้งแต่ขั้นที่ 1 จึงรวมเวลาของ preStop ไว้ด้วย

ถ้าแอปปิดตัวทันทีที่ได้ SIGTERM ทุก request ที่ยังประมวลผลอยู่จะล้มเหลว แอปควรหยุดรับ connection ใหม่ ทำ request ที่ค้างให้เสร็จ แล้วค่อยออก ตัวอย่างใน Go:

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 อัปเดตเสร็จก่อน:

yaml
          lifecycle:
            preStop:
              sleep:
                seconds: 10

action sleep แบบ built-in ใช้ได้ใน Kubernetes 1.30 ขึ้นไป คลัสเตอร์ที่เก่ากว่าให้ใช้ exec กับ ["sleep", "10"] ซึ่ง image ต้องมีคำสั่ง sleep และต้องตั้ง terminationGracePeriodSeconds ให้มากกว่าเวลา preStop บวกกับ timeout ของการ shutdown ในแอป

Deployment manifest สำหรับ zero downtime

รวมทุกอย่างเข้าด้วยกัน:

yaml
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 ของเราต้องตรวจจับและจัดการเอง:

bash
kubectl rollout status deployment/api --timeout=10m || kubectl rollout undo deployment/api

rollout status จะคืน exit code ที่ไม่ใช่ศูนย์เมื่อเกิน deadline หรือชน timeout จึงใช้เป็นด่านใน CI ได้ดี

Rollback ด้วย kubectl rollout

ทุกครั้งที่ pod template เปลี่ยนจะเกิด revision ใหม่ และ Deployment จะเก็บ ReplicaSet เก่าไว้ตามจำนวน revisionHistoryLimit:

bash
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

yaml
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 จริง
  • preStop sleep 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 ครอบคลุมเรื่องเหล่านี้

พร้อมสร้างสิ่งที่ยิ่งใหญ่หรือยัง?

เล่าโปรเจคของคุณให้เราฟัง — เราจะพาคุณไปถึง

เลือกคุกกี้ที่เราใช้ได้ คุกกี้ที่จำเป็นช่วยให้เว็บไซต์ทำงานได้และเปิดใช้งานเสมอ