Hình dung một nhà hàng nhận đặt bàn. Có hai con số rất khác nhau: số ghế đã đặt trước và số người thật sự đang ngồi ăn. Nếu bạn nhìn sổ đặt bàn thấy "2000 ghế" rồi hoảng lên tưởng nhà hàng vỡ trận, bạn đã đọc nhầm con số — vì thực tế chỉ có 100 khách đang ngồi, phần còn lại là chỗ giữ trước mà chưa ai tới. top bày ra đủ loại con số kiểu đó cạnh nhau, và gần như cột nào cũng có một cách đọc sai. Bài này đo từng cột để biết mỗi con số thật ra trả lời câu hỏi gì.
top là công cụ ai cũng mở đầu tiên. Ta đo từng cột.
Hai thang đo %CPU khác nhau
%Cpu(s): 33.3 us, 0.0 sy, 0.0 ni, 50.0 id, 16.7 wa
PID ... S %CPU %MEM COMMAND
4732 R 100.0 1.1 stress-ng
4733 R 100.0 1.1 stress-ng
4727 R 100.0 0.0 stress-ng
4728 R 100.0 0.0 stress-ng
4729 R 100.0 0.0 stress-ng
Năm tiến trình, mỗi cái 100%. Nhưng dòng tổng chỉ nói 33,3%. Cả hai đều đúng, chỉ khác thang đo:
%CPUcủa tiến trình tính theo một nhân. 100% nghĩa là tiến trình đó đang dùng đầy đúng một nhân. Một tiến trình đa luồng có thể hiện 800%.- Dòng
%Cpu(s)tính trung bình trên tất cả nhân.
Năm nhân bận trên máy 16 nhân là 31% — khớp với 33,3 trong dòng đầu. Hệ quả: cộng cột %CPU của mọi tiến trình có thể ra 1.600% trên máy 16 nhân, và đó không phải lỗi. Với htop, phím H bật/tắt việc hiển thị từng luồng riêng, và nó đổi hẳn con số bạn nhìn thấy.
VIRT không phải bộ nhớ đang dùng
Một tiến trình ánh xạ 2.048 MB rồi chạm vào từng phần:
chạm 100 MB VIRT = 2.110.580 kB RES = 109.656 kB
chạm 800 MB VIRT = 2.110.580 kB RES = 826.464 kB
VIRT không đổi. RES bám theo phần đã thực sự chạm vào. VIRT là tổng không gian địa chỉ ảo — mọi thứ tiến trình đã yêu cầu ánh xạ, kể cả phần chưa bao giờ đụng tới, đúng như số ghế đã đặt trước. Linux cấp phát lười: một mmap 2 GB không tốn một byte RAM nào cho tới khi bạn ghi vào.
RES (resident set size) là phần thực sự nằm trong RAM — số khách đang ngồi. Đây là con số gần nhất với "tiến trình này tốn bao nhiêu bộ nhớ". Báo động vì VIRT lớn là báo động nhầm. JVM, runtime Go, và mọi thứ dùng mmap đều có VIRT lớn hơn RES nhiều lần — đó là thiết kế, không phải rò rỉ.
Nhưng RES cũng không phải câu trả lời cuối
RES gồm cả bộ nhớ dùng chung: thư viện động, tệp ánh xạ, vùng nhớ chia sẻ. Cột SHR cho biết phần đó. Nghĩa là cộng RES của mọi tiến trình sẽ ra nhiều hơn RAM thật, vì cùng một thư viện libc được tính cho mọi tiến trình dùng nó. Cột %MEM cũng vậy — tổng của nó có thể vượt 100%.
Muốn con số không trùng lặp thì cần PSS (proportional set size), chia phần dùng chung theo số tiến trình:
grep Pss /proc/<pid>/smaps_rollup
top không hiển thị PSS vì tính nó tốn kém. Nhưng khi cần biết "container này thật sự tốn bao nhiêu", đó mới là con số đúng.
Dòng Tasks đáng đọc
Tasks: 15 total, 6 running, 7 sleeping, 0 stopped, 2 zombie
Ba con số đáng để ý:
running — số tiến trình đang chạy hoặc sẵn sàng chạy. So nó với số nhân: lớn hơn nhiều nghĩa là đang xếp hàng.
zombie — tiến trình đã chết mà cha chưa gọi wait(). Một hai cái là bình thường và thoáng qua. Con số tăng dần nghĩa là có một tiến trình cha không dọn con, và mỗi zombie chiếm một mục trong bảng tiến trình cho tới khi cha chết.
total — tăng đều mà không giảm là dấu hiệu rò rỉ tiến trình, thường tệ hơn rò rỉ bộ nhớ vì nó chạm trần pid_max và làm cả máy không tạo được tiến trình mới.
Cột S — trạng thái
| Nghĩa | |
|---|---|
R |
Đang chạy hoặc sẵn sàng chạy |
S |
Ngủ, ngắt được — chờ mạng, chờ sự kiện |
D |
Ngủ không ngắt được — gần như luôn là chờ đĩa |
Z |
Zombie |
T |
Bị dừng |
D là trạng thái đáng chú ý nhất. Như đo ở phần 2, đó là nhóm làm load average tăng mà CPU không bận. Tiến trình ở D không giết được bằng kill -9 — nó phải chờ I/O xong. Nhiều tiến trình D kéo dài gần như luôn nghĩa là tầng lưu trữ có vấn đề, không phải ứng dụng.
Ba thứ top không nói
Vì sao tiến trình chậm. top cho biết nó dùng bao nhiêu CPU, không cho biết nó đang làm gì. Cho câu hỏi đó cần strace, perf, hoặc profiler của ngôn ngữ.
Bộ nhớ đi đâu bên trong tiến trình. RES là một con số duy nhất. Muốn biết phần nào là heap, phần nào là stack, phần nào là tệp ánh xạ thì đọc /proc/<pid>/smaps.
Tình trạng của container. top trong container đọc /proc của nhân, nên nó thấy toàn máy. Với ứng dụng chạy trong container, con số đáng tin nằm ở /sys/fs/cgroup/, không ở top.
htop thêm gì
htop hiển thị cùng dữ liệu nhưng dễ đọc hơn ở ba chỗ: thanh CPU từng nhân (thấy ngay tải có đều không), cây tiến trình (F5), và tìm kiếm (F3). Hai phím đáng nhớ: H ẩn/hiện luồng, và F6 đổi cột sắp xếp. Với ứng dụng nhiều luồng, mặc định của htop liệt kê từng luồng và làm danh sách rất dài — H là phím đầu tiên nên bấm.
Mẫu số chung: dashboard bày ra con số dễ lấy, không phải con số trả lời câu hỏi của bạn
Bài học lớn nhất từ top không phải về Linux: mọi công cụ theo dõi mặc định trình bày những con số rẻ để tính và dễ hiển thị, không nhất thiết là con số trả lời câu hỏi bạn đang hỏi — và mỗi cột được định nghĩa theo một cách riêng mà nếu không biết, bạn đọc sai. top giấu PSS vì nó đắt, phơi VIRT vì nó sẵn; đó là lựa chọn của công cụ, không phải của bạn.
Trong thế giới Java, đây đúng là chuyện đọc nhầm các số mặc định: VisualVM/JConsole hiện "heap used" leo sát trần và người ta hoảng, trong khi con số trả lời "có rò rỉ không" là live set sau Full GC; và Runtime.freeMemory() không phải "bộ nhớ còn dùng được" mà là "phần chưa cấp trong heap hiện tại" — một cái tên gây hiểu nhầm y hệt VIRT.
Trong Go, runtime.MemStats có cả chục trường và người ta hay lấy nhầm Sys (tổng xin từ OS, gồm cả phần đã trả lười) làm "bộ nhớ dùng"; con số gần đúng hơn là HeapInuse/HeapAlloc. Cột nào cũng đúng theo định nghĩa của nó, nhưng chỉ một cột trả lời câu bạn hỏi.
Còn ở cơ sở dữ liệu, dashboard mặc định gần như luôn hiện CPU, số kết nối, dung lượng — vì chúng dễ lấy — trong khi thứ trả lời "vì sao chậm" là wait events, cache hit ratio, và độ trễ p99, thường phải bật thêm mới thấy. Người ta tối ưu theo cái dashboard cho sẵn, không theo cái đang thật sự nghẽn.
Mẫu số chung: một bảng theo dõi mặc định là tập hợp những con số rẻ để đo và dễ bày, mỗi con số mang một định nghĩa hẹp riêng — nên đọc nó mà không biết từng cột thật sự đếm gì sẽ dẫn tới báo động nhầm (VIRT lớn, heap sát trần) và bỏ sót cái quan trọng (PSS, live set, wait event) chỉ vì nó không có sẵn trên màn hình. Nên trước khi kết luận từ một con số trên dashboard, câu hỏi đúng là "cột này chính xác đo cái gì, nó có đếm trùng hay tính cả phần chưa dùng không, và con số trả lời câu tôi đang hỏi có nằm trên màn hình này không hay tôi phải đi lấy chỗ khác".
Muốn tự xem tiến trình nào thật sự tốn bộ nhớ, không bị VIRT và bộ nhớ dùng chung làm nhiễu:
for p in $(ps -eo pid --no-headers); do
pss=$(awk '/^Pss:/ {s+=$2} END {print s+0}' /proc/$p/smaps_rollup 2>/dev/null)
[ "${pss:-0}" -gt 10000 ] 2>/dev/null || continue
echo "$pss $(tr -d '\0' < /proc/$p/cmdline 2>/dev/null | cut -c1-60)"
done | sort -rn | head -15
Cột đầu là kilobyte PSS — bộ nhớ không trùng lặp. Tổng của nó xấp xỉ đúng RAM đang dùng, khác với tổng RES vốn luôn thổi phồng. So nó với những gì top sắp xếp theo %MEM: thứ tự thường khác, và với ứng dụng dùng nhiều thư viện chung thì khác rất nhiều.
Phần sau: thời gian người dùng, hệ thống và chờ I/O — mỗi loại tải để lại dấu vết gì.