Workload Patterns
StatefulSet — เมื่อ Pod ต้อง "มีตัวตน" คงที่
Deployment ปฏิบัติกับ Pod ทุกตัวเหมือนกันหมด (interchangeable) — เหมาะกับ stateless app
แต่ database/message queue ต้องการ:
- Stable network identity — ชื่อ Pod คงที่เสมอ (
db-0,db-1,db-2ไม่ใช่ random suffix) - Stable storage — แต่ละ Pod ผูกกับ
PersistentVolumeClaimของตัวเองถาวร แม้ Pod จะถูก restart ไปอยู่ Node อื่นก็ยังได้ data เดิมกลับมา - Ordered deployment/scaling — สร้าง/ลบทีละตัวตามลำดับ (
db-0ต้องพร้อมก่อนdb-1ถึงจะ เริ่มสร้าง) สำคัญกับระบบที่มี leader election หรือ replication ตามลำดับ
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db # ต้องคู่กับ Headless Service
replicas: 3
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
volumeClaimTemplates คือจุดต่างสำคัญจาก Deployment — แต่ละ replica ได้ PVC ของตัวเอง
(data-db-0, data-db-1, data-db-2) ไม่ share กัน
"StatefulSet = อพาร์ตเมนต์ที่มีเลขห้องถาวร, Deployment = โรงแรมที่สุ่มห้องให้ใหม่ทุกครั้ง" —
Pod ใน StatefulSet (db-0, db-1) กลับมาเลขห้องเดิมพร้อมของเดิม (PVC เดิม) เสมอแม้จะ "เช็คอิน
ใหม่" (restart) ส่วน Deployment สุ่มเลขห้องใหม่ทุกครั้งไม่สนใจว่าของเก่าอยู่ห้องไหน
เมื่อ scale StatefulSet ลง (เช่นจาก 3 เหลือ 1) PVC ของ replica ที่ถูกลบไป (data-db-1,
data-db-2) จะไม่ถูกลบตามไปด้วย — Kubernetes จงใจเก็บ PVC ไว้เผื่อ scale กลับขึ้นมาใหม่
ภายหลังแล้วอยากได้ data เดิมกลับคืน พฤติกรรม default นี้ถูกควบคุมโดย field
persistentVolumeClaimRetentionPolicy (เพิ่มเข้ามาใน Kubernetes 1.27+) ซึ่งมีค่าแยกกันสำหรับ
ตอน scale down (whenScaled) และตอนลบ StatefulSet ทั้งก้อน (whenDeleted) — ทีมที่ไม่รู้
เรื่องนี้มักเจอปัญหา orphaned PVC สะสมเต็ม cluster เรื่อยๆ จนเปลือง storage โดยไม่รู้ตัวว่า
มันมาจากการ scale StatefulSet ขึ้นลงบ่อยๆ
DaemonSet — 1 Pod ต่อ 1 Node เสมอ
ใช้เมื่อต้องการให้ทุก Node มี Pod ชนิดเดียวกันรันอยู่พอดี 1 ตัว (ไม่ใช่กำหนด replicas เอง —
จำนวนถูกกำหนดโดยจำนวน Node ที่ match เงื่อนไข) ตัวอย่างการใช้งานจริง: log collector
(Fluentd/Fluent Bit), monitoring agent (node-exporter), network plugin (CNI agent)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
containers:
- name: node-exporter
image: prom/node-exporter
"DaemonSet = ยามรักษาความปลอดภัยประจำทุกตึก" — ไม่ใช่ "จ้างยาม 5 คนไปประจำที่ไหนก็ได้" (เหมือน Deployment ที่ไม่สนว่า Node ไหน) แต่คือ "ทุกตึกต้องมียามของตัวเองคนละ 1 คนเป๊ะ" เพิ่ม ตึกใหม่ (Node ใหม่) ก็มียามไปประจำอัตโนมัติทันทีโดยไม่ต้องสั่งเพิ่ม
DaemonSet Pod มีพฤติกรรมพิเศษที่ scheduler ปกติไม่ทำกับ workload อื่น: มันถูกสร้างให้ ignore
taint บางชนิดโดย default (ผ่าน automatic toleration ที่ DaemonSet controller ใส่ให้เอง) ทำให้
Pod อย่าง log agent หรือ CNI plugin ขึ้นไปรันบน control-plane Node ได้แม้ Node นั้นจะมี taint
NoSchedule กันไม่ให้ workload ทั่วไปขึ้นไปก็ตาม — นี่คือเหตุผลที่ monitoring/logging agent
"ครบทุก Node รวมถึง control plane" ได้โดยไม่ต้องตั้ง toleration เองด้วยมือ ขณะที่ Deployment
ทั่วไปจะถูกกันออกจาก control-plane Node เสมอถ้าไม่ได้ใส่ toleration ชัดเจน
Job & CronJob — งานที่ "รันจบแล้วจบ"
Job รัน Pod จนสำเร็จแล้วหยุด (ต่างจาก Deployment ที่พยายาม restart Pod ให้ "รันตลอด") เหมาะกับ
batch processing, database migration, data import
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
template:
spec:
containers:
- name: migrate
image: my-app:1.0.0
command: ["./migrate", "up"]
restartPolicy: Never
backoffLimit: 3 # retry สูงสุด 3 ครั้งถ้า container fail
CronJob คือ Job ที่รันตาม schedule (cron syntax):
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 2 * * *" # ทุกวันตี 2
jobTemplate:
spec:
template:
spec:
containers:
- name: report
image: my-app:1.0.0
command: ["./generate-report"]
restartPolicy: OnFailure
"Job = พนักงาน freelance ทำจบงานเดียวแล้วเลิก, CronJob = freelance คนเดิมที่มาทำงานตามตารางนัด" — ต่างจาก Deployment ที่เหมือนพนักงานประจำที่ "ต้องอยู่ตลอดเวลา" ถ้าหายไปต้องหาคนมาแทนทันที
CronJob ไม่รับประกัน exactly-once execution — ถ้า kube-controller-manager เพิ่ง restart
พอดีตอนใกล้เวลา schedule หรือ clock skew เล็กน้อย มีโอกาสที่ Job จะถูกสร้างซ้ำสองครั้งหรือ
ถูกข้ามไปเลยสำหรับรอบนั้น field startingDeadlineSeconds ช่วยจำกัดว่าถ้า Job พลาด
schedule ไปนานเกินนี้ให้ข้ามรอบนั้นไปเลย (ไม่ใช่พยายามรันย้อนหลัง) และ concurrencyPolicy
(Allow/Forbid/Replace) ควบคุมว่าถ้า Job รอบก่อนยังไม่จบแล้วถึงเวลารอบใหม่จะทำยังไง —
ทีมที่เขียนงาน CronJob ที่ "ต้องรันแค่ครั้งเดียวเป๊ะๆ" (เช่น ส่ง invoice) โดยไม่ออกแบบให้
idempotent (รันซ้ำแล้วผลลัพธ์เหมือนเดิมไม่ error) มักเจอบั๊ก duplicate execution ที่ debug
ยากมากเพราะเกิดไม่บ่อยและ reproduce ยาก
Rolling Update Strategy
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25% # Pod เก่าหายไปได้สูงสุดกี่ % ระหว่าง update
maxSurge: 25% # สร้าง Pod ใหม่เกินจำนวน desired ได้สูงสุดกี่ % ระหว่าง update
maxUnavailable: 0, maxSurge: 1 คือ pattern ที่ปลอดภัยที่สุดสำหรับ workload ที่ ห้าม downtime
แม้แต่วินาทีเดียว (สร้าง Pod ใหม่ก่อน ค่อยลบตัวเก่า) แลกกับการใช้ resource ชั่วคราวสูงขึ้น
"maxSurge = ยอมเกินโต๊ะชั่วคราว, maxUnavailable = ยอมขาดโต๊ะชั่วคราว" — ร้านอาหารที่ไม่อยากให้ ลูกค้าตกค้าง (zero downtime) ต้อง "เปิดโต๊ะเกินปกติชั่วคราว" (surge) ก่อนเก็บโต๊ะเก่า ไม่ใช่ "เก็บโต๊ะเก่าก่อนแล้วค่อยเปิดใหม่" (ซึ่งจะมีช่วงที่โต๊ะไม่พอ = unavailable)
maxUnavailable/maxSurge เป็นเปอร์เซ็นต์ของ replicas ที่ตั้งไว้ตอนนั้น ไม่ใช่ตัวเลข
คงที่ — ทำให้พฤติกรรมของ rolling update เปลี่ยนไปเองถ้า replicas เปลี่ยน (เช่นหลัง HPA
scale) และมีจุดที่คนมักพลาดคือ ทั้งคู่ถูก round ลง (floor) เป็นค่าเริ่มต้น เช่น
replicas: 3 กับ maxUnavailable: 25% จะคำนวณได้ 0.75 ซึ่งถูก floor เป็น 0 ไม่ใช่ 1 —
หมายความว่า deployment ที่มี replicas น้อยมากพร้อม percentage เล็กๆ อาจมีพฤติกรรมเหมือน
maxUnavailable: 0 โดยไม่ได้ตั้งใจ ควรใช้ตัวเลขคงที่ (maxUnavailable: 1) แทน percentage
เมื่อ replicas count มีค่าน้อยเพื่อให้พฤติกรรมชัดเจนและคาดเดาได้
Horizontal Pod Autoscaler (HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
HPA ปรับ replicas ขึ้น/ลงอัตโนมัติตาม metric (CPU/memory/custom metric) — ต้องมี
resources.requests กำหนดไว้ก่อนเสมอ เพราะ HPA คำนวณ % utilization เทียบกับค่า request
ไม่ใช่ limit ถ้าไม่ตั้ง request ไว้ HPA จะทำงานไม่ได้เลย
"HPA เทียบกับ 'สัญญาที่ขอไว้' ไม่ใช่ 'เพดานสูงสุด'" — averageUtilization: 70 หมายถึง 70%
ของสิ่งที่ "ขอ" (requests) ไว้ตอนแรก ไม่ใช่ 70% ของ limits ถ้าไม่ตั้ง requests ก็ไม่มี
"สัญญา" ให้เทียบ เลย scale ไม่ได้
HPA มี stabilization window ที่ป้องกันปัญหา "flapping" (scale up-down สลับไปมาถี่ๆ จนระบบ
ไม่นิ่ง) — ค่า default คือรอ 0 วินาที ก่อน scale up (ขึ้นทันทีที่ metric เกิน) แต่รอ
300 วินาที (5 นาที) ก่อน scale down (เพื่อไม่ให้รีบลด replica ลงเร็วเกินไปจนโดน traffic
spike รอบถัดไปซ้ำ) ปรับค่านี้ได้ผ่าน behavior.scaleUp/scaleDown.stabilizationWindowSeconds
ใน autoscaling/v2 — ทีมที่ไม่รู้เรื่องนี้มักสงสัยว่า "ทำไม load ลดไปตั้งนานแล้ว replica ยัง
ไม่ลดลง" ทั้งที่จริงๆ มันทำงานตามที่ออกแบบไว้ (ป้องกัน flapping) ไม่ใช่บั๊ก
StatefulSet gives Pods stable identity, stable per-replica storage (via
volumeClaimTemplates), and ordered create/scale — needed for databases and leader-election
systems. Note that scaling a StatefulSet down does not delete the orphaned PVCs by default
(persistentVolumeClaimRetentionPolicy controls this, Kubernetes 1.27+). DaemonSet runs
exactly one Pod per matching Node and automatically tolerates certain taints (including control-
plane taints), unlike regular Deployments. Job/CronJob run-to-completion workloads; CronJob
does not guarantee exactly-once execution, so jobs should be idempotent, and concurrencyPolicy/
startingDeadlineSeconds control overlap and missed-schedule behavior. Rolling-update
maxUnavailable/maxSurge are percentages of current replicas and round down — with small
replica counts, prefer fixed numbers over percentages to avoid accidental zero-unavailable
surprises. HorizontalPodAutoscaler requires resources.requests (it scales against the
requested value, not the limit) and has an asymmetric default stabilization window — instant
scale-up, a 5-minute delay before scale-down — to prevent flapping.