Hình dung pod như một căn hộ chung cư nhỏ mà vài người ở chung. Họ dùng chung một địa chỉ nhận thư (một IP), một đường điện thoại nội bộ (localhost), và khi hết hợp đồng thuê thì tất cả cùng dọn đi một lượt — không ai ở lại. Đó chính là ý nghĩa của câu "pod là đơn vị nhỏ nhất, không phải container". Nghe qua thì như một chi tiết thuật ngữ khô khan, nhưng đo thử mới thấy nó có hệ quả thật — kể cả một cái bẫy về thời gian xoá mà rất nhiều người hiểu sai.
Một pod, một IP
Dựng một pod có hai container: web chạy nginx, ban chạy busybox.
podIP: 10.244.0.13
container trong pod: ban web
Một IP cho cả pod, không phải một IP mỗi container.
Cho ban gọi http://localhost/:
<!DOCTYPE html>
Hai container khác nhau nói chuyện với nhau qua localhost. Chúng chung một không gian mạng, một địa chỉ IP, một dải cổng.
Đây là toàn bộ lý do pod tồn tại. Nếu container đã đủ, Kubernetes đã dùng container làm đơn vị. Pod là cách gói vài container phải chạy cùng máy và chia sẻ tài nguyên với nhau.
Hệ quả trực tiếp, và cũng là chỗ người ta hay vấp: hai container trong cùng pod không được nghe cùng một cổng. Chúng chung dải cổng, nên hai nginx trong một pod là xung đột.
Khi nào cần nhiều container trong một pod
Ba mẫu, và cả ba đều dựa vào việc chung không gian mạng hoặc chung thư mục:
Sidecar — container phụ chạy cùng suốt vòng đời: thu log, proxy mạng (Envoy trong lưới dịch vụ), làm mới chứng chỉ.
Adapter — đổi định dạng đầu ra của ứng dụng chính thành thứ hệ thống theo dõi hiểu được.
Ambassador — đứng giữa ứng dụng và thế giới bên ngoài, ví dụ một proxy tới cơ sở dữ liệu để ứng dụng chỉ cần nói localhost:5432.
Ngoài ba mẫu này, gần như luôn nên tách thành hai pod. Nhiều container trong một pod nghĩa là chúng cùng sống cùng chết, cùng được xếp lên một node, và không co giãn độc lập được.
Bốn giai đoạn khởi động
Scheduled Successfully assigned default/t2 to kt-control-plane
Pulled Container image "nginx:alpine" already present on machine
Created Created container: t2
Started Started container t2
Bốn dòng, bốn thành phần khác nhau ghi ra — đúng chuỗi ở phần trước.
Thời gian tới Ready:
| ảnh đã có sẵn trên node | 2,4 giây |
| ảnh phải kéo về | 6,7 giây |
| 2 initContainer × 3 giây + 2 container | 9,0 giây |
Chênh lệch 2,4 so với 6,7 giây là thời gian kéo ảnh, và đó thường là phần lớn nhất trong thời gian khởi động pod. Ảnh nhỏ khởi động nhanh hơn, và đó là lý do alpine phổ biến đến vậy.
initContainer chạy tuần tự
Hai initContainer, mỗi cái sleep 3, cộng hai container chính: 9,0 giây.
Sáu giây trong đó là sàn bắt buộc — initContainer chạy lần lượt, cái sau đợi cái trước kết thúc thành công. Không có cách nào chạy song song.
Chúng dùng để làm việc phải xong trước khi ứng dụng khởi động: chạy migration cơ sở dữ liệu, đợi một dịch vụ khác sẵn sàng, tải cấu hình về thư mục chung.
Ba initContainer mỗi cái mất 10 giây là 30 giây thêm vào mọi lần khởi động pod — nhân với số bản, nhân với số lần triển khai. Đây là chỗ đáng đo trước khi thêm cái thứ tư.
Xoá pod: thời gian không phụ thuộc ứng dụng
Đây là phát hiện phản trực giác nhất của bài, và là chỗ tôi khuyên bạn dừng lại đọc kỹ.
Bốn phép đo, hai giá trị terminationGracePeriodSeconds, hai hành vi container trái ngược:
| grace | Container | kubectl delete mất |
|---|---|---|
| 5 s | sleep — chết ngay khi nhận SIGTERM |
7,0 giây |
| 30 s | sleep — chết ngay khi nhận SIGTERM |
31,1 giây |
| 5 s | trap '' TERM — bỏ qua SIGTERM hoàn toàn |
7,0 giây |
| 30 s | trap '' TERM — bỏ qua SIGTERM hoàn toàn |
31,5 giây |
Hai hàng đầu và hai hàng cuối giống hệt nhau. Container chết ngay hay bướng bỉnh không chịu chết — kết quả như nhau.
Kết quả chỉ phụ thuộc terminationGracePeriodSeconds:
thời gian xoá = grace period + khoảng 1-2 giây
Nghĩa là:
Thời gian
kubectl delete podkhông đo được tốc độ tắt của ứng dụng.
Rất nhiều người dùng con số này để đánh giá "ứng dụng của tôi tắt có sạch không". Nó không nói gì về điều đó.
Tôi không tách được từ bên ngoài là kubelet giữ pod đủ grace period, hay chính kubectl chờ. Cái đo được chắc chắn là: hành vi container không thay đổi kết quả.
Muốn biết ứng dụng tắt nhanh hay chậm thì phải nhìn log của chính nó, hoặc đo thời gian giữa lúc nhận SIGTERM và lúc tiến trình thoát.
Vì sao vẫn nên đặt grace period đúng
Dù thời gian delete không đổi, grace period vẫn quan trọng — nó là trần thời gian ứng dụng được phép dọn dẹp:
terminationGracePeriodSeconds: 30 # mặc định
Hết thời gian đó là SIGKILL, không thương lượng. Ứng dụng đang xử lý một yêu cầu HTTP dài, hoặc đang ghi nốt một lô vào cơ sở dữ liệu, sẽ bị cắt ngang.
Đặt quá ngắn: mất công việc đang dở. Đặt quá dài: mọi lần triển khai cuốn chiếu chậm đi đúng chừng đó, nhân với số pod.
Con số đúng là thời gian tắt sạch tệ nhất của ứng dụng, cộng biên. Đo nó, đừng đoán.
Bốn pha của pod
Pending đã tạo trong etcd, chưa chạy — đang đợi xếp chỗ hoặc đang kéo ảnh
Running ít nhất một container đang chạy
Succeeded mọi container đã kết thúc với mã 0
Failed ít nhất một container kết thúc với mã khác 0
Pending là pha đáng chú ý nhất khi gỡ lỗi, vì nó gộp hai nguyên nhân rất khác nhau: chưa được xếp chỗ (thiếu tài nguyên, không node nào hợp) và đang kéo ảnh. Phân biệt bằng:
kubectl describe pod <ten> | tail -20
Có dòng Scheduled nghĩa là đã xếp chỗ xong, vấn đề nằm ở ảnh. Không có nghĩa là bộ lập lịch chưa tìm được node — và Events sẽ nói vì sao.
Cùng một câu hỏi "gộp hay tách", đổi ngăn xếp
Quyết định "cái gì phải ở chung một pod, cái gì nên tách ra" không phải chuyện riêng của Kubernetes. Nó là một trong những câu hỏi thiết kế cơ bản nhất ở mọi tầng: đâu là ranh giới của số phận chung.
Trong thế giới Java, đó là lằn ranh giữa nhiều luồng trong một JVM với nhiều JVM riêng. Các luồng chung một heap, một vòng đời — một OutOfMemoryError hay một lần crash giết sạch tất cả, đúng như các container cùng pod cùng sống cùng chết. Tách sang hai tiến trình riêng là để chúng hỏng độc lập, đổi lại mất đi khả năng chia sẻ bộ nhớ trực tiếp. Bạn gộp vào một tiến trình những gì buộc phải chia sẻ trạng thái, và tách ra những gì cần chịu lỗi riêng.
Trong Go, goroutine cũng vậy: mọi goroutine sống trong cùng một tiến trình, chung không gian địa chỉ, và chết cùng nhau khi tiến trình chết — gộp công việc vào goroutine khi chúng cần chia sẻ dữ liệu qua bộ nhớ, tách thành dịch vụ riêng khi cần biên giới hỏng hóc.
Còn ở cơ sở dữ liệu, đơn vị gộp chính là giao dịch: các câu lệnh trong một giao dịch commit hoặc roll back cùng nhau — số phận chung nguyên khối. Và đúng như việc nhét quá nhiều container vào một pod, một giao dịch quá rộng cũng phải trả giá: nó giữ khoá lâu hơn và cột chặt những phần việc lẽ ra chẳng liên quan gì đến nhau.
Mẫu số chung: đơn vị gộp là một lựa chọn có chủ đích về số phận chung — chỉ nên đặt chung những thứ buộc phải cùng sống, cùng chết và cùng được đặt ở một chỗ; mọi thứ khác gộp vào sẽ phải trả một khoản thuế kết dính: không co giãn riêng được, không hỏng riêng được, không nâng cấp riêng được. Và có một bẫy đo lường đi kèm: khi một con số đo được bị chi phối bởi một ngưỡng cấu hình (như kubectl delete bị chi phối bởi grace period), nó không còn đo cái bạn tưởng nó đo — nó đo cái trần, không đo phần việc thật. Nên câu hỏi đúng khi gói kiến trúc không phải "gộp cho tiện hay tách cho gọn" mà là "hai thứ này có thật sự phải chung số phận không, và con số tôi đang nhìn đo phần việc thật hay chỉ đo một ngưỡng tôi tự đặt".
Muốn tự kiểm cái bẫy grace period, chạy thử:
kubectl run nhanh --image=busybox:1.36 --restart=Never \
--overrides='{"spec":{"terminationGracePeriodSeconds":30}}' -- sleep 3000
kubectl wait --for=condition=ready pod/nhanh
time kubectl delete pod nhanh
sleep chết ngay khi nhận SIGTERM, nhưng lệnh trên vẫn mất hơn ba mươi giây. Nếu bạn từng nghĩ triển khai chậm là do ứng dụng tắt chậm, đây là chỗ nên kiểm lại trước.
Phần sau đo Deployment và ReplicaSet: cơ chế đứng sau việc cập nhật không gián đoạn.