Ba con số quen thuộc ở góc trên top hay uptime — load average: 2.31, 1.84, 0.97 — là thứ ai cũng liếc để đoán máy "bận" hay "rảnh". Nhưng chúng đo cái gì chính xác? Hầu hết mọi người tin "load = mức bận CPU", và trên Linux điều đó sai theo một cách có thể dẫn tới chẩn đoán lệch hẳn. Bài này đo load average phản ứng thế nào với hai loại tải khác nhau — và phát hiện hai lầm tưởng của chính tôi về con số quen thuộc này.

Load average đo cái gì

Load average là số tiến trình trung bình đang "cần chạy" trong 1, 5, và 15 phút vừa qua. Nhưng "cần chạy" gồm những gì? Đây là chỗ Linux khác các Unix khác. Trên hầu hết hệ thống, load chỉ đếm tiến trình R-state — đang chạy hoặc đang chờ CPU (nhu cầu CPU thuần túy). Nhưng Linux đếm thêm các tiến trình D-state — "uninterruptible sleep", ngủ không ngắt được, thường là đang kẹt chờ I/O (đọc/ghi đĩa, NFS, thiết bị chậm).

Nghĩa là trên Linux, load = (số tiến trình dùng/chờ CPU) + (số tiến trình kẹt chờ I/O). Một hệ quả lớn: load cao không chắc là CPU quá tải — nó có thể là một đống tiến trình đang đứng chờ một cái đĩa chậm, trong khi CPU nhàn rỗi. Và có một đặc tính nữa: load là trung bình trượt làm mượt theo hàm mũ (hằng số thời gian ~1 phút cho con số 1-phút), nên nó dâng và hạ chậm, không phản ánh tức thời. Tôi muốn đo cả hai điều này.

Đo: load dâng chậm, và D-state cũng đẩy load

Máy đo có 10 nhân, load lúc rảnh ~0,26. Kịch bản A: tôi bật 4 tiến trình ngốn CPU (vòng lặp bận, ở trạng thái R), rồi đọc load 1-phút sau mỗi 20 giây:

t=20s: load 1,32   t=40s: load 2,08   t=60s: load 2,63   (đang bò dần về 4)
CPU busy = 41% (4 tiến trình trên 10 nhân)

Điều đầu tiên đập vào mắt: load không nhảy ngay lên 4. Nó bò từ từ — 1,32 rồi 2,08 rồi 2,63 — và sau một phút vẫn chưa tới 4, vì phép làm mượt hàm mũ cần vài phút để hội tụ. Đúng như kỳ vọng "4 tiến trình cần CPU trên máy dư nhân": CPU chỉ bận 41% (bốn phần mười số nhân), và load tiến tới 4.

Kịch bản B: tôi bật 4 tiến trình kẹt chờ I/O — ghi đĩa đồng bộ qua một thiết bị bị bóp băng thông xuống 512 KB/s, nên mỗi tiến trình dành gần hết thời gian ở trạng thái D, đứng chờ ghi:

load tăng thêm ~4 (ví dụ 4,04 -> 8,34), cả 4 tiến trình đều ở trạng thái D

Đây mới là điều đáng nói. Bốn tiến trình này không tính toán gì — chúng chỉ nằm chờ I/O hoàn tất, ngủ không ngắt được. Vậy mà chúng đẩy load average tăng thêm 4, y như bốn tiến trình ngốn CPU. Load không phân biệt "bận CPU" với "kẹt chờ I/O" — nó đếm cả hai như nhau.

Hai lầm tưởng về load, vấp cả hai

Tôi vấp hai lần trong bài này. Lần thứ nhất, khi bắt đầu: tôi bật các tiến trình rồi đọc load ngay, thấy nó vẫn thấp, và suýt kết luận "load không phản ứng với tải này". Sai — vì load là trung bình trượt làm mượt ~1 phút, không phải giá trị tức thời. Phải quan sát đủ dài (một phút) mới thấy nó dâng về giá trị thật; đọc ngay lập tức là đo một con số còn đang "nhớ" quá khứ. Đây đúng là kỷ luật "đo hiện tượng có mốc thời gian phải quan sát đủ dài" — load có một hằng số thời gian, và bỏ qua nó là đọc sai.

Lần thứ hai, sâu hơn: tôi mặc định "load cao nghĩa là CPU đang quá tải". Kịch bản B bác bỏ thẳng: 4 tiến trình kẹt chờ đĩa, CPU không hề bão hòa, mà load vẫn tăng 4. Trên Linux, load đếm cả tiến trình D-state, nên một máy load 8 có thể đang ở hai tình huống hoàn toàn khác nhau: CPU cháy với 8 tiến trình tranh nhau tính toán, hoặc CPU nhàn rỗi với 8 tiến trình đứng chờ một hệ thống lưu trữ nghẽn. Cùng một con số load, hai căn bệnh trái ngược, hai cách chữa khác hẳn. Điều rút ra: một con số tổng hợp như load average gộp nhiều hiện tượng vào một; nhìn nó một mình có thể dẫn tới chẩn đoán sai — phải bóc ra CPU% và trạng thái tiến trình (R so với D) mới biết bệnh gì.

(Có một chi tiết đo lường nhỏ tôi cũng gặp: load "có trí nhớ" — nó lì và decay chậm, nên các phép đo liên tiếp của tôi còn dính load của lần trước, làm số nền trôi từ 0,26 lên mấy đơn vị. Càng nhắc: hiện tượng có mốc thời gian phải để lắng đủ giữa các lần đo, không chỉ quan sát đủ dài lúc đo.)

Về đến code hằng ngày

Điều đầu tiên là đừng chẩn đoán "CPU quá tải" chỉ từ load average. Khi thấy load cao, câu hỏi tiếp theo phải là "cao vì CPU hay vì I/O?". Xem CPU% (từ top, mpstat): nếu CPU gần 100% thì đúng là nghẽn CPU; nếu CPU nhàn mà load vẫn cao, thủ phạm là I/O — tìm các tiến trình D-state (ps cột STAT có D) đang chờ đĩa/mạng. Nhầm hai cái này là đi mua thêm CPU cho một vấn đề I/O, hoặc ngược lại.

Điều thứ hai là so load với số nhân, và nhớ nó trễ. Load 4 trên máy 1 nhân là quá tải nặng; load 4 trên máy 16 nhân là nhàn. Luôn chia load cho số nhân để biết mức bận tương đối. Và vì load làm mượt theo phút, nó trễ so với thực tế: một đợt tải đột ngột phải vài phút mới hiện đủ vào con số 1-phút, còn con số 15-phút thì mượt tới mức chỉ hợp để nhìn xu hướng dài. Muốn thấy tình trạng ngay lúc này, đừng nhìn load — nhìn CPU% và độ dài hàng đợi tức thời.

Điều cuối, về đo lường: một chỉ số tổng hợp có thể trộn lẫn nhiều nguyên nhân — đừng đọc nó một mình. Gói lại bằng số: trên Linux, load average đếm CẢ tiến trình dùng/chờ CPU (R-state) LẪN tiến trình kẹt chờ I/O không ngắt được (D-state) — nên 4 tiến trình chờ đĩa đẩy load lên 4 dù CPU rảnh (4,04→8,34), y như 4 tiến trình ngốn CPU; và vì là trung bình trượt làm mượt ~1 phút, load dâng/hạ chậm và trễ (1,32→2,08→2,63 mới bò về 4). Load cao là tín hiệu "có gì đó đang chờ nhiều", không phải "CPU đang cháy"; muốn biết bệnh gì, phải bóc CPU% và trạng thái R/D.

Cái bẫy "một con số tổng hợp trộn nhiều nguyên nhân" này không riêng gì load của Linux — nó là bệnh chung của mọi chỉ số gộp mà lập trình viên hay tin sạch. Trong Java, người ta nhìn "CPU của JVM cao" rồi kết luận code nặng, trong khi phần lớn có thể là GC đang quét (một loại "bận" khác hẳn tính toán nghiệp vụ) — phải bóc bằng GC log hay jstat mới biết; và thread ở trạng thái BLOCKED/WAITING giống hệt D-state ở đây, chúng "đang cần" nhưng không tiêu CPU. Go phơi cái này rõ hơn qua GOMAXPROCS và goroutine chờ trên channel/syscall — một chương trình "chậm" có thể do goroutine kẹt I/O chứ không phải thiếu CPU. Cơ sở dữ liệu là ca kinh điển: "database chậm" nhìn từ một con số như active connections rất dễ đánh lừa; phải bóc ra wait_event của PostgreSQL (đang chờ khóa? chờ I/O? chờ CPU?) mới biết nên tăng gì. Mẫu số chung: mỗi khi một dashboard đưa cho bạn một con số duy nhất để đại diện cho "mức bận", hãy hỏi "con số này đang gộp bao nhiêu loại 'bận' khác nhau vào một?" — vì chữa đúng bệnh đòi bóc nó ra thành từng thành phần, không đọc cái tổng.

Muốn tự thấy thì uptime cho ba con số load; nproc cho số nhân để chia; rồi top (hay mpstat 1) xem CPU% có thật sự cao không. Đếm tiến trình đang kẹt I/O: ps -eo stat,comm | grep '^D' — mỗi dòng bắt đầu bằng D là một tiến trình ngủ không ngắt được đang góp vào load mà không dùng CPU. Nếu load cao nhưng CPU% thấp và có nhiều D, bạn đang gặp nghẽn I/O chứ không phải nghẽn CPU. Và nhớ: bật một tải rồi nhìn load ngay sẽ thấy nó chưa phản ứng — chờ một phút, vì nó là trung bình trượt, đúng cái độ trễ bài này đo.