Đây là phần cuối của sê-ri. Năm mươi phần, mỗi phần một phép đo tự chạy. Bài này gom lại những con số đáng mang theo và những bài học về chính việc đo.
Chủ đề xuyên suốt
Đọc lại năm mươi bảng số liệu, hai câu lặp lại nhiều nhất:
Giá nằm ở mỗi thao tác, không nằm ở mỗi byte. Đọc 512 byte tốn bằng đọc 16 KB. Gửi 512 byte tốn bằng gửi 64 KB. Một lời gọi hệ thống làm việc thật và một lời gọi thất bại ngay lập tức tốn như nhau. Đó là lý do mọi tối ưu I/O trong sê-ri này có cùng một hình dạng: gom nhiều việc vào ít lời gọi hơn.
Thứ đắt nhất luôn là việc buộc CPU dừng lại chờ. Chờ nhân (134 ns), chờ RAM (156 ns), chờ ổ đĩa (35 µs), chờ gói tin (21 ms), chờ hết chu kỳ quota (100 ms). Tối ưu tốt nhất không phải làm cái chờ nhanh hơn mà là có việc khác để làm trong lúc chờ — đó chính là độ sâu hàng đợi, là mức song song bộ nhớ, là pipelining.
Bảy bài học về cách đo
Những bài này tôi học được bằng cách đo sai, và chúng có giá trị hơn phần lớn con số ở trên.
1. Một lần đo không phải một phép đo. Phần 22 cho "ext4 nhanh gấp 3,2 lần xfs". Chạy lại ba lần, hai dải trùm hoàn toàn lên nhau. Con số 3,2 lần là nhiễu.
2. Con số quá đẹp là dấu hiệu, không phải kết quả. Phần 9 đo được 63 GB/s từ RAM — nhanh hơn băng thông bộ nhớ của máy. Nguyên nhân: trình biên dịch bỏ vòng lặp vì kết quả không được dùng. Cách phát hiện duy nhất là so với giới hạn vật lý.
3. Khi so hai môi trường, mọi thứ khác phải giống nhau. Phần 48 kết luận "ảo hoá làm CPU chậm 1,67 lần". Đổi sang cùng một trình biên dịch ở hai bên: 1,05 lần. Toàn bộ khác biệt là gcc so với clang.
4. Công cụ có thể trả lời câu hỏi khác câu bạn hỏi. Phần 23: stat của GNU không đi theo liên kết mềm trừ khi có -L. Phép đo báo "chuỗi 200 mức vẫn đọc được", thực ra chưa bao giờ đi qua liên kết nào. Giới hạn thật là 40.
5. Môi trường ảo hoá nói dối về I/O. drop_caches trong container không chạm tới bộ đệm của máy chủ bên dưới. Ba lần "lạnh" liên tiếp cho 995, 1.969 rồi 3.081 MB/s. Muốn lạnh thật thì phải O_DIRECT.
6. Đo hai việc cùng lúc là không đo gì cả. Có lần tôi chạy fio trong khi đang chạy benchmark mạng: cùng một lệnh cho 24.863 / 12.802 / 3.883 IOPS trong ba lần liên tiếp.
7. "Không kết luận được" là một kết quả. Phần 14 không tách được fsync khỏi fdatasync — dao động giữa các lần chạy của cùng một lệnh lớn hơn khác biệt giữa hai lệnh. Phần 32 không đổi được bộ mã TLS 1.3 từ Python. Ghi rõ ra tốt hơn là đăng một cặp số có lợi.
Danh sách kiểm khi hệ thống chậm
Loại trừ theo tài nguyên, ba trong bốn bước trả lời được bằng một lệnh cat.
# 1. CPU — bi ghim hay that su het?
grep -E 'nr_periods|nr_throttled' /sys/fs/cgroup/cpu.stat
cat /sys/fs/cgroup/cpu.max; nproc; ls /proc/1/task | wc -l
grep some /proc/pressure/cpu
# 2. Bo nho — anon hay chi la bo dem trang?
awk '/^anon |^file /{printf "%-6s %d MB\n",$1,$2/1048576}' /sys/fs/cgroup/memory.stat
cat /sys/fs/cgroup/memory.events
grep some /proc/pressure/memory
# 3. O dia
grep -E 'some|full' /proc/pressure/io
grep -E '^(Dirty|Writeback):' /proc/meminfo
# 4. Mang
ss -tin state established | grep -oE 'cwnd:[0-9]+|retrans:[0-9/]+' | head
netstat -s | grep -iE 'retransmit|listen queue|overflow'
# 5. Roi moi den ung dung
perf record -F 99 -g -p <pid> -- sleep 30
Ngưỡng đọc kết quả:
| Chỉ số | Bình thường | Đáng lo |
|---|---|---|
nr_throttled / nr_periods |
< 1% | > 5% |
pressure/cpu some avg10 |
< 10 | > 30 |
pressure/io full avg10 |
~0 | > 5 |
anon so với memory.max |
< 70% | > 85% |
ss retrans |
0 | tăng đều |
ss cwnd khi đang gửi nhiều |
> 100 | < 100 |
pids.current / pids.max |
< 50% | > 70% |
Mười thứ đáng kiểm trước khi tối ưu bất cứ gì
Xếp theo tỷ lệ lợi ích trên công sức, dựa trên số đo trong sê-ri:
- Số luồng có khớp quota CPU không (phần 34, 35) — p99 chênh tới 44 lần.
- Kết nối có được dùng lại không (phần 27, 32) — mỗi kết nối mới tốn 1 RTT cộng ~1 ms CPU nếu có TLS.
ndotsvà miền tìm kiếm DNS (phần 31) — 24 giây so với 1,2 ms.- Kích thước mỗi lần đọc/ghi/gửi (phần 21, 26) — điểm gãy ở 16–64 KB.
- Có
fsynctrên đường đi của mỗi yêu cầu không (phần 14, 41) — 516 µs mỗi lần. - Mất gói (phần 29) — 1% mất gói làm chậm 40 lần, và không bộ đệm nào chữa được.
read_ahead_kbcho tải quét tuần tự (phần 20) — 10,7 lần, một dòngsysctl.malloc_trimhoặc đổi sang jemalloc (phần 18) — RSS giảm 95 lần với dịch vụ phân mảnh.- Trang lớn trong suốt cho CSDL (phần 17) — tắt đi nếu ứng dụng cấp phát rải rác.
- Số dòng log mỗi yêu cầu (phần 41) — nhân với 0,3 µs.
Bảy trong mười thứ này sửa bằng cấu hình, không cần đổi mã nguồn.
Những gì sê-ri này không nói tới
Để không ai đi tìm nhầm chỗ:
- Không đo trên kim loại trần. Mọi số liệu chạy trong container trên máy ảo. Hình dạng đáng tin; con số tuyệt đối thì không.
- Không đo NUMA. Máy đo chỉ có một nút (phần 8).
- Không đo BBR (phần 29), không đo trang lớn tĩnh dưới tải thật (phần 17), không đo
clone3hayio_uringcho việc tạo tiến trình (phần 46). - Không đo đĩa quay. Nhiều quy tắc cũ — tuần tự nhanh hơn ngẫu nhiên, bộ lập lịch I/O quan trọng — vẫn đúng ở đó và tôi không kiểm chứng được.
- Không nói về tối ưu thuật toán. Đổi O(n²) thành O(n log n) thắng mọi thứ trong sê-ri này.
Cách dùng sê-ri này
Từng phần đứng riêng được, nhưng có bốn chuỗi nên đọc liền:
| Chuỗi | Các phần |
|---|---|
| CPU và lịch trình | 1 → 8 |
| Bộ nhớ, từ bộ đệm CPU tới OOM | 9 → 18 |
| Ổ đĩa, từ khối tới hệ tệp | 19 → 25 |
| Mạng, từ ổ cắm tới TLS | 26 → 32 |
| Container và công cụ | 33 → 49 |
Và nếu chỉ đọc được ba phần: 9 (bộ nhớ đệm CPU — giải thích vì sao truy cập độc lập nhanh hơn phụ thuộc), 35 (điều tiết CPU — giải thích vì sao CPU 60% mà vẫn chậm), 47 (chi phí lời gọi hệ thống — giải thích vì sao mọi tối ưu I/O giống nhau).
Thử ba mươi giây
Chạy toàn bộ danh sách kiểm trên chính hệ thống của bạn:
echo "=== CPU ==="
grep -E 'nr_periods|nr_throttled' /sys/fs/cgroup/cpu.stat 2>/dev/null
echo "cpu.max=$(cat /sys/fs/cgroup/cpu.max 2>/dev/null) nproc=$(nproc)"
grep some /proc/pressure/cpu 2>/dev/null
echo "=== BO NHO ==="
awk '/^anon |^file /{printf " %-6s %d MB\n",$1,$2/1048576}' /sys/fs/cgroup/memory.stat 2>/dev/null
grep some /proc/pressure/memory 2>/dev/null
echo "=== O DIA ==="
grep -E '^(some|full)' /proc/pressure/io 2>/dev/null
grep -E '^(Dirty|Writeback):' /proc/meminfo
echo "=== MANG ==="
ss -tin state established 2>/dev/null | grep -oE 'retrans:[0-9/]+' | head -3
echo "TIME_WAIT: $(ss -tan state time-wait 2>/dev/null | wc -l)"
echo "=== TIEN TRINH ==="
echo "pids: $(cat /sys/fs/cgroup/pids.current 2>/dev/null)/$(cat /sys/fs/cgroup/pids.max 2>/dev/null)"
echo "zombie: $(ps -eo stat 2>/dev/null | grep -c '^Z')"
Lưu đoạn này lại. Lần tới khi có ai nói "hệ thống chậm mà không biết vì sao", chạy nó trước khi mở mã nguồn — và ghi lại kết quả, vì lần đo thứ hai chỉ có ý nghĩa khi có lần đo thứ nhất để so.
Cảm ơn bạn đã đọc hết năm mươi phần.