Skip to main content

Workload Patterns

StatefulSet — เมื่อ Pod ต้อง "มีตัวตน" คงที่​

Deployment ปฏิบัติกับ Pod ทุกตัวเหมือนกันหมด (interchangeable) — เหมาะกับ stateless app แต่ database/message queue ต้องการ:

  1. Stable network identity — ชื่อ Pod คงที่เสมอ (db-0, db-1, db-2 ไม่ใช่ random suffix)
  2. Stable storage — แต่ละ Pod ผูกกับ PersistentVolumeClaim ของตัวเองถาวร แม้ Pod จะถูก restart ไปอยู่ Node อื่นก็ยังได้ data เดิมกลับมา
  3. 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 สุ่มเลขห้องใหม่ทุกครั้งไม่สนใจว่าของเก่าอยู่ห้องไหน

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

เมื่อ 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 ใหม่) ก็มียามไปประจำอัตโนมัติทันทีโดยไม่ต้องสั่งเพิ่ม

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

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 ที่เหมือนพนักงานประจำที่ "ต้องอยู่ตลอดเวลา" ถ้าหายไปต้องหาคนมาแทนทันที

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

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)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

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 ไม่ได้

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

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) ไม่ใช่บั๊ก

English Summary

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.