MySQL trên Kubernetes: Đừng để Data “bay màu” – Kinh nghiệm thực chiến với StatefulSet

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Nỗi đau khi chạy Database “sai cách” trên K8s

Hồi mới làm quen với Kubernetes, mình cứ nghĩ đóng gói mọi thứ vào Deployment là xong. Với web app hay microservices thì đúng là rất mượt. Thế nhưng, khi đưa MySQL lên bằng Deployment, mình đã nếm trái đắng đầu tiên. Chỉ một lần Pod restart, toàn bộ dữ liệu “bay màu” sạch sẽ. Chưa kể, mỗi lần sống lại, IP của Pod lại thay đổi khiến ứng dụng mất kết nối hoàn toàn. Database rõ ràng không thể vận hành như một app thông thường.

MySQL là ứng dụng có trạng thái (Stateful). Không giống các stateless app có thể copy-paste hàng loạt, mỗi instance MySQL cần một định danh duy nhất và một ổ đĩa gắn bó trọn đời. Đây chính là lúc StatefulSet tỏa sáng. Nó đảm bảo Pod có tên cố định kiểu mysql-0, mysql-1. Quan trọng nhất, nó tự động gắn lại đúng Persistent Volume (PV) cũ kể cả khi Pod bị xóa hoặc chuyển sang Node khác.

Hệ thống production mình đang quản lý chạy MySQL 8.0 với 50GB dữ liệu, xử lý khoảng 200-300 transactions/giây. Việc chuyển sang StatefulSet kết hợp lưu trữ SSD đã giúp hệ thống chạy êm ru. Cảnh tượng thức đêm xử lý lỗi ổ đĩa gần như biến mất.

Thiết lập “nhà” cho dữ liệu (ConfigMap & Secret)

Trước khi gõ manifest, bạn cần chuẩn bị nền móng thật kỹ. Nếu cấu hình ẩu, hiệu năng DB sẽ tụt dốc không phanh khi tải cao.

1. Tách cấu hình với ConfigMap

Đừng bao giờ để file my.cnf chết cứng trong image. Mình luôn đưa chúng vào ConfigMap để dễ dàng tinh chỉnh innodb_buffer_pool_size hay max_connections mà không cần build lại mọi thứ.

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  my.cnf: |
    [mysqld]
    # Tối ưu cho database 50GB của mình
    innodb_buffer_pool_size = 2G
    innodb_log_file_size = 512M
    max_connections = 500
    innodb_flush_log_at_trx_commit = 1
    character-set-server = utf8mb4
    collation-server = utf8mb4_unicode_ci

2. Bảo mật mật khẩu

Quên việc ghi thẳng pass vào file YAML đi nhé. Hãy dùng Secret để giữ an toàn cho hệ thống.

kubectl create secret generic mysql-pass --from-literal=password='MatKhauSieuCap123'

Triển khai Headless Service và StatefulSet

Để các ứng dụng trong cụm K8s tìm thấy MySQL, bạn cần một Service. Với StatefulSet, mình chọn Headless Service (clusterIP: None). Kiểu này giúp app kết nối trực tiếp tới IP của từng Pod qua DNS cố định thay vì phải đi vòng qua load balancer.

Tạo Headless Service

apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
    - port: 3306
      name: mysql
  clusterIP: None
  selector:
    app: mysql

Bản manifest StatefulSet chuẩn

Dưới đây là cấu hình mình thường dùng cho các dự án thực tế. Điểm đáng tiền nhất nằm ở volumeClaimTemplates. Nó sẽ yêu cầu cloud provider cấp một ổ đĩa riêng biệt cho từng Pod một cách tự động.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: "mysql"
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: config
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: config
        configMap:
          name: mysql-config
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 50Gi

Nếu bạn chạy on-premise, hãy nhớ cài một StorageClass (như Longhorn hoặc NFS) để K8s biết chỗ lấy ổ cứng nhé.

Kiểm chứng độ bền và Giám sát

Chạy xong lệnh kubectl apply chưa phải là kết thúc. Bạn cần chắc chắn rằng dữ liệu thực sự sống sót qua giông bão.

1. Thử thách “Xóa Pod”

Mình thường làm một bài test tâm linh: Tạo thử một database, sau đó mạnh tay xóa thẳng Pod đó đi.

# Truy cập vào pod
kubectl exec -it mysql-0 -- mysql -u root -p

# Tạo dữ liệu mẫu
CREATE DATABASE test_persistence;

# "Trảm" pod để thử độ bền
kubectl delete pod mysql-0

Chờ vài giây để K8s hồi sinh Pod mới. Nếu database test_persistence vẫn nằm đó chờ bạn, hệ thống đã cấu hình Persistent Volume chuẩn xác.

2. Theo dõi chỉ số sống còn

Đừng để đến lúc sập mới biết. Với MySQL, mình luôn đẩy metrics về Grafana để soi các thông số sau:

  • InnoDB Buffer Pool Usage: Để biết khi nào cần bơm thêm RAM cho cache.
  • Disk I/O Latency: Rất quan trọng khi dùng Network Storage. Latency trên 20ms là bắt đầu có chuyện rồi đấy.
  • Slow Queries: Để gáy với team Dev khi họ viết SQL làm treo DB.

Lời khuyên cuối cùng: Dù K8s có xịn đến mấy, backup ngoài cluster vẫn là “phao cứu sinh” cuối cùng. Hãy thiết lập một CronJob đẩy bản backup lên S3 mỗi đêm. Chúc anh em DevOps ngủ ngon giấc với cụm MySQL của mình!

Share: