Phân tích chi phí CPU khi chạy nhiều BPF program đồng thời
Khi triển khai nhiều chương trình eBPF cùng lúc trên Raspberry Pi CM5, chi phí CPU tăng theo cấp số nhân do cơ chế lock của kernel và overhead của JIT compilation.
Chúng ta sẽ sử dụng perf để đo lường chính xác thời gian CPU tiêu tốn cho từng hook eBPF trước và sau khi tối ưu.
Mở terminal và chạy lệnh sau để thu thập dữ liệu profiling trong 10 giây với sự kiện cpu-clock:
perf stat -e cpu-clock -a -I 1000 -d sleep 10
Kết quả mong đợi: Bạn sẽ thấy dòng cpu-clock hiển thị tổng thời gian CPU tiêu thụ (ví dụ: 10.000000 seconds) và các sự kiện bpf_tracepoint hoặc kprobe nếu có.
Để xác định cụ thể function nào trong kernel đang gây ra overhead khi gọi vào eBPF, hãy chạy lệnh:
perf record -e bpf_tracepoint -a -g sleep 5 && perf report --stdio
Kết quả mong đợi: Một danh sách stack trace hiện ra, chỉ rõ các function kernel (như __tracepoint__ hoặc sys_enter) đang chiếm nhiều CPU nhất.
So sánh overhead bằng cách chạy 5 chương trình eBPF đơn giản (chỉ log) cùng lúc và so sánh với 1 chương trình phức tạp.
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @count = count(); }' & bpftrace -e 'tracepoint:syscalls:sys_enter_close { @count = count(); }' & sleep 5 && killall bpftrace
Kết quả mong đợi: Bạn sẽ thấy CPU usage trên CM5 tăng đột biến (có thể lên 20-30% core) so với khi chỉ chạy 1 chương trình.
Verify kết quả phân tích CPU
Sử dụng lệnh top hoặc htop để quan sát thực tế phần trăm CPU tiêu thụ bởi tiến trình kworker hoặc softirq khi eBPF đang chạy.
htop
Kết quả mong đợi: Nếu overhead cao, bạn sẽ thấy các core CPU của CM5 bị load liên tục bởi các task liên quan đến interrupt hoặc worker thread xử lý eBPF.
Kỹ thuật tối ưu code BPF: Giảm instruction và tránh Map Lookup
Trên kiến trúc ARM64 của CM5, mỗi instruction eBPF đều tốn cycles. Việc giảm số lượng instruction và tối thiểu hóa truy cập Map là yếu tố then chốt.
Chúng ta sẽ viết một chương trình C cho eBPF, biên dịch bằng clang và kiểm tra số lượng instruction bằng llvm-objdump.
Tạo file /root/bpf_optimize.c với nội dung sau, chú ý kỹ thuật sử dụng BPF_CORE_READ thay vì struct pointer trực tiếp để giảm instruction:
#include "vmlinux.h"
#include
#include
char LICENSE[] SEC("license") = "GPL";
// Map để lưu kết quả, dùng loại BPF_MAP_TYPE_HASH
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} stats_map SEC(".maps");
SEC("kprobe/sys_enter_openat")
int BPF_KPROBE(opt_openat, struct openat_common_args *ctx)
{
// Kỹ thuật 1: Tránh Map Lookup không cần thiết
// Chỉ thực hiện lookup khi điều kiện được thỏa mãn
u32 pid = bpf_get_current_pid_tgid() >> 32;
// Kiểm tra điều kiện trước khi gọi map lookup
if (pid == 1) {
u64 val = 1;
bpf_map_update_elem(&stats_map, &pid, &val, BPF_ANY);
}
// Kỹ thuật 2: Sử dụng BPF_CORE_READ để an toàn và tối ưu
// Tránh truy cập pointer trực tiếp gây ra instruction extra
u64 fd = BPF_CORE_READ(ctx, fd);
if (fd > 0) {
// Logic xử lý tối ưu
}
return 0;
}
Kết quả mong đợi: File opt_openat.c được tạo thành công.
Biên dịch file này sang object file ARM64 và kiểm tra số lượng instruction bằng llvm-objdump:
clang -O2 -g -target bpf -c /root/bpf_optimize.c -o /root/bpf_optimize.o && llvm-objdump -D -m bpf /root/bpf_optimize.o
Kết quả mong đợi: Bạn sẽ thấy danh sách các instruction eBPF. Đếm số dòng trong section opt_openat để biết độ dài hàm.
Để tối ưu hơn, hãy so sánh với phiên bản không dùng kỹ thuật BPF_CORE_READ và không kiểm tra điều kiện trước khi lookup.
Tạo file /root/bpf_naive.c (phiên bản chưa tối ưu) để so sánh:
#include "vmlinux.h"
#include
char LICENSE[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} stats_map SEC(".maps");
SEC("kprobe/sys_enter_openat")
int BPF_KPROBE(naive_openat, struct openat_common_args *ctx)
{
// Không tối ưu: Luôn lookup map
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 val = 1;
bpf_map_update_elem(&stats_map, &pid, &val, BPF_ANY);
// Truy cập pointer trực tiếp (có thể gây crash hoặc instruction thừa)
u64 fd = ctx->fd;
return 0;
}
Kết quả mong đợi: File naive_openat.c được tạo.
Biên dịch và so sánh số instruction:
clang -O2 -g -target bpf -c /root/bpf_naive.c -o /root/bpf_naive.o && llvm-objdump -D -m bpf /root/bpf_naive.o
Kết quả mong đợi: Bạn sẽ thấy số lượng instruction trong naive_openat nhiều hơn opt_openat đáng kể, đặc biệt là các instruction mov và call cho map lookup.
Verify kết quả tối ưu code
Chạy cả hai chương trình cùng lúc và dùng perf để đo thời gian thực thi:
bpftrace -c /root/bpf_optimize.o & sleep 2 && killall bpftrace
Kết quả mong đợi: Chương trình tối ưu chạy nhanh hơn, tiêu thụ ít CPU hơn và không gây latency cho hệ thống.
Cấu hình CPU affinity và IRQ balancing để tối đa hóa hiệu năng
Trên Raspberry Pi CM5, việc phân bổ đều các task eBPF và IRQ (Interrupt Request) lên tất cả core là quan trọng để tránh nghẽn cổ chai trên một core duy nhất.
Chúng ta sẽ cấu hình /proc/irq để ép các IRQ mạng và timer (thường kích hoạt eBPF) về các core cụ thể.
Đầu tiên, kiểm tra số lượng core CPU của CM5:
lscpu | grep "CPU(s)"
Kết quả mong đợi: Thông báo số lượng core (ví dụ: 8 CPU(s) cho CM4/CM5 8-core).
Cấu hình CPU affinity cho các IRQ liên quan đến mạng (ví dụ: eth0) để chạy trên core 0 và 1, tránh core đang xử lý eBPF nặng:
echo 3 > /proc/irq/$(cat /proc/interrupts | grep eth0 | awk '{print $1} | head -n1)/smp_affinity_list
Kết quả mong đợi: Lệnh chạy thành công, các IRQ mạng được ép vào core 0, 1 và 2 (binary mask 3 = 11 binary = core 0,1).
Để cân bằng tự động các task eBPF (kworker) lên các core ít tải, chỉnh sửa cấu hình /proc/sys/kernel/irq_balancer:
echo 1 > /proc/sys/kernel/irq_balancer
Kết quả mong đợi: Kernel bật chế độ cân bằng IRQ tự động.
Cấu hình /etc/sysctl.d/99-bpf-affinity.conf để giữ cấu hình này sau khi reboot:
# /etc/sysctl.d/99-bpf-affinity.conf
kernel.irq_balancer = 1
kernel.sched_migration_cost_ns = 1000000
kernel.sched_min_granularity_ns = 1000000
Kết quả mong đợi: File cấu hình được tạo, sẵn sàng áp dụng sau khi chạy sysctl --system.
Áp dụng cấu hình ngay lập tức:
sysctl --system
Kết quả mong đợi: Không có lỗi, các tham số được cập nhật.
Sử dụng taskset để ép tiến trình chạy eBPF (ví dụ: bpftrace hoặc bpftool) vào core cụ thể:
taskset -c 4-7 bpftrace -e 'tracepoint:syscalls:sys_enter_openat { print("open"); }'
Kết quả mong đợi: Chương trình eBPF chỉ chạy trên core 4, 5, 6, 7, tránh xung đột với các task mạng trên core 0-3.
Verify kết quả cấu hình CPU
Kiểm tra lại affinity của các IRQ và tiến trình:
cat /proc/interrupts | grep eth0 && cat /proc/$(pidof bpftrace)/status | grep Cpu
Kết quả mong đợi: Dòng CPU(s) trong file status của tiến trình bpftrace khớp với core bạn đã set (4-7), và IRQ eth0 nằm trên core 0-3.
Chạy test tải cao và quan sát top để xem load có được phân bổ đều không:
stress -c 4 --timeout 10s & top -H
Kết quả mong đợi: CPU usage trên các core được phân bổ tương đối đều, không có core nào bị quá tải (100%) trong khi core khác nhàn rỗi.
Điều hướng series:
Mục lục: Series: Tối ưu hóa Linux Kernel cho Raspberry Pi CM5 với BPF và eBPF
« Phần 6: Giám sát hiệu năng mạng với BPF Tracepoint và XDP
Phần 8: Xây dựng dashboard giám sát thời gian thực với eBPF »