CFS chia CPU giữa các tiến trình. Bài này đo xem nó chia theo quy tắc nào — và tìm ra một điều kiện mà không tài liệu nào nhấn mạnh.

CFS chia đều, và nice chỉ có tác dụng khi hai tiến trình cùng một nhân

CFS chia đều

Container giới hạn 1 CPU:

1 tiến trình   93,3%
2 tiến trình   ~50% mỗi cái
4 tiến trình   ~30% mỗi cái
8 tiến trình    12% mỗi cái

Mỗi tiến trình được 1/N. Đây là điều CFS hứa và nó giữ đúng.

Tên đầy đủ là Completely Fair Scheduler, và ý tưởng đơn giản: mỗi tiến trình có một bộ đếm "thời gian ảo đã dùng"; bộ lập lịch luôn chạy tiến trình có bộ đếm nhỏ nhất. Kết quả là mọi tiến trình tiến gần bằng nhau.

nice không có tác dụng gì

Hai tiến trình, không ghim nhân:

nice 0 vs 0    49,8% / 50,2%
nice 0 vs 5    49,8% / 50,2%
nice 0 vs 10   50,2% / 49,8%
nice 0 vs 19   50,2% / 49,8%

nice 19 — mức ưu tiên thấp nhất có thể — và nó vẫn nhận đúng một nửa.

Tôi tưởng phép đo hỏng. Đo lại bằng hiệu số thời gian CPU trong /proc/<pid>/stat thay vì đọc %CPU của ps, kết quả không đổi.

Cho tới khi ghim cả hai vào cùng một nhân

nice 0 vs 0    49,9% / 50,1%
nice 0 vs 5    75,4% / 24,6%
nice 0 vs 10   90,4% /  9,6%
nice 0 vs 19   98,7% /  1,3%

Bây giờ nó hoạt động đúng như tài liệu.

Và tỉ lệ khớp gần như chính xác với bảng trọng số của CFS:

nice trọng số Tỉ lệ so với nice 0 Đo được
0 1024
5 335 3,06 3,07
10 110 9,31 9,4
19 15 68 76

Vì sao: nice là trọng số trong một hàng đợi chạy

Mỗi nhân CPU có hàng đợi chạy riêng. nice quyết định tiến trình được bao nhiêu phần của hàng đợi đó.

Hai tiến trình ở hai nhân khác nhau không nằm cùng hàng đợi. Không có gì để cân — mỗi cái là tiến trình duy nhất trên nhân của nó, và nó nhận 100% nhân đó.

Trong phép đo, cả hai bị giới hạn bởi hạn ngạch cgroup (--cpus 1), và cơ chế hạn ngạch bóp CPU theo chu kỳ mà không nhìn nice. Nên hai tiến trình chia đôi hạn ngạch bất kể ưu tiên.

Kết luận thực dụng:

Trên máy nhiều nhân, nice chỉ có tác dụng khi số tiến trình sẵn sàng chạy vượt số nhân.

Máy 16 nhân với 4 tiến trình nặng: nice không làm gì cả. Cùng máy đó với 40 tiến trình: nice bắt đầu quyết định.

Đây là lý do nice thường bị coi là "không hiệu quả" — người ta thử nó trên hệ thống chưa bão hoà và thấy không có gì đổi.

Và trong container, nice càng ít tác dụng

Với --cpus, Docker đặt hạn ngạch cgroup. Hạn ngạch là một cái trần cứng theo chu kỳ 100 ms — đúng cơ chế đo trong sê-ri Kubernetes phần 44.

Muốn chia tỉ lệ giữa các container, dùng cpu.weight (cgroup v2) thay vì hạn ngạch:

docker run --cpu-shares 512 ...    # nua phan so voi mac dinh 1024

--cpu-shares là trọng số tương đối và chỉ có tác dụng khi CPU bão hoà — cùng điều kiện với nice. --cpus là trần tuyệt đối và luôn có tác dụng.

Hai thứ này hay bị nhầm, và chúng giải quyết hai bài toán khác nhau: --cpus để giới hạn, --cpu-shares để ưu tiên.

Hai lớp lập lịch trên CFS

CFS chỉ áp cho lớp SCHED_OTHER — mặc định. Có hai lớp thời gian thực đứng trên nó:

chrt -f 50 ./chuong-trinh    # SCHED_FIFO, uu tien 50
chrt -r 50 ./chuong-trinh    # SCHED_RR

Tiến trình thời gian thực luôn chạy trước mọi tiến trình CFS, bất kể nice. Đây là công cụ thật cho công việc nhạy độ trễ — nhưng một vòng lặp bận ở SCHED_FIFO có thể treo cả máy, vì không gì đẩy nó ra được.

Ngược lại, SCHED_IDLE chạy chỉ khi không còn gì khác:

chrt -i 0 ./cong-viec-nen

Cái này an toàn hơn nice 19 nhiều cho công việc nền thật sự — vì nice 19 vẫn nhận phần, còn SCHED_IDLE thì gần như không.

Thử ba mươi giây

Xem nice có tác dụng gì trên máy của bạn không:

# 1. so tien trinh san sang chay so voi so nhan
nproc
vmstat 1 3 | tail -1 | awk '{print "r=" $1}'

# 2. lop lap lich va nice cua cac tien trinh nang nhat
ps -eo pid,ni,cls,pcpu,comm --sort=-pcpu | head -10

Cột r nhỏ hơn số nhân nghĩa là hệ thống chưa bão hoà, và nice sẽ không đổi gì — đừng mất công chỉnh nó.

Cột cls là lớp lập lịch: TS là CFS thường, FF/RR là thời gian thực, IDL là nền. Thấy FF hoặc RR ở một tiến trình không cố ý là điều đáng hỏi ngay — nó đứng trên tất cả những gì còn lại.

Phần sau: cgroup v2 — cách nhân thực sự giới hạn tài nguyên.