Cách nhanh nhất để có database cho website trên cluster là chạy MySQL ngay trong đó: một StatefulSet, dữ liệu nằm trên Block Storage, website gọi qua DNS nội bộ. Nhưng sau một thời gian vận hành, nhiều đội nhận ra phần có trạng thái này gánh nhiều rủi ro hơn phần còn lại của cluster. Tài liệu này phân tích việc chạy MySQL trong Kubernetes: cấu hình điển hình, những chỗ vướng khi vận hành, tiêu chí để quyết định giữ trong cluster hay đưa ra VPS riêng, cách chuyển pipeline tạo database, và cách giữ kết nối từ node Kubernetes tới VPS cho an toàn.

1. MySQL trong Kubernetes trông thế nào

Bài Manifest Kubernetes đầu tiên: namespace, Deployment và MySQL StatefulSet đã có MySQL StatefulSet tối thiểu. Khi chuẩn hoá cho nhiều site dùng chung, mô hình MySQL trong Kubernetes thường đặt trong namespace riêng database:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: database
spec:
  serviceName: mysql                 # headless Service, clusterIP: None
  replicas: 1
  template:
    spec:
      containers:
      - name: mysql
        image: mysql:8.4             # nhánh LTS
        resources:
          requests: { memory: "1500Mi", cpu: "700m" }
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom: { secretKeyRef: { name: mysql-secret, key: MYSQL_ROOT_PASSWORD } }
        volumeMounts:
        - { name: data, mountPath: /var/lib/mysql }
      affinity:                        # ghim vào node pool dành cho database
        nodeAffinity: { ... lke.linode.com/pool-id In ["<pool-db>"] }
  volumeClaimTemplates:
  - metadata: { name: data }
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: linode-block-storage-retain
      resources: { requests: { storage: 30Gi } }

Website trong cùng cluster gọi database qua DNS nội bộ: mysql.database.svc.cluster.local. Đây là điểm mạnh lớn nhất của MySQL trong Kubernetes: không cần IP, không cần mở cổng ra ngoài.

2. StatefulSet và Block Storage: nền móng của MySQL trong Kubernetes

Với MySQL trong Kubernetes, StatefulSet khác Deployment ở ba điểm: pod có tên cố định (mysql-0), có DNS riêng qua headless Service, và mỗi pod có PVC riêng sinh từ volumeClaimTemplates. Khi pod bị xoá hoặc dời node, pod mới mang đúng tên cũ và gắn lại đúng PVC cũ (xem tài liệu StatefulSet).

PVC ở đây là Linode Block Storage với chế độ ReadWriteOnce: tại một thời điểm, volume chỉ gắn được vào một node. Pod MySQL muốn chạy trên node khác thì volume phải tháo khỏi node cũ rồi gắn sang node mới. Reclaim policy Retain giữ volume lại cả khi PVC bị xoá: an toàn cho dữ liệu, nhưng volume cũ vẫn bị tính tiền nếu không ai dọn (xem bài Volume, PersistentVolume, PVC và StorageClass trên Kubernetes).

3. Những chỗ vướng khi chạy MySQL trong Kubernetes

Một replica, một volume, một node

MySQL trong Kubernetes dạng đơn giản là replicas: 1. Node chứa pod gặp sự cố, hoặc LKE thay node khi nâng cấp phiên bản Kubernetes, thì pod phải chờ volume tháo và gắn sang node mới rồi MySQL mới khởi động lại. Trong khoảng đó, mọi website dùng database ấy đều lỗi kết nối. StatefulSet không tự cho bạn replication: muốn có replica phải tự dựng thêm, hoặc dùng operator.

Tài nguyên và cấu hình mặc định

Nếu chart chỉ đặt requests và không có file my.cnf riêng, MySQL trong Kubernetes chạy với tham số mặc định của image: buffer pool, max_connections, slow log đều chưa chỉnh theo khối lượng thật. Trên VPS, đội đã quen chỉnh bằng một file cấu hình; trong container, mỗi lần chỉnh là thêm ConfigMap, mount vào pod và khởi động lại pod. Dùng image innovation (như 8.2, 8.3) thì vòng đời ngắn hơn nhánh LTS 8.4.

Vận hành phải đi qua kubectl

Mọi thao tác quen thuộc của DBA đều phải bọc trong kubectl exec: tạo database cho site mới, dump để backup, kill truy vấn treo. Pipeline tạo site, pipeline backup, script tự xử lý truy vấn chạy quá lâu đều phải mang mật khẩu root đi gõ vào container. Thêm một bước gián tiếp là thêm một chỗ có thể lỗi, và mật khẩu xuất hiện ở nhiều nơi hơn.

Volume là tài sản phải tự quản

Dữ liệu của MySQL trong Kubernetes nằm trên PVC, và thay đổi dung lượng trong volumeClaimTemplates không áp lại cho PVC đã tạo, vì trường này của StatefulSet không sửa được sau khi tạo. Muốn tăng dung lượng phải sửa trực tiếp PVC (StorageClass phải cho phép allowVolumeExpansion). Theo thời gian rất dễ xuất hiện PVC tạo tay ngoài template, nằm rải ở nhiều namespace, mỗi lần là một lần thao tác tay với dữ liệu.

Service kiểu LoadBalancer cho MySQL

Đặt service.type: LoadBalancer cho MySQL trên LKE sẽ tạo NodeBalancer có IP public, tức là đưa cổng 3306 ra Internet. MySQL trong Kubernetes luôn để Service kiểu ClusterIP hoặc headless. Ai cần truy cập từ ngoài thì dùng kubectl port-forward hoặc đi qua VPN.

4. MySQL trong Kubernetes hay VPS riêng: tiêu chí chọn

Tiêu chíMySQL trong Kubernetes (StatefulSet + Block Storage)VPS MySQL riêng
Kết nốiDNS nội bộ cluster, không mở cổngQua mạng private, phải tự giới hạn nguồn kết nối
Khi node bị thayPod dời node, chờ tháo và gắn volumeKhông bị ảnh hưởng bởi vòng đời node Kubernetes
Tài nguyênChia node với pod website, giới hạn bằng request/limitRAM và đĩa dành riêng cho MySQL
Tinh chỉnhConfigMap + restart podSửa my.cnf, restart dịch vụ như mọi VPS khác
Backup, kill queryQua kubectl execSSH và công cụ quen thuộc
Kinh nghiệm độiCần học operator nếu muốn HAThường đã có sẵn từ thời Cloud VPS

Tiêu chí cuối thường nặng ký nhất. Nếu đội đã vận hành MySQL trên VPS cho hàng trăm website (như bài Tầng Database: MySQL đa tenant trên VPS cấu hình nhỏ), tách database ra khỏi cluster cho phép cluster chỉ chạy những thứ không trạng thái, còn phần có trạng thái quay về nơi đội đã quen xử lý sự cố. Ngược lại, nếu cần replication tự động và đội sẵn sàng học operator, giữ MySQL trong Kubernetes là hướng hợp lý.

5. Chuyển pipeline tạo database sang VPS

Thay đổi trong pipeline tạo site nhỏ hơn tưởng tượng, vì phần khó của MySQL trong Kubernetes nằm ở vận hành chứ không ở lệnh tạo database. Trước đây job chạy kubectl exec vào pod MySQL; giờ DATABASE_HOST là IP private của VPS và job gọi thẳng tới đó:

# trước: DATABASE_HOST="mysql.database.svc.cluster.local"
#        kubectl exec -i mysql-0 -n database -- mysql ... < create_db.sql
ssh -i "$SSH_PRIVATE_KEY" deploy@$DATABASE_HOST "mysql --defaults-extra-file=/root/.my-admin.cnf" <<SQL
CREATE DATABASE \`$DATABASE_NAME\`;
CREATE USER '$DATABASE_USERNAME'@'10.0.%' IDENTIFIED BY '$DATABASE_PASSWORD';
GRANT ALL ON \`$DATABASE_NAME\`.* TO '$DATABASE_USERNAME'@'10.0.%';
SQL

Bước rollback khi tạo site lỗi cũng đổi theo: DROP DATABASE và DROP USER chạy qua cùng đường. Nên đẩy thay đổi này lúc ít truy cập và giữ sẵn cách lùi về bản pipeline cũ. Với các database đang chạy ở MySQL trong Kubernetes, quy trình chuyển:

  1. mysqldump --single-transaction từ pod, import vào VPS.
  2. Sửa dbHost trong values của site rồi helm upgrade.
  3. Đổi ConfigMap không tự khởi động lại pod: chạy kubectl rollout restart để website đọc host mới.
  4. Kiểm tra site, giữ PVC cũ một thời gian rồi mới xoá.

6. Bảo mật kết nối từ node Kubernetes tới VPS MySQL

Bỏ MySQL trong Kubernetes nghĩa là cổng 3306 phải mở cho node Kubernetes. Đây là phần cần làm kỹ nhất.

  • Giới hạn nguồn kết nối: chỉ cho dải IP private của node vào cổng 3306, bằng firewall của VPS hoặc Cloud Firewall. Node LKE có thể đổi IP khi bị thay mới, nên quy tắc theo dải private ổn định hơn theo từng IP. Không bao giờ mở 3306 ra Internet.
  • Không grant cho '%': user của site là 'user'@'10.0.%' (dải private của bạn) thay vì 'user'@'%', và không kèm WITH GRANT OPTION.
  • Cân nhắc skip_name_resolve: mặc định, với mỗi client mới, MySQL tra ngược DNS từ IP sang tên rồi tra xuôi để kiểm tra. IP private của node thường không có bản ghi PTR, nên mỗi kết nối mới tốn thêm một lượt tra DNS và có thể sinh cảnh báo "could not be resolved" trong error log. Host cache giảm bớt nhưng không bỏ hẳn việc này (xem tài liệu host cache của MySQL 8.4). Bật skip_name_resolve thì mọi grant phải viết theo IP hoặc dải IP.
  • Mật khẩu không nằm trên dòng lệnh: mysql -u root -p"..." qua SSH để lộ mật khẩu trong danh sách tiến trình. Dùng option file quyền 600 hoặc mysql_config_editor trên máy đích, như mẫu ở mục 5.

7. Khuyến nghị

MySQL trong Kubernetes không sai. Dạng một replica trên Block Storage RWO mang lại sự tiện lợi của DNS nội bộ, nhưng phải gánh vòng đời node, cấu hình mặc định và mọi thao tác qua kubectl exec. Khi chưa có operator và kinh nghiệm HA trong cluster, để cluster chạy website còn database ở VPS là lựa chọn an toàn hơn. Chart MySQL trong bài Chart dùng chung: tái sử dụng Helm cho Laravel, MySQL và Redis vẫn hữu ích cho dự án nhỏ hoặc môi trường thử cần một database đi kèm.

  • Giữ MySQL trong Kubernetes: dùng nhánh LTS, có my.cnf qua ConfigMap, node pool riêng, Service ClusterIP, backup định kỳ ra ngoài cluster.
  • Đưa ra VPS: firewall theo dải private, grant theo dải, không mật khẩu trên dòng lệnh, có kế hoạch rollback pipeline.

Bài tiếp theo

Helm chart với NFS PersistentVolume tĩnh, claimRef và ba loại probe. Thay volume NFS khai báo thẳng trong Deployment bằng PV/PVC tĩnh, và cách ba loại probe giúp website khởi động an toàn.