Scheduler, load average và context switch

4 câu hỏi~25 phútTừ khóa: CPU scheduler, CFS/EEVDF, run queue, time slice, load average, context switch, cs, us/sy/wa/st, vmstat, pidstat, nice, CPU-bound, IO-bound

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 đề

3. Giải pháp: hiểu scheduler

Đọc load: so với số core. Load 12 / 4 core:

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ệuCPU-boundIO-bound
top us+syGần 100%Thấp/vừa
wa~0Cao (disk); với network thì wa không tăng, thread chỉ ở S
vmstat r> số coreNhỏ
vmstat b~0Cao (nếu disk)
Context switchInvoluntary nhiềuVoluntary nhiều
Cách xử lýTối ưu code, profile, thêm coreAsync, cache, nhanh hơn ở downstream; thêm core không giúp
Kích thước pool hợp lý≈ số coreLớ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

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:

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


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.