Init container có từ lâu: chạy trước, chạy tuần tự, chạy xong mới tới container chính. Từ Kubernetes 1.29 nó gánh thêm một việc thứ hai, và việc đó chữa một lỗi mà ai chạy Job với service mesh đều từng gặp.
Sidecar là một init container không bao giờ kết thúc
initContainers:
- name: sc
image: busybox:1.36
restartPolicy: Always # <- dòng này biến nó thành sidecar
command: ["sh","-c","while true; do sleep 5; done"]
restartPolicy: Always trên một init container đổi hai điều: nó không chặn bước sau, và nó sống suốt đời pod.
Thứ tự, đo bằng dấu thời gian
Pod có ba init container: i1 thường, sc sidecar, i2 thường; rồi tới app.
i1 16 -> 22 (thường, 6 giây)
sc 23 -> ... (sidecar, còn chạy)
i2 24 -> 30 (thường, 6 giây)
app 31 -> ...
sc bắt đầu lúc giây 23 và giây 24 thì i2 đã chạy — không đợi sc kết thúc, vì nó không bao giờ kết thúc. Còn i1 thì chặn: sc chỉ khởi động sau khi i1 xong hẳn.
Sau khi mọi init container "xong" theo nghĩa của nó — thường thì thoát, sidecar thì khởi động — app mới chạy. Pod hiện 2/2 vì sc được đếm vào tổng số container đang sống.
Điều này giải quyết vấn đề kinh điển của service mesh: proxy phải sẵn sàng trước ứng dụng, nếu không mọi kết nối trong vài giây đầu đều hỏng. Đặt proxy vào containers thì thứ tự không có gì bảo đảm; đặt vào initContainers với restartPolicy: Always thì có.
Job có sidecar: hai cách khai, hai kết cục
Hai Job giống hệt nhau — một container làm việc sleep 5 rồi thoát, một sidecar chạy vô hạn — chỉ khác chỗ khai sidecar.
| Cách khai | Sau 105 giây |
|---|---|
sidecar trong containers |
Running 0/1, pod 1/2 NotReady |
sidecar trong initContainers + restartPolicy: Always |
Complete 1/1 |
Job thứ nhất không bao giờ kết thúc. Nó đợi mọi container trong pod dừng, mà sidecar thì không tự dừng.
Ai chạy CronJob trong cụm có service mesh đều gặp: công việc xong từ lâu, pod vẫn NotReady mãi, và bản chạy tiếp theo đâm vào concurrencyPolicy. Cách chữa cũ là cho container chính curl vào cổng quản trị của proxy để tự tắt nó — một mẹo vặt phải nhớ ở từng Job.
Với cách khai mới, kubelet giết sidecar khi container chính thoát. Không cần mẹo gì.
Nhưng Job "xong" vẫn tốn 41 giây
Job thứ hai kết thúc, nhưng DURATION là 41 giây cho một công việc 5 giây. Tôi tưởng đo nhầm.
Dấu thời gian nói rõ:
viec 16 -> 21 (5 giây, đúng)
sidecar 16 -> 46 (bị giết lúc giây 46)
Container chính xong lúc giây 21, sidecar mãi giây 46 mới chết — chậm 25 giây. Đo lại với terminationGracePeriodSeconds: 5:
| Hạn ân hạn | DURATION |
|---|---|
| 30 (mặc định) | 41 giây |
| 5 | 16 giây |
Chênh đúng 25 giây, bằng 30 − 5. Nguyên nhân giống hệt điều đo được ở phần nói về kubectl delete: sleep của busybox không bắt SIGTERM, nên kubelet phải chờ hết hạn ân hạn rồi mới SIGKILL.
Nghĩa là sidecar không bắt tín hiệu sẽ cộng thẳng hạn ân hạn vào thời lượng của mọi lần chạy Job. CronJob mỗi phút một lần, mỗi lần công việc 5 giây, mà mất 41 giây — nhìn vào bảng điều khiển thì cứ tưởng công việc chậm.
Hai cách chữa, ưu tiên cách đầu:
- Làm sidecar bắt
SIGTERMvà thoát ngay. Với proxy thật (Envoy, linkerd-proxy) thì chúng đã bắt sẵn. - Hạ
terminationGracePeriodSecondscho Job. Chỉ là che triệu chứng, và hạ quá tay thì sidecar không kịp xả log hoặc đóng kết nối.
Khi nào dùng init thường, khi nào dùng sidecar
| Việc | Loại |
|---|---|
| Chờ CSDL sẵn sàng | init thường |
| Chạy migration lược đồ | init thường |
Tải cấu hình về một emptyDir |
init thường |
| Proxy của service mesh | sidecar |
| Bộ gom log đọc thư mục dùng chung | sidecar |
| Bộ làm mới bí mật định kỳ | sidecar |
Quy tắc: việc có điểm kết thúc thì là init thường; việc chạy song song với ứng dụng thì là sidecar.
Init container thường còn một điểm hay bị quên: nó dùng chung volume với container chính nhưng có thể có securityContext riêng. Cần root để chown một volume ư — cho init container chạy root, còn container chính vẫn runAsNonRoot: true. Đặc quyền chỉ tồn tại trong vài giây đầu.
Thử ba mươi giây
Tìm Job và CronJob đang khai sidecar theo cách cũ:
kubectl get jobs,cronjobs -A -o json | python3 -c '
import sys,json
for o in json.load(sys.stdin)["items"]:
sp=o["spec"]
t=sp.get("template") or sp["jobTemplate"]["spec"]["template"]
n=len(t["spec"]["containers"])
if n>1:
print(o["kind"], o["metadata"]["namespace"], o["metadata"]["name"],
f"-> {n} container trong containers")
'
Mỗi dòng in ra là một Job có thể không bao giờ kết thúc. Kiểm nhanh bằng cách so DURATION với thời gian công việc thật — chênh đúng bằng hạn ân hạn thì thủ phạm là sidecar chứ không phải công việc.
Phần sau: kubectl từ phía người dùng — cache nằm ở đâu và vì sao lệnh đầu tiên sau khi đổi cụm luôn chậm hơn hẳn.