Phần 38 cho thấy perf đo chính xác tới 0,15 điểm phần trăm. Bài này chỉ ra vì sao con số chính xác đó vẫn có thể vô dụng — và cách sửa.
Hồ sơ phẳng, và vì sao nó không giúp gì
Chương trình mô phỏng một dịch vụ: xử lý yêu cầu (phân tích JSON, mã hoá, nén) cộng một tác vụ nền cũng nén. Cả bốn hàm cuối cùng đều gọi chung một hàm tiện ích tinh().
perf report --sort symbol
99,90% tinh
(không còn dòng nào khác trên 1%)
Con số này đúng. Và nó vô dụng.
Nó nói rằng toàn bộ CPU nằm trong một hàm tiện ích được gọi từ khắp nơi. Tối ưu tinh() thì được — nhưng nếu không tối ưu được nữa thì sao? Hồ sơ phẳng không cho biết ai đang gọi nó nhiều nhất.
Đây là hình dạng thường gặp trong mã nguồn thật: hàm tốn nhất là memcpy, malloc, json_parse, hoặc một hàm nào đó trong thư viện chuẩn. Biết tên nó không giúp bạn sửa gì.
Cùng số liệu, tách theo ngăn xếp
perf script | stackcollapse-perf.pl | sort | uniq -c | sort -rn
| Đường gọi | Số mẫu | Tỷ lệ |
|---|---|---|
main; xu_ly_yeu_cau; nen; tinh |
597 | 60,43% |
main; tac_vu_nen; nen; tinh |
247 | 25,00% |
main; xu_ly_yeu_cau; phan_tich_json; tinh |
73 | 7,39% |
main; xu_ly_yeu_cau; ma_hoa; tinh |
71 | 7,19% |
Bây giờ mọi thứ rõ ràng:
- Nén chiếm 85% CPU (60,43 + 25,00), chia làm hai đường gọi.
- Nén trong đường xử lý yêu cầu chiếm 60,43% — đây là chỗ ảnh hưởng tới độ trễ người dùng.
- Nén trong tác vụ nền chiếm 25,00% — có thể dời sang giờ thấp điểm.
- Phân tích JSON và mã hoá cộng lại chỉ 14,58%.
Cùng một hàm nen() xuất hiện hai lần với hai tỷ lệ khác nhau, tuỳ ai gọi nó. Đó chính là thứ hồ sơ phẳng không bao giờ nói được.
Kiểm chứng: tỷ lệ tính từ mã nguồn là 60% / 25% / 7,5% / 7,5%. Đo được 60,43 / 25,00 / 7,39 / 7,19. Sai lệch lớn nhất 0,43 điểm.
Dựng biểu đồ
git clone https://github.com/brendangregg/FlameGraph
perf record -F 99 -g -p <pid> -- sleep 30
perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > ho-so.svg
Ba bước, ba định dạng:
| Bước | Ra cái gì |
|---|---|
perf record |
tệp nhị phân perf.data |
perf script |
mỗi mẫu một khối văn bản nhiều dòng |
stackcollapse-perf.pl |
mỗi ngăn xếp một dòng: a;b;c 597 |
flamegraph.pl |
SVG tương tác |
Định dạng ở bước ba đơn giản tới mức làm được bằng awk, và nó nhận đầu vào từ mọi nguồn — Java, Go, Python, eBPF. Đó là lý do biểu đồ ngọn lửa dùng được cho mọi ngôn ngữ.
Cách đọc
Chiều ngang là tỷ lệ, không phải thời gian. Trục X không phải dòng thời gian — nó chỉ được sắp xếp theo bảng chữ cái để các khối giống nhau gộp lại. Đọc biểu đồ ngọn lửa như một dòng thời gian là hiểu sai thường gặp nhất.
Chiều cao là độ sâu ngăn xếp. Đọc từ dưới lên: mỗi tầng là một mức gọi sâu hơn.
Khối rộng ở gần đỉnh là chỗ đáng sửa. Đỉnh của một khối là nơi CPU thực sự đang chạy. Một khối rộng ở tầng thấp chỉ nghĩa là nó gọi nhiều thứ tốn kém, không phải bản thân nó tốn.
Khối hẹp không đáng quan tâm — trừ khi bạn đang tìm đuôi độ trễ, và lúc đó biểu đồ ngọn lửa CPU không phải công cụ đúng.
Bốn loại biểu đồ ngọn lửa
Loại thông thường đo CPU. Ba loại khác trả lời câu hỏi khác:
Ngoài CPU — thời gian tiến trình bị chặn, chờ khoá, chờ I/O:
perf record -e sched:sched_switch -g -p <pid> -- sleep 10
Phần 38 đã nêu: hồ sơ CPU không thấy phần chờ. Với dịch vụ tốn 90% thời gian chờ cơ sở dữ liệu, đây mới là biểu đồ cần xem.
Cấp phát bộ nhớ — ai gọi malloc nhiều nhất:
perf record -e probe_libc:malloc -g -p <pid> -- sleep 10
Biểu đồ khác biệt — chồng hai hồ sơ trước và sau một thay đổi, tô đỏ phần chậm đi và xanh phần nhanh lên. difffolded.pl trong cùng kho FlameGraph. Đây là cách tốt nhất để trả lời "thay đổi này làm gì".
Ba lỗi khi dựng
Ngăn xếp cụt. Nếu biểu đồ chỉ có hai ba tầng, phần lớn khả năng là con trỏ khung bị bỏ. Xem lại bảng ba cách lấy ngăn xếp ở phần 38.
Tên hàm là địa chỉ. Với JVM, Node, .NET thì mã sinh lúc chạy không có ký hiệu. Cần tệp ánh xạ (--perf-basic-prof cho Node, libperfmap cho Java).
Lấy mẫu quá ít. Dưới vài trăm mẫu thì các khối hẹp là nhiễu. -F 99 trong 30 giây cho khoảng 3.000 mẫu — đủ cho phần lớn việc.
Không có perf? Vẫn dựng được
Cách nghèo nhất nhưng vẫn hiệu quả — lấy ngăn xếp bằng gdb hoặc eu-stack theo chu kỳ:
for i in $(seq 1 100); do
eu-stack -p <pid> 2>/dev/null | awk '/^#/{print $3}' | tac | paste -sd';'
sleep 0.1
done | sort | uniq -c | sort -rn | head
Chậm và thô, nhưng nó dựa trên nguyên lý giống hệt: lấy mẫu ngăn xếp đủ nhiều thì phân bố mẫu hội tụ về phân bố thời gian. Với một tiến trình đang treo mà không cài được gì, đây thường là công cụ duy nhất.
Thử ba mươi giây
Dựng biểu đồ ngọn lửa dạng văn bản mà không cần cài gì ngoài perf:
p=<pid>
perf record -F 99 -g -o /tmp/f.data -p $p -- sleep 20 2>/dev/null
perf script -i /tmp/f.data 2>/dev/null | awk '
/^$/ { if (n>0) { s=fr[n]; for(i=n-1;i>=1;i--) s=s";"fr[i]; print s } n=0; next }
/^[[:space:]]/ { sym=$2; if (sym!="" && sym!~/^0x/) { n++; fr[n]=sym } }
' | sed 's/+0x[0-9a-f]*//g' | sort | uniq -c | sort -rn | head -10
Mười dòng đầu ra là mười đường gọi tốn CPU nhất của dịch vụ bạn. Nếu dòng đầu chiếm hơn một phần ba tổng số mẫu, bạn vừa tìm được chỗ đáng sửa đầu tiên — và bạn biết cả đường gọi dẫn tới nó, không chỉ tên hàm.
Phần sau: eBPF và bcc — đo những thứ công cụ khác không thấy.