Winston is a highly experienced digital marketing professional, specializing in Cybersecurity, IT services, and Software as a Service (SaaS).
Triển khai các ứng dụng stateful (có trạng thái) trong cụm Kubernetes luôn là một công việc phức tạp. Nguyên nhân là do các ứng dụng này thường yêu cầu kiến trúc primary-replica và tên pod cố định. StatefulSets ra đời để giải quyết vấn đề này khi triển khai ứng dụng có trạng thái trong Kubernetes. Bài viết này sẽ giúp bạn hiểu rõ hơn về Kubernetes StatefulSets, khi nào nên sử dụng chúng, cũng như cách triển khai ứng dụng stateful với giải pháp này.
StatefulSet trong Kubernetes là gì?
Ứng dụng stateful và stateless
Ứng dụng stateful là những ứng dụng lưu trữ và theo dõi dữ liệu của chúng. Các cơ sở dữ liệu như MySQL, Oracle hay PostgreSQL là những ví dụ điển hình cho loại ứng dụng này. Chúng cần duy trì trạng thái và dữ liệu qua nhiều phiên làm việc.
Ngược lại, ứng dụng stateless không lưu trữ dữ liệu. Mỗi khi nhận yêu cầu, ứng dụng stateless sẽ xử lý dữ liệu mới mà không cần biết về các yêu cầu trước đó. Node.js và Nginx là hai ví dụ phổ biến cho loại ứng dụng này.
Trong một ứng dụng web hiện đại, các thành phần stateless thường kết nối với các thành phần stateful để phục vụ yêu cầu của người dùng.
Ví dụ, một ứng dụng Node.js (stateless application) sẽ nhận dữ liệu mới từ mỗi request của người dùng. Sau đó, nó kết nối với một cơ sở dữ liệu MySQL (stateful application) để xử lý dữ liệu. MySQL lưu trữ và cập nhật dữ liệu dựa trên các yêu cầu từ người dùng.
Hiểu được sự khác biệt này rất quan trọng khi triển khai ứng dụng trên Kubernetes, đặc biệt là khi sử dụng StatefulSet để quản lý các ứng dụng stateful.
StatefulSet là gì?
StatefulSet là một controller trong Kubernetes được sử dụng để chạy các ứng dụng stateful dưới dạng container (pod) trong cluster Kubernetes. Khác với các controller khác, StatefulSet gán cho mỗi pod một định danh cố định – một số thứ tự bắt đầu từ 0 – thay vì gán ID ngẫu nhiên cho mỗi pod replica.
Khi tạo pod mới, StatefulSet sẽ sao chép dữ liệu từ pod trước đó. Nếu pod trước đó đang ở trạng thái chờ, pod mới sẽ không được tạo ra.
Khi xóa pod, quá trình sẽ diễn ra theo thứ tự ngược lại, không phải ngẫu nhiên. Ví dụ, nếu bạn có 4 replica và giảm xuống còn 3, StatefulSet sẽ xóa pod có số thứ tự 3.
File manifest YAML của StatefulSet định nghĩa một template cho các pod của nó. Kubernetes sẽ tự động tạo, thay thế và xóa các pod khi bạn scale StatefulSet, đồng thời giữ nguyên các định danh đã được gán trước đó.
Khác biệt giữa Deployment và Statefulset
Tương tự như Kubernetes Deployments, StatefulSets cũng quản lý các pod có cấu hình giống nhau. Tuy nhiên, chúng có nhiều điểm khác biết căn bản bạn cần phải biết.
Một số đặc điểm chính của StatefulSets:
- Mỗi pod có một định danh riêng, được tạo từ tên StatefulSet và chỉ số tạo pod.
- Các pod trong StatefulSet không thể thay thế lẫn nhau. Mỗi pod thường đảm nhận một vai trò cụ thể, ví dụ như làm primary hoặc read-only replica trong ứng dụng cơ sở dữ liệu.
- Kubernetes đảm bảo tạo và xóa pod theo trình tự. Khi giảm số lượng pod, Kubernetes sẽ xóa pod được tạo gần đây nhất.
- Mỗi pod trong StatefulSet được gán một Persistent Volume (PV) và Persistent Volume Claim (PVC) riêng.
Trong khi ở Deployment:
- Các pod được gán định danh ngẫu nhiên, dựa trên tên Deployment và một chuỗi ngẫu nhiên duy nhất.
- Tất cả pod đều giống hệt nhau nên có thể thay thế cho nhau và được thay thế bất cứ lúc nào.
- Không hỗ trợ sắp xếp thứ tự khi triển khai. Khi bạn giảm quy mô Deployment, Kubernetes sẽ xóa một pod ngẫu nhiên.
- Tất cả pod dùng chung một PV và PVC.
StatefulSet và Deployment đều quản lý Pod, nhưng chúng nhắm đến các trường hợp sử dụng cụ thể.
Nên chọn StatefulSet trong các tình huống sau:
- Bạn đang chạy ứng dụng stateful cần storage ổn định, ví dụ như triển khai database có sao chép, file server, key-value store hoặc hàng đợi tin nhắn.
- Các pod không thể thay thế cho nhau vì mỗi pod có vai trò riêng biệt như primary, replica hoặc worker.
- Bạn cần triển khai theo thứ tự có thể dự đoán trước, với việc xóa pod diễn ra theo thứ tự ngược lại.
Nên chọn Deployment khi đáp ứng các tiêu chí sau:
- Ứng dụng của bạn là stateless, không cần storage bền vững, hoặc tất cả pod có thể dùng chung một volume storage.
- Bạn cần tạo nhiều bản sao của một pod từ một cấu hình được khai báo.
- Bạn cần kiểm soát việc triển khai và rollback dựa trên những thay đổi bạn thực hiện với trạng thái đã khai báo.
Sử dụng StatefulSet cho ứng dụng cơ sở dữ liệu
Để hiểu rõ hơn về StatefulSet, hãy xem xét trường hợp cần mở rộng một ứng dụng cơ sở dữ liệu.
Ban đầu, chúng ta có một pod duy nhất là mysql-0, thực hiện cả việc đọc và ghi dữ liệu. Dữ liệu được lưu trữ trên ổ đĩa sử dụng Persistent Volume.
Khi muốn thêm pod mới, chúng ta không thể đơn giản nhân bản pod đầu tiên. Nếu làm vậy, cả hai pod sẽ cùng đọc và ghi vào cùng một dữ liệu, dẫn đến mâu thuẫn.
Để giải quyết vấn đề này, chỉ một pod được phép đọc và ghi dữ liệu, còn pod kia chỉ có quyền đọc. Cách này giúp đảm bảo tính toàn vẹn dữ liệu của ứng dụng.
Chúng ta có thể tiếp tục thêm các pod khác để tăng khả năng đọc của cơ sở dữ liệu. pod có quyền đọc/ghi được gọi là Master, các Pod chỉ đọc được gọi là Slave.
Một đặc điểm quan trọng của StatefulSet là mỗi pod có bộ nhớ riêng biệt, mặc dù dữ liệu được sao chép và đồng nhất giữa các pod. Mỗi pod duy trì phân vùng lưu trữ riêng, và dữ liệu giữa các phân vùng được đồng bộ hóa liên tục.
Cần lưu ý rằng khi một pod bị xóa hoặc gặp sự cố, dữ liệu của nó không bị mất mà vẫn được lưu trữ trên volume. Volume này được tạo ra bởi PV (Persistent Volume) và PVC (Persistent Volume Claim).
Trạng thái của Pod
Mỗi pod trong StatefulSet có trạng thái riêng: Master hoặc Slave. Thông tin trạng thái của pod được lưu trữ trên Persistent Volume (PV), nên nếu pod gặp sự cố, dữ liệu này vẫn được bảo toàn.
Ví dụ, nếu pod có tên mysql-0 bị lỗi, hệ thống sẽ tạo một pod mới cùng tên để thay thế. Pod mới này sẽ lấy lại trạng thái (Master) từ PV và tiếp tục xử lý dữ liệu.
Để duy trì trạng thái pod, điều quan trọng là storage của pod phải truy cập được từ bất kỳ worker node nào. Nhờ đó, nếu pod được chuyển sang node khác, nó vẫn có thể kết nối với PV cũ.
Định danh của Pod
Trong Deployment, tên pod được tạo bằng hash ngẫu nhiên. Còn trong StatefulSet, Pod được đặt tên theo thứ tự cố định: statefulset-(số thứ tự pod). Ví dụ, StatefulSet cho ứng dụng MySQL với 3 replica sẽ có các pod tên là mysql-0 (Master), mysql-1 (Slave) và mysql-2 (Slave).
StatefulSet tạo pod theo đúng thứ tự tên. Pod tiếp theo chỉ được tạo khi pod trước đó đã hoạt động. Nếu pod trước gặp lỗi hoặc đang chờ, các pod sau sẽ không được tạo.
Việc xóa pod trong StatefulSet cũng phải theo thứ tự ngược lại với thứ tự tạo. Quá trình xóa Pod có thể xảy ra khi bạn xóa StatefulSet hoặc giảm số lượng pod. Tương tự, nếu pod trước chưa xóa thành công, các pod sau sẽ không bị xóa. Thứ tự xóa này giúp bảo vệ dữ liệu và trạng thái của ứng dụng.
Endpoint của Pod
Khi tạo StatefulSet và định nghĩa ServiceName, Kubernetes tự động tạo DNS record cho mỗi Pod theo format:
PodName).(ServiceName).(NameSpace).svc.(CluserName)
Bạn cũng có thể tạo service ClusterIP với giá trị clusterIP là ‘none’ để thiết lập headless service cho ứng dụng.
Cơ chế hoạt động của Kubernetes StatefulSet
Thay thế Pod bị mất
Khi một pod do StatefulSet quản lý bị xóa, StatefulSet sẽ tạo một pod mới thay thế. Pod mới này sẽ có cùng tên và hostname với pod cũ.
Điều này khác với ReplicaSet – vốn tạo ra một pod hoàn toàn mới với định danh khác. StatefulSet giúp duy trì định danh của pod bị mất.
Mở rộng quy mô Pod
Khi tăng số lượng pod, StatefulSet sẽ thêm pod mới với chỉ số tiếp theo chỉ số cao nhất hiện tại. Ví dụ, nếu đang có 2 pod là <tên-pod>-0 và <tên-pod>-1, khi tăng lên 3 pod, pod mới sẽ có tên là <tên-pod>-2.
Ngược lại, khi giảm số lượng, pod có chỉ số cao nhất sẽ bị xóa. Cách này cho phép kiểm soát chính xác việc đặt tên và quản lý pod.
Để duy trì trạng thái của Pod, việc lưu trữ phải truy cập được từ bất kỳ worker node nào. Như vậy, nếu pod được gán cho một node khác, nó vẫn có thể kết nối với PV trước đó của mình.
Cung cấp lưu trữ riêng biệt cho từng Pod
Mỗi pod cần có không gian lưu trữ riêng. Khi ta tăng hoặc giảm số lượng pod, một pod mới được tạo ra với cùng chỉ số như pod trước đó cần giữ nguyên dữ liệu lưu trữ ban đầu mà không phải tạo mới.
StatefulSets giải quyết vấn đề này bằng cách tách biệt lưu trữ khỏi pod thông qua PersistentVolumeClaim. StatefulSets tự động tạo một PersistentVolumeClaim cho mỗi pod và gắn nó vào pod tương ứng.
Khi mở rộng số lượng pod trong StatefulSets, một pod mới và một PersistentVolumeClaim mới sẽ được tạo ra. Tuy nhiên, khi thu hẹp quy mô, chỉ có pod bị xóa còn PersistentVolumeClaim vẫn được giữ nguyên. Điều này đảm bảo rằng nếu pod được mở rộng lại, nó có thể kết nối lại với cùng một PersistentVolumeClaim, giữ nguyên dữ liệu của nó.
Cách StatefulSets xử lý khi Node gặp sự cố
Khác với ReplicaSet, StatefulSet đảm bảo không có hai pod nào có cùng định danh. Do đó, khi một node gặp sự cố, StatefulSet sẽ không tạo pod mới cho đến khi chắc chắn pod cũ đã được xóa hoàn toàn.
Khi một node gặp sự cố, các pod trên node đó sẽ có trạng thái Unknown. Nếu node không phục hồi sau một thời gian, các pod trên node đó sẽ bị xóa và trạng thái của chúng sẽ chuyển thành Terminating.
Đối với ReplicaSet, vì các pod do nó quản lý không có định danh duy nhất nên tên của chúng sẽ không bị trùng lặp. Do đó, nếu một pod có trạng thái Terminating, ReplicaSet sẽ tạo một pod mới. Khi pod mới được tạo xong, pod cũ sẽ bị xóa khỏi cluster, bất kể nó đã kết thúc quá trình terminating hay chưa.
Tuy nhiên, với StatefulSet, mỗi pod có một định danh duy nhất, nên pod mới tạo sẽ có cùng tên với pod cũ. Vì vậy, nếu một pod trên node bị lỗi có trạng thái Terminating, StatefulSet sẽ không tạo pod mới cho đến khi chắc chắn pod cũ đã bị xóa hoàn toàn.
Nhưng vì node đã chết, nó không thể báo cáo lại cho Kubernetes master về việc pod đã được xóa thành công hay chưa. Do đó, pod sẽ ở trạng thái Terminating vô thời hạn, và lúc này, bạn cần phải xóa pod thủ công.
Hướng dẫn sử dụng StatefulSet
Tạo một StatefulSet
Đầu tiên, chúng ta sẽ tạo một file có tên kubia-statefulset.yaml sử dụng image luksa/kubia-pet.
const http = require('http');
const os = require('os');
const fs = require('fs');
const dataFile = "/var/data/kubia.txt";
function fileExists(file) {
try {
fs.statSync(file);
return true;
} catch (e) {
return false;
}
}
var handler = function(request, response) {
if (request.method == 'POST') {
var file = fs.createWriteStream(dataFile);
file.on('open', function (fd) {
request.pipe(file);
console.log("New data has been received and stored.");
response.writeHead(200);
response.end("Data stored on pod " + os.hostname() + "\\n");
});
} else {
var data = fileExists(dataFile) ? fs.readFileSync(dataFile, 'utf8') : "No data posted yet";
response.writeHead(200);
response.write("You've hit " + os.hostname() + "\\n");
response.end("Data stored on this pod: " + data + "\\n");
}
};
var www = http.createServer(handler);
www.listen(8080);
Còn đây là nội dung file kubia-statefulset.yaml:
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None
selector:
app: kubia
ports:
- name: http
port: 80
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kubia
spec:
serviceName: kubia # the name of service
replicas: 2
template: # pod template
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia-pet
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data
mountPath: /var/data
volumeClaimTemplates: # pvc template
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
Trong file cấu hình này, chúng ta sẽ khai báo một Headless Service và một StatefulSet có tên kubia. Khi cấu hình StatefulSet, bạn cần chỉ định tên service để nhận dạng mạng cho các Pod, một template cho Pod, và một template cho PersistentVolumeClaims.
Khác với cấu hình ReplicaSet, bạn cần thêm template cho PersistentVolumeClaims mà StatefulSet sẽ sử dụng để tạo các PVC riêng cho từng Pod.
Dùng lệnh này để tạo StatefulSet:
$ kubectl create -f kubia-statefulset.yaml
service "kubia" created
statefulset "kubia" created
Sau khi tạo xong StatefulSet, hãy liệt kê các Pod để xem kết quả:
$ kubectl get po
NAME READY STATUS RESTARTS AGE
kubia-0 1/1 Running 0 8s
kubia-1 0/1 ContainerCreating 0 2s
Các Pod được tạo ra sẽ có tên kèm theo chỉ số. Tiếp theo, chúng ta sẽ liệt kê các PVC:
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
data-kubia-0 Bound pv-0 0 37s
data-kubia-1 Bound pv-1 0 37s
Tên của các PersistentVolumeClaim được tạo bằng cách thêm chỉ số vào tên được chỉ định trong volumeClaimTemplate. Mỗi Pod do đó sẽ có định danh riêng và sử dụng một PVC riêng biệt, rất phù hợp để xây dựng hệ thống lưu trữ phân tán.
Tương tác với pod qua định danh của nó
Đầu tiên, chúng ta cần khởi động proxy bằng lệnh sau:
kubectl proxy
Proxy sẽ bắt đầu chạy trên địa chỉ 127.0.0.1 và cổng 8001. Mở một cửa sổ terminal mới và chạy lệnh curl để truy cập vào Pod kubia-0:
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
You've hit kubia-0
Data stored on this pod: No data posted yet
$ curl -X POST -d "Hey there! This greeting was submitted to kubia-0." localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
Data stored on pod kubia-0
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
You've hit kubia-0
Data stored on this pod: Hey there! This greeting was submitted to kubia-0.
Kết quả trả về cho thấy chúng ta đã kết nối thành công với Pod kubia-0.
Tiếp theo, chúng ta sẽ xóa Pod kubia-0 để xem liệu một Pod mới có sử dụng lại PVC cũ không:
kubectl delete po kubia-0
Theo dõi quá trình xóa và tạo lại Pod bằng lệnh:
kubectl get po
Chúng ta thấy Pod mới được tạo ra với cùng identifier kubia-0. Kiểm tra lại dữ liệu đã lưu trước đó:
curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
Dữ liệu vẫn còn nguyên vẹn như mong đợi. Việc sử dụng StatefulSet đảm bảo rằng các Pod và PVC của chúng được định danh đúng cách.
Tuy nhiên, làm thế nào để truy cập một Pod cụ thể mà không cần dùng proxy? Trước đây chúng ta sử dụng service để tương tác với Pod, nhưng service phân phối yêu cầu ngẫu nhiên giữa các Pod thay vì nhắm đến một Pod cụ thể.
Để truy cập trực tiếp Pod, chúng ta sử dụng một kỹ thuật gọi là Headless Service. Đây là một Service với thuộc tính clusterIP được đặt thành None, giúp gán một định danh duy nhất cho mỗi Pod.
Headless Service
Headless Service là một loại service đặc biệt trong Kubernetes. Khác với ClusterIP thông thường, Headless Service không tạo ra Virtual IP (VIP) riêng. Thay vào đó, nó chỉ tạo một bản ghi DNS trỏ trực tiếp đến từng pod đằng sau service.
Để tạo Headless Service, bạn chỉ cần thêm cấu hình clusterIP: None. Khi đó, mỗi pod sẽ có một bản ghi DNS riêng. Ví dụ, pod có tên kubia-0 sẽ có bản ghi DNS tương ứng là kubia-0.kubia.default.svc.cluster.local.
Bạn có thể truy cập trực tiếp đến một pod cụ thể trong cluster thông qua tên DNS của nó. Trong cùng namespace, bạn có thể dùng kubia-0.kubia hoặc kubia-1.kubia. Từ namespace khác, sử dụng tên đầy đủ như kubia-0.kubia.default.svc.cluster.local.
Headless Service cho phép truy cập chính xác đến pod mong muốn, thay vì bị chuyển hướng ngẫu nhiên như khi dùng service thông thường. Khi kết hợp với StatefulSet, Headless Service giúp mỗi pod có một định danh mạng ổn định. Nhờ đó, bạn có thể xác định và kết nối chính xác đến pod cần thiết dựa trên định danh của nó.
Xem thêm hướng dẫn bảo mật Kubernetes
Lời kết
Chúng ta vừa tìm hiểu về Kubernetes StatefulSet, cách sử dụng nó cho các ứng dụng và cơ sở dữ liệu có trạng thái (stateful). Trong nhiều trường hợp, đây là lựa chọn phù hợp hơn ReplicaSet hoặc Deployment. Nếu bạn còn thắc mắc cần giải đáp, đừng ngần ngại để lại câu hỏi trong phần bình luận bên dưới.