StatefulSet และ DaemonSet
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”StatefulSet ให้ Pod แต่ละตัวมี identity ที่คงที่และ unique พร้อม storage ของตัวเองที่อยู่ข้ามการ reschedule ส่วน DaemonSet การันตีว่ามี Pod ที่ตรงเงื่อนไขรันอยู่บนทุก node พอดีหนึ่งตัว สอง controller นี้แก้ปัญหาเรื่อง “identity” กับ “coverage” ต่างจาก Deployment ที่ให้แค่จำนวน replica เฉย ๆ
StatefulSet: identity คงที่และ storage ต่อ Pod
หัวข้อที่มีชื่อว่า “StatefulSet: identity คงที่และ storage ต่อ Pod”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: v1kind: Servicemetadata: name: db labels: app: dbspec: clusterIP: None selector: app: db ports: - port: 5432 name: postgres---apiVersion: apps/v1kind: StatefulSetmetadata: name: dbspec: 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 ที่บังเอิญมีอยู่ตอนนั้น
# Pods come up in order: db-0, then db-1, then db-2kubectl get pods -l app=db -o wide
# Each Pod has its own stable DNS name via the headless Servicekubectl 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 firstkubectl scale statefulset/db --replicas=2การ deploy, scale และลบ Pod ของ StatefulSet เกิดขึ้น ทีละตัว ตามลำดับ เสมอ db-1 จะไม่เริ่มจนกว่า db-0 จะรันและ ready แล้ว ส่วนการ scale ลงก็ลบ ordinal สูงสุดก่อน การเรียงลำดับแบบนี้เองที่ทำให้ logic ตอน bootstrap ของระบบ distributed พึ่งพา “สมาชิกก่อนหน้าขึ้นแล้ว” ได้อย่างปลอดภัย
podManagementPolicy: OrderedReady เทียบกับ Parallel วัดเวลาจริง
หัวข้อที่มีชื่อว่า “podManagementPolicy: OrderedReady เทียบกับ Parallel วัดเวลาจริง”พฤติกรรม “ทีละตัว” นั้นคือ podManagementPolicy: OrderedReady ซึ่งเป็นค่า default และมันถูกบังคับด้วยการรอให้ Pod หนึ่งตัว Ready จริง ๆ ก่อนจะ create ตัวถัดไป Parallel จะ create และ start ทุก replica พร้อมกันเลย ไม่มี guarantee เรื่องลำดับตอน startup เลย (ยังคง identity กับ storage ต่อ Pod ไว้เหมือนเดิม) ความต่างนี้ไม่ใช่เรื่องเล็ก ๆ พิสูจน์ 3 replica แต่ละ container ใช้เวลา ~5 วินาทีกว่าจะ Ready:
apiVersion: apps/v1kind: StatefulSetmetadata: name: sts-orderedspec: 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.37smanifest เดียวกัน เปลี่ยนแค่ 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.17s18.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.yamlstatefulset.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.localServer: 10.96.0.10Name: sts-good-0.sts-svc.default.svc.cluster.localAddress: 10.244.1.10
$ kubectl exec sts-good-0 -- nslookup sts-good-0.sts-clusterip.default.svc.cluster.localServer: 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.localServer: 10.96.0.10Name: sts-clusterip.default.svc.cluster.localAddress: 10.96.140.243DNS 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 สมบูรณ์แบบ
Canary rollout ด้วย updateStrategy.rollingUpdate.partition
หัวข้อที่มีชื่อว่า “Canary rollout ด้วย updateStrategy.rollingUpdate.partition”partition กัน rolling update ให้ทำแค่กับ Pod ที่ ordinal มากกว่าหรือเท่ากับ เลข partition เท่านั้น ตัวที่ต่ำกว่านั้นถูกปล่อยไว้เฉย ๆ แม้ Pod template จะเปลี่ยนไปแล้วก็ตาม พิสูจน์ StatefulSet 3 replica, partition: 2 แล้วเปลี่ยน image:
apiVersion: apps/v1kind: StatefulSetmetadata: name: sts-goodspec: 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.37statefulset.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.38sts-good-1 busybox:1.38sts-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 retention: persistentVolumeClaimRetentionPolicy
หัวข้อที่มีชื่อว่า “PVC retention: persistentVolumeClaimRetentionPolicy”ปกติแล้ว PVC ที่ create จาก volumeClaimTemplates จะ อยู่รอดยาวกว่า StatefulSet และ Pod ของมันเสมอ การลบ StatefulSet หรือ scale ลง จะทิ้ง PVC (และ PV/data ที่ bind อยู่) ไว้แบบตั้งใจ เพราะการทำ disk ของ db-0 หายไปโดยไม่ตั้งใจมักเป็น failure ที่แย่กว่า persistentVolumeClaimRetentionPolicy ให้คุณเลือก opt-in การลบอัตโนมัติแทนได้:
apiVersion: apps/v1kind: StatefulSetmetadata: name: sts-pvc-demospec: 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-demoNAME STATUS VOLUME AGEdata-sts-pvc-demo-0 Bound pvc-e233e68c-64ce-4ec8-9248-d37388c6adb8 17sdata-sts-pvc-demo-1 Bound pvc-dcc96b32-548f-4983-9540-18e503b4bcef 11s
$ kubectl scale statefulset sts-pvc-demo --replicas=0statefulset.apps/sts-pvc-demo scaled
# ... after sts-pvc-demo-1 finishes terminating ...$ kubectl get pvc -l app=sts-pvc-demoNAME STATUS VOLUME AGEdata-sts-pvc-demo-0 Bound pvc-e233e68c-64ce-4ec8-9248-d37388c6adb8 44sdata-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: หนึ่ง Pod บนทุก node
หัวข้อที่มีชื่อว่า “DaemonSet: หนึ่ง Pod บนทุก node”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/v1kind: DaemonSetmetadata: name: node-log-agentspec: 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# One Pod per matching node -- the count follows the cluster, not a replicas fieldkubectl get pods -l app=node-log-agent -o wideflowchart 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 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 เดียวกันก่อน แล้วค่อยเอาตัวเก่าออกเมื่อตัวใหม่พร้อมแล้วเท่านั้น:
apiVersion: apps/v1kind: DaemonSetmetadata: name: node-agentspec: 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 wideNAME READY STATUS NODEnode-agent-5bk74 1/1 Running workloads-control-plane # old generation, still servingnode-agent-t2wvw 0/1 Running workloads-control-plane # new generation, surged in, not Ready yetnode-agent-swgpf 1/1 Running workloads-worker2node-agent-wh4m7 1/1 Running workloads-workerPod สองตัวที่มี label app=node-agent อยู่บน workloads-control-plane พร้อมกัน นั่นคือ capacity ส่วนเกินที่ maxSurge: 1 ซื้อมา และเป็นเหตุผลว่าทำไม maxUnavailable: 0 ถึงปลอดภัยที่จะคู่กับมันตรงนี้ (ต่างจาก Deployment การผสมนี้ valid สำหรับ DaemonSet ตราบใดที่ maxSurge ไม่เป็น 0): Pod เก่าจะถูกเอาออกก็ต่อเมื่อตัวแทนถูกยืนยันว่า Ready แล้วเท่านั้น
Node selection: DaemonSet ไม่จำเป็นต้องหมายถึง ทุก node
หัวข้อที่มีชื่อว่า “Node selection: DaemonSet ไม่จำเป็นต้องหมายถึง ทุก node”nodeSelector (หรือ node affinity) จำกัดขอบเขต DaemonSet ให้เหลือแค่บาง node เหมือน Pod template ทั่วไป DaemonSet controller จะ schedule ลงเฉพาะ node ที่ตรงเงื่อนไขเป๊ะ ๆ ไม่เกินนั้น:
apiVersion: apps/v1kind: DaemonSetmetadata: name: gpu-agentspec: 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=truenode/workloads-worker labeled
$ kubectl apply -f ds-nodeselect.yaml && kubectl rollout status daemonset/gpu-agentdaemon set "gpu-agent" successfully rolled out
$ kubectl get pods -l app=gpu-agent -o wideNAME READY STATUS NODEgpu-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 ด้วยซ้ำ
สาม controller สามความกังวลที่ต่างกัน
หัวข้อที่มีชื่อว่า “สาม controller สามความกังวลที่ต่างกัน”Deployment ไม่สนใจว่า replica แต่ละตัวจะไปลง node ไหน และ Pod ของตัวเองถูกออกแบบให้สลับกันได้ StatefulSet สนใจ identity คงที่ต่อ ordinal ชื่อเดิม storage เดิมทุกครั้ง DaemonSet สนใจ coverage ระดับ node หนึ่ง Pod ต่อหนึ่ง node ที่ตรงเงื่อนไข จำนวนที่คุณไม่ต้องตั้งเองเพราะตามจำนวน node (หรือ node ส่วนย่อยที่ label ไว้) ของ cluster ไปเอง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน 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: 0default อยู่หน้าอะไรที่ 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 ในโค้ดนี้
apiVersion: apps/v1kind: StatefulSetmetadata:name: dbspec:serviceName: dbpodManagementPolicy: Parallelreplicas: 3selector: matchLabels: app: dbtemplate: metadata: labels: app: db spec: containers: - name: postgres image: myregistry/postgres:18volumeClaimTemplates: - 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 เป็นอิสระจากกันจริง ๆ
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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.partition | canary template ใหม่บน ordinal สูงสุดก่อน พิสูจน์แล้วข้างบน ก่อนจะเชื่อมันทุกที่ | ต้องลด/เอา partition ออกด้วยมือเพื่อให้ rollout จบ ไม่มีอะไรทำให้อัตโนมัติ |
persistentVolumeClaimRetentionPolicy: Delete | scale ลงหรือลบเคลียร์ 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 เดียวกัน
OrderedReadyserialize การ 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-inDeleteพิสูจน์แล้วข้างบน - “DaemonSet รันบนทุก node ใน cluster เสมอ” พิสูจน์แล้วข้างบน
nodeSelector(หรือ affinity/toleration) จำกัดขอบเขตให้เหลือบางส่วน และstatus.desiredNumberScheduledรายงานตรง ๆ ว่ามี node ที่ match กี่ตัว