StatefulSet: danh tính ổn định và thứ tự

K
Kai··7 min read

Bài 24 cho thấy Deployment đối xử với pod như một đàn cá vô danh: ba pod rollout-demo-7545d5669f-xxxxx tên ngẫu nhiên, thay con nào cũng được, mất con này ReplicaSet đẻ con khác tên khác cũng không sao, vì chúng giống hệt nhau và không giữ gì riêng. Mô hình đó hoàn hảo cho web stateless. Nhưng database, message queue, hay chính etcd ta dựng ở Bài 6 thì khác: mỗi node có một danh tính (etcd member controller-0 không thể bị thay bằng một tên lạ), khởi động có thứ tự, và giữ dữ liệu riêng. Đó là khi cần StatefulSet.

Bốn bảo đảm

Tài liệu liệt kê đúng bốn thứ StatefulSet bảo đảm mà Deployment không:

"Stable, unique network identifiers. Stable, persistent storage. Ordered, graceful deployment and scaling. Ordered, automated rolling updates."

Bài này kiểm chứng ba trong bốn bằng test thật: danh tính mạng ổn định, thứ tự deploy/scale, và (gián tiếp) cập nhật có thứ tự. Cái thứ hai, lưu trữ bền, cần một StorageClass và bộ cấp phát động (EBS CSI) mà ta chưa dựng; nó để dành cho phần Storage, nhưng cuối bài sẽ chỉ rõ cơ chế.

Một điều kiện bắt buộc trước đã, theo tài liệu: "StatefulSets currently require a Headless Service to be responsible for the network identity of the Pods. You are responsible for creating this Service." Headless service là Service có clusterIP: None — không cấp một IP ảo gộp như Service thường (Bài 16), mà để DNS trả thẳng IP của từng pod. StatefulSet dựa vào nó để cho mỗi pod một tên DNS riêng.

apiVersion: v1
kind: Service
metadata: {name: web, labels: {app: web}}
spec:
  clusterIP: None           # <-- headless
  selector: {app: web}
  ports: [{port: 80, name: web}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: web}
spec:
  serviceName: web          # <-- trỏ tới headless service ở trên
  replicas: 3
  selector: {matchLabels: {app: web}}
  template:
    metadata: {labels: {app: web}}
    spec:
      containers:
      - name: app
        image: busybox:1.36
        command: ["sleep","3600"]
        readinessProbe:
          exec: {command: ["true"]}
          initialDelaySeconds: 2
          periodSeconds: 2

Danh tính: tên cố định, không phải băm ngẫu nhiên

Khác biệt thấy ngay ở tên pod. Tài liệu: "Each Pod in a StatefulSet derives its hostname from the name of the StatefulSet and the ordinal of the Pod. The pattern for the constructed hostname is $(statefulset name)-$(ordinal)." Với replicas: 3, ta được web-0, web-1, web-2 — đánh số từ 0, không hậu tố ngẫu nhiên. Và hostname bên trong pod đúng bằng tên đó:

for p in web-0 web-1 web-2; do echo "$p -> hostname: $(kubectl exec $p -- hostname)"; done
web-0 -> hostname: web-0
web-1 -> hostname: web-1
web-2 -> hostname: web-2

Mỗi pod biết mình là ai: web-0 luôn là web-0. So với Deployment nơi hostname là một chuỗi băm vô nghĩa, đây là nền tảng để một cụm có trạng thái tự cấu hình (ví dụ etcd --name lấy từ hostname).

DNS riêng từng pod qua headless service

Danh tính không chỉ là tên, mà còn là địa chỉ gọi được. Headless service cho mỗi pod một tên DNS theo dạng $(podname).$(service).$(namespace).svc.cluster.local. Phân giải thử từ trong web-0:

kubectl exec web-0 -- nslookup web-1.web.default.svc.cluster.local
Name:   web-1.web.default.svc.cluster.local
Address: 10.200.0.25

web-1.web.default.svc.cluster.local phân giải đúng ra IP của pod web-1. Đây là thứ Deployment + Service thường không có: ở Bài 16, Service ClusterIP cho một IP ảo gộp rồi load-balance ngẫu nhiên vào pod bất kỳ, bạn không gọi đích danh một pod được. Headless thì ngược lại. Tra thẳng tên service (không kèm podname) sẽ trả tất cả IP pod:

kubectl exec web-0 -- nslookup web.default.svc.cluster.local
kubectl get svc web -o jsonpath='clusterIP={.spec.clusterIP}{"\n"}'
Address: 10.200.0.26
Address: 10.200.0.25
Address: 10.200.1.24

clusterIP=None

Ba địa chỉ ứng đúng ba pod (đối chiếu: web-0=10.200.1.24, web-1=10.200.0.25, web-2=10.200.0.26). clusterIP: None xác nhận đây là headless: không có IP ảo, client tự chọn pod theo tên DNS. Đó là cách một client của cụm có trạng thái tìm đúng node nó cần (ví dụ kết nối tới đúng primary của database).

Tạo theo thứ tự, xóa theo thứ tự ngược

Bảo đảm thứ ba là thứ tự. StatefulSet tạo pod tuần tự {0..N-1}, và mỗi pod chỉ được tạo sau khi pod trước đã Running và Ready. Bắt tận mắt bằng cách poll ngay sau khi apply:

for i in $(seq 1 12); do
  echo "t=$i: $(kubectl get pods -l app=web --no-headers | awk '{print $1"="$3}' | tr '\n' ' ')"
  sleep 2
done
t=1: web-0=ContainerCreating
t=2: web-0=Running
t=3: web-0=Running web-1=Running
t=4: web-0=Running web-1=Running web-2=Running

web-0 xuất hiện một mình trước; chỉ khi nó Running thì web-1 mới được tạo; rồi mới tới web-2. Không như Deployment dựng cả ba cùng lúc. Quy luật của tài liệu: "For a StatefulSet with N replicas, when Pods are being deployed, they are created sequentially, in order from {0..N-1}""Before a scaling operation is applied to a Pod, all of its predecessors must be Running and Ready." Điều này quan trọng với cụm cần một thứ tự khởi động (node 0 là seed, các node sau join vào nó).

Danh tính ổn định còn nghĩa là: xóa một pod thì nó hồi sinh với đúng tên cũ, không phải tên mới như Deployment:

kubectl delete pod web-1 --now
# poll:
t=1: web-0=Running web-1=ContainerCreating web-2=Running
t=2: web-0=Running web-1=Running       web-2=Running

web-1 chết rồi sống lại vẫn là web-1, đúng tên, đúng tên DNS (dù IP pod có thể đổi). Một etcd member tên web-1 vì thế vẫn là chính nó sau khi pod được lên lịch lại. Khi scale xuống, thứ tự đảo ngược, xóa từ ordinal cao nhất:

kubectl scale statefulset/web --replicas=2
# poll:
t=1: web-0=Running web-1=Running web-2=Terminating

web-2 (ordinal cao nhất) bị xóa trước, web-0/web-1 giữ nguyên. Tài liệu: "When Pods are being deleted, they are terminated in reverse order, from {N-1..0}""Before a Pod is terminated, all of its successors must be completely shutdown." Thu nhỏ cụm có trạng thái phải gỡ từ node mới nhất ngược về, tránh gỡ nhầm node seed đang giữ quorum.

Lưu trữ bền: volumeClaimTemplates (xem trước)

Bảo đảm còn lại, lưu trữ bền, là lý do quan trọng nhất người ta chọn StatefulSet, nhưng nó cần hạ tầng lưu trữ ta chưa dựng. Cơ chế: thay vì khai một volume dùng chung, StatefulSet khai volumeClaimTemplates, và "For each VolumeClaimTemplate entry defined in a StatefulSet, each Pod receives one PersistentVolumeClaim." Mỗi pod được cấp một PVC riêng, đặt tên theo dạng <tên-claim>-<tên-pod> (ví dụ www-web-0, www-web-1...).

  volumeClaimTemplates:        # (thêm vào spec StatefulSet)
  - metadata: {name: www}
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "..."   # cần StorageClass + bộ cấp phát động
      resources: {requests: {storage: 1Gi}}

Điểm cốt lõi cho dữ liệu: PVC này dính theo danh tính, không theo vòng đời pod. web-0 chết rồi lên lại vẫn gắn đúng www-web-0 cũ, dữ liệu còn nguyên. Và tài liệu cảnh báo một chủ ý an toàn: "Deleting and/or scaling a StatefulSet down will not delete the volumes associated with the StatefulSet. This is done to ensure data safety." Tức xóa StatefulSet không xóa volume, bạn phải tự dọn PVC, đề phòng mất dữ liệu ngoài ý muốn.

Ta sẽ làm thật phần này ở phần Storage (Bài 43): dựng StorageClass với EBS CSI, tạo StatefulSet có volumeClaimTemplates, ghi dữ liệu vào web-0, xóa pod rồi thấy dữ liệu còn nguyên. Giờ chỉ cần nắm: danh tính ổn định + PVC-theo-danh-tính là cặp bài trùng làm nên StatefulSet.

🧹 Dọn dẹp

kubectl delete statefulset web
kubectl delete svc web

Vì chưa khai volumeClaimTemplates nên không có PVC nào để dọn. (Khi có volume, nhớ kubectl delete pvc -l app=web riêng — như cảnh báo ở trên, chúng không tự mất.) Cụm về lại hai pod CoreDNS. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 25-statefulset.

Tổng kết

StatefulSet dành cho ứng dụng có trạng thái, nơi pod không thể thay cho nhau tùy tiện. Nó cần một headless service (clusterIP: None) và cho bốn bảo đảm: tên ổn định (web-0..N-1 đánh số từ 0, hostname = tên pod); DNS riêng từng pod (web-0.web.default.svc.cluster.local, gọi đích danh được vì headless trả IP từng pod thay vì một ClusterIP gộp); thứ tự (tạo tuần tự 0→N-1 chờ pod trước Ready, xóa ngược N-1→0, xóa pod thì hồi sinh đúng tên cũ); và lưu trữ bền qua volumeClaimTemplates, mỗi pod một PVC <claim>-<pod> dính theo danh tính, không bị xóa khi scale down (test thật ở phần Storage). So với "đàn cá vô danh" của Deployment, mỗi pod StatefulSet có một danh tính và một chỗ lưu trữ riêng.

Bài 26 sang DaemonSet, controller cho mô hình thứ ba: không phải "N bản sao" mà là "đúng một bản sao trên mỗi node", dùng cho agent log, CNI, node exporter... những thứ phải hiện diện ở mọi máy.