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.

Dòng thời gian đo được và kết cục của hai cách khai sidecar trong Job

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/2sc đượ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 DURATION41 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 SIGTERM và thoát ngay. Với proxy thật (Envoy, linkerd-proxy) thì chúng đã bắt sẵn.
  • Hạ terminationGracePeriodSeconds cho 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.