Phần 2 đã phân biệt VIRT và RES trong top. Bài này đi tới câu hỏi thực tế hơn: khi cần biết một nhóm tiến trình đang ăn bao nhiêu RAM, con số nào dùng được?
Câu trả lời ngắn: không phải RSS.
Hai thí nghiệm, khác nhau đúng một dòng
Chương trình cấp 512 MB, chạm hết, rồi fork ra 7 tiến trình con. Tổng cộng 8 tiến trình.
Bản A — con chỉ đọc:
for (j = 0; j < n; j += 4096) s += a[j];
Bản B — con ghi:
for (j = 0; j < n; j += 4096) a[j] = 2;
Không đổi gì khác. Kết quả:
| Con chỉ đọc | Con ghi | |
|---|---|---|
| Tổng RSS 8 tiến trình | 4.105 MB | 4.105 MB |
| Tổng PSS 8 tiến trình | 513 MB | 4.097 MB |
| Hệ thống tốn thêm thật | 526 MB | 4.050 MB |
RSS ra con số y hệt trong cả hai trường hợp, còn bộ nhớ thật chênh nhau gần 8 lần.
Đây là điều đáng nhớ nhất của bài. RSS không sai — nó trả lời một câu hỏi khác câu bạn đang hỏi.
Vì sao
fork không sao chép bộ nhớ. Nó đánh dấu mọi trang là sao chép khi ghi: cha và con cùng trỏ vào một trang vật lý, chỉ khi có ai ghi thì nhân mới tách ra bản riêng.
Con đọc thì không có gì bị tách. 512 MB nằm trong RAM đúng một lần, tám tiến trình cùng trỏ vào.
RSS đếm "số trang tiến trình này đang ánh xạ tới" — không quan tâm bảy tiến trình khác cũng ánh xạ tới đúng trang đó. Nên mỗi tiến trình báo 513 MB và cộng lại thành 4.105 MB, gấp tám lần thực tế.
Con ghi thì mọi trang đều bị tách. Bây giờ đúng là 8 bản, và 4.105 MB là con số thật.
PSS chia phần
Pss — Proportional Set Size — chia mỗi trang cho số tiến trình đang dùng chung nó. Một trang bị 8 tiến trình dùng chung thì mỗi tiến trình được tính 1/8 trang.
Nhờ vậy PSS cộng lại được. Trong thí nghiệm A, tổng PSS là 513 MB và hệ thống thật sự tốn 526 MB. Trong thí nghiệm B, tổng PSS 4.097 MB, hệ thống tốn 4.050 MB. Cả hai lần đều khớp.
awk '/^Pss:/{s+=$2} END{print s/1024, "MB"}' /proc/<pid>/smaps_rollup
Với smaps_rollup (có từ nhân 4.14) thì chỉ có một dòng Pss: — nhân đã cộng sẵn. Với smaps cũ thì phải cộng qua từng vùng, và nó chậm hơn nhiều với tiến trình có hàng nghìn vùng ánh xạ.
Bốn con số cho bốn câu hỏi
| Con số | Trả lời câu hỏi | Lấy ở đâu |
|---|---|---|
| VSZ | Tiến trình đăng ký bao nhiêu vùng địa chỉ | ps -o vsz, VmSize |
| RSS | Bao nhiêu trang đang được ánh xạ, kể cả dùng chung | ps -o rss, VmRSS |
| PSS | Phần bộ nhớ công bằng chia cho tiến trình này | smaps_rollup, Pss |
| USS | Giết nó thì lấy lại được bao nhiêu | smaps_rollup, cộng Private_* |
Trong thí nghiệm B, USS đo được 4.096 MB — gần bằng PSS, vì lúc đó không còn gì dùng chung.
Khi cần xếp hạng "tiến trình nào đáng giết nhất", dùng USS. Nó trả lời đúng câu hỏi đó. PSS thì hợp để tính "nhóm dịch vụ này chiếm bao nhiêu phần trăm RAM máy chủ".
Chỗ chuyện này cắn thật
Ba tình huống hay gặp, cả ba đều cho RSS thổi phồng:
Máy chủ web kiểu prefork — nginx, php-fpm, Gunicorn, Puma trong chế độ cluster. Một tiến trình cha nạp hết mã và dữ liệu tĩnh rồi fork ra hàng chục worker. ps sẽ hiện mỗi worker chiếm vài trăm MB; cộng lại vượt xa RAM máy. Cộng PSS mới ra con số thật.
Thư viện dùng chung. libc, libssl, runtime của Python hay Java được ánh xạ vào mọi tiến trình và tính đủ vào RSS của từng cái.
Bộ đệm trang của tệp mmap. Nhiều tiến trình đọc cùng một tệp bằng mmap thì trang bộ đệm được tính vào RSS của tất cả.
Chiều ngược lại cũng có: RSS thiếu phần đã bị đẩy sang vùng tráo đổi. VmSwap trong /proc/<pid>/status giữ phần đó, và không con số nào trong bốn cái trên bao gồm nó.
Rồi free cũng không nói cái bạn tưởng
Cột used trong free -m đã trừ bộ đệm trang, nhưng số liệu tôi dùng ở trên là hiệu giữa hai lần đo chứ không phải giá trị tuyệt đối — trên một máy đang chạy 30 container thì giá trị tuyệt đối gần như vô nghĩa.
Con số đáng nhìn là available, không phải free. Nó ước lượng lượng RAM cấp được cho tiến trình mới mà không phải tráo đổi, tính cả phần bộ đệm trang có thể thu hồi. Phần 12 sẽ đo phần bộ đệm trang này.
Thử ba mươi giây
Xem mười tiến trình ngốn RAM nhất theo PSS thay vì RSS:
for p in $(ls /proc | grep -E '^[0-9]+$'); do
pss=$(awk '/^Pss:/{s+=$2} END{print s+0}' /proc/$p/smaps_rollup 2>/dev/null)
rss=$(awk '/^Rss:/{s+=$2} END{print s+0}' /proc/$p/smaps_rollup 2>/dev/null)
[ "${pss:-0}" -gt 0 ] && printf "%8d %8d %6s %s\n" "$pss" "$rss" "$p" \
"$(tr -d '\0' < /proc/$p/cmdline | cut -c1-40)"
done 2>/dev/null | sort -rn | head -10 | \
awk 'BEGIN{print " PSS(KB) RSS(KB) PID lenh"} {print " "$0}'
Cần quyền root để đọc smaps_rollup của tiến trình người khác; 2>/dev/null ở đây là để bỏ qua tiến trình vừa thoát giữa chừng chứ không phải để giấu lỗi.
Cột RSS lớn hơn cột PSS nhiều lần ở đâu, chỗ đó đang dùng chung bộ nhớ — và mọi con số RSS bạn từng cộng cho nhóm đó đều đã sai.
Phần sau: bộ đệm trang — vì sao đọc lần hai nhanh hơn nghìn lần, và vì sao RAM "hết" là chuyện bình thường.