eBPF Từ Số Không
Học eBPF từ nền tảng tới tự viết chương trình thật — máy ảo eBPF, verifier, maps, các hook (XDP/tc/kprobe/tracepoint/LSM); tracing với bpftrace; viết chương trình bằng libbpf + CO-RE (C) rồi nạp từ Go (cilium/ebpf); networking, observability và security. Dùng một cụm Kubernetes thật (kernel 6.17, Cilium 1.19 eBPF kube-proxy-less với hàng trăm chương trình BPF đang chạy) làm phòng lab xuyên suốt. Test thật, bám docs chính thức (ebpf.io, kernel.org, libbpf, cilium). Mã nguồn tại github.com/nghiadaulau/ebpf-from-scratch.
tc/sched_cls và Mổ Datapath Cilium Đang Chạy
Sau XDP là tc — hook nơi gói đã có sk_buff, thấy được cả ingress lẫn egress, và là nơi Cilium đặt gần như toàn bộ datapath của nó. Bài này không viết tc mẫu cho có; nó mổ thẳng 74 chương trình sched_cls đang chạy thật trên một node của cụm: chúng gắn vào đâu (card mạng, từng pod), gọi nhau qua tail call thế nào, và tra những BPF map nào để cân bằng tải một Service hay áp NetworkPolicy. Load balancing kube-proxy-less hóa ra chỉ là một lần tra map.
Tự Viết Một Chương Trình tc: __sk_buff và Chuỗi tcx
Bài 12 đọc datapath tc của Cilium từ ngoài. Bài này tự viết một chương trình tc — đếm và phân loại gói egress theo giao thức — để nắm __sk_buff từ trong ra. Điểm khác cốt lõi với XDP: tc thấy sk_buff với metadata đã điền sẵn (skb->protocol, skb->len), không phải gói thô. Ta gắn nó bằng tcx trên một interface thật, chạy ra số đếm đúng, rồi gặp một bài học thật: cùng chương trình đó gắn sau Cilium trên card mạng lại không chạy lần nào, vì cách chuỗi tcx kết thúc.
LSM BPF: Cưỡng Chế An Ninh Ngay Trong Nhân
Tới giờ eBPF của ta chỉ quan sát. LSM BPF thì cưỡng chế: gắn vào các điểm kiểm soát an ninh của nhân (Linux Security Modules) mà SELinux, AppArmor dùng, và một chương trình trả về 0 cho qua hay -EPERM để chặn. Bài này viết một chương trình LSM chặn mở file, và gặp một bài học thật: lần đầu nó nạp và gắn được nhưng không chặn gì — vì bpf chưa nằm trong danh sách LSM hoạt động. Bật bpf bằng tham số boot rồi reboot, cùng chương trình đó chặn thật — cat và python đều nhận Operation not permitted.
seccomp-bpf: BPF Cổ Điển Lọc Syscall Trong Mọi Container
Trước eBPF có cBPF — BPF cổ điển, thứ tcpdump dùng. Và nó vẫn đang chạy: seccomp-bpf lọc syscall bằng cBPF, là lớp sandbox nền của container. Bài này phân biệt cBPF với eBPF, soi seccomp thật trên cụm (pause container và CSI sidecar bị giới hạn, pod privileged thì không, systemd-resolved chồng 28 filter), rồi tự viết một filter cBPF chặn mkdir bằng EPERM — tám lệnh thao tác trên struct seccomp_data, cài bằng prctl, chặn thật trong khi printf vẫn chạy.
Kiểu Tetragon: Từ Quan Sát Đến Cưỡng Chế Bằng bpf_send_signal
Tetragon là công cụ an ninh runtime của hệ Cilium: nó quan sát bằng kprobe/tracepoint (đúng những hook Part II dùng) rồi cưỡng chế ngay trong nhân. Cơ chế cưỡng chế thật của nó là hai helper — bpf_send_signal gửi SIGKILL giết tiến trình, và bpf_override_return ghi đè giá trị trả về syscall. Bài này tự dựng đúng cơ chế đó: một tracepoint exec gọi bpf_send_signal(SIGKILL) để giết một tiến trình ngay khi nó chạy — binary cấm nhận exit 137, binary thường vẫn chạy. Không cần LSM hay reboot.
CPU Profiling Bằng perf_event: Lấy Mẫu Stack, Nền Của Flame Graph
Muốn biết CPU đang bận làm gì, ta lấy mẫu: vài chục lần mỗi giây, đóng băng mỗi CPU và ghi lại stack đang chạy. eBPF làm việc này qua loại chương trình perf_event — gắn vào một bộ đếm lấy mẫu của nhân, mỗi lần nổ thì chụp stack rồi gộp ngay trong nhân. Bài này profile một node thật ở 99Hz, thấy dd ngốn CPU qua đường đọc /dev/zero còn lõi rảnh thì nằm trong vòng idle, gộp theo tiến trình ra dd 479 mẫu — và đây chính là dữ liệu mà flame graph vẽ lên.
Off-CPU và Độ Trễ Scheduler: Đo Thời Gian Tiến Trình KHÔNG Chạy
Profiling on-CPU (Bài 17) chỉ thấy lúc CPU bận. Nhưng phần lớn độ trễ một ứng dụng cảm nhận lại nằm ở lúc nó KHÔNG chạy: chờ đĩa, chờ khóa, chờ tới lượt CPU. eBPF đo được khoảng off-CPU đó qua tracepoint của scheduler. Bài này đo hai thứ trên node thật: run-queue latency — thời gian từ lúc một tác vụ được đánh thức tới lúc thật sự chạy, phơi ra cái đuôi 16-32ms khi CPU bị tranh; và off-CPU time — một tác vụ nằm ngoài CPU bao lâu mỗi lần, với đuôi tới vài giây cho tác vụ bị chặn.
Hubble Bên Trong: Từ Sự Kiện eBPF Tới Luồng Mạng Toàn Cụm
Hubble cho ta xem mọi kết nối trong cụm Kubernetes theo tên pod, service, verdict policy — mà không cài sidecar vào pod nào. Bài này mổ cơ chế: datapath eBPF của Cilium (74 chương trình sched_cls ở Bài 12) gọi bpf_perf_event_output đẩy sự kiện vào một perf ring buffer tên cilium_events; cilium-agent đọc ra (cilium monitor cho thấy sự kiện thô với identity dạng số); rồi Hubble giàu hóa — biến identity 18203 thành kube-system/coredns — bằng cách tra kho identity-nhãn. Ta xem cả ba tầng chạy thật.
Case-study: Một Gói Đi Qua Datapath eBPF Của Cilium
Mười chín bài đã mổ từng mảnh: verifier, maps, XDP, tc, tail call, perf ring, identity. Bài này ghép chúng lại thành một câu chuyện liền mạch — đi theo đúng một gói khi một pod gọi Service DNS của cụm, từ lúc rời pod nguồn tới lúc tới pod CoreDNS, qua từng chương trình eBPF và từng BPF map mà nó chạm vào. Không khái niệm mới; chỉ là thấy toàn bộ cỗ máy chạy như một thể thống nhất, với dữ liệu thật từ chính cụm đã dùng suốt series.
Capstone: Tự Viết connmon — Monitor Kết Nối TCP Toàn Node
Bài cuối: ghép mọi thứ đã học thành một công cụ thật. connmon gắn kprobe vào tcp_connect trong nhân, đẩy mỗi kết nối TCP mới qua ring buffer, và một loader Go in chúng ra theo thời gian thực — pid, tiến trình, đích IP:port. Chỉ hơn trăm dòng, một binary tĩnh, chạy trên cụm thấy ngay coredns, kubelet, curl đang kết nối đi đâu. Kèm một cái bẫy build thật của kprobe. Rồi nhìn lại toàn bộ hành trình eBPF từ số không.