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 so với ngăn xếp gọi

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.