ข้ามไปยังเนื้อหา

StatefulSet และ DaemonSet

StatefulSet ให้ Pod แต่ละตัวมี identity ที่คงที่และ unique พร้อม storage ของตัวเองที่อยู่ข้ามการ reschedule ส่วน DaemonSet การันตีว่ามี Pod ที่ตรงเงื่อนไขรันอยู่บนทุก node พอดีหนึ่งตัว สอง controller นี้แก้ปัญหาเรื่อง “identity” กับ “coverage” ต่างจาก Deployment ที่ให้แค่จำนวน replica เฉย ๆ

Pod ของ Deployment สลับกันได้ ส่วน Pod ของ StatefulSet ได้ ordinal name ที่คาดเดาได้ — db-0, db-1, db-2 — และแต่ละ ordinal ก็เก็บ PersistentVolumeClaim ของตัวเอง ที่ create จาก volumeClaimTemplates ไว้ข้ามการ restart และ reschedule db-1 จะกลับมาพร้อม volume เดิมเสมอ ไม่ว่าจะไปลงที่ node ไหนก็ตาม StatefulSet ยังต้องมี headless Service (clusterIP: None) ด้วย เพื่อให้ Pod แต่ละตัวได้ DNS name คงที่ของตัวเอง แทนที่ Service จะ load-balance กระจายไปทุกตัว

apiVersion: v1
kind: Service
metadata:
name: db
labels:
app: db
spec:
clusterIP: None
selector:
app: db
ports:
- port: 5432
name: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 3
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: myregistry/postgres:18
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ['ReadWriteOnce']
resources:
requests:
storage: 10Gi

นี่คือรูปแบบที่ใช้ทุกครั้งที่สมาชิกต้องการ identity ที่คงที่ระหว่างกัน เช่น database และระบบ distributed ใด ๆ ที่ node 0 มีความพิเศษ หรือ peer ต้องหากันด้วยชื่อคงที่ ไม่ใช่หา Pod ที่บังเอิญมีอยู่ตอนนั้น

Terminal window
# Pods come up in order: db-0, then db-1, then db-2
kubectl get pods -l app=db -o wide
# Each Pod has its own stable DNS name via the headless Service
kubectl run -it --rm debug --image=busybox:1.38 --restart=Never -- \
nslookup db-0.db.default.svc.cluster.local
# Scaling and deletion are ordered too: highest ordinal first
kubectl scale statefulset/db --replicas=2

การ deploy, scale และลบ Pod ของ StatefulSet เกิดขึ้น ทีละตัว ตามลำดับ เสมอ db-1 จะไม่เริ่มจนกว่า db-0 จะรันและ ready แล้ว ส่วนการ scale ลงก็ลบ ordinal สูงสุดก่อน การเรียงลำดับแบบนี้เองที่ทำให้ logic ตอน bootstrap ของระบบ distributed พึ่งพา “สมาชิกก่อนหน้าขึ้นแล้ว” ได้อย่างปลอดภัย

พฤติกรรม “ทีละตัว” นั้นคือ podManagementPolicy: OrderedReady ซึ่งเป็นค่า default และมันถูกบังคับด้วยการรอให้ Pod หนึ่งตัว Ready จริง ๆ ก่อนจะ create ตัวถัดไป Parallel จะ create และ start ทุก replica พร้อมกันเลย ไม่มี guarantee เรื่องลำดับตอน startup เลย (ยังคง identity กับ storage ต่อ Pod ไว้เหมือนเดิม) ความต่างนี้ไม่ใช่เรื่องเล็ก ๆ พิสูจน์ 3 replica แต่ละ container ใช้เวลา ~5 วินาทีกว่าจะ Ready:

k8s/statefulsets-and-daemonsets/sts-ordered.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-ordered
spec:
serviceName: sts-svc
podManagementPolicy: OrderedReady
replicas: 3
selector:
matchLabels:
app: sts-demo
template:
metadata:
labels:
app: sts-demo
spec:
containers:
- name: app
image: busybox:1.38
command: ['sh', '-c', 'sleep 5 && touch /tmp/ready && sleep 3600']
readinessProbe:
exec:
command: ['test', '-f', '/tmp/ready']
periodSeconds: 1
$ time (kubectl apply -f sts-ordered.yaml && kubectl rollout status statefulset/sts-ordered)
Waiting for 3 pods to be ready...
Waiting for 2 pods to be ready...
Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 3 new pods have been updated...
OrderedReady: all 3 Ready in 18.37s

manifest เดียวกัน เปลี่ยนแค่ podManagementPolicy: Parallel:

$ time (kubectl apply -f sts-parallel.yaml && kubectl rollout status statefulset/sts-parallel)
Waiting for 3 pods to be ready...
Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 3 new pods have been updated...
Parallel: all 3 Ready in 6.17s

18.4 วินาที เทียบกับ 6.2 วินาที คือประมาณ 3 เท่า เพราะ OrderedReady รอให้ startup 5 วินาทีของแต่ละตัวจบจริง ๆ ตามลำดับ ก่อนจะ create ตัวถัดไปด้วยซ้ำ (sts-1 จะไม่ถูก create จนกว่า sts-0 จะ Ready) ส่วน Parallel create ทั้งสามตัวทันที แล้วแต่ละตัวก็ใช้เวลา ~5 วินาทีของตัวเองพร้อมกัน เลือกใช้ Parallel เมื่อ replica ไม่ได้พึ่งพาลำดับ bootstrap จริง ๆ (cache ส่วนใหญ่ worker ที่ shard แบบไม่พึ่งกันหลายตัวที่ใช้ StatefulSet แค่เพื่อ identity/storage คงที่) คง default OrderedReady ไว้เมื่อ logic ตอน startup ของ ordinal หลังพึ่งพาว่า ordinal ก่อนหน้าขึ้นแล้ว

serviceName ที่หายไปหรือไม่ใช่ headless: DNS record ไม่มีอยู่จริง ๆ

หัวข้อที่มีชื่อว่า “serviceName ที่หายไปหรือไม่ใช่ headless: DNS record ไม่มีอยู่จริง ๆ”

serviceName ไม่ถูก validate กับ Service จริงตอน create เลย API server ยอมรับ StatefulSet ที่ serviceName ชี้ไปที่ไม่มีอะไรเลย:

$ kubectl apply --dry-run=server -f sts-badsvc.yaml
statefulset.apps/sts-bad created (server dry run) # accepted -- no check that "sts-badsvc" exists

ผลที่ตามมาโผล่มาแค่ตอน DNS lookup fail เท่านั้น พิสูจน์ด้วย nslookup จากข้างใน Pod เทียบ headless Service จริง (sts-svc, clusterIP: None) กับ Service แบบ ClusterIP ธรรมดา (sts-clusterip) ที่มี Pod selector เดียวกัน:

$ kubectl exec sts-good-0 -- nslookup sts-good-0.sts-svc.default.svc.cluster.local
Server: 10.96.0.10
Name: sts-good-0.sts-svc.default.svc.cluster.local
Address: 10.244.1.10
$ kubectl exec sts-good-0 -- nslookup sts-good-0.sts-clusterip.default.svc.cluster.local
Server: 10.96.0.10
** server can't find sts-good-0.sts-clusterip.default.svc.cluster.local: NXDOMAIN
$ kubectl exec sts-good-0 -- nslookup sts-clusterip.default.svc.cluster.local
Server: 10.96.0.10
Name: sts-clusterip.default.svc.cluster.local
Address: 10.96.140.243

DNS name ต่อ Pod (<pod>.<service>...) มีอยู่ได้ก็เพราะ CoreDNS สร้างมันขึ้นจาก endpoint list ของ Service ที่เป็น headless เท่านั้น Service แบบ ClusterIP ธรรมดายัง resolve ชื่อของตัวเองได้ปกติ (A record เดียว คือ cluster IP) แค่ไม่เคยได้ record subdomain ต่อ Pod เลย ชี้ serviceName ไปที่ Service ที่ไม่มีอยู่จริง หรือ Service จริงแต่ไม่ใช่ headless แล้วทุก lookup <pod-name>.<serviceName> ที่ peer-discovery bootstrap พึ่งพาอยู่จะได้ NXDOMAIN กลับมาทุกครั้ง โดยไม่มี error โผล่ที่ kubectl get/describe เลยสักที่ เพราะตัว StatefulSet เองก็ healthy สมบูรณ์แบบ

partition กัน rolling update ให้ทำแค่กับ Pod ที่ ordinal มากกว่าหรือเท่ากับ เลข partition เท่านั้น ตัวที่ต่ำกว่านั้นถูกปล่อยไว้เฉย ๆ แม้ Pod template จะเปลี่ยนไปแล้วก็ตาม พิสูจน์ StatefulSet 3 replica, partition: 2 แล้วเปลี่ยน image:

k8s/statefulsets-and-daemonsets/sts-partition.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-good
spec:
serviceName: sts-svc
replicas: 3
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2
selector:
matchLabels:
app: sts-demo
template:
metadata:
labels:
app: sts-demo
spec:
containers:
- name: app
image: busybox:1.37
$ kubectl set image statefulset/sts-good app=busybox:1.37
statefulset.apps/sts-good image updated
$ kubectl get pods -l app=sts-demo -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'
sts-good-0 busybox:1.38
sts-good-1 busybox:1.38
sts-good-2 busybox:1.37

มีแค่ sts-good-2 (ordinal 2 >= partition 2) เท่านั้นที่ได้ image ใหม่ sts-good-0 กับ sts-good-1 ไม่ถูกแตะเลย นี่แหละคือ pattern canary เป๊ะ ๆ ตรวจสอบ template ใหม่บน ordinal สูงสุดก่อน แล้วค่อยลด partition (หรือเอาออก) เพื่อให้ rollout เดินหน้าต่อลงมาจนถึง 0

ปกติแล้ว PVC ที่ create จาก volumeClaimTemplates จะ อยู่รอดยาวกว่า StatefulSet และ Pod ของมันเสมอ การลบ StatefulSet หรือ scale ลง จะทิ้ง PVC (และ PV/data ที่ bind อยู่) ไว้แบบตั้งใจ เพราะการทำ disk ของ db-0 หายไปโดยไม่ตั้งใจมักเป็น failure ที่แย่กว่า persistentVolumeClaimRetentionPolicy ให้คุณเลือก opt-in การลบอัตโนมัติแทนได้:

k8s/statefulsets-and-daemonsets/sts-pvc-retain.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-pvc-demo
spec:
serviceName: sts-svc
replicas: 2
persistentVolumeClaimRetentionPolicy:
whenDeleted: Delete
whenScaled: Delete
selector:
matchLabels:
app: sts-pvc-demo
template:
metadata:
labels:
app: sts-pvc-demo
spec:
containers:
- name: app
image: busybox:1.38
command: ['sh', '-c', 'sleep 3600']
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ['ReadWriteOnce']
resources:
requests:
storage: 1Mi
$ kubectl get pvc -l app=sts-pvc-demo
NAME STATUS VOLUME AGE
data-sts-pvc-demo-0 Bound pvc-e233e68c-64ce-4ec8-9248-d37388c6adb8 17s
data-sts-pvc-demo-1 Bound pvc-dcc96b32-548f-4983-9540-18e503b4bcef 11s
$ kubectl scale statefulset sts-pvc-demo --replicas=0
statefulset.apps/sts-pvc-demo scaled
# ... after sts-pvc-demo-1 finishes terminating ...
$ kubectl get pvc -l app=sts-pvc-demo
NAME STATUS VOLUME AGE
data-sts-pvc-demo-0 Bound pvc-e233e68c-64ce-4ec8-9248-d37388c6adb8 44s

data-sts-pvc-demo-1 หายไปแล้ว whenScaled: Delete ลบมันทิ้งทันทีที่ Pod ของมัน terminate เสร็จสมบูรณ์ (ใช้เวลาตาม termination grace period ปกติของ container ไม่ใช่ตัว retention policy เองที่ควบคุมเวลานี้) scale ขึ้นกลับมาจะ create PVC ใหม่เอี่ยม ว่างเปล่าให้ ordinal 1 ไม่ใช่ตัวเก่า ถ้าไม่มี policy นี้ (default คือเหมือน Retain ทั้งคู่) การ scale sts-pvc-demo ขึ้นกลับมาจะ rebind PVC data-sts-pvc-demo-1 ตัวเดิม พร้อม data เก่าครบเลย

DaemonSet ไม่มีค่า replicas เลย แต่จะ schedule Pod ที่ตรงเงื่อนไขพอดีหนึ่งตัวลงทุก node ใน cluster (หรือบางส่วน ถ้าจำกัดขอบเขตด้วย node selector หรือ affinity rule) พอ node ใหม่เข้าร่วม cluster DaemonSet controller ก็จะ schedule Pod ของตัวเองลง node นั้นให้อัตโนมัติ พอ node ออกไป Pod ก็หายไปด้วย นี่คือรูปแบบสำหรับ agent ระดับ node เช่น log collector, monitoring และ metrics agent และ network plugin

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-log-agent
spec:
selector:
matchLabels:
app: node-log-agent
template:
metadata:
labels:
app: node-log-agent
spec:
containers:
- name: log-agent
image: myregistry/log-agent:3.0.0
volumeMounts:
- name: varlog
mountPath: /var/log
volumes:
- name: varlog
hostPath:
path: /var/log
Terminal window
# One Pod per matching node -- the count follows the cluster, not a replicas field
kubectl get pods -l app=node-log-agent -o wide
flowchart TB
  subgraph ss["StatefulSet db (ordered identity)"]
    w0["db-0"] --- s0[("data-db-0")]
    w1["db-1"] --- s1[("data-db-1")]
    w2["db-2"] --- s2[("data-db-2")]
  end
  subgraph ds["DaemonSet node-log-agent (one per node)"]
    n1["Node 1"] --> a1["log-agent"]
    n2["Node 2"] --> a2["log-agent"]
    n3["Node 3"] --> a3["log-agent"]
  end
StatefulSet: Pod เรียงลำดับพร้อม storage ของตัวเอง. DaemonSet: หนึ่ง Pod ต่อหนึ่ง node

DaemonSet updateStrategy: maxSurge ให้ตัวแทนขึ้นมาก่อนตัวเก่าจะลง

หัวข้อที่มีชื่อว่า “DaemonSet updateStrategy: maxSurge ให้ตัวแทนขึ้นมาก่อนตัวเก่าจะลง”

RollingUpdate default ของ DaemonSet (maxUnavailable: 1, maxSurge: 0) ฆ่า Pod เก่าบน node ก่อนจะ create ตัวแทน สำหรับ agent ระดับ node นี่หมายถึงช่วง coverage ขาดหายไปสั้น ๆ บน node นั้น การตั้ง maxSurge (Kubernetes 1.22+) ให้ Pod surge ของ generation ใหม่ขึ้นมา คู่กับ ตัวเก่าบน node เดียวกันก่อน แล้วค่อยเอาตัวเก่าออกเมื่อตัวใหม่พร้อมแล้วเท่านั้น:

k8s/statefulsets-and-daemonsets/ds-maxsurge.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-agent
spec:
selector:
matchLabels:
app: node-agent
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: node-agent
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: agent
image: busybox:1.38
command: ['sh', '-c', 'sleep 3600']

กลาง rollout ด้วย image ตัวใหม่ที่ ready ช้า kubectl get pods -o wide จับได้ว่า surge Pod กับ Pod เก่าอยู่ บน node เดียวกัน พร้อมกัน:

$ kubectl get pods -l app=node-agent -o wide
NAME READY STATUS NODE
node-agent-5bk74 1/1 Running workloads-control-plane # old generation, still serving
node-agent-t2wvw 0/1 Running workloads-control-plane # new generation, surged in, not Ready yet
node-agent-swgpf 1/1 Running workloads-worker2
node-agent-wh4m7 1/1 Running workloads-worker

Pod สองตัวที่มี label app=node-agent อยู่บน workloads-control-plane พร้อมกัน นั่นคือ capacity ส่วนเกินที่ maxSurge: 1 ซื้อมา และเป็นเหตุผลว่าทำไม maxUnavailable: 0 ถึงปลอดภัยที่จะคู่กับมันตรงนี้ (ต่างจาก Deployment การผสมนี้ valid สำหรับ DaemonSet ตราบใดที่ maxSurge ไม่เป็น 0): Pod เก่าจะถูกเอาออกก็ต่อเมื่อตัวแทนถูกยืนยันว่า Ready แล้วเท่านั้น

nodeSelector (หรือ node affinity) จำกัดขอบเขต DaemonSet ให้เหลือแค่บาง node เหมือน Pod template ทั่วไป DaemonSet controller จะ schedule ลงเฉพาะ node ที่ตรงเงื่อนไขเป๊ะ ๆ ไม่เกินนั้น:

k8s/statefulsets-and-daemonsets/ds-nodeselect.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: gpu-agent
spec:
selector:
matchLabels:
app: gpu-agent
template:
metadata:
labels:
app: gpu-agent
spec:
nodeSelector:
gpu: 'true'
containers:
- name: agent
image: busybox:1.38
command: ['sh', '-c', 'sleep 3600']
$ kubectl label node workloads-worker gpu=true
node/workloads-worker labeled
$ kubectl apply -f ds-nodeselect.yaml && kubectl rollout status daemonset/gpu-agent
daemon set "gpu-agent" successfully rolled out
$ kubectl get pods -l app=gpu-agent -o wide
NAME READY STATUS NODE
gpu-agent-pdtsz 1/1 Running workloads-worker
$ kubectl get daemonset gpu-agent -o jsonpath='{.status.desiredNumberScheduled}{" desired, "}{.status.currentNumberScheduled}{" scheduled (out of 3 nodes)"}'
1 desired, 1 scheduled (out of 3 nodes)

desiredNumberScheduled: 1 บน cluster 3 node คือ controller บอกตรง ๆ ว่ามี node ที่ match กี่ตัว อีกสอง node ที่เหลือไม่เคยได้ Pod เลย ไม่เคยเป็น candidate ด้วยซ้ำ

Deployment ไม่สนใจว่า replica แต่ละตัวจะไปลง node ไหน และ Pod ของตัวเองถูกออกแบบให้สลับกันได้ StatefulSet สนใจ identity คงที่ต่อ ordinal ชื่อเดิม storage เดิมทุกครั้ง DaemonSet สนใจ coverage ระดับ node หนึ่ง Pod ต่อหนึ่ง node ที่ตรงเงื่อนไข จำนวนที่คุณไม่ต้องตั้งเองเพราะตามจำนวน node (หรือ node ส่วนย่อยที่ label ไว้) ของ cluster ไปเอง

ตอน review StatefulSet หรือ DaemonSet ที่ AI generate มา เช็คพวกนี้

  • StatefulSet ที่ไม่มี headless Service เลย หรือ serviceName ชี้ไปที่ Service แบบ ClusterIP ธรรมดา พิสูจน์แล้วข้างบนว่านี่ไม่ error ที่ไหนเลย แค่ทำให้ทุก <pod>.<service> DNS lookup คืน NXDOMAIN
  • podManagementPolicy: Parallel บน StatefulSet ที่ init logic คาดว่า ordinal-1 ขึ้นแล้ว (การเปลี่ยนแบบ copy-paste “ให้เร็วขึ้น” ที่พบบ่อย) พิสูจน์แล้วข้างบนว่า Parallel เอา guarantee เรื่องลำดับ startup ออกไปจริง ๆ ไม่ใช่แค่ปรับ performance
  • ไม่มี persistentVolumeClaimRetentionPolicy บน StatefulSet ที่ผู้เขียนคาดว่าจะ ephemeral เต็มตัว (เช่น ใน environment ทดสอบ/CI แบบใช้แล้วทิ้ง) default จะเก็บ PVC ไว้ตลอดไป พิสูจน์แล้วข้างบน scale ไป 0 แล้วกลับมาไม่ได้ทำให้ข้อมูลว่างเปล่าเลย เว้นแต่ตั้ง whenScaled: Delete
  • DaemonSet ที่ใช้ maxUnavailable: 1, maxSurge: 0 default อยู่หน้าอะไรที่ latency-sensitive (webhook injector sidecar ของ service mesh, agent ที่ใกล้ชิดกับ CNI) ที่ช่วง coverage ขาดหายบน node ตอน rollout มีค่าใช้จ่ายจริง maxSurge (พิสูจน์แล้วข้างบน) คือทางแก้ ไม่ใช่การ freeze rollout ด้วยมือ
  • คิดว่า DaemonSet รันบนทุก node ใน cluster แน่นอนโดยไม่เช็ค nodeSelector/affinity/toleration เลย พิสูจน์แล้วข้างบน status.desiredNumberScheduled อาจน้อยกว่าจำนวน node มาก และ node control-plane ต้องมี toleration ชัดเจนถึงจะได้ Pod

🐛 หา bug ในโค้ดนี้

k8s/statefulsets-and-daemonsets/db-ephemeral.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
podManagementPolicy: Parallel
replicas: 3
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: myregistry/postgres:18
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ['ReadWriteOnce']
resources:
requests:
storage: 10Gi
เฉลย

มีปัญหาจริงสองอย่าง ไม่ใช่แค่หนึ่ง อย่างแรก podManagementPolicy: Parallel บน StatefulSet ของ database ทิ้ง guarantee เรื่องลำดับ (“node 0 ขึ้นก่อนที่ node 1 จะพยายาม join”) ที่ bootstrap logic ของ clustered database ส่วนใหญ่พึ่งพาอยู่ พิสูจน์แล้วข้างบนว่า Parallel create ทั้งสาม Pod พร้อมกันโดยไม่มีลำดับ startup เลย ซึ่งเป็น correctness bug สำหรับ Postgres cluster ไม่ใช่แค่เรื่องความเร็ว อย่างที่สอง ไม่มี Service ชื่อ db ถูก define ไว้ที่ไหนเลยใน manifest นี้ (หรือ fence อื่นในบทเดียวกัน) serviceName: db reference Service ที่อาจไม่มีอยู่จริง และพิสูจน์แล้วข้างบนว่า API server ไม่ validate เรื่องนี้ตอน create เลย อาการเดียวที่จะเห็นคือทุก <pod>.db.<ns>.svc.cluster.local lookup ของ peer คืน NXDOMAIN เงียบ ๆ โดยตัว StatefulSet เองรายงานว่า healthy สมบูรณ์แบบ ทางแก้: สร้าง headless Service ที่ชื่อ db (clusterIP: None, selector ตรงกัน) คู่กับ StatefulSet นี้ แล้วเอา podManagementPolicy: Parallel ออก (หรือเปลี่ยนเป็น default OrderedReady) เว้นแต่ทุก replica bootstrap เป็นอิสระจากกันจริง ๆ

StatefulSet ไม่ใช่ “Deployment ที่มี storage” มันคือ controller ที่หน้าที่ทั้งหมดคือ ผูก ordinal เข้ากับ identity คงที่ (ชื่อ, DNS record, PVC) แล้วบังคับลำดับรอบ ๆ การเปลี่ยนแปลงของ binding นั้น ทุกปุ่มตรงนี้ (podManagementPolicy, partition, PVC retention) จริง ๆ แล้วคือคำถามว่า “จะบังคับลำดับเข้มแค่ไหน และ identity อยู่รอดนานแค่ไหน” DaemonSet เป็น controller แบบตรงกันข้าม มันไม่จัดการ identity เลย มันจัดการ coverage desired state ไม่ใช่ตัวเลขที่คุณเลือกเอง มันคือ “หนึ่ง Pod ทุกที่ที่ selector นี้ match node” และทุกปุ่มตรงนี้ (nodeSelector, maxSurge) คือคำถามว่า “node ไหนบ้าง และ overlap แค่ไหนระหว่างเปลี่ยนแปลง”

ทางเลือกได้อะไรเสียอะไร
podManagementPolicy: OrderedReady (default)พึ่งพา “ordinal ก่อนหน้าขึ้นแล้ว” ใน bootstrap logic ได้อย่างปลอดภัยพิสูจน์แล้วข้างบน: ช้ากว่า Parallel ประมาณ 3 เท่ากว่าจะ Ready ครบสำหรับจำนวน replica เท่ากัน
podManagementPolicy: Parallelพิสูจน์แล้วข้างบน: ทุก replica start พร้อมกัน เร็วกว่ามากกว่าจะ Ready ครบไม่มี guarantee ลำดับ startup เลย ตัวเลือกผิดสำหรับอะไรที่ bootstrap พึ่งพามัน
updateStrategy.rollingUpdate.partitioncanary template ใหม่บน ordinal สูงสุดก่อน พิสูจน์แล้วข้างบน ก่อนจะเชื่อมันทุกที่ต้องลด/เอา partition ออกด้วยมือเพื่อให้ rollout จบ ไม่มีอะไรทำให้อัตโนมัติ
persistentVolumeClaimRetentionPolicy: Deletescale ลงหรือลบเคลียร์ storage จริง พิสูจน์แล้วข้างบนdefault คือ Retain ด้วยเหตุผล การเลือก Delete หมายความว่า scale-to-0 ผิดพลาดจะทำลาย data
DaemonSet maxSurgeไม่มีช่วง coverage ขาดหายระดับ node ระหว่าง rollout พิสูจน์แล้วข้างบนรันสอง Pod ต่อ node ที่กำลังอัปเดตชั่วคราว node ที่ทรัพยากรตึงอาจไม่มีที่พอสำหรับทั้งคู่
  • “OrderedReady เทียบกับ Parallel เป็นแค่รายละเอียด implementation ข้างใน ไม่ได้เปลี่ยนพฤติกรรมจริง” พิสูจน์แล้วข้างบนด้วยเวลาจริง 18.4 วินาที เทียบกับ 6.2 วินาที สำหรับ 3 replica เดียวกัน OrderedReady serialize การ create จริง ๆ ไม่ใช่แค่ report readiness ต่างกัน
  • “ถ้า serviceName ผิด StatefulSet จะโชว์ error” พิสูจน์แล้วข้างบน API server ยอมรับ serviceName ที่ไม่มีอยู่จริงหรือไม่ใช่ headless โดยไม่บ่นอะไรเลย อาการเดียวคือ NXDOMAIN บน DNS name ต่อ Pod
  • “partition หยุด rollout ทั้งหมด” พิสูจน์แล้วข้างบน มันไม่ได้หยุดอะไรเลย มันขีดเส้น ทุก ordinal >= partition อัปเดตทันทีตามปกติ มีแค่ ordinal ที่ต่ำกว่าเท่านั้นที่ถูกปล่อยไว้เฉย ๆ
  • “ลบ StatefulSet จะเคลียร์ storage ของมันด้วย” พฤติกรรม default ตรงกันข้ามเลย PVC จาก volumeClaimTemplates อยู่รอดยาวกว่า StatefulSet เสมอ เว้นแต่ persistentVolumeClaimRetentionPolicy จะ opt-in Delete พิสูจน์แล้วข้างบน
  • “DaemonSet รันบนทุก node ใน cluster เสมอ” พิสูจน์แล้วข้างบน nodeSelector (หรือ affinity/toleration) จำกัดขอบเขตให้เหลือบางส่วน และ status.desiredNumberScheduled รายงานตรง ๆ ว่ามี node ที่ match กี่ตัว
StatefulSet ให้ identity คงที่แบบไหนกับ Pod ที่ Deployment ไม่ให้
ทำไม StatefulSet ต้องมี headless Service (clusterIP: None)
ทำไมถึงใช้ DaemonSet สำหรับ log-collection agent แทน Deployment
สิ่งที่ Deployment ตั้งใจไม่การันตี ต่างจาก StatefulSet คืออะไร
StatefulSet 3 replica เดียวกัน container ที่ ready ใน ~5 วินาทีเหมือนกัน เปลี่ยนแค่ podManagementPolicy เวลาจริงนี้พิสูจน์อะไร
OrderedReady: all 3 Ready in 18.37s
Parallel:     all 3 Ready in 6.17s
เกิดอะไรขึ้นเมื่อ apply สิ่งนี้บน cluster จริง แล้วจะสังเกตเห็นได้ยังไง
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db-svc-that-does-not-exist
  replicas: 3