Vòng Đời Một Request: Từ kubectl apply Tới Pod Chạy

K
Kai··7 min read

Bài 16 chứng minh cluster chạy: app lên, chia tải, tự chữa lành. Nhưng "chạy" và "hiểu vì sao chạy" là hai chuyện. Bài này lấy một câu lệnh kubectl apply duy nhất rồi lần theo nó đi qua từng thành phần ta đã dựng (api-server, etcd, controller-manager, scheduler, kubelet, containerd, CNI) theo đúng trình tự thời gian. Đây là bài bản lề: nó khâu phần "dựng" của series lại thành một bức tranh động, và mô hình rút ra từ đây sẽ theo ta suốt các phần concept phía sau.

Có một điều cần nói trước, vì nó định hình mọi thứ: trong Kubernetes không có một nhạc trưởng đứng ra điều phối. Khi bạn apply, không ai gọi một chuỗi hàm "tạo pod → chọn node → chạy container". Thay vào đó, lệnh của bạn chỉ ghi lại điều mong muốn vào một nơi, rồi nhiều thành phần độc lập, mỗi cái một vòng lặp riêng, quan sát nơi đó và lần lượt kéo hiện thực về phía mong muốn. Hiểu được điểm này thì phần còn lại của bài là hệ quả.

Hai nửa của hành trình

Hành trình tách làm hai nửa rạch ròi. Nửa đầu (write path) đồng bộ và nhanh: kubectl gửi đối tượng, api-server kiểm duyệt rồi ghi vào etcd, trả kết quả. Xong nửa này, đối tượng tồn tại nhưng chưa có gì chạy. Nửa sau (reconcile path) bất đồng bộ: các controller, scheduler, kubelet lần lượt phản ứng với đối tượng mới và biến nó thành một pod đang chạy.

   ── WRITE PATH (đồng bộ, mili-giây) ──────────────────────────
   kubectl ─TLS─► api-server ─[authn▸authz▸admission▸validate]─► etcd
                     │                                            
                     └── trả 201 Created cho kubectl              
   ── RECONCILE PATH (bất đồng bộ, mỗi bên một vòng lặp) ────────
   etcd ◄─watch─ Deployment ctrl ─► tạo ReplicaSet ─► tạo Pod (Pending)
        ◄─watch─ scheduler ─► gán nodeName (Binding)
        ◄─watch─ kubelet@node ─► CRI ▸ containerd ▸ CNI ─► Pod Running

Nửa một: kubectl tới api-server

Bật -v=8 để thấy kubectl thực sự làm gì; nó là một HTTP client gọi REST:

kubectl get --raw /api/v1/namespaces/default/pods/trace-845c78d578-vf8zg -v=8
round_trippers.go:527] "Request" verb="GET" url="https://203.0.113.10:6443/api/v1/namespaces/default/pods/trace-845c78d578-vf8zg"
round_trippers.go:632] "Response" status="200 OK"

Hai dòng này gói cả nửa đầu. kubectl mở một kết nối TLS tới https://203.0.113.10:6443, Elastic IP của HAProxy (Bài 9), không phải tới một api-server cụ thể. Nó trình certificate admin để xác thực, và load balancer chuyển nguyên xi luồng tới một trong ba api-server (Bài 9: mode tcp passthrough, mTLS đầu-cuối). Với một lệnh ghi như apply, đây là POST, và đáp lại là 201 Created.

Bên trong api-server: hàng rào kiểm duyệt

Trước khi bất kỳ thứ gì chạm tới etcd, request phải qua một chuỗi cổng trong api-server. Thứ tự này quan trọng:

  1. Authentication — bạn là ai? api-server đọc client certificate, lấy ra danh tính (admin, O=system:masters). Cấu hình ở Bài 7.
  2. Authorization — bạn được làm việc này không? Chế độ Node,RBAC (Bài 7) tra xem danh tính đó có quyền create deployments trong namespace này không.
  3. Admission control — request nên được sửa hay từ chối không? Các plugin admission (Bài 7 bật NodeRestriction) có thể thay đổi đối tượng (mutating) hoặc chặn nó (validating). Đây là điểm cắm cho hầu hết chính sách cụm, ta sẽ đào sâu ở phần Security và Extending.
  4. Validation — đối tượng có hợp lệ theo schema không?
  5. Ghi vào etcd — qua đúng kết nối TLS client tới etcd (Bài 6), và với Secret thì mã hóa at-rest trước khi ghi (Bài 7, đã kiểm chứng bằng hexdump).

Chỉ khi qua hết năm cổng, api-server mới trả 201 cho kubectl. Lúc này đối tượng Deployment đã nằm trong etcd, nhưng chưa một pod nào tồn tại, chưa node nào được chọn, chưa container nào chạy. Nửa đầu kết thúc ở đây, và kubectl của bạn đã trả prompt về.

Nửa hai: các vòng lặp thức dậy

Đây là chỗ mô hình "không nhạc trưởng" hiện rõ. api-server không gọi ai cả. Thay vào đó, mỗi thành phần đang watch api-server, và việc một Deployment mới xuất hiện làm chúng lần lượt phản ứng.

Deployment controller (trong controller-manager, Bài 8) thấy có Deployment chưa có ReplicaSet tương ứng, bèn tạo một ReplicaSet. ReplicaSet controller thấy ReplicaSet muốn 1 bản sao mà chưa có pod nào, bèn tạo một Pod. Chuỗi tạo ra này để lại dấu vết trong trường ownerReferences:

# Pod thuộc về ReplicaSet nào, ReplicaSet thuộc Deployment nào
kubectl get pod trace-845c78d578-vf8zg -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}'
kubectl get rs trace-845c78d578 -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}'
ReplicaSet/trace-845c78d578
Deployment/trace
   Deployment/trace
        ▲ owns
   ReplicaSet/trace-845c78d578
        ▲ owns
   Pod/trace-845c78d578-vf8zg

Chuỗi sở hữu này không chỉ để xem cho vui: nó là cách Kubernetes biết phải xóa gì khi bạn xóa Deployment (garbage collection theo ownerReferences, một concept ta sẽ mổ riêng ở phần Objects). Pod vừa tạo có status.phase: Pendingspec.nodeName còn rỗng: nó tồn tại, nhưng chưa thuộc về node nào.

Scheduler chọn nhà cho pod

Scheduler (Bài 8) watch những pod có nodeName rỗng. Thấy pod trace chưa được gán, nó chấm điểm các node khả dĩ (lọc theo tài nguyên và ràng buộc, chủ đề cả một phần riêng sau này) rồi chọn một, và ghi lựa chọn đó ngược lại api-server qua một subresource tên Binding. Kết quả là spec.nodeName được điền:

kubectl get pod trace-845c78d578-vf8zg -o jsonpath='nodeName={.spec.nodeName}{"\n"}podIP={.status.podIP}'
nodeName=worker-1
podIP=10.200.1.8

nodeName=worker-1 là chữ ký của scheduler. Để ý scheduler không nói chuyện với worker-1; nó chỉ cập nhật đối tượng trong etcd qua api-server. Việc node kia biết được mình vừa nhận một pod lại là nhờ chính node đó đang watch.

kubelet biến đối tượng thành tiến trình

kubelet trên worker-1 (Bài 11) watch những pod có nodeName trỏ về mình. Thấy pod trace vừa được gán, nó bắt tay vào việc thật: gọi containerd qua CRI (Bài 10) để kéo image và tạo container, gọi CNI (Bài 14) để cấp IP và nối mạng, rồi khởi động. Mỗi bước nó ghi một Event lên api-server, và đây là chuỗi sự kiện thật của pod ta vừa tạo:

kubectl get events --field-selector involvedObject.name=trace-845c78d578-vf8zg
REASON      FROM                MESSAGE
Scheduled   default-scheduler   Successfully assigned default/trace-845c78d578-vf8zg to worker-1
Pulled      kubelet             Container image "...agnhost:2.52" already present on machine
Created     kubelet             Container created
Started     kubelet             Container started

Đọc từ trên xuống là đọc đúng vòng đời: Scheduled (scheduler gán node) → Pulled (containerd có image) → Created (container dựng xong) → Started (tiến trình chạy). Xong bước cuối, kubelet cập nhật status.phase của pod thành Running và điền podIP (10.200.1.8, từ dải pod của worker-1). Nếu pod đứng sau một Service, controller endpoint thêm IP này vào EndpointSlice và kube-proxy (Bài 12) cập nhật rule, rồi pod bắt đầu nhận lưu lượng.

Mô hình thật: watch và reconcile

Nhìn lại cả hành trình, điều đáng mang theo không phải thứ tự các bước, mà là cách chúng nối nhau. Không thành phần nào ra lệnh cho thành phần khác. Mỗi cái, từ Deployment controller, ReplicaSet controller, scheduler tới kubelet, chạy một vòng lặp giống nhau: quan sát trạng thái mong muốn, so với trạng thái thực tế, làm một việc nhỏ để thu hẹp khoảng cách, lặp lại. api-server (cùng etcd) là nguồn sự thật duy nhất mà tất cả cùng nhìn vào; không ai khác chạm trực tiếp vào etcd.

Mô hình này gọi là level-triggered reconciliation, và nó khác hẳn một chuỗi RPC tuần tự. Một chuỗi RPC mà đứt giữa chừng thì hỏng dở dang; còn một vòng lặp reconcile thì chỉ cần nhìn lại trạng thái hiện tại và tiếp tục, không quan tâm trước đó đã tới đâu. Cluster tự chữa lành ở Bài 16 chính nhờ cơ chế này: ta xóa một pod, ReplicaSet controller ở vòng lặp kế tiếp thấy "muốn 3, có 2", và tạo bù. Không có "xử lý sự kiện xóa" nào cả, chỉ có so sánh mong muốn với thực tế, mãi mãi.

Tổng kết

Một kubectl apply đi qua hai nửa: ghi đối tượng vào etcd sau hàng rào authn/authz/admission, rồi để một chuỗi vòng lặp độc lập biến đối tượng đó thành pod chạy: controller tạo ReplicaSet rồi Pod, scheduler gán node, kubelet gọi runtime và mạng. Tất cả nối nhau qua việc cùng watch một nguồn sự thật, không qua mệnh lệnh trực tiếp. Đó là lý do Kubernetes co giãn và tự lành: thêm một controller chỉ là thêm một vòng lặp quan sát cùng nguồn sự thật ấy.

Phần "dựng" của series khép lại ở đây, và cũng là lúc đổi nhịp. Từ bài sau, ta không thêm thành phần mới vào cluster nữa mà đi sâu vào các concept chạy trên nó, bắt đầu từ thứ gần người dùng nhất và cũng là đơn vị nền của mọi workload: Pod. Bài 18 mở phần Pods chuyên sâu với vòng đời của một pod (các phase, các condition, restartPolicy), đào kỹ hơn nhiều so với cái nhìn thoáng qua trong chuỗi Event ở trên.

Related Posts