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

DevOps

Kubernetes Probe และ CrashLoopBackOff: ตั้งค่า Liveness/Readiness และ debug ทีละขั้น

เข้าใจว่า CrashLoopBackOff คืออะไร liveness, readiness และ startup probe ทำงานต่างกันอย่างไร พร้อมขั้นตอน debug ทีละขั้นด้วย describe, logs --previous, events และ exit code

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

kubernetes probes and crashloopbackoff

CrashLoopBackOff ไม่ใช่ error โดยตัวมันเอง แต่เป็นสถานะที่ Kubernetes แสดงเมื่อ container ตายซ้ำไปซ้ำมา และ kubelet กำลังรอนานขึ้นเรื่อย ๆ ก่อนจะ restart รอบถัดไป ปัญหาจริงคือสิ่งที่ทำให้ container ตาย และส่วนใหญ่หาเจอได้ด้วยสามอย่าง คือ kubectl describe pod, kubectl logs --previous และการอ่าน exit code

เรื่องนี้ผูกกับ probe อย่างใกล้ชิด liveness probe ที่ตั้งค่าผิดเป็นหนึ่งในสาเหตุที่พบบ่อยที่สุดที่ทำให้แอปซึ่งจริง ๆ ยังปกติดี ติดวน restart ไม่จบ ส่วนการไม่มี readiness probe ก็มักเป็นเหตุให้ผู้ใช้เจอ error ระหว่าง deploy บทความนี้ครอบคลุมทั้งสองเรื่อง ทั้งการทำงานของ liveness, readiness และ startup probe และขั้นตอน debug CrashLoopBackOff ทีละขั้น

CrashLoopBackOff หมายถึงอะไรจริง ๆ

เมื่อใช้ restartPolicy: Always (ซึ่ง Deployment บังคับใช้ค่านี้) kubelet จะ restart container ทุกครั้งที่มันจบการทำงาน ไม่ว่าจะ crash หรือจบแบบปกติก็ตาม ถ้ามันจบซ้ำ ๆ kubelet จะเพิ่มเวลารอระหว่างรอบแบบ exponential back-off คือ 10 วินาที, 20, 40 ไปเรื่อย ๆ จนถึงเพดาน 5 นาที ช่วงที่รออยู่นี้เองที่ pod แสดงสถานะ CrashLoopBackOff และตัวนับจะรีเซ็ตเมื่อ container รันได้ต่อเนื่อง 10 นาทีโดยไม่มีปัญหา

bash
$ kubectl get pods
NAME                     READY   STATUS             RESTARTS      AGE
api-7d9f8c6b5-x2kqp      0/1     CrashLoopBackOff   6 (2m ago)    9m

จากตรงนี้ได้ข้อสรุปสองข้อ:

  • container เริ่มทำงานได้แล้ว ถ้าดึง image ไม่ได้จะเห็นเป็น ImagePullBackOff ถ้าหา key ใน ConfigMap หรือ Secret ไม่เจอจะเห็นเป็น CreateContainerConfigError ดังนั้น CrashLoopBackOff แปลว่า process รันขึ้นมาแล้วค่อยตาย
  • การลบ pod ทิ้งแทบไม่ช่วยอะไร pod ใหม่ใช้ image เดิมและ config เดิม ก็จะตายแบบเดิม

ถ้าตัวที่พังคือ init container สถานะจะขึ้นเป็น Init:CrashLoopBackOff วิธี debug เหมือนกันทุกอย่าง แค่เติม -c <ชื่อ init container> ตอนดู log

Liveness, readiness และ startup probe

kubelet บนแต่ละ node เป็นตัวรัน probe กับ container ของเรา (ดูว่า kubelet อยู่ตรงไหนในระบบได้ที่ สถาปัตยกรรม Kubernetes) probe แต่ละชนิดตอบคำถามต่างกัน และมีผลต่างกันเมื่อ fail

Probeถามว่าอะไรถ้า fail จะเกิดอะไรขึ้น
Livenessprocess ค้างจนกู้เองไม่ได้แล้วหรือยังkubelet kill container แล้ว restart ใหม่
Readinessตอนนี้ pod รับ traffic ได้ไหมpod ถูกถอดออกจาก endpoint ของ Service โดยไม่ restart
Startupแอปเริ่มต้นเสร็จหรือยังliveness และ readiness จะยังไม่ทำงานจนกว่าจะผ่าน ถ้าไม่ผ่านเลย container จะถูก kill

probe แต่ละตัวเลือกกลไกได้สี่แบบ คือ httpGet (status 2xx หรือ 3xx ถือว่าผ่าน), tcpSocket, exec (exit code 0 คือผ่าน) และ grpc สำหรับ service ที่ implement gRPC health checking protocol

ค่าด้านเวลาใช้ร่วมกันทั้งสาม probe:

Fieldค่า defaultความหมาย
initialDelaySeconds0รอก่อน probe ครั้งแรก
periodSeconds10probe ทุกกี่วินาที
timeoutSeconds1probe หนึ่งครั้งใช้เวลาได้นานสุดเท่าไร
failureThreshold3fail ติดกันกี่ครั้งถึงจะลงมือ
successThreshold1ผ่านติดกันกี่ครั้งถึงนับว่าปกติ (liveness และ startup ต้องเป็น 1)

timeout default แค่ 1 วินาทีมักสั้นเกินไปสำหรับ endpoint ที่ต้องแตะ network พอโหลดสูง health check ช้าลง fail สามครั้งติด kubelet ก็ restart container ที่แค่ยุ่งอยู่ ไม่ได้พังจริง

ตัวอย่างการตั้งค่า probe ที่เหมาะสม

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: registry.example.com/api:1.8.2
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              memory: 512Mi
          startupProbe:
            httpGet:
              path: /livez
              port: 8080
            periodSeconds: 5
            failureThreshold: 24     # up to 120s to start
          livenessProbe:
            httpGet:
              path: /livez
              port: 8080
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /readyz
              port: 8080
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2

startup probe ให้เวลาแอปที่เริ่มช้าได้สูงสุด failureThreshold × periodSeconds (ในตัวอย่างคือ 120 วินาที) พอผ่านแล้ว liveness probe จึงรับช่วงต่อด้วยงบเวลาที่เข้มกว่ามาก วิธีนี้ดีกว่าการตั้ง initialDelaySeconds ยาว ๆ ให้ liveness แบบเดิม ซึ่งทำให้ตรวจจับปัญหาได้ช้าไปตลอดอายุของ container

health endpoint แต่ละตัวควรเช็กอะไร

กฎข้อที่สำคัญที่สุดคือ liveness เช็กตัว process เอง ส่วน readiness เช็กว่า pod พร้อมรับ traffic หรือไม่

go
package main

import (
	"context"
	"database/sql"
	"net/http"
	"sync/atomic"
	"time"
)

type health struct {
	db       *sql.DB
	draining atomic.Bool
}

// Liveness: can this process still handle a request at all?
// No external calls. If the database is down, restarting us won't fix it.
func (h *health) livez(w http.ResponseWriter, _ *http.Request) {
	w.WriteHeader(http.StatusOK)
}

// Readiness: should the Service send us traffic right now?
func (h *health) readyz(w http.ResponseWriter, r *http.Request) {
	if h.draining.Load() {
		http.Error(w, "shutting down", http.StatusServiceUnavailable)
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), time.Second)
	defer cancel()
	if err := h.db.PingContext(ctx); err != nil {
		http.Error(w, "db unavailable", http.StatusServiceUnavailable)
		return
	}
	w.WriteHeader(http.StatusOK)
}

func (h *health) register(mux *http.ServeMux) {
	mux.HandleFunc("GET /livez", h.livez)
	mux.HandleFunc("GET /readyz", h.readyz)
}

ลองนึกภาพว่าถ้า liveness ไป ping database ด้วย เมื่อ database สะดุดแค่ไม่กี่วินาที liveness จะ fail พร้อมกันทุก replica Kubernetes ก็ restart ทั้งหมด แล้วทุกตัวก็กลับมาแย่งกัน reconnect พร้อมกันอีก จากปัญหาเล็ก ๆ ที่ dependency กลายเป็นระบบล่มทั้งระบบ ในขณะที่ readiness ที่ fail แค่ถอด pod ออกจาก endpoint ของ Service ชั่วคราว ซึ่งเป็นการตอบสนองที่ถูกต้องต่อสถานการณ์ "ตอนนี้ยังให้บริการไม่ได้"

flag draining มีประโยชน์ตอน shutdown ให้ตั้งค่าเมื่อ process ได้รับ SIGTERM readiness จะ fail และ pod จะหยุดรับ request ใหม่ก่อนที่จะปิดตัวจริง ใช้คู่กับเทคนิคใน rolling update แบบ zero downtime ได้ดี

ขั้นตอน debug CrashLoopBackOff ทีละขั้น

ไล่ตามลำดับนี้ เคสส่วนใหญ่จบได้ภายในขั้นที่ 3

1. describe pod

bash
kubectl describe pod api-7d9f8c6b5-x2kqp

ให้ดูสองส่วน ส่วนแรกคือ Last State ใต้ container ซึ่งบอกว่ารอบก่อนหน้าจบลงอย่างไร:

text
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Thu, 24 Sep 2026 10:02:11 +0700
      Finished:     Thu, 24 Sep 2026 10:02:19 +0700

ส่วนที่สองคือ Events ด้านล่างสุด ซึ่งบอกว่า kubelet ทำอะไรไปบ้าง ถ้าเห็น Liveness probe failed: ... context deadline exceeded ตามด้วย Container api failed liveness probe, will be restarted ก็ชี้ไปที่ probe ได้เลย ส่วน Back-off restarting failed container แค่ยืนยันว่ากำลังวนอยู่ ไม่ได้บอกสาเหตุ

2. อ่าน log ของรอบที่ตาย

bash
kubectl logs api-7d9f8c6b5-x2kqp --previous
kubectl logs api-7d9f8c6b5-x2kqp -c migrate --previous   # specific container

kubectl logs เฉย ๆ จะแสดง log ของ container ปัจจุบัน ซึ่งอาจเพิ่งเริ่มและยังไม่ได้พิมพ์อะไรออกมา ส่วน --previous จะแสดง output ของรอบที่ตายไปแล้ว ซึ่งมักเป็นที่อยู่ของ stack trace หรือบรรทัดอย่าง missing required env DATABASE_URL

3. ตีความ exit code

Exit codeความหมายสาเหตุที่พบบ่อย
0process จบแบบสำเร็จคำสั่งไม่ใช่ server ที่รันค้าง เช่นเป็น script หรือ shell ที่รันจบแล้วออก
1error ทั่วไปของแอปexception ที่ไม่มีใครจับ, validate config ไม่ผ่าน, ต่อ dependency ไม่ได้ตอนเริ่ม
126รันคำสั่งไม่ได้entrypoint ไม่มีสิทธิ์ execute
127หาคำสั่งไม่เจอcommand/args ผิด, ไม่มี binary ใน image, ใช้ base image ผิด
137ถูก kill ด้วย SIGKILL (128 + 9)OOMKilled หรือถูก kill หลัง liveness fail แล้วไม่ยอมจบเมื่อได้ SIGTERM
139Segmentation fault (128 + 11)native crash, binary คนละ CPU architecture
143ถูกสั่งจบด้วย SIGTERM (128 + 15)container ถูกสั่งให้หยุด มักเกิดหลัง liveness fail

exit code 137 คู่กับ Reason: OOMKilled แปลว่า container ใช้ memory เกิน limit อาจเพราะ limit ต่ำเกินกว่างานจริง หรือแอปมี memory leak วิธีกำหนดขนาดให้เหมาะอยู่ในบทความ requests, limits และ QoS class

4. ดู events ทั้ง namespace

bash
kubectl get events --sort-by=.lastTimestamp
kubectl get events --field-selector involvedObject.name=api-7d9f8c6b5-x2kqp

โดย default event จะหายไปหลังประมาณหนึ่งชั่วโมง จึงควรรีบดู event ยังช่วยให้เห็นปัญหาที่ไม่ได้อยู่ใน container โดยตรง เช่น volume ที่ mount ไม่สำเร็จ

5. เข้า shell เมื่อ log ยังไม่พอ

container ที่ตายภายในสองวินาทีแทบจะ exec เข้าไปไม่ทัน มีสองทางเลือก:

bash
# Attach an ephemeral debug container that shares the target's process namespace
kubectl debug -it api-7d9f8c6b5-x2kqp --image=busybox:1.36 --target=api

# Or copy the pod, replacing the command so it stays up
kubectl debug api-7d9f8c6b5-x2kqp -it --copy-to=api-debug --container=api -- sh

pod ที่ copy มาใช้ image, env และ volume ชุดเดียวกัน เราจึงเข้าไปเปิดไฟล์ config, ลองรัน binary เองด้วยมือ และเห็น error ตรง ๆ ได้ อย่าลืมลบ pod copy ทิ้งเมื่อใช้เสร็จ

สาเหตุที่พบบ่อยและวิธีแก้

อาการสาเหตุที่น่าจะเป็นวิธีแก้
log แจ้ง error เรื่อง config หรือ envenv var หายหรือผิด, ค่าใน ConfigMap ผิดแก้ config และ validate ค่าที่จำเป็นตอนเริ่มพร้อมข้อความที่อ่านเข้าใจ
OOMKilled, exit 137memory limit ต่ำไป หรือมี leakขยับ limit ตามการใช้งานที่วัดได้จริง แล้วค่อย profile
events มี liveness fail แต่ log ดูปกติprobe เข้มเกินไป timeout สั้น ไม่มี startup probeเพิ่ม startup probe, เพิ่ม timeoutSeconds, ทำ liveness ให้เบา
exit 1 พร้อม "connection refused" ไปที่ databaseแอปปิดตัวเมื่อ dependency ยังไม่พร้อมretry แบบ back-off ตอนเริ่ม แทนการ exit
exit 0 แล้ว restart ไม่จบprocess ไม่ได้รันค้างรัน server แบบ foreground หรือใช้ Job สำหรับงานที่รันครั้งเดียว
exit 127 หรือ 126คำสั่งผิด, ไม่มี binary หรือสิทธิ์ไม่พอเทียบ command/args กับ entrypoint ของ image

คำถามที่พบบ่อย

CrashLoopBackOff รอนานเท่าไรก่อน restart รอบถัดไป

เริ่มที่ 10 วินาที แล้วเพิ่มเป็นสองเท่าทุกครั้งที่ crash จนถึงเพดาน 5 นาที และจะรีเซ็ตเมื่อ container รันได้ต่อเนื่อง 10 นาทีโดยไม่ตาย

liveness กับ readiness ควรใช้ endpoint เดียวกันไหม

โดยทั่วไปไม่ควร liveness ควรเช็กแค่ว่าตัว process ยังตอบสนองได้ ส่วน readiness เช็ก dependency และสถานะ shutdown ได้ ถ้าชี้ทั้งคู่ไปที่ endpoint ที่เช็ก database พอ database ล่ม pod ทุกตัวจะถูก restart

ทำไม pod ถึง restart ทั้งที่ log ไม่มี error

ให้ดู events ว่ามี liveness probe fail หรือเปล่า kubelet อาจกำลัง kill container ที่ปกติดีแต่ตอบช้าเพราะ probe timeout และให้เช็ก Last State ว่าเป็น OOMKilled หรือไม่ เพราะกรณีนี้แอปไม่มีโอกาสเขียน log ทิ้งไว้

exit code 137 ใน Kubernetes หมายถึงอะไร

process ถูก kill ด้วย SIGKILL ส่วนใหญ่เป็น OOM killer ของ kernel ที่บังคับใช้ memory limit ซึ่งจะเห็นเป็น Reason: OOMKilled อีกกรณีคือ container ไม่ยอมจบเมื่อได้ SIGTERM ตอน shutdown จึงถูก kill หลังหมด grace period

ต้องมี startup probe ทุกครั้งไหม

ไม่จำเป็น ควรใส่เมื่อแอปใช้เวลาเริ่มนานหรือไม่แน่นอน เช่นแอป JVM, แอปที่ต้อง warm cache หรือแอปที่รัน migration ตอนเริ่ม ส่วน Go binary ที่เริ่มได้ในไม่ถึงวินาที มีแค่ liveness กับ readiness ก็พอ

checklist สั้น ๆ

  1. kubectl describe pod อ่าน Last State, exit code และ events
  2. kubectl logs --previous อ่าน output ของรอบที่ crash
  3. จับคู่ exit code กับสาเหตุ ว่าเป็น config, OOM, probe, คำสั่ง หรือสิทธิ์
  4. ทำ liveness ให้เบาและไม่พึ่ง dependency ใช้ readiness ในการถอด pod ออกจากวง
  5. ใช้ startup probe กับแอปที่เริ่มช้า แทนการตั้ง initialDelaySeconds ยาว ๆ

ทบทวนค่า probe ทุกครั้งที่เวลาเริ่มต้นหรือ dependency ของแอปเปลี่ยนไป ถ้าอยากฝึก troubleshoot แบบนี้บน cluster จริง Vectorkub มี คอร์ส DevOps ฟรี ที่ครอบคลุมเรื่องนี้

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

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

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