Làm sao đài truyền hình biết đêm qua bao nhiêu phần trăm dân số xem một chương trình, mà không cần gõ cửa từng nhà? Họ lấy mẫu: chọn vài nghìn hộ ngẫu nhiên, hỏi họ xem gì, rồi suy ra cả nước. Kỳ lạ là con số ấy chính xác đến bất ngờ — sai vài phần trăm là cùng — dù chỉ chạm tới một phần rất nhỏ dân số. perf làm y hệt vậy với chương trình của bạn: nó không theo dõi từng lời gọi (đó là việc của strace, và ta đã thấy nó đắt tới mức nào), mà chỉ thỉnh thoảng ngắt ra hỏi "lúc này CPU đang chạy ở đâu?" rồi đếm. Bài này đo xem cái phép thăm dò ấy chính xác và rẻ tới đâu.

Phần 37 kết luận strace làm chậm 446 lần. Bài này đo công cụ ở đầu kia của thang: perf.

Kiểm tra bằng chương trình đã biết đáp án

Cách duy nhất để tin một công cụ đo là đưa cho nó một bài toán mình đã biết đáp án. Chương trình có bốn hàm, mỗi hàm chạy một vòng lặp có số vòng khác nhau. Tỷ lệ CPU thật tính được từ mã nguồn:

Hàm Số vòng lặp Tỷ lệ thật perf báo
rat_dat 100.000.000 62,50% 62,65%
dat 40.000.000 25,00% 24,89%
vua 15.000.000 9,38% 9,31%
re 5.000.000 3,13% 3,16%

Sai lệch lớn nhất: 0,15 điểm phần trăm. Với một công cụ chỉ liếc nhìn vài trăm lần thay vì đếm từng lệnh, đó là độ chính xác đáng nể.

perf không đoán và không mô phỏng. Nó ngắt chương trình vài trăm tới vài nghìn lần mỗi giây, mỗi lần ghi lại con trỏ lệnh đang ở đâu, rồi đếm. Đủ mẫu thì phân bố mẫu hội tụ về phân bố thời gian thật — đúng cái nguyên lý của thăm dò ý kiến.

Và nó gần như miễn phí

Cách chạy Thời gian Số mẫu
Không perf 3,99 s —
perf record -F 99 3,97 s 392
perf record -F 999 3,96 s 3.957
perf record -F 9999 3,95 s ~3.000
perf record -F 49999 3,90 s ~3.000

Không đo được chênh lệch nào. Các lần chạy có perf thậm chí nhỉnh hơn — đó là nhiễu. Nói thẳng ra bên cạnh phần trước: strace làm chậm 446 lần, perf record làm chậm không đo được.

Lý do khác nhau ở bản chất. strace dừng tiến trình ở mỗi lời gọi hệ thống — như chặn từng người ở cửa để hỏi. perf để tiến trình chạy tự do và chỉ thỉnh thoảng ngắt ra ghi một con số — như người điều tra chỉ ghé thăm vài hộ ngẫu nhiên.

Một cái bẫy nhỏ trong bảng trên

Bạn có để ý -F 9999 và -F 49999 đều cho khoảng 3.000 mẫu — bằng đúng -F 999? Nhân đã hạ tần số xuống mà không báo một tiếng:

cat /proc/sys/kernel/perf_event_max_sample_rate     # 100000 tren may toi do

Đặt -F cao hơn giới hạn này thì perf chạy ở tần số bị hạ, và bạn cứ ngỡ mình đang lấy mẫu dày hơn thực tế. Cách kiểm tra: chia số mẫu cho thời gian chạy, xem có khớp -F không.

Với phần lớn việc, -F 99 là đủ. 392 mẫu đã đủ để xếp hạng bốn hàm; sai số chỉ ảnh hưởng tới những hàm chiếm dưới 1%.

Quy trình ba bước

# 1. Thu thap
perf record -F 99 -g --call-graph dwarf -p <pid> -- sleep 30

# 2. Xem xep hang phang
perf report --stdio --sort symbol --percent-limit 1

# 3. Xem theo ngan xep goi
perf report --stdio -g graph,0.5,caller

-g bật ghi ngăn xếp lời gọi — thứ trả lời câu hỏi "hàm này bị gọi từ đâu", thường quan trọng hơn "hàm nào tốn nhất".

Ba cách lấy ngăn xếp, và lựa chọn này quan trọng:

Cách Yêu cầu Ghi chú
--call-graph fp biên dịch với -fno-omit-frame-pointer rẻ nhất, chính xác
--call-graph dwarf có thông tin gỡ lỗi chạy được với binary phát hành, tốn hơn
--call-graph lbr CPU Intel đời mới rất rẻ, giới hạn độ sâu

Nhiều bản phân phối biên dịch với -fomit-frame-pointer, và khi đó fp cho ngăn xếp cụt. Nếu perf report chỉ hiện một tầng, đó là nguyên nhân.

Biểu đồ ngọn lửa

perf report dạng văn bản khó đọc với ngăn xếp sâu. Biểu đồ ngọn lửa giải quyết việc đó:

perf record -F 99 -g -p <pid> -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > ho-so.svg

Trục ngang là tỷ lệ thời gian, không phải thời gian trôi qua. Chiều rộng của một khối là phần CPU nó chiếm; chiều cao là độ sâu ngăn xếp. Khối rộng ở gần đỉnh là chỗ đáng tối ưu.

perf không chỉ đo CPU

perf list | head -30                          # moi su kien do duoc
perf stat -e cache-misses,cache-references ./ct
perf stat -e branch-misses,branches ./ct
perf stat -e context-switches,cpu-migrations ./ct
perf stat -e 'syscalls:sys_enter_*' -p <pid> -- sleep 5

Tỷ lệ trượt bộ nhớ đệm là con số nối thẳng với phần 9: nếu cache-misses / cache-references cao, vấn đề nằm ở bố cục dữ liệu chứ không ở thuật toán.

perf stat không cần lấy mẫu — nó đọc bộ đếm phần cứng, nên chi phí gần bằng 0 và con số là chính xác, không phải ước lượng.

Chạy perf trong container

cat /proc/sys/kernel/perf_event_paranoid
Giá trị Cho phép
3 không gì
2 chỉ sự kiện mức người dùng của tiến trình mình
1 thêm sự kiện nhân
0 thêm bộ đếm cấp CPU
−1 tất cả

Máy tôi đo có giá trị 2, và tôi phải chạy container --privileged để thu thập. Trong Kubernetes:

securityContext:
  capabilities:
    add: ["SYS_ADMIN", "PERFMON"]

PERFMON (từ nhân 5.8) là lựa chọn hẹp hơn SYS_ADMIN và nên dùng khi có.

Chỗ perf không nhìn tới

Thời gian chờ. perf record mặc định lấy mẫu theo chu kỳ CPU, nên tiến trình đang ngủ chờ I/O hoặc chờ khoá không xuất hiện. Một dịch vụ tốn 90% thời gian chờ cơ sở dữ liệu sẽ cho hồ sơ trông rất sạch — sạch một cách lừa dối. Muốn thấy phần chờ, dùng sự kiện khác:

perf record -e sched:sched_switch -g -p <pid> -- sleep 10     # ho so ngoai CPU

Mã sinh lúc chạy. JVM, V8, .NET biên dịch mã ngay lúc chạy nên perf chỉ thấy địa chỉ, không thấy tên hàm. Cần tệp ánh xạ:

java -XX:+PreserveFramePointer -agentpath:/path/libperfmap.so ...
node --perf-basic-prof app.js

Mẫu số chung: lấy mẫu ngẫu nhiên đủ nhiều thì hội tụ về sự thật

Cái nguyên lý làm perf vừa rẻ vừa chính xác — không đếm từng sự kiện mà lấy mẫu ngẫu nhiên đủ nhiều rồi để thống kê lo phần còn lại — không riêng gì Linux. Nó là cách người ta đo hiệu năng ở mọi tầng, một khi việc đếm-tất-cả trở nên quá đắt.

Trong thế giới Java, async-profiler và JFR làm đúng vậy: lấy mẫu ngăn xếp theo chu kỳ (hoặc theo sự kiện cấp phát) thay vì chèn mã đo vào từng method. Chi phí vài phần trăm, bật được ở production — trong khi profiler kiểu chèn bytecode làm chậm gấp bội và còn bóp méo chính cái nó đo, y như strace dừng-đọc-thả.

Trong Go, pprof là lấy mẫu theo thiết kế: CPU profile lấy mẫu ngăn xếp ~100 Hz — đúng con số -F 99 ở đây — nhẹ tới mức chạy liên tục được, nền của cả ngành continuous profiling. Và Go cũng vấp đúng cái bẫy "perf không thấy phần chờ": CPU profile của Go không thấy goroutine đang chặn, phải dùng block/mutex profile riêng — song ánh với sched:sched_switch ở trên.

Còn ở cơ sở dữ liệu, cùng đánh đổi hiện ra giữa lấy mẫu trạng thái phiên (Active Session History của Oracle, pg_stat_activity lấy mẫu mỗi giây "phiên nào đang chạy gì") và bật full statement logging (log_min_duration_statement = 0 ghi mọi câu — chính xác nhưng làm ngập I/O khi tải cao). ASH rẻ, always-on, đủ chỉ ra câu query nóng — đúng vai trò -F 99.

Mẫu số chung: khi đếm mọi sự kiện quá đắt hoặc quá xâm lấn, lấy mẫu ngẫu nhiên đủ dày cho ra cùng câu trả lời với chi phí tỷ lệ theo tần số bạn đặt — cố định, biết trước, không phụ thuộc tải — chứ không tỷ lệ theo khối lượng công việc. Nên câu hỏi đúng khi chọn công cụ đo hiệu năng không phải "nó theo dõi được từng lời gọi không" mà là "chi phí của nó tỷ lệ với tần số tôi đặt hay với tải hệ thống, và nó có phân biệt được CPU-bận với đang-chờ không" — vì chỉ loại lấy mẫu mới dám bật lên giữa lúc hệ thống đang cháy.

Muốn tự thấy, lập hồ sơ một tiến trình đang chạy:

p=<pid>
perf record -F 99 -g -o /tmp/p.data -p $p -- sleep 20
perf report -i /tmp/p.data --stdio --sort symbol --percent-limit 1 2>/dev/null | head -20

echo "--- kiem tra so mau co dung tan so khong ---"
perf report -i /tmp/p.data --stdio 2>/dev/null | grep -E '^# (Samples|Event count)'

Chia số mẫu cho 20 giây. Nếu nó thấp hơn nhiều so với 99, tiến trình phần lớn thời gian không chạy trên CPU — và lúc đó vấn đề của bạn không phải CPU, mà là chờ. Đó cũng là một kết luận có giá trị, chỉ là nó nằm ở chỗ hồ sơ CPU không nhìn tới.

Phần sau: bẫy khi đo — những sai lầm tôi đã mắc trong ba mươi tám phần trước.