Có hai cách nhìn một nhóm thứ giống nhau. Cách thứ nhất: chúng thay thế được cho nhau — như những tờ tiền cùng mệnh giá, tờ nào cũng vậy, mất tờ này lấy tờ khác. Cách thứ hai: mỗi cái là một cá thể mang danh tính và tài sản riêng — như những chiếc tủ khoá có số, mỗi tủ chứa đồ của đúng một người, đánh tráo là mất đồ. Deployment nhìn các bản theo cách thứ nhất; StatefulSet nhìn theo cách thứ hai. Với một cơ sở dữ liệu, chọn nhầm cách nhìn là mất dữ liệu. Bài này đo ba thứ StatefulSet có mà Deployment không.
Khởi động tuần tự
db-0 Running sau 4,3 giây
db-1 Running sau 9,6 giây
db-2 Running sau 14,9 giây
Mỗi bản chờ bản trước sẵn sàng rồi mới bắt đầu. Deployment tạo cả ba cùng lúc. Thu gọn thì ngược lại:
db-2 biến mất sau 1,1 giây
db-1 biến mất sau 2,2 giây
Xoá từ số lớn tới số nhỏ. Thứ tự này quan trọng với hệ thống có khái niệm "bản đầu tiên": db-0 thường là bản chính, và các bản sau kết nối tới nó để đồng bộ. Khởi động song song sẽ khiến db-1 tìm db-0 khi nó chưa có. Với hệ thống không cần thứ tự, đặt podManagementPolicy: Parallel để bỏ hẳn ràng buộc này và khởi động nhanh gấp ba.
Danh tính ổn định
Tên DNS riêng cho từng bản, qua headless service (phần 56):
db-0.db -> 10.244.3.112
db-1.db -> 10.244.1.40
db-2.db -> 10.244.2.113
PVC riêng cho từng bản, sinh từ volumeClaimTemplates:
data-db-0 data-db-1 data-db-2
Xoá pod db-1 rồi để nó tạo lại:
| tên | db-1 — giữ nguyên |
| podIP | ...40 → ...41 — đổi |
| dữ liệu | "du lieu cua db-1" — còn |
| PVC | data-db-1 Bound — giữ nguyên |
IP đổi, mọi thứ khác không. Đó là toàn bộ ý nghĩa của "danh tính ổn định": ứng dụng khác gọi db-1.db và luôn tới đúng bản đó, với đúng dữ liệu của nó. Với Deployment, pod mới có tên ngẫu nhiên và không có gì gắn nó với dữ liệu cũ.
Thu gọn không xoá PVC
scale --replicas=1 -> db-1, db-2 biến mất
PVC còn nguyên: data-db-0 data-db-1 data-db-2 đều Bound
scale --replicas=3 -> db-1 đọc lại "du lieu cua db-1"
Đây là hành vi có chủ ý: thu gọn tạm thời không được làm mất dữ liệu. Nhưng nó là cái bẫy chi phí: thu gọn để tiết kiệm thì vẫn trả tiền đĩa. Xoá PVC là việc tay, và không lùi lại được (phần 62 — reclaimPolicy: Delete là mặc định).
Từ Kubernetes 1.27 có persistentVolumeClaimRetentionPolicy để đổi hành vi này:
spec:
persistentVolumeClaimRetentionPolicy:
whenScaled: Delete # xoá PVC khi thu gọn
whenDeleted: Retain # giữ khi xoá cả StatefulSet
Mặc định cả hai là Retain.
serviceName là bắt buộc, và nó phải là headless
spec:
serviceName: db
Không có Service headless tương ứng thì tên db-0.db không phân giải được, và StatefulSet mất một nửa giá trị của nó. Đây là chỗ dễ quên vì StatefulSet vẫn tạo pod bình thường mà không báo lỗi gì — chỉ có DNS không hoạt động.
Khi nào dùng, khi nào không
Dùng StatefulSet khi cần ít nhất một trong ba:
- Tên ổn định — bản này phải gọi được bản kia theo tên cố định. Mọi hệ thống có bầu chọn hoặc phân mảnh đều cần.
- Thứ tự — bản chính phải lên trước bản sao.
- Ổ đĩa riêng cho từng bản — mỗi bản một tập dữ liệu, không dùng chung.
Cơ sở dữ liệu, hàng đợi, hệ thống đồng thuận (etcd, ZooKeeper), lưu trữ phân tán — tất cả đều cần.
Không dùng khi: ứng dụng không trạng thái. StatefulSet chậm hơn khi triển khai, phức tạp hơn khi vận hành, và không cho gì thêm. Một lỗi thường gặp theo chiều ngược lại: dùng Deployment cho cơ sở dữ liệu với một PVC RWO dùng chung. Bản thứ hai sẽ không gắn được volume (phần 62), và triệu chứng là pod kẹt ở ContainerCreating mà không rõ vì sao.
Cập nhật cuốn chiếu cũng ngược
StatefulSet cập nhật từ số lớn xuống số nhỏ, mỗi lần một bản, chờ bản đó Ready rồi mới sang bản kế. Với 20 bản và mỗi bản khởi động 30 giây, đó là mười phút cho một lần triển khai. Deployment với maxUnavailable=25% xong trong một phần thời gian đó (phần 50).
partition cho phép triển khai từng phần:
updateStrategy:
rollingUpdate: {partition: 2}
Chỉ bản có số ≥ 2 được cập nhật. Hạ dần partition là cách triển khai theo kiểu canary cho ứng dụng có trạng thái.
Node chết: cùng vấn đề, hậu quả nặng hơn
Phần 42 và 49 đã đo: node chết thì pod ma vẫn báo Running nhiều phút. Với StatefulSet hậu quả nặng hơn, vì Kubernetes không được phép tạo db-1 mới khi db-1 cũ có thể vẫn đang chạy — hai bản cùng tên ghi vào cùng dữ liệu là hỏng dữ liệu. Nên nó chờ. Và nếu node không bao giờ quay lại, bạn phải xoá pod bằng tay với --force:
kubectl delete pod db-1 --force --grace-period=0
Lệnh này nói với Kubernetes: "tôi bảo đảm bản cũ đã chết". Nếu bạn sai — node chỉ mất mạng chứ chưa chết — thì hai bản cùng ghi, và đó là cách mất dữ liệu.
Mẫu số chung: thay-thế-được hay mang-danh-tính, và một danh tính chỉ được một người giữ
Bài này gói hai nguyên tắc của mọi hệ có trạng thái. Thứ nhất: trước khi xử lý một nhóm thứ giống nhau, hỏi chúng thay thế được cho nhau hay mỗi cái mang một danh tính và trạng thái không thể thay. Bản của Deployment là những tờ tiền cùng mệnh giá — mất cái này lấy cái khác; bản của StatefulSet là những chiếc tủ có số, gắn liền với đúng dữ liệu của nó. Chọn nhầm cách xử lý là hỏng: dùng Deployment cho một cơ sở dữ liệu là ném đi chính cái danh tính giữ dữ liệu. Cùng ranh giới ấy khắp nơi: một worker trong thread pool (thay thế được) so với một actor Akka có hộp thư và trạng thái riêng (mang danh tính); một HTTP handler không trạng thái so với một đối tượng gắn phiên; một dòng trong CSDL có khoá chính — mỗi dòng không hoán đổi cho dòng khác. Hỏi đúng câu "cái này hoán đổi được hay mang trạng thái riêng" là chọn đúng vòng đời cho nó.
Thứ hai, sắc hơn: một danh tính chỉ được có tối đa một người giữ đang sống — và muốn cất nhắc bản thay thế một cách an toàn, phải có bằng chứng bản cũ đã chết, nếu không sẽ split-brain. Kubernetes chờ trước khi tạo db-1 mới chính vì lẽ đó; --force là lời cam đoan "bản cũ chết rồi", cam đoan sai thì hai bản cùng ghi. Đây đúng là bài toán fencing trong hệ phân tán: một khoá phân tán cần fencing token để kẻ giữ cũ đã treo không còn hành động được; bầu chọn thủ lĩnh cần quorum để tránh hai thủ lĩnh; chuyển vai trò chính của cơ sở dữ liệu phải cách ly (fence) bản chính cũ trước khi cất nhắc bản sao, nếu không có hai bản chính cùng ghi — đúng cách mất dữ liệu tồi tệ nhất. Nguyên tắc: đừng bao giờ cất nhắc một bản thay thế dựa trên giả định bản cũ đã chết; hãy dựa trên bằng chứng.
Mẫu số chung: phân biệt thứ hoán-đổi-được với thứ mang-danh-tính rồi chọn công cụ khớp; và với thứ mang danh tính, giữ bất biến "một danh tính, một người giữ sống" bằng bằng chứng chứ không bằng niềm tin.
Muốn tự kiểm, xem StatefulSet của bạn có headless service không:
kubectl get statefulset <ten> -o jsonpath='{.spec.serviceName}{"\n"}'
kubectl get svc <ten-service> -o jsonpath='{.spec.clusterIP}{"\n"}'
Dòng thứ hai phải là None. Nếu nó là một IP, service không phải headless và tên <ten>-0.<service> sẽ không phân giải — StatefulSet của bạn đang chạy mà thiếu mất phần định danh.
Phần sau đo DaemonSet: một bản trên mỗi node, và nó phản ứng trong 0,1 giây khi tập node đổi.