Nhìn một cái bàn làm việc chất đầy 40% mặt bàn, bạn có kết luận "sắp hết chỗ" không? Còn tuỳ. Nếu phần lớn đống đó là giấy nháp mà bạn quét tay một cái là gọn ngay khi cần chỗ, thì cái bàn thực ra còn trống thênh thang. Đo bộ nhớ trong Kubernetes vấp đúng chỗ này: "container đang dùng 413 MB" nghe như sắp chạm trần, nhưng 400 MB trong đó là giấy nháp kernel giữ hộ và vứt trong tích tắc. Hoảng lên vì con số ấy là hoảng vì đống giấy nháp. Bài này quay lại đo chính những con số mà bốn mươi phần trước đã dựa vào.
Cùng một container, bốn con số bộ nhớ
Container giới hạn 1 Gi, ghi rồi đọc một tệp 400 MB.
memory.current 413 Mi
inactive_file (bộ đệm tệp) 400 Mi
anon (bộ nhớ thật của app) 0 Mi
working_set = current - inactive_file
13 Mi
kubectl top 12 Mi
413 hay 12? Cả hai đều đúng — chúng trả lời hai câu hỏi khác nhau. memory.current là tổng bộ nhớ mà cgroup này đang giữ, gồm cả bộ đệm tệp mà kernel giữ hộ. working_set là phần không thu hồi được — phần mà nếu thiếu thì container chết. 400 MB kia là bộ đệm tệp sạch: kernel vứt đi trong tích tắc khi cần chỗ. Container này không hề gần giới hạn 1 Gi.
Đây là nguồn của phần lớn báo động giả
Bảng điều khiển Grafana quen thuộc vẽ container_memory_usage_bytes. Đó chính là memory.current — 413 Mi, tức 40% của giới hạn, và nó tăng đều theo lượng tệp ứng dụng đọc. Đội trực nhìn đường cong đi lên, kết luận "rò bộ nhớ", nâng limit. Đường cong lại đi lên tiếp — vì bộ đệm tệp luôn nở ra lấp đầy chỗ được cho.
Chỉ số đúng để cảnh báo là container_memory_working_set_bytes. Nó là thứ OOM killer nhìn vào, và là thứ kubectl top hiển thị. Nếu bảng điều khiển của bạn dùng chỉ số kia, đây là một dòng đáng sửa ngay hôm nay:
container_memory_working_set_bytes{container!=""}
/ on(pod,container) group_left
kube_pod_container_resource_limits{resource="memory"}
Còn khi muốn biết ứng dụng thật sự cấp phát bao nhiêu — để đặt requests — thì nhìn container_memory_rss hoặc anon trong memory.stat, ở phép đo trên là 0 Mi.
CPU: kubectl top chậm hơn thực tế 30 giây
Tôi cho container quay vòng lặp bận rồi lấy mẫu mỗi 8 giây, đối chiếu với usage_usec của cgroup.
| giây | kubectl top |
usage_usec |
|---|---|---|
| 10 | 106m | 8.340.332 |
| 20 | 106m | 16.444.064 |
| 30 | 1000m | 24.565.380 |
| 40 | 1000m | 30.339.002 |
| 50 | 1001m | 30.342.276 |
| 60 | 210m | 30.345.826 |
| 70 | 210m | 30.350.182 |
| 80 | 1m | 30.353.865 |
usage_usec ngừng tăng từ khoảng giây 42 — CPU đã rảnh. kubectl top vẫn báo 1001m ở giây 50 và 210m ở giây 70, mãi giây 80 mới về 1m. Độ trễ đến từ chuỗi: kubelet cập nhật theo chu kỳ, metrics-server lấy mẫu 15 giây một lần, mỗi mẫu tính trên cửa sổ 10 giây.
Có một chi tiết nhỏ hơn nhưng đáng chú ý: các cặp giá trị lặp lại y hệt (106/106, 1000/1000, 210/210). Hỏi hai lần cách nhau 8 giây vẫn nhận đúng một mẫu. kubectl top không đo gì cả — nó đọc lại mẫu gần nhất mà metrics-server còn giữ. Nên đừng dùng kubectl top để kết luận về một đợt tăng tải vừa xảy ra. Đợt tải ngắn hơn 15 giây có thể không lọt vào mẫu nào. Điều này cũng giải thích luôn kết quả đo ở bài về HPA: HPA đọc chính nguồn này, nên độ trễ trên là sàn của mọi phản ứng tự động mở rộng.
metrics-server không giữ lịch sử
window: 10.021s
API metrics.k8s.io chỉ trả về một mẫu: cửa sổ gần nhất. Không có tham số thời gian, không có lịch sử, không lưu xuống đĩa. metrics-server khởi động lại là mất sạch. Nó được thiết kế đúng như vậy, cho hai người dùng: HPA và kubectl top. Cả hai chỉ cần biết "ngay bây giờ".
Từ đó ra ranh giới rõ ràng:
| Cần gì | Dùng gì |
|---|---|
HPA, VPA, kubectl top |
metrics-server |
| Đồ thị, cảnh báo, điều tra sự cố cũ | Prometheus |
Và cả hai đều lấy dữ liệu từ cùng một nguồn: cAdvisor nằm trong kubelet, đọc cgroup. Chúng khác nhau ở chỗ lấy mẫu bao lâu một lần và giữ lại bao lâu — chứ không phải ở chỗ đo cái gì. Đó cũng là câu trả lời cho "vì sao hai con số không khớp": khác cửa sổ, khác thời điểm lấy mẫu, và thường là khác cả chỉ số được chọn.
Ba chỉ số đáng cảnh báo hơn cả CPU và RAM
Sau bốn mươi bài, đây là ba thứ tôi thấy đáng đặt cảnh báo nhất mà bảng điều khiển mặc định thường không có:
# 1. Bóp CPU — pod chậm mà CPU vẫn dưới limit
rate(container_cpu_cfs_throttled_periods_total[5m])
/ rate(container_cpu_cfs_periods_total[5m]) > 0.25
# 2. Pod khởi động lại — nguyên nhân gốc thường đã mất log
increase(kube_pod_container_status_restarts_total[1h]) > 0
# 3. Rò tiến trình — chạm trần PID trước khi chạm trần RAM
container_processes / 19162 > 0.5
Cái đầu tiên là cái hay bị bỏ sót nhất: pod bị bóp CPU trông hệt như "ứng dụng chậm", và mọi đồ thị CPU đều bình thường vì nó đúng là dưới limit — đó chính là lý do nó bị bóp.
Mẫu số chung: một con số không có định nghĩa thì vô nghĩa, và một mẫu luôn kèm cửa sổ với độ trễ
Bài này gói hai kỷ luật đo lường dùng ở mọi hệ. Thứ nhất: một cái tên chỉ số chưa nói gì cho tới khi bạn biết định nghĩa chính xác của nó — "bộ nhớ đang dùng" tách thành thu hồi được (bộ đệm) và không thu hồi được (working set), và cảnh báo nhầm vế sẽ vừa kêu oan vừa dẫn tới cách sửa tự nuôi chính nó: nâng limit → bộ đệm nở ra lấp đầy → đường cong lại lên → lại nâng. Đây đúng là chuyện Linux free báo RAM "đã dùng" gồm cả buff/cache — cột available mới là phần thật sự còn dùng được (meme "Linux ate my RAM" sinh ra từ đúng hiểu nhầm này). Trong Java, cùng cái bẫy giữa heap used, committed và max: một JVM committed cao không phải rò, và GC chỉ chạy theo used. "Dung lượng đĩa đã dùng" gồm cache có thể xoá cũng vậy. Quy tắc: trước khi cảnh báo trên một chỉ số, hỏi nó đo chính xác đại lượng nào — và chọn đúng vế mà nếu thiếu thì hệ thật sự chết.
Thứ hai: một chỉ số lấy mẫu luôn mang theo một cửa sổ, một độ trễ và một hạn giữ — nên hai con số "đều đúng" từ cùng một nguồn vẫn lệch nhau, và một đợt tăng ngắn hơn khoảng lấy mẫu có thể vô hình hoàn toàn. kubectl top trễ 30 giây và lặp lại mẫu cũ vì nó đọc một cửa sổ 10 giây, làm mới mỗi 15 giây. Đây chính là hiện tượng aliasing trong lý thuyết lấy mẫu (Nyquist): tín hiệu biến thiên nhanh hơn nhịp lấy mẫu thì hoặc biến mất hoặc hiện ra méo mó — cùng lý do với cửa sổ rate() của Prometheus, với tần số của một profiler, với khoảng scrape của mọi hệ giám sát. Muốn bắt gai nhọn thì phải lấy mẫu dày hơn gai, hoặc đo bằng bộ đếm tích luỹ thay vì gauge tức thời.
Mẫu số chung: đừng tin một con số cho tới khi biết nó đo đại lượng nào (thu hồi được hay không, tổng hay phần) và lấy mẫu ra sao (cửa sổ bao rộng, trễ bao lâu, giữ bao lâu) — thiếu vế đầu thì cảnh báo kêu oan và sửa sai hướng; thiếu vế sau thì bạn kết luận về một đợt tải mà công cụ của mình chưa từng nhìn thấy.
Muốn tự soi, xem chênh lệch giữa hai cách đo bộ nhớ trên cụm của bạn:
for p in $(kubectl get pods -A -o jsonpath='{range .items[?(@.status.phase=="Running")]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}'); do
ns=${p%%/*}; n=${p##*/}
v=$(kubectl exec -n "$ns" "$n" -- sh -c \
'cur=$(cat /sys/fs/cgroup/memory.current 2>/dev/null); inf=$(awk "/^inactive_file /{print \$2}" /sys/fs/cgroup/memory.stat 2>/dev/null); [ -n "$cur" ] && echo "$((cur/1048576)) $(((cur-inf)/1048576))"' 2>/dev/null)
[ -n "$v" ] && echo "$v $p"
done | awk '{d=$1-$2; if (d>50) printf "%4d Mi bo dem (current %d, working_set %d) %s\n", d, $1, $2, $3}'
Mỗi dòng in ra là một pod mà bảng điều khiển đang phóng đại lượng bộ nhớ. Chênh càng lớn thì cảnh báo hiện tại càng vô nghĩa.
Phần sau: sự kiện của cụm — cái gì được ghi lại, và nó biến mất sau bao lâu.