รีวิวจริง 2: stateful database พร้อม backup CronJob
Feature ที่ขอ
หัวข้อที่มีชื่อว่า “Feature ที่ขอ”Prompt คือ: “รัน database 3 replica แบบ StatefulSet มี storage แยกต่อ Pod, มี backup job รายคืน แล้ว password เก็บใน Secret” สิ่งที่ตามมาคือ draft แรกที่ดูสมเหตุสมผลพอ ๆ กับรีวิว 1 — ทุก object แยกดูก็เหมือน StatefulSet tutorial จริง มี bug จริง 7 ตัว เช็ค AI bug catalog สำหรับ pattern ทั่วไปเบื้องหลังแต่ละตัว ทุกข้อความอ้างด้านล่างพิสูจน์จริงบน kind cluster 2 node เดียวกับที่ใช้ใน รีวิวจริง 1 (reading-b, Kubernetes 1.37.0) — kubectl apply จริง, nslookup จริงจากใน Pod, kubectl drain แบบจับเวลาจริง และรัน CronJob schedule จริงประมาณ 3 นาที
โค้ดที่ AI generate ให้
หัวข้อที่มีชื่อว่า “โค้ดที่ AI generate ให้”# db headless Service, as generated -- see review belowapiVersion: v1kind: Servicemetadata: name: dbspec: clusterIP: None selector: app: db ports: - port: 5432# db StatefulSet, as generated -- see review belowapiVersion: apps/v1kind: StatefulSetmetadata: name: dbspec: serviceName: db-headless replicas: 3 selector: matchLabels: app: db template: metadata: labels: app: db spec: containers: - name: db image: busybox:1.36 command: ["sh", "-c", "sleep 3600"] volumeMounts: - name: data mountPath: /var/lib/db volumes: - name: data hostPath: path: /mnt/db-data# db-credentials Secret, as generated -- see review belowapiVersion: v1kind: Secretmetadata: name: db-credentials# "encrypted" -- see reviewdata: password: c3VwZXJzZWNyZXQxMjMh# @expect-error Job is invalid: spec.template.spec.restartPolicy: Required value: valid values: "OnFailure", "Never"apiVersion: batch/v1kind: Jobmetadata: name: db-backup-manualspec: template: spec: restartPolicy: Always containers: - name: backup image: busybox:1.36 command: ["sh", "-c", "echo backing up && sleep 2"]# db-backup CronJob, as generated -- see review belowapiVersion: batch/v1kind: CronJobmetadata: name: db-backupspec: schedule: '0 2 * * *' jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: backup image: busybox:1.36 command: ["sh", "-c", "echo backup start; sleep 90; echo backup done"]# db-app PodDisruptionBudget, as generated -- see review belowapiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: db-app-pdbspec: minAvailable: 3 selector: matchLabels: app: db-appเดินรีวิวทีละจุด
หัวข้อที่มีชื่อว่า “เดินรีวิวทีละจุด”StatefulSet: serviceName เพี้ยนไปจาก Service ที่สร้างจริง
หัวข้อที่มีชื่อว่า “StatefulSet: serviceName เพี้ยนไปจาก Service ที่สร้างจริง”StatefulSet บอก serviceName: db-headless แต่ Service block ด้านบนชื่อ db API server ไม่เคย validate serviceName กับ Service จริงเลย — ทั้งคู่ apply ผ่านเนียน ๆ Pod ทั้ง 3 ถึง Running หมด พิสูจน์ด้วย per-Pod DNS lookup จริงจากใน db-0:
$ kubectl exec db-0 -- nslookup db-1.db-headless.wr2.svc.cluster.local
** server can't find db-1.db-headless.wr2.svc.cluster.local: NXDOMAIN🐛 หา bug ในโค้ดนี้
spec:serviceName: db-headless # the Service above is actually named "db"เฉลย
spec.serviceName ของ StatefulSet ตั้ง spec.subdomain ของแต่ละ Pod (เช็คตรงแล้ว: kubectl get pod db-1 -o jsonpath='{.spec.subdomain}' คืน db-headless) — และ per-Pod DNS record จะถูก publish ก็ต่อเมื่ออยู่ใต้ Service ที่ชื่อของมันเองตรงกับ subdomain นั้น ไม่ว่า Service นั้นจะ select Pod ถูกหรือไม่ก็ตาม ตรงนี้ไม่มี Service ชื่อ db-headless เลย เลยไม่มีอะไรให้ CoreDNS publish ใต้ชื่อนั้น lookup เลยได้ NXDOMAIN สะอาด ๆ — ขณะที่ kubectl get statefulset db รายงานว่า StatefulSet เองแข็งแรงดี 3/3 นี่คือ rename-drift แบบที่ AI assistant มักทำเวลาเขียน Service กับ StatefulSet แยกเป็นคนละ step แล้วไม่กลับมาเช็คตัวที่สองกับตัวแรกอีกที วิธีแก้คือทำให้ชื่อสองฝั่งตรงกัน: serviceName: db (ให้ตรงกับ Service ที่มีอยู่จริง) หรือเปลี่ยนชื่อ Service เป็น db-headless ถ้านั่นคือชื่อที่ตั้งใจไว้ทั้งโปรเจกต์
StatefulSet: hostPath สำหรับข้อมูล persistent ของ database
หัวข้อที่มีชื่อว่า “StatefulSet: hostPath สำหรับข้อมูล persistent ของ database”Container mount volume แบบ hostPath (/mnt/db-data) แทนที่จะใช้ volumeClaimTemplates แยกต่อ Pod บทเรียน Volumes and Volume Types ของคอร์สนี้พิสูจน์ตรงไปแล้วว่าข้อมูล hostPath ไม่ตามไปกับ Pod ที่ถูก reschedule (marker file ที่เขียนไว้บน node หนึ่งหายไปเฉย ๆ — No such file or directory จริง — จาก Pod ที่เหมือนกันเป๊ะแต่ถูก pin ไปคนละ node) ข้อเท็จจริงเดียวกันนี้ใช้ได้ตรงนี้โดยไม่ต้องพิสูจน์ซ้ำ
🐛 หา bug ในโค้ดนี้
volumes:- name: datahostPath: path: /mnt/db-dataเฉลย
hostPath ผูกข้อมูลไว้กับnodeที่ Pod บังเอิญไปตกอยู่ ไม่ใช่กับตัวตนของ Pod เอง — ตรงข้ามกับสิ่งที่ ordinal identity ของ StatefulSet (db-0, db-1, db-2) ควรจะรับประกันให้ 3 replica ใช้ path /mnt/db-data เดียวกันเป๊ะผิดสองต่อ: ถ้าทั้งสามไปตกที่ node เดียวกัน ก็แชร์ directory เดียวกันเงียบ ๆ แทนที่จะมีข้อมูลแยกกันจริง; ถ้า scheduler กระจาย (กรณีปกติ และเป็นเหตุผลที่ PersistentVolumeClaim มีอยู่เพื่อทำให้ปลอดภัย) db-1 ที่ถูก reschedule จะเริ่มด้วย directory ว่างเปล่า (หรือของเหลือจาก Pod อื่น) บน node ใหม่ โดยไม่มี error ที่ไหนเลย วิธีแก้คือใช้ volumeClaimTemplates แยกต่อ replica ที่ backed ด้วย PersistentVolumeClaim จริง — storage ที่อยู่ได้เองไม่ขึ้นกับ node ไหน แล้วตามไปกับ ordinal ไม่ใช่ตาม node
Secret: comment ที่อ้างว่า “encrypted”
หัวข้อที่มีชื่อว่า “Secret: comment ที่อ้างว่า “encrypted””comment ในตัว manifest เองเรียกข้อมูลของ Secret ว่า “encrypted” พิสูจน์ด้วย command เดียวกันเป๊ะที่ใครก็ตามที่มี API read access รันได้:
$ kubectl get secret db-credentials -o jsonpath='{.data.password}'c3VwZXJzZWNyZXQxMjMh
$ kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 --decodesupersecret123!🐛 หา bug ในโค้ดนี้
# "encrypted" -- see reviewdata:password: c3VwZXJzZWNyZXQxMjMhเฉลย
ค่า data ของ Secret เป็นแค่ base64-encode ไม่ใช่ encrypt — เป็นการ encode ไม่ใช่ cipher ใครก็ตามที่มี read access เข้าถึง object นี้ (หรือเข้าถึง etcd ตรง ๆ) reverse กลับได้ในคำสั่งเดียว ไม่ต้องใช้ key เลย พิสูจน์แล้วด้านบน comment นี้คือแบบที่ AI assistant เขียนตอน pattern-match “เป็น Secret object” เข้ากับ “นี่คือตัวที่ปลอดภัยแล้ว” อย่างมั่นใจแต่ผิด วิธีแก้ไม่ใช่ field ใน YAML — แต่เป็นเรื่อง process: ความลับจริงต้องอาศัย encryption at rest (EncryptionConfiguration ระดับ cluster ซึ่งอยู่นอกเหนือการควบคุมของ Secret object นี้เลย), RBAC ที่รัดกุมว่าใคร get/list Secret ได้บ้าง หรือดึงค่าจาก external secret manager แทนการเก็บใน field data ที่ใกล้เคียง plaintext
Job: restartPolicy: Always บน manual backup Job
หัวข้อที่มีชื่อว่า “Job: restartPolicy: Always บน manual backup Job”$ kubectl apply -f db-backup-manual.yamlThe Job "db-backup-manual" is invalid: spec.template.spec.restartPolicy: Required value: valid values: "OnFailure", "Never"🐛 หา bug ในโค้ดนี้
spec:template: spec: restartPolicy: Alwaysเฉลย
หน้าที่ทั้งหมดของ Job คือรันจนเสร็จแล้วหยุด — restartPolicy: Always (ค่า default ของ Pod และเป็นค่าเดียวที่ Deployment ใช้ได้) เป็นค่าเดียวที่ Pod template ของ Job ใช้ไม่ได้เลย API server ปฏิเสธตรง ๆ แทนที่จะรับแล้วพฤติกรรมประหลาด วิธีแก้คือ OnFailure (retry Pod เดิมซ้ำ นับรวมกับ backoffLimit) หรือ Never (สร้าง Pod ใหม่เมื่อ fail) — jobTemplate ของ CronJob ด้านล่างใช้ OnFailure ถูกต้องอยู่แล้ว เลยเห็นได้ว่า drift เกิดขึ้นตรงสำเนา Job แบบ manual นี้เอง
CronJob: ไม่มี concurrencyPolicy ทำให้ backup ซ้อนกัน
หัวข้อที่มีชื่อว่า “CronJob: ไม่มี concurrencyPolicy ทำให้ backup ซ้อนกัน”CronJob ไม่มี concurrencyPolicy เลย (default เป็น Allow) และ backup body รันนาน 90 วินาที — นานกว่า schedule interval 1 นาที พิสูจน์ด้วยการตั้ง schedule เป็น * * * * * แล้วรันจริงประมาณ 3 นาทีกับ CronJob สองตัวที่เหมือนกันทุกอย่างยกเว้นตัวหนึ่งปล่อย default อีกตัวตั้ง concurrencyPolicy: Forbid:
# default (Allow): ทุก tick สร้าง Job ใหม่ ไม่สนว่าตัวก่อนหน้าทำอะไรอยู่$ kubectl describe cronjob backup-noconcurrencyEvents: Normal SuccessfulCreate 2m59s cronjob-controller Created job backup-noconcurrency-29840476 Normal SuccessfulCreate 119s cronjob-controller Created job backup-noconcurrency-29840477 Normal SawCompletedJob 86s cronjob-controller Saw completed job: backup-noconcurrency-29840476, condition: Complete Normal SuccessfulCreate 59s cronjob-controller Created job backup-noconcurrency-29840478backup-noconcurrency-29840476 สร้างตอน :16:00 และ (duration 93s) เสร็จประมาณ :17:33; ...-29840477 สร้างตอน :17:00 — มีช่วงจริงประมาณ 33 วินาทีที่ backup Job สองตัวรันพร้อมกัน
# concurrencyPolicy: Forbid: controller ข้าม/เลื่อนแทนการซ้อนกัน$ kubectl describe cronjob backup-fixedEvents: Normal SuccessfulCreate 2m59s cronjob-controller Created job backup-fixed-29840476 Normal SawCompletedJob 86s cronjob-controller Saw completed job: backup-fixed-29840476, condition: Complete Normal SuccessfulCreate 86s cronjob-controller Created job backup-fixed-29840477 Normal JobAlreadyActive 59s (x4 over 119s) cronjob-controller Not starting job because prior execution is running and concurrency policy is Forbid🐛 หา bug ในโค้ดนี้
spec:schedule: '0 2 * * *'jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: backup command: ["sh", "-c", "echo backup start; sleep 90; echo backup done"]เฉลย
บทเรียน Jobs and CronJobs ของคอร์สนี้เองยืนยันไว้แล้วว่า successfulJobsHistoryLimit/failedJobsHistoryLimit default เป็น 3/1 — ดังนั้น “ไม่มี history limit” เองไม่ใช่ bug แบบสะสมไม่จำกัดบน cluster ปัจจุบัน ความเสี่ยงจริงที่พิสูจน์แล้วด้านบนคือ concurrencyPolicy ที่ปล่อย default Allow ไว้: พอ backup body ช้ากว่า schedule interval controller ของ CronJob ก็สร้าง Job ถัดไปตามตารางไม่สนว่าตัวก่อนหน้าเสร็จหรือยัง พิสูจน์แล้วด้านบนด้วยคู่ที่ซ้อนกันจริง (-29840476 ยังรันอยู่ตอน -29840477 เริ่ม) backup process สองตัวชนกับ database เดียวกันพร้อมกันคือความเสี่ยงจริง (lock contention, snapshot ไม่สอดคล้อง หรือเขียนแข่งกันไปที่ปลายทางเดียวกัน) ไม่ใช่แค่เปลือง compute วิธีแก้คือ concurrencyPolicy: Forbid (ข้าม tick ถ้าตัวก่อนหน้ายังไม่เสร็จ พิสูจน์แล้วด้านบนด้วย event JobAlreadyActive จริง) — หรือ Replace ถ้าอยากให้ run ล่าสุดชนะเสมอ
PodDisruptionBudget: minAvailable เท่ากับจำนวน replica เต็ม
หัวข้อที่มีชื่อว่า “PodDisruptionBudget: minAvailable เท่ากับจำนวน replica เต็ม”db-app-pdb ตั้ง minAvailable: 3 ให้ Deployment ที่มี 3 replica พอดี พิสูจน์ด้วยการ drain node ที่ host Pod ทั้ง 3 ตัวจริงแบบจับเวลา:
$ kubectl get pdb db-app-pdbNAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGEdb-app-pdb 3 N/A 0 1s
$ kubectl drain reading-b-worker --ignore-daemonsets --delete-emptydir-data --timeout=15sevicting pod wr2/db-app-64bfd95585-m686zerror when evicting pods/"db-app-64bfd95585-m686z" -n "wr2" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.evicting pod wr2/db-app-64bfd95585-8pcpnerror when evicting pods/"db-app-64bfd95585-8pcpn" -n "wr2" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.evicting pod wr2/db-app-64bfd95585-l4xwjerror when evicting pods/"db-app-64bfd95585-l4xwj" -n "wr2" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.... (ซ้ำทุก 5 วินาทีจนกว่าจะ timeout)error: unable to drain node "reading-b-worker" due to error: [... global timeout reached: 15s ...]
$ kubectl get pods -o wide -l app=db-appNAME READY STATUS NODEdb-app-64bfd95585-8pcpn 1/1 Running reading-b-workerdb-app-64bfd95585-l4xwj 1/1 Running reading-b-workerdb-app-64bfd95585-m686z 1/1 Running reading-b-worker🐛 หา bug ในโค้ดนี้
spec:minAvailable: 3selector: matchLabels: app: db-appเฉลย
พอ minAvailable เท่ากับจำนวน replica เต็ม ALLOWED DISRUPTIONS เป็น 0 จริง ๆ — ไม่ใช่แค่ “ระมัดระวังมาก” แต่เป็นเพดานจริงถาวรที่ voluntary eviction ข้ามไม่ได้เลยตราบใดที่ต้องมีครบ 3 replica kubectl drain จริง (node upgrade, scale-down, cordon เพื่อ maintenance) จะบล็อคตลอดไป พิสูจน์แล้วด้านบนด้วย Cannot evict pod as it would violate the pod's disruption budget ซ้ำเดิมตลอดทั้ง timeout และ Pod ทั้ง 3 ยัง Running บน node ตอนจบ วิธีแก้คือตั้ง budget ที่ยอมให้เสีย replica ไปอย่างน้อย 1 ตัวได้จริง — minAvailable: 2 (หรือ maxUnavailable: 1) สำหรับ workload 3 replica เพื่อให้ drain จริงเดินหน้าได้ทีละ Pod แทนที่จะค้างไม่มีกำหนด
StatefulSet: ไม่มี podManagementPolicy ตอนที่ shard เป็นอิสระต่อกัน
หัวข้อที่มีชื่อว่า “StatefulSet: ไม่มี podManagementPolicy ตอนที่ shard เป็นอิสระต่อกัน”3 replica ของ database นี้ bootstrap แบบอิสระจากกันเต็มที่ (แต่ละตัวมีข้อมูลของตัวเอง ไม่มีลำดับ leader-follower join) — shape แบบที่การรับประกันของ OrderedReady ไม่ได้ประโยชน์อะไรเลย บทเรียน StatefulSets and DaemonSets ของคอร์สนี้วัดต้นทุนตรงไว้แล้ว (18.37s ของ OrderedReady เทียบ 6.17s ของ Parallel สำหรับ 3 replica ที่ timing เหมือนกัน — ประมาณ 3 เท่า) ทำ timing เดิมซ้ำบน cluster ของรีวิวนี้เอง ด้วย StatefulSet 3 replica ที่มี startup delay 5 วินาทีต่อ Pod ยืนยัน shape เดียวกันแม้ตัวเลขจริงจะต่างกัน:
# podManagementPolicy ไม่ได้ตั้ง (OrderedReady, default)$ kubectl apply -f sts-shard.yaml # แล้ว poll จนกว่าทั้ง 3 จะเป็น 1/1OrderedReady (default) time: 19s
# StatefulSet เดียวกันเป๊ะ เพิ่มแค่ podManagementPolicy: ParallelParallel time: 7s🐛 หา bug ในโค้ดนี้
spec:serviceName: shardreplicas: 3# no podManagementPolicy -- defaults to OrderedReadyเฉลย
OrderedReady (default ที่ปล่อยไว้ไม่ได้ตั้งตรงนี้) สร้างแล้วรอแต่ละ ordinal ให้ Ready ก่อนที่จะสร้างตัวถัดไปด้วยซ้ำ — พิสูจน์แล้วด้านบน 19s สำหรับ 3 replica ที่ startup 5 วินาทีต่อตัว เทียบกับ 7s ตอนใส่ podManagementPolicy: Parallel ที่สร้างทั้งสามพร้อมกัน การ serialize แบบนั้นเหมาะมากถ้า bootstrap logic ของ ordinal ถัดไปสมมติว่าตัวก่อนหน้าขึ้นแล้ว — แต่เป็น latency เปล่า ๆ ที่หลีกเลี่ยงได้สำหรับ shard หรือ replica ที่ไม่พึ่งพากันตอน startup เลย ซึ่งตรงกับสิ่งที่ manifest นี้อธิบายจริง ๆ วิธีแก้คือใส่ podManagementPolicy: Parallel ให้ชัดเจนเมื่อยืนยันความอิสระต่อกันแล้ว — มันแค่เอาการรับประกันลำดับ startup ออก ไม่กระทบตัวตนหรือ storage ต่อ Pod ที่ยังอยู่เหมือนเดิม
โค้ดที่แก้แล้ว
หัวข้อที่มีชื่อว่า “โค้ดที่แก้แล้ว”apiVersion: v1kind: Servicemetadata: name: dbspec: clusterIP: None selector: app: db ports: - port: 5432apiVersion: apps/v1kind: StatefulSetmetadata: name: dbspec: serviceName: db replicas: 3 podManagementPolicy: Parallel selector: matchLabels: app: db template: metadata: labels: app: db spec: containers: - name: db image: busybox:1.36 command: ["sh", "-c", "sleep 3600"] volumeMounts: - name: data mountPath: /var/lib/db volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 1GiapiVersion: v1kind: Secretmetadata: name: db-credentials# base64-encoded, NOT encrypted -- see reviewdata: password: c3VwZXJzZWNyZXQxMjMhapiVersion: batch/v1kind: CronJobmetadata: name: db-backupspec: schedule: '0 2 * * *' concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: backup image: busybox:1.36 command: ["sh", "-c", "echo backup start; sleep 90; echo backup done"]apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: db-app-pdbspec: minAvailable: 2 selector: matchLabels: app: db-appnpm run verify dry-run ทั้ง 5 ไฟล์ด้านบนกับ verify-k8s cluster ที่ใช้ร่วมกัน ได้ 0 error และ node tools/verify-manifests.mjs --apply reading-k8s/worked-review-2 apply จริงลง namespace ชั่วคราว: Pod ทั้ง 3 ของ StatefulSet ถึง Running พร้อม PVC bound จริงต่อตัว (db-data-db-0/-1/-2) และ serviceName: db ตรงกับ headless Service ที่สร้างขึ้นจริง
สรุปว่าแก้อะไรทำไม
หัวข้อที่มีชื่อว่า “สรุปว่าแก้อะไรทำไม”| # | Bug | วิธีแก้ |
|---|---|---|
| 1 | serviceName: db-headless แต่ Service ที่สร้างจริงชื่อ db | ชื่อตรงกัน — NXDOMAIN จริงบน per-Pod DNS name ก่อน resolve ได้หลัง |
| 2 | hostPath สำหรับข้อมูล persistent ของ database | volumeClaimTemplates แยกต่อ replica — อ้างพิสูจน์: ข้อมูลไม่ตาม Pod ที่ reschedule ไปเมื่อใช้ hostPath |
| 3 | Comment อ้างว่าข้อมูลของ Secret “encrypted” | แก้ comment เป็น base64-encoded ไม่ใช่ encrypted — base64 --decode แบบ offline พิสูจน์จริง |
| 4 | restartPolicy: Always บน manual backup Job | OnFailure — API server ปฏิเสธจริงทันทีถ้าไม่แก้ |
| 5 | ไม่มี concurrencyPolicy บน backup CronJob | concurrencyPolicy: Forbid — Job ซ้อนกันจริงพิสูจน์ก่อน, event JobAlreadyActive จริงหลัง |
| 6 | PodDisruptionBudget.minAvailable เท่ากับจำนวน replica เต็ม | minAvailable: 2 — drain ค้างถาวรจริงพิสูจน์ก่อน |
| 7 | ไม่มี podManagementPolicy บน StatefulSet ของ shard ที่อิสระต่อกัน | podManagementPolicy: Parallel — startup ช้าจริงประมาณ 3 เท่าพิสูจน์ก่อน |
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มีแค่ 1 ใน bug เจ็ดตัวนี้ (4, restartPolicy ของ Job) ที่ API server ปฏิเสธตรง ๆ ทันที อีก 6 ตัวที่เหลือ apply ผ่านเนียนหมด เพราะแต่ละตัวเป็นการอ้างข้อเท็จจริงที่ API server ไม่มีทางเช็คได้เลย: serviceName ชี้ไปที่ Service จริงที่ตรงกันไหม, path ที่ mount ไว้เป็น storage ที่ทนทานและแชร์ได้จริงหรือแค่ disk local ของ node เดียว, “ข้อมูลอยู่ใน Secret object” เหมือนกับ “ข้อมูลถูกป้องกันแล้ว” จริงไหม, backup body จะเสร็จภายใน schedule tick เดียวเสมอไหม, disruption budget ยังเหลือช่องให้ evict ได้จริงไหม, Pod ที่ไม่มี dependency ตอน startup จริง ๆ ได้ประโยชน์จากการจ่ายต้นทุนรอคิวไหม ไม่มีตัวไหนโผล่เป็น error เลย มีแต่ช่องว่างจริงแต่เงียบระหว่าง “controller รายงานว่าแข็งแรงดี” กับสิ่งที่เกิดขึ้นจริงตอน DNS lookup, node drain หรือ schedule จริง
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ถ้า StatefulSet กับ Service ของมันมีอยู่ทั้งคู่
serviceNameต้องชี้ไปตัวที่ถูกต้องแน่ ๆ” พิสูจน์แล้วด้านบน: API server ไม่เคย validateserviceNameกับ Service object จริงเลย — เปลี่ยนชื่อฝั่งใดฝั่งหนึ่งโดยไม่อัปเดตอีกฝั่ง StatefulSet ยังรายงานว่าแข็งแรงดี ขณะที่ per-Pod DNS lookup ทุกอันคืนNXDOMAIN - “Secret ปลอดภัยกว่า ConfigMap โดยธรรมชาติ” พิสูจน์แล้วด้านบนด้วย
base64 --decodeแบบ offline: fielddataของมันเอง reverse กลับได้ง่ายพอ ๆ กับ ConfigMap — ต่างกันแค่ convention ความปลอดภัยจริงต้องอาศัย encryption at rest, RBAC หรือ external secret manager ซึ่งไม่มีอันไหนที่ Secret object ให้มาเองเลย - “ข้าม
podManagementPolicyไปเลยคือทางเลือกที่ปลอดภัยและระมัดระวังเสมอ” พิสูจน์แล้วด้านบน:OrderedReady“ปลอดภัย” จริงเฉพาะ workload ที่ต้องการลำดับ startup จริง ๆ สำหรับ shard ที่อิสระต่อกัน มันคือ latency เปล่า ๆ ที่วัดได้ (ช้ากว่าประมาณ 3 เท่าในการทดสอบนี้) โดยไม่ได้ correctness เพิ่มเลย
flowchart TD S["Service: db (headless)"] ST["StatefulSet: serviceName=db-headless"] -->|"1: name drift -- NXDOMAIN"| ST ST --> V["hostPath /mnt/db-data"] V -->|"2: not portable across nodes"| V ST --> PM["no podManagementPolicy"] PM -->|"7: 3x slower than needed"| PM SEC["Secret: db-credentials"] -->|"3: 'encrypted' comment is false"| SEC J["Job: db-backup-manual, restartPolicy Always"] -->|"4: real API rejection"| J CJ["CronJob: db-backup, no concurrencyPolicy"] -->|"5: overlapping backups"| CJ PDB["PDB: minAvailable == replicas"] -->|"6: blocks drain forever"| PDB