Mỗi khi một hệ thống nói "làm cho mọi X", hãy dừng lại hỏi: mọi X — trừ những X nào? Gần như luôn có một danh sách loại trừ ngầm mà tài liệu không in đậm, và nó là nguồn của những lần đếm hụt khó hiểu. DaemonSet hứa "một bản trên mỗi node", nhưng cụm bốn node của tôi ra ba bản — và cái node thứ tư biến đi đâu là bài học đầu tiên. DaemonSet chạy đúng một bản trên mỗi node; nghe đơn giản, nhưng "mỗi node" có một điều kiện ẩn mà phép đo đầu tiên đã cho thấy.
Bốn node, ba bản
agent-qpbmg sc-worker
agent-9jjrn sc-worker2
agent-hq278 sc-worker3
DESIRED=3 CURRENT=3 READY=3
Cụm có bốn node. DaemonSet tạo ba bản. Node control plane bị bỏ qua vì nó có vết (taint), và DaemonSet mặc định không khai toleration nào — cùng cơ chế đã đo ở phần 47.
Thêm một dòng:
tolerations:
- {operator: Exists}
DESIRED=4 READY=4
operator: Exists không kèm key nghĩa là chịu được mọi vết. Đó chính là cách kube-proxy, CNI plugin và các CSI driver phủ được cả control plane — chúng phải có mặt ở mọi node để cụm hoạt động. Với DaemonSet của bạn, operator: Exists không kèm điều kiện là quá rộng: nó cũng khiến pod chạy trên node đang bị drain để bảo trì. Nên khai cụ thể:
tolerations:
- {key: node-role.kubernetes.io/control-plane, operator: Exists, effect: NoSchedule}
Phản ứng trong 0,1 giây
Thêm nodeSelector rồi gán và gỡ nhãn:
chưa node nào có nhãn: DESIRED=0 READY=0
gán nhãn cho sc-worker -> có pod sau 0,1 giây
gán nhãn cho sc-worker2 -> có pod sau 0,1 giây
gỡ nhãn khỏi sc-worker2 -> pod bị xoá sau 2,2 giây
Một phần mười giây. Bộ điều khiển DaemonSet theo dõi node qua watch (phần 46), nên nó thấy thay đổi ngay khi apiserver ghi nhận. Chú ý sự tương phản với phần 42: node chết thì mất gần sáu phút mới có gì xảy ra. Node đổi nhãn thì 0,1 giây. Khác biệt không nằm ở bộ điều khiển mà ở chỗ Kubernetes phải mất bao lâu để kết luận rằng node đã chết.
Hệ quả thực dụng: khi cụm tự co giãn thêm node, DaemonSet có mặt trên node mới gần như ngay lập tức — trước cả khi pod ứng dụng được xếp lên đó. Đó là điều bắt buộc với agent thu log và plugin mạng.
Ba việc DaemonSet dùng để làm
Thu log — mỗi node một agent đọc /var/log/containers qua hostPath.
Đo đạc — node-exporter, cAdvisor, agent của hệ thống giám sát.
Hạ tầng mạng và lưu trữ — kube-proxy, CNI plugin (phần 60 đo kindnet và Calico, cả hai đều là DaemonSet), CSI node driver.
Điểm chung: việc gắn với node, không gắn với lượng người dùng. Thêm node là phải có thêm một bản; thêm tải thì không. Đó cũng là lý do DaemonSet không có replicas. Số bản bằng số node khớp bộ chọn, và Kubernetes tự cộng trừ. Nó không co giãn được, và cũng không cần.
Cập nhật cuốn chiếu
updateStrategy:
type: RollingUpdate
rollingUpdate: {maxUnavailable: 1}
maxUnavailable ở đây có ý nghĩa khác Deployment: nó là số node được phép thiếu bản trong một lúc, không phải tỉ lệ pod. Và không có maxSurge theo mặc định — vì hai bản trên cùng một node thường xung đột (cùng gắn hostPath, cùng cổng hostPort). Từ 1.22 có maxSurge cho DaemonSet nhưng phải bật rõ ràng.
Với cụm 100 node và maxUnavailable: 1, cập nhật một DaemonSet mất 100 lượt. Đặt maxUnavailable: 10% là cách rẻ để rút ngắn. OnDelete là chiến lược thứ hai: chỉ cập nhật khi bạn tự xoá pod. Hữu ích với agent mà việc khởi động lại có rủi ro — bạn kiểm soát từng node một.
Ba thứ thường đi kèm
hostPath để đọc thư mục của node — log, /proc, socket của runtime.
hostNetwork: true để dùng thẳng không gian mạng của node. kube-proxy cần điều này vì nó sửa iptables của node.
priorityClassName: system-node-critical để DaemonSet hạ tầng không bị đuổi khi node cạn tài nguyên (phần 55) và được ưu tiên khi xếp chỗ.
Ba thứ này cộng lại khiến DaemonSet có quyền lớn hơn pod thường rất nhiều — nên chúng cũng là mục tiêu đáng chú ý về bảo mật. Một DaemonSet bị chiếm quyền có hostPath: / là quyền root trên mọi node.
Mẫu số chung: chọn nguyên thuỷ theo trục mà thứ đó co giãn, và "mọi" luôn kèm bộ lọc ẩn
Bài này gói hai bài học dùng ở mọi hệ. Thứ nhất, và quan trọng nhất: hãy hỏi một khối lượng công việc co giãn theo trục nào, rồi chọn nguyên thuỷ khớp trục đó — chọn nhầm trục là một lỗi phân loại, không phải lỗi tinh chỉnh. DaemonSet không có replicas vì số bản của nó suy ra từ số node: nó co theo hạ tầng, không theo tải. Cùng câu hỏi ấy chọn đúng công cụ khắp nơi: thứ một-cho-mỗi-node (agent, plugin) là DaemonSet; thứ co-theo-tải là Deployment cộng HPA; thứ một-cho-mỗi-danh-tính là StatefulSet. Trong Java/Spring, đây đúng là chọn phạm vi bean: @Singleton (một cho cả ứng dụng), @RequestScope (một cho mỗi yêu cầu), @SessionScope (một cho mỗi phiên) — đặt một thứ đáng lẽ theo-yêu-cầu thành singleton (hoặc ngược lại) là một lỗi kinh điển sinh ra rò rỉ trạng thái hoặc nghẽn. Cũng như một thread pool nên định cỡ theo số lõi chứ không theo số request, một connection pool theo số worker. Nguyên tắc: hỏi "cái này tỉ lệ với gì" — số lõi, số node, số request, số tenant — và để câu trả lời chọn vòng đời cho nó.
Thứ hai: một mặc định "làm cho mọi X" gần như luôn kèm một bộ lọc ẩn. "Mỗi node" của DaemonSet thực ra là "mỗi node mà tôi chịu được vết"; node control plane rơi ra lặng lẽ. Cùng cái bẫy với SELECT * bỏ cột ẩn/tính toán, với kubectl get all chỉ lấy 8/34 loại (phần về sao lưu), với "tất cả người dùng" quên mất người đã bị xoá mềm, với một glob bỏ qua tệp ẩn. "Tất cả" hiếm khi là tất cả — luôn hỏi bộ lọc ngầm nằm ở đâu.
Mẫu số chung: trước khi chọn công cụ cho một khối lượng việc, xác định nó tỉ lệ với trục nào và chọn nguyên thuỷ khớp trục đó; và mỗi khi thấy chữ "mọi/tất cả", tìm cho ra danh sách loại trừ ngầm đi kèm.
Muốn tự soi, xem DaemonSet nào của bạn không phủ hết node:
kubectl get ds -A -o custom-columns=\
NS:.metadata.namespace,TEN:.metadata.name,\
MONG_MUON:.status.desiredNumberScheduled,\
SAN_SANG:.status.numberReady
kubectl get nodes --no-headers | wc -l
MONG_MUON nhỏ hơn số node là bình thường — thường vì vết hoặc nodeSelector. SAN_SANG nhỏ hơn MONG_MUON thì không: có node đang thiếu agent, và với agent thu log nghĩa là log của node đó đang mất.
Phần sau đo Job và CronJob: chạy một lần rồi thôi, và ba cách nó hỏng.