Phần 5 đo được chuyển ngữ cảnh qua nhân đắt gấp 19 lần. Bài này đo mặt còn lại: chi phí của việc chính tiến trình bị chuyển sang nhân khác.
Cùng phép đo, đổi mỗi kích thước tập làm việc
Một tiến trình duyệt ngẫu nhiên qua một vùng nhớ, hoặc ở yên một nhân, hoặc đổi nhân sau mỗi vòng.
| Tập làm việc | Ở yên một nhân | Nhảy mỗi vòng | Chênh |
|---|---|---|---|
| 1 MB | 41.739 MB/s | 20.445 MB/s | 2,04× |
| 4 MB | 45.272 MB/s | 32.905 MB/s | 1,38× |
| 16 MB | 34.537 MB/s | 28.346 MB/s | 1,22× |
| 64 MB | 11.607 MB/s | 11.586 MB/s | 1,00× |
| 256 MB | 8.220 MB/s | 8.054 MB/s | 1,02× |
Đường cong rất rõ: chậm gấp đôi khi dữ liệu vừa bộ nhớ đệm, không ảnh hưởng gì khi dữ liệu vượt bộ nhớ đệm.
Vì sao
Bộ nhớ đệm L1 và L2 thuộc về từng nhân. Nhảy sang nhân khác là bỏ lại toàn bộ bộ nhớ đệm đã nóng và bắt đầu lại từ đầu.
Với tập 1 MB, dữ liệu vừa gọn trong L2 và mọi lần đọc đều trúng bộ đệm — cho tới khi tiến trình đổi nhân. Lúc đó nó phải nạp lại toàn bộ.
Với tập 256 MB, không có bộ đệm nào chứa nổi. Mọi lần đọc đều phải ra RAM dù ở nhân nào, nên việc ở đâu không còn ý nghĩa.
Nghĩa là ghim CPU chỉ đáng làm khi
Hai điều kiện, cần cả hai:
- Tập dữ liệu nóng vừa trong bộ nhớ đệm của một nhân.
- Tiến trình chạy liên tục đủ lâu để bộ đệm kịp nóng.
Với ứng dụng duyệt qua hàng trăm megabyte mỗi giây — phần lớn dịch vụ web, cơ sở dữ liệu quét bảng — ghim CPU không cho gì.
Và nó có cái giá: ghim làm mất khả năng cân bằng tải của nhân. Nhân bị ghim quá tải trong khi nhân khác rảnh, và bộ lập lịch không sửa được.
Đo trước khi ghim. Nếu chênh lệch dưới 10%, đừng làm.
Lý do thứ hai để ghim, không liên quan tới bộ nhớ đệm
Nhắc lại phần 5: hai tiến trình trao đổi dữ liệu dày đặc trên cùng một nhân chuyển ngữ cảnh nhanh hơn 19 lần — 0,63 µs so với 12,4 µs.
Lý do đó độc lập hoàn toàn với kích thước tập làm việc. Với một cặp tiến trình ping-pong qua ống hoặc socket, ghim chung một nhân có thể là thay đổi lớn nhất bạn làm được.
Hai lý do, hai cách ghim:
taskset -c 3 ./mot-tien-trinh-tinh-toan # giu bo nho dem nong
taskset -c 3 ./ca-hai-tien-trinh-trao-doi # giam chi phi danh thuc
NUMA: tầng tiếp theo của cùng ý tưởng
Trên máy chủ nhiều socket, RAM cũng thuộc về từng nhóm nhân. Truy cập RAM của nhóm khác đi qua liên kết giữa các socket và chậm hơn đáng kể.
numactl --hardware
cat /sys/devices/system/node/node0/cpulist
Máy tôi đo chỉ có một nút NUMA — /sys/devices/system/node/ trống — nên tôi không đo được phần này. Đó là giới hạn của phép đo, và tôi không suy diễn con số.
Điều chắc chắn: nguyên tắc giống hệt, chỉ ở tầng lớn hơn. Bộ nhớ đệm thuộc về nhân; RAM thuộc về nút. Chạy trên nút này mà dữ liệu ở nút kia là phiên bản NUMA của việc nhảy nhân.
Công cụ:
numactl --cpunodebind=0 --membind=0 ./chuong-trinh # buoc ca hai ve nut 0
numastat -p <pid> # xem truy cap tu xa
numastat có cột numa_miss và numa_foreign — chúng khác 0 nghĩa là tiến trình đang đọc RAM của nút khác.
Bộ lập lịch đã cố giữ ái lực rồi
Linux không tuỳ tiện chuyển tiến trình sang nhân khác. CFS có khái niệm ái lực mềm: nó ưu tiên giữ tiến trình ở nhân cũ và chỉ chuyển khi mất cân bằng đủ lớn.
Nên phép đo trong bài — ép đổi nhân mỗi vòng — là trường hợp xấu nhất, không phải hành vi mặc định. Trong thực tế, một tiến trình chạy liên tục thường ở yên một nhân khá lâu.
Kiểm tiến trình của bạn có bị chuyển nhiều không:
grep -E 'processor|se.nr_migrations' /proc/<pid>/sched
nr_migrations tăng nhanh nghĩa là bộ lập lịch đang chuyển nó thường xuyên — thường vì có quá nhiều tiến trình sẵn sàng chạy, hoặc vì tiến trình liên tục ngủ và thức.
Thử ba mươi giây
Xem tiến trình của bạn có bị chuyển nhân nhiều không:
p=<pid-can-xem>
last=""; doi=0
for i in $(seq 1 200); do
c=$(awk '{print $39}' /proc/$p/stat 2>/dev/null) || break
[ -n "$last" ] && [ "$c" != "$last" ] && doi=$((doi+1))
last=$c
done
echo "200 lan lay mau: $doi lan doi nhan, nhan cuoi = $last"
Trường thứ 39 của /proc/<pid>/stat là nhân đang chạy. Lấy mẫu liên tục rồi đếm số lần nó đổi cho biết bộ lập lịch đang giữ tiến trình ở yên tới đâu.
Số lần đổi gần 0 với một tiến trình tính toán liên tục là bình thường — CFS giữ ái lực mềm khá tốt. Con số cao nghĩa là có tranh chấp CPU, và đó mới là lúc taskset đáng thử — nhưng chỉ sau khi đã kiểm tập làm việc có vừa bộ nhớ đệm không.
Nếu nhân của bạn được biên dịch với CONFIG_SCHED_DEBUG, có cách trực tiếp hơn:
grep nr_migrations /proc/<pid>/sched
Nhiều nhân dựng sẵn — kể cả nhân của Docker Desktop mà tôi đo — không có tệp này, nên cách lấy mẫu ở trên là phương án chạy được ở mọi nơi.
Phần sau: cgroup v2 — cách nhân thực sự giới hạn CPU, bộ nhớ và I/O.