Ở bài Kubernetes là gì, control plane, node và pod đã được giới thiệu ở mức khái niệm. Bài này chuyển sang những object mà dev sẽ gặp hằng ngày khi một site Website AI chạy trên Kubernetes: Namespace để tách site, Pod và vòng đời của nó, Deployment và ReplicaSet để giữ đúng số bản chạy và thay phiên bản không gián đoạn, Service để có một địa chỉ ổn định phía trước các pod. Cuối bài là các lệnh kubectl cơ bản và những lỗi mà ai mới học cũng sẽ gặp. YAML trong bài là mẫu tối thiểu để học, bạn có thể áp thử lên một cluster thử nghiệm.

1. Namespace: mỗi website một namespace

Namespace chia một cluster thành nhiều không gian tên riêng. Hai object cùng tên có thể tồn tại ở hai namespace khác nhau. Với Website AI, một quy ước hiệu quả là mỗi website một namespace, tên lấy từ domain, thay dấu chấm bằng gạch ngang: site-a.example.com thành site-a-example-com. Tên namespace phải theo chuẩn nhãn DNS: chữ thường, số, gạch ngang, tối đa 63 ký tự.

Quy ước này mang lại mấy lợi ích rõ ràng:

  • Mọi thứ của một site (Deployment, Service, cấu hình, bí mật) nằm gọn trong một chỗ. kubectl get all -n site-a-example-com là thấy hết.
  • Phân quyền theo namespace: cấp cho dev quyền xem log của đúng site họ phụ trách.
  • Gỡ site bằng kubectl delete namespace: mọi object bên trong bị xoá theo, trừ các tài nguyên cấp cluster như PersistentVolume. Trên Cloud VPS, gỡ site là đi xoá thư mục, file cấu hình nginx, dòng cron, file supervisor ở nhiều máy.
  • Đặt giới hạn tài nguyên (ResourceQuota) cho từng site nếu cần.

2. Pod và vòng đời của nó

Pod đi qua các pha (phase) sau:

PhaÝ nghĩa
PendingĐã được API server nhận nhưng chưa chạy: đang chờ xếp node hoặc đang kéo image
RunningĐã gán node, ít nhất một container đang chạy
SucceededMọi container đã kết thúc thành công, không chạy lại (thường gặp ở Job)
FailedMọi container đã kết thúc, ít nhất một container lỗi
UnknownKhông lấy được trạng thái, thường do mất liên lạc với node

Bên trong pha Running, từng container lại có trạng thái riêng: Waiting, Running, Terminated. Những chữ như CrashLoopBackOff hay ImagePullBackOff mà ta thấy ở cột STATUS thực chất là lý do khiến container đang ở trạng thái Waiting.

Điều quan trọng nhất: pod không tự hồi sinh. Kubelet có thể chạy lại container bên trong pod, nhưng nếu pod bị xoá hoặc node chết, pod đó mất hẳn. Việc tạo pod mới thay thế là của controller, mà phổ biến nhất là Deployment.

3. Deployment và ReplicaSet: giữ đúng số bản chạy

Deployment mô tả "tôi muốn N bản pod chạy theo mẫu này". Deployment không tự tạo pod mà tạo ra một ReplicaSet, và ReplicaSet mới là thứ đảm bảo đúng N pod tồn tại. Chuỗi quan hệ là Deployment → ReplicaSet → Pod, và tên pod phản ánh đúng chuỗi đó:

site-a-example-com-<hash của pod template>-<5 ký tự ngẫu nhiên>
└── tên Deployment ─┘└──── ReplicaSet ────┘└──── Pod ────┘

Mẫu Deployment và Service cho một site Website AI, dùng image all-in-one đã nói ở bài Docker hoá Laravel:

apiVersion: v1
kind: Namespace
metadata:
  name: site-a-example-com
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: site-a-example-com
  namespace: site-a-example-com
spec:
  replicas: 1
  selector:
    matchLabels:
      app: site-a-example-com
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: site-a-example-com
    spec:
      containers:
        - name: web
          image: <registry>/websiteai:<tag>
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: http
---
apiVersion: v1
kind: Service
metadata:
  name: site-a-example-com
  namespace: site-a-example-com
spec:
  type: ClusterIP
  selector:
    app: site-a-example-com
  ports:
    - name: http
      port: 80
      targetPort: http

Ba chỗ cần để ý: selector.matchLabels của Deployment phải khớp nhãn trong template; selector của Service cũng chọn pod theo đúng nhãn đó; và mọi thứ nằm trong namespace của site.

4. Rolling update: thay phiên bản không gián đoạn

Mỗi khi phần template của Deployment thay đổi (đổi tag image, đổi biến môi trường, đổi nhãn), Deployment tạo một ReplicaSet mới và chuyển dần pod từ ReplicaSet cũ sang. Tốc độ chuyển do hai tham số quyết định, chi tiết trong tài liệu Deployment trên kubernetes.io:

  • maxSurge: được phép chạy dư bao nhiêu pod so với replicas trong lúc chuyển. Mặc định 25%, làm tròn lên.
  • maxUnavailable: được phép thiếu bao nhiêu pod sẵn sàng. Mặc định 25%, làm tròn xuống.

Với site chỉ có một bản (replicas: 1), mặc định cho ra maxSurge = 1, maxUnavailable = 0: pod mới được tạo, chờ tới khi sẵn sàng rồi pod cũ mới bị tắt. Chữ "sẵn sàng" phụ thuộc vào readiness probe. Không có probe, Kubernetes coi container vừa chạy là sẵn sàng, trong khi Laravel có thể còn đang khởi động, và người dùng sẽ gặp lỗi trong vài giây đó. Đây là lý do mẫu ở trên đã có readinessProbe.

Các lệnh đi kèm rolling update:

kubectl rollout status deployment/site-a-example-com -n site-a-example-com
kubectl rollout history deployment/site-a-example-com -n site-a-example-com
kubectl rollout undo deployment/site-a-example-com -n site-a-example-com
kubectl rollout restart deployment/site-a-example-com -n site-a-example-com

rollout undo quay về ReplicaSet trước, thứ mà pipeline SSH trên Cloud VPS chưa bao giờ có. Mặc định Deployment giữ lại 10 ReplicaSet cũ (revisionHistoryLimit). rollout restart thì tạo lại toàn bộ pod theo kiểu cuốn chiếu mà không đổi image, hữu ích khi cấu hình bên ngoài thay đổi mà Deployment không biết.

5. Service: địa chỉ ổn định phía trước các pod

IP của pod đổi mỗi lần pod được tạo lại, nên cần một địa chỉ cố định. Service kiểu ClusterIP (mặc định) cấp một IP ảo chỉ dùng trong cluster, kèm một tên DNS:

site-a-example-com.site-a-example-com.svc.cluster.local
└─ tên Service ──┘ └── namespace ───┘

Service tìm pod bằng selector, và chỉ đưa vào danh sách endpoint những pod đang Ready. port là cổng của Service, targetPort là cổng của container (ở đây trỏ theo tên http). Kiểm tra Service đang trỏ tới pod nào:

kubectl get endpoints site-a-example-com -n site-a-example-com

Danh sách rỗng gần như luôn là một trong hai lý do: selector không khớp nhãn pod (gõ sai một ký tự là đủ), hoặc pod chưa qua readiness probe. Service kiểu ClusterIP chưa nhận traffic từ Internet. Đưa traffic từ ngoài vào là việc của Ingress, chủ đề của một bài sau.

6. kubectl hằng ngày: get, describe, logs, exec

Trên Cloud VPS, dev SSH vào minion để xem log và chạy artisan. Trên Kubernetes, mọi việc đi qua kubectl:

Việc cần làmLệnh
Xem pod của sitekubectl get pods -n site-a-example-com -o wide
Xem chi tiết và sự kiệnkubectl describe pod <tên-pod> -n site-a-example-com
Đọc log (theo dõi liên tục)kubectl logs deploy/site-a-example-com -n site-a-example-com -f
Log của lần chạy trước khi container chếtkubectl logs <tên-pod> -n site-a-example-com --previous
Vào trong containerkubectl exec -it deploy/site-a-example-com -n site-a-example-com -- sh
Xem sự kiện của namespacekubectl get events -n site-a-example-com --sort-by=.lastTimestamp

Kết quả kubectl get pods có các cột READY (số container sẵn sàng trên tổng số), STATUS, RESTARTS (số lần container bị chạy lại) và AGE. RESTARTS tăng đều là dấu hiệu container đang chết đi sống lại. kubectl exec rất tiện để chạy php artisan, nhưng mọi thay đổi file làm trong container sẽ mất khi pod được tạo lại, nên đừng sửa code bằng cách này.

Một điều dev hay bất ngờ: kubectl delete pod một pod do Deployment quản lý thì vài giây sau sẽ có pod mới tên khác. Đó không phải lỗi, mà là ReplicaSet đang làm đúng việc giữ đủ số bản.

7. Lỗi thường gặp: CrashLoopBackOff, ImagePullBackOff, Pending

CrashLoopBackOff

Container khởi động rồi thoát, kubelet chạy lại, lại thoát. Mỗi lần chờ lâu hơn, từ 10 giây tăng gấp đôi tới tối đa 5 phút. Cách điều tra: kubectl logs --previous để xem log của lần chết gần nhất, kubectl describe pod để xem mã thoát. Với Laravel, nguyên nhân hay gặp là tiến trình chính chạy nền khiến container thoát ngay, thiếu biến môi trường quan trọng, hoặc lỗi quyền ghi storage/. Mã thoát 137 kèm lý do OOMKilled nghĩa là container vượt giới hạn RAM.

ImagePullBackOff, ErrImagePull

Node không kéo được image. Kiểm tra theo thứ tự: tên và tag image có đúng không, registry riêng đã khai báo imagePullSecrets chưa, và với Docker Hub, có chạm giới hạn số lần pull ẩn danh không. Phần Events trong kubectl describe pod ghi rõ thông báo lỗi từ registry.

Pending

Pod không được xếp lên node nào. Sự kiện FailedScheduling sẽ cho biết lý do, dạng 0/3 nodes are available: 3 Insufficient memory. Các nguyên nhân phổ biến: tổng tài nguyên pod đăng ký vượt chỗ trống trên các node, điều kiện chọn node (nodeSelector, affinity) không node nào khớp, hoặc pod chờ một volume chưa sẵn sàng.

Running nhưng READY 0/1

Container vẫn chạy nhưng readiness probe thất bại, nên Service không gửi traffic tới. Xem phần Events để biết probe trả mã gì, rồi thử gọi chính đường dẫn đó từ trong container.

8. Đánh đổi khi áp dụng Deployment cho nhiều site

Bốn object trong bài là bộ khung tối thiểu cho một site Website AI: một Namespace, một Deployment, một Service, sau này thêm cấu hình và lưu trữ. So với cách Website AI chạy trên Cloud VPS, ta được rolling update có chờ sẵn sàng, quay lui bằng một lệnh, và pod tự được tạo lại khi node chết. Đổi lại, số object phải quản lý nhân lên theo số site, YAML lặp lại gần như y hệt giữa các site, và dev cần học cách debug qua kubectl. Bài toán YAML lặp lại sẽ cần một công cụ sinh cấu hình như Helm, nhưng trước hết nên hiểu thật chắc từng object thô.

Bài tiếp theo

ConfigMap và Secret: biến môi trường Laravel trên Kubernetes. File .env của từng site đi đâu khi image dùng chung cho mọi site, và vì sao Secret không phải là mã hoá.