Scheduler, load average và context switch
Tiếp đi: CPU. VM của order-service có 4 core, uptime báo load average 12. Sếp bảo "load 12 là quá tải rồi, tăng gấp ba máy đi". Mà top lại báo CPU chỉ 35%. Ai đúng?
Chưa ai đúng cả, vì load average không phải phần trăm CPU. Để đọc được, cần biết Linux chia CPU cho các thread thế nào.
1. Bài toán
uptime
10:02:11 up 12 days, load average: 12.04, 11.80, 9.30
top
%Cpu(s): 25.1 us, 6.0 sy, 0.0 ni, 30.2 id, 38.5 wa, 0.0 st
order-service mỗi request ghi một file PDF hóa đơn ra disk mạng rồi trả về. Lúc cao điểm, API chậm.
2. Vấn đề
- Thêm máy theo load average có thể vô ích nếu nút cổ chai là disk.
- Nhìn CPU% thấp rồi kết luận "máy rảnh" cũng sai, vì nhiều thread đang chờ.
3. Giải pháp: hiểu scheduler
- Đơn vị Linux schedule là thread (kernel gọi là task). Mỗi Java platform thread là một task (bài 10.5).
- Mỗi core có một run queue: các task ở trạng thái R đang chờ tới lượt.
- Scheduler (CFS, từ kernel 6.6 là EEVDF) chia CPU công bằng theo trọng số; mỗi task được một lát thời gian rồi nhường.
- Load average (1, 5, 15 phút) = trung bình số task ở trạng thái R (đang chạy + chờ CPU) cộng D (chờ I/O không ngắt được).
- Context switch: CPU dừng task này, lưu thanh ghi, nạp task khác. Voluntary: task tự ngủ (chờ I/O, lock). Involuntary: hết lượt, bị giành CPU.
Đọc load: so với số core. Load 12 / 4 core:
- Nếu phần lớn là R → CPU quá tải thật, có task chờ CPU.
- Nếu phần lớn là D → đang chờ disk/NFS, CPU có rảnh cũng vô ích. Đây là trường hợp của bạn (
wa38.5%).
4. Code từng bước
Bước 1: vmstat, xem R và D tách nhau
vmstat 1 5
r b swpd free ... in cs us sy id wa st
3 9 0 2.1G 3200 9800 25 6 30 38 0
4 8 0 2.1G 3100 9600 24 6 31 39 0
# r = task chờ/đang chạy CPU (3–4 < 4 core: CPU không thiếu)
# b = task blocked ở D (8–9: đây là phần đẩy load lên 12)
# cs = context switch/giây
Bước 2: Xem theo process/thread
pidstat -u -w -p $(pgrep -f order/app.jar) 1
%usr %system %CPU cswch/s nvcswch/s
24.0 6.0 30.0 8800.0 120.0 ← cswch: tự ngủ (I/O), nvcswch: bị giành CPU
iostat -x 1 # %util và await của disk
cat /proc/pressure/io /proc/pressure/cpu # PSI: % thời gian task bị chặn
Bước 3: Sửa đúng chỗ
// Trước: ghi PDF đồng bộ lên NFS trong request thread
invoicePdfWriter.write(order); // block 300ms trên I/O
// Sau: đẩy sang queue/worker, request trả về ngay
@Async("invoiceExecutor")
public void writeInvoice(Order order) { invoicePdfWriter.write(order); }
Hoặc ghi lên object storage, ghi ra disk local. Mục tiêu: giảm cột b và wa.
Bước 4: Ưu tiên việc nền bằng nice
nice -n 10 /opt/order/bin/nightly-report.sh # job báo cáo nhường CPU cho API
renice -n 10 -p 5120
# hoặc trong systemd: Nice=10, CPUWeight=50
5. Cách hoạt động
4 core, mỗi core một run queue
core0: [T-exec-1 ▶] ← T-exec-7 ← GC-1 (R)
core1: [T-exec-2 ▶] ← C2-compiler (R)
core2: [idle]
core3: [T-exec-4 ▶]
load = R + D
Ngủ chờ I/O NFS (D): T-exec-3, 5, 6, 8, 9, 10, 11, 12 ...
Context switch trên core0:
T-exec-1 hết lượt → lưu thanh ghi, stack pointer
→ nạp T-exec-7 → cache L1/L2 "lạnh" → chạy chậm lúc đầu
Chi phí trực tiếp ~1–5 µs, gián tiếp (cache, TLB) còn lớn hơn
6. So sánh
| Dấu hiệu | CPU-bound | IO-bound |
|---|---|---|
top us+sy | Gần 100% | Thấp/vừa |
wa | ~0 | Cao (disk); với network thì wa không tăng, thread chỉ ở S |
vmstat r | > số core | Nhỏ |
vmstat b | ~0 | Cao (nếu disk) |
| Context switch | Involuntary nhiều | Voluntary nhiều |
| Cách xử lý | Tối ưu code, profile, thêm core | Async, cache, nhanh hơn ở downstream; thêm core không giúp |
| Kích thước pool hợp lý | ≈ số core | Lớn hơn, hoặc virtual thread |
Cột st (steal) cao: hypervisor đang lấy CPU của VM cho VM khác; phải đổi loại instance.
7. Khi nào nên dùng / không nên dùng
- Đọc load average luôn kèm
vmstatđể tách R và D. - Dùng
nice/CPUWeightcho batch job chạy chung máy với API. - Đừng scale số máy chỉ vì load cao khi
wacao: nút cổ chai ở I/O.
java.io.IOException: Too many open files. File gì mà mở nhiều thế? Bài 10.4.Vậy để chịu tải thì mình cứ tăng thread pool lên 2000 thread? Thread ngủ chờ I/O thì đâu tốn CPU.
Thread ngủ thì đúng là không tốn CPU. Nhưng 2000 thread không chỉ có ngủ. Bài 3.11 đã nói về giới hạn số thread trong Java; ở đây nhìn từ phía Linux:
- Context switch: khi nhiều thread cùng thức dậy (đợt traffic), run queue dài, mỗi core switch liên tục.
cstrong vmstat lên 100k+/giây,sytăng: CPU lo chuyển việc thay vì làm việc. - Cache: mỗi lần switch, dữ liệu của thread mới không có trong L1/L2 → chậm.
- Lock contention: 2000 thread tranh một connection pool 20 kết nối → phần lớn chỉ xếp hàng.
- RAM: mỗi thread có stack (bài 10.5), cộng vào RSS.
- Giới hạn:
ulimit -u,/proc/sys/kernel/threads-max,pids.maxcủa cgroup → "unable to create native thread".
vmstat 1
r b in cs us sy id
64 0 9000 180000 40 45 15 ← r = 64 trên 4 core, sy 45%: thrashing
Pool nên dừng ở mức downstream chịu được (DB pool, API đối tác). Cần nhiều request chờ I/O cùng lúc thì dùng virtual thread: một nhóm nhỏ OS thread chạy rất nhiều tác vụ.
Load average có tính theo thread hay theo process? order-service là một process mà sao đẩy load lên 12 được?
Câu hỏi hay. Theo thread. Linux đếm task, và mỗi thread là một task. Một process java với 12 thread cùng ở R hoặc D là đủ đẩy load lên 12.
ps -eLo stat | sort | uniq -c # đếm thread theo trạng thái trên toàn máy
ps -L -o tid,stat,comm -p $(pgrep -f order/app.jar) | awk '$2 ~ /^[RD]/'
Vì vậy cùng một "load 12": có thể là 12 thread tính toán (CPU thiếu) hoặc 12 thread chờ NFS (disk chậm). Luôn tách R và D trước khi kết luận.
Phỏng vấn hỏi load average và context switch thì trả lời sao?
Q1. Load average là gì? Load 8 trên máy 4 core có phải quá tải?
🇻🇳 Tiếng Việt:
Load average là số task trung bình ở trạng thái runnable cộng uninterruptible sleep trong 1, 5, 15 phút. Trên Linux nó gồm cả task chờ I/O, nên load 8 trên 4 core chưa chắc là thiếu CPU. Em xem vmstat: nếu cột r lớn hơn số core thì thiếu CPU, nếu cột b và wa cao thì nghẽn I/O.
🇬🇧 English:
Load average is the average number of tasks that are runnable or in uninterruptible sleep over 1, 5 and 15 minutes. On Linux it includes tasks waiting on I/O, so a load of 8 on 4 cores does not necessarily mean CPU saturation. I check vmstat: if r is above the core count the CPU is short, if b and wa are high it is an I/O bottleneck.
Q2. Context switch là gì, tại sao nhiều thread lại chậm?
🇻🇳 Tiếng Việt:
Context switch là khi CPU lưu trạng thái thread đang chạy và nạp thread khác. Mỗi lần chỉ vài micro giây, nhưng làm mất cache và TLB. Khi có quá nhiều thread runnable, CPU dành nhiều thời gian cho việc chuyển đổi, sy tăng, throughput giảm. Vì vậy pool CPU-bound nên xấp xỉ số core.
🇬🇧 English:
A context switch is when the CPU saves the state of the running thread and loads another. Each switch costs only microseconds, but it also loses cache and TLB contents. With too many runnable threads the CPU spends a lot of time switching, system time rises and throughput drops. That is why a CPU-bound pool should be close to the core count.
Q3. Phân biệt CPU-bound và IO-bound trên server thế nào?
🇻🇳 Tiếng Việt:
CPU-bound: us và sy gần 100%, run queue dài, nhiều involuntary context switch. IO-bound: CPU còn rảnh, wa cao hoặc thread ngủ chờ, nhiều voluntary context switch. CPU-bound thì profile và thêm core; IO-bound thì xử lý downstream, cache hoặc async, thêm core không giúp.
🇬🇧 English:
CPU-bound means user and system time near 100%, a long run queue and many involuntary context switches. IO-bound means spare CPU, high iowait or sleeping threads, and many voluntary switches. For CPU-bound work I profile and add cores; for IO-bound work I fix the downstream, add caching or go async, because more cores do not help.
Cách nhớ ngắn gọn
- Load = R + D, đếm theo thread, so với số core.
vmstat: r = chờ CPU, b = chờ I/O, cs = context switch.- Nhiều thread → switch nhiều → sy cao.
nicecho job nền;stcao là lỗi của hypervisor.
Tự kiểm tra
Load average trên Linux đếm những trạng thái nào?
R (chạy + chờ CPU) và D (uninterruptible sleep, thường chờ disk).
Load 12, CPU 35%, wa 38%. Kết luận?
Nghẽn I/O, không phải thiếu CPU. Thêm máy/core ít tác dụng.
Voluntary và involuntary context switch khác gì?
Voluntary: thread tự ngủ (I/O, lock). Involuntary: bị scheduler giành CPU khi hết lượt.
Cột st trong top là gì?
Steal time: thời gian hypervisor lấy CPU của VM cho VM khác.
Lệnh để job báo cáo không giành CPU của API?
nice -n 10/renice, hoặc Nice=/CPUWeight= trong systemd.