bpftrace: Maps, Đếm và Histogram
Bài 6 in mỗi sự kiện một dòng — hợp khi sự kiện thưa, nhưng "mọi syscall" hay "mọi lần đọc đĩa" thì ngập màn hình ngay. Sức mạnh thật của bpftrace (và của eBPF nói chung) là gộp dữ liệu trong nhân: đếm, lập biểu đồ phân phối tại chỗ, rồi chỉ trả về bản tóm tắt. Đây là lý do eBPF dùng được cho quan sát sản xuất — không phải bơm hàng triệu sự kiện ra userspace.
Map: gộp theo khóa
bpftrace có biến map ký hiệu @, chính là BPF map (Bài 3) được bpftrace dựng tự động. Gán @name[key] = phép_gộp để tích lũy theo khóa. Đếm xem tiến trình nào gọi openat nhiều nhất:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @opens[comm] = count(); }'
@opens[bash]: 6
@opens[cilium-agent]: 30
@opens[containerd]: 43
@opens[kubelet]: 85
@opens[cat]: 93
@opens[ls]: 123
@opens[iptables]: 360
@opens[comm] = count() — khóa là tên tiến trình, giá trị là bộ đếm tăng mỗi lần probe nổ. Việc đếm xảy ra trong nhân; khi bpftrace thoát, nó in toàn bộ map một lần. Trên cụm này iptables mở file nhiều nhất (Cilium/kube-proxy thao tác rule), rồi tới ls, cat. Đổi khóa thành args.filename thì ra file nào bị mở nhiều nhất; đổi count() thành sum(...) thì cộng dồn một đại lượng.
Histogram: thấy phân phối, không chỉ trung bình
hist() lập biểu đồ phân phối theo lũy thừa 2 (lhist() cho khoảng tuyến tính) — ngay trong nhân. Đây là thứ con số trung bình giấu đi: trung bình 1ms có thể che một cái đuôi 100ms hiếm mà chính nó gây sự cố.
Đo độ trễ vfs_read
Mẫu thường dùng: ghi thời điểm vào ở kprobe (lúc hàm bắt đầu), tính hiệu ở kretprobe (lúc hàm trả về). Đo độ trễ hàm vfs_read của nhân:
sudo bpftrace -e '
kprobe:vfs_read { @s[tid] = nsecs; }
kretprobe:vfs_read /@s[tid]/ { @ns = hist(nsecs - @s[tid]); delete(@s[tid]); }'
@ns:
[256, 512) 973 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[512, 1K) 1306 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[1K, 2K) 1145 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[2K, 4K) 1273 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[4K, 8K) 792 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[8K, 16K) 153 |@@@@@@ |
[16K, 32K) 78 |@@@ |
[64K, 128K) 29 |@ |
[128K, 256K) 6 | |
[2M, 4M) 1 | |
Đọc cơ chế từng phần:
kprobe:vfs_read { @s[tid] = nsecs; }— lúcvfs_readbắt đầu, lưu timestamp (nsecs) vào map@skhóa theotid(thread id). Khóa theo tid là bắt buộc: nhiều thread chạyvfs_readcùng lúc, mỗi cái cần timestamp riêng.kretprobe:vfs_read /@s[tid]/ { ... }— lúc hàm trả về, filter/@s[tid]/đảm bảo có timestamp vào tương ứng;nsecs - @s[tid]là độ trễ.@ns = hist(...)— đưa độ trễ vào histogram;delete(@s[tid])giải mục đã dùng để map không phình.
Đơn vị nano-giây. Phần lớn vfs_read mất 512ns–4µs (đọc từ page cache, nhanh), nhưng có cái đuôi ra tới [2M, 4M) — một lần đọc mất 2–4ms (có thể chạm đĩa thật). Cái đuôi đó là thứ một con số trung bình sẽ làm biến mất, còn histogram phơi ra ngay. Toàn bộ phép đo + dựng histogram chạy trong nhân; userspace chỉ nhận về mấy chục dòng tóm tắt, dù vfs_read nổ hàng nghìn lần mỗi giây.
🧹 Dọn dẹp
bpftrace tự gỡ chương trình + maps khi thoát. Không có gì để dọn tay; node về 140 chương trình. Lệnh ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 07-bpftrace-aggregation.
Tổng kết
Điểm mạnh của bpftrace là gộp dữ liệu trong nhân rồi chỉ trả tóm tắt — không bơm từng sự kiện ra userspace. Map @name[key] = phép_gộp (dựa BPF map Bài 3) đếm/cộng theo khóa: @opens[comm] = count() cho thấy iptables mở file nhiều nhất trên cụm. hist() dựng biểu đồ phân phối ngay trong nhân (cột ASCII), phơi ra cái đuôi mà số trung bình giấu. Mẫu đo độ trễ thường dùng: kprobe lưu nsecs khóa theo tid, kretprobe tính hiệu rồi hist() — ta đo độ trễ vfs_read thấy phần lớn dưới 4µs nhưng có lần tới 2–4ms. Khóa theo tid để tách các thread đồng thời, delete() để map khỏi phình. Tất cả gộp trong nhân, hiệu quả với cả triệu sự kiện.
Bài 8 mở rộng tầm với của bpftrace ra ngoài nhân: uprobe và USDT — gắn vào hàm của chương trình userspace (kể cả trong container), và dùng bpftrace để soi một pod thật trên cụm đang làm gì.