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 นาทีโดยไม่มีปัญหา
$ 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 จะเกิดอะไรขึ้น |
|---|---|---|
| Liveness | process ค้างจนกู้เองไม่ได้แล้วหรือยัง | 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 | ความหมาย |
|---|---|---|
initialDelaySeconds | 0 | รอก่อน probe ครั้งแรก |
periodSeconds | 10 | probe ทุกกี่วินาที |
timeoutSeconds | 1 | probe หนึ่งครั้งใช้เวลาได้นานสุดเท่าไร |
failureThreshold | 3 | fail ติดกันกี่ครั้งถึงจะลงมือ |
successThreshold | 1 | ผ่านติดกันกี่ครั้งถึงนับว่าปกติ (liveness และ startup ต้องเป็น 1) |
timeout default แค่ 1 วินาทีมักสั้นเกินไปสำหรับ endpoint ที่ต้องแตะ network พอโหลดสูง health check ช้าลง fail สามครั้งติด kubelet ก็ restart container ที่แค่ยุ่งอยู่ ไม่ได้พังจริง
ตัวอย่างการตั้งค่า probe ที่เหมาะสม
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: 2startup probe ให้เวลาแอปที่เริ่มช้าได้สูงสุด failureThreshold × periodSeconds (ในตัวอย่างคือ 120 วินาที) พอผ่านแล้ว liveness probe จึงรับช่วงต่อด้วยงบเวลาที่เข้มกว่ามาก วิธีนี้ดีกว่าการตั้ง initialDelaySeconds ยาว ๆ ให้ liveness แบบเดิม ซึ่งทำให้ตรวจจับปัญหาได้ช้าไปตลอดอายุของ container
health endpoint แต่ละตัวควรเช็กอะไร
กฎข้อที่สำคัญที่สุดคือ liveness เช็กตัว process เอง ส่วน readiness เช็กว่า pod พร้อมรับ traffic หรือไม่
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
kubectl describe pod api-7d9f8c6b5-x2kqpให้ดูสองส่วน ส่วนแรกคือ Last State ใต้ container ซึ่งบอกว่ารอบก่อนหน้าจบลงอย่างไร:
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 ของรอบที่ตาย
kubectl logs api-7d9f8c6b5-x2kqp --previous
kubectl logs api-7d9f8c6b5-x2kqp -c migrate --previous # specific containerkubectl logs เฉย ๆ จะแสดง log ของ container ปัจจุบัน ซึ่งอาจเพิ่งเริ่มและยังไม่ได้พิมพ์อะไรออกมา ส่วน --previous จะแสดง output ของรอบที่ตายไปแล้ว ซึ่งมักเป็นที่อยู่ของ stack trace หรือบรรทัดอย่าง missing required env DATABASE_URL
3. ตีความ exit code
| Exit code | ความหมาย | สาเหตุที่พบบ่อย |
|---|---|---|
| 0 | process จบแบบสำเร็จ | คำสั่งไม่ใช่ server ที่รันค้าง เช่นเป็น script หรือ shell ที่รันจบแล้วออก |
| 1 | error ทั่วไปของแอป | exception ที่ไม่มีใครจับ, validate config ไม่ผ่าน, ต่อ dependency ไม่ได้ตอนเริ่ม |
| 126 | รันคำสั่งไม่ได้ | entrypoint ไม่มีสิทธิ์ execute |
| 127 | หาคำสั่งไม่เจอ | command/args ผิด, ไม่มี binary ใน image, ใช้ base image ผิด |
| 137 | ถูก kill ด้วย SIGKILL (128 + 9) | OOMKilled หรือถูก kill หลัง liveness fail แล้วไม่ยอมจบเมื่อได้ SIGTERM |
| 139 | Segmentation 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
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 เข้าไปไม่ทัน มีสองทางเลือก:
# 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 -- shpod ที่ copy มาใช้ image, env และ volume ชุดเดียวกัน เราจึงเข้าไปเปิดไฟล์ config, ลองรัน binary เองด้วยมือ และเห็น error ตรง ๆ ได้ อย่าลืมลบ pod copy ทิ้งเมื่อใช้เสร็จ
สาเหตุที่พบบ่อยและวิธีแก้
| อาการ | สาเหตุที่น่าจะเป็น | วิธีแก้ |
|---|---|---|
| log แจ้ง error เรื่อง config หรือ env | env var หายหรือผิด, ค่าใน ConfigMap ผิด | แก้ config และ validate ค่าที่จำเป็นตอนเริ่มพร้อมข้อความที่อ่านเข้าใจ |
OOMKilled, exit 137 | memory 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 สั้น ๆ
kubectl describe podอ่านLast State, exit code และ eventskubectl logs --previousอ่าน output ของรอบที่ crash- จับคู่ exit code กับสาเหตุ ว่าเป็น config, OOM, probe, คำสั่ง หรือสิทธิ์
- ทำ liveness ให้เบาและไม่พึ่ง dependency ใช้ readiness ในการถอด pod ออกจากวง
- ใช้ startup probe กับแอปที่เริ่มช้า แทนการตั้ง
initialDelaySecondsยาว ๆ
ทบทวนค่า probe ทุกครั้งที่เวลาเริ่มต้นหรือ dependency ของแอปเปลี่ยนไป ถ้าอยากฝึก troubleshoot แบบนี้บน cluster จริง Vectorkub มี คอร์ส DevOps ฟรี ที่ครอบคลุมเรื่องนี้
