Case-study: Một Gói Đi Qua Datapath eBPF Của Cilium
Suốt mười chín bài, ta mổ từng mảnh eBPF riêng lẻ. Part VII ghép lại. Bài này không có khái niệm mới — nó đi theo đúng một gói qua datapath Cilium trên cụm, và mỗi bước sẽ chỉ về bài đã giải thích mảnh đó. Kịch bản: một pod gọi DNS — hỏi Service kube-dns ở ClusterIP 10.32.0.10:53, được cân bằng tải tới pod CoreDNS thật 10.200.0.44:53.
Toàn cảnh: gói đi qua những gì
Pod nguồn Pod CoreDNS
(vd 10.200.0.64) (10.200.0.44)
│ gửi tới Service 10.32.0.10:53 ▲
▼ │
veth lxc… ──► cil_from_container (tc, Bài 12) ──┐ │
│ tail call (Bài 4)│
tail_handle_ipv4 ───────┤ │
tail_ipv4_ct_* ──────┤ │
▼ │
┌──────────────── tra BPF map (Bài 3) ─────────────┐ │
│ cilium_lb4_services: 10.32.0.10:53 -> 10.200.0.44:53 (Bài 12)
│ cilium_ct4_global: tạo entry theo dõi kết nối (Bài 12)
│ cilium_lxc/policy: cho qua theo identity 18203 (Bài 12/19)
└───────────────────────────────────────────────────┘
│ DNAT đích -> backend
▼
cùng node? redirect veth->veth ───────────┘
│ khác node
▼
cil_to_netdev ─► ens5 ─► node đích ─► cil_from_netdev ─► … ─► pod
│
└─► bpf_perf_event_output -> cilium_events -> Hubble (Bài 19)
Bước 1 — Rời pod: cil_from_container
Pod gửi gói tới 10.32.0.10:53. Gói ra đầu veth của pod (lxc…), nơi Cilium gắn cil_from_container ở tc ingress (Bài 12). Đây là điểm đầu tiên eBPF nắm gói — chương trình sched_cls, chạy sau khi nhân dựng sk_buff nên có đủ metadata (Bài 13). Mọi truy cập gói trong đây đều đã qua verifier chứng minh an toàn lúc nạp (Bài 2), và chạy bằng mã JIT native (Bài 1).
Bước 2 — Tail call: chẻ datapath
cil_from_container không làm hết mọi việc trong một chương trình — giới hạn 1 triệu lệnh (Bài 2) không cho phép. Nó tail call (Bài 4) qua prog_array cilium_call_policy sang chuỗi chương trình, đúng các tên đã thấy ở Bài 12:
cil_from_container -> tail_handle_ipv4 -> tail_ipv4_ct_ingress -> cil_lxc_policy -> ...
Mỗi chương trình lo một việc rồi nhảy tiếp, mỗi cái nằm gọn trong giới hạn verifier.
Bước 3 — Cân bằng tải: tra cilium_lb4_services rồi DNAT
10.32.0.10:53 là ClusterIP ảo, không phải địa chỉ thật của pod nào. Datapath tra cilium_lb4_services (một BPF hash map, Bài 3 & 12) — bảng Service mà ta đã đọc được dạng người hiểu:
10.32.0.10:53/UDP (2) 10.200.0.44:53/UDP (6) (2) <- CoreDNS backend
Tìm thấy backend, datapath chọn một slot và DNAT đích từ 10.32.0.10:53 thành 10.200.0.44:53 — ngay trong nhân, tại veth của pod, không qua iptables, không qua kube-proxy (cụm này kube-proxy-less). Đây là toàn bộ "load balancing" của Kubernetes rút về một lần tra hash map + sửa địa chỉ.
Bước 4 — Conntrack và policy
Để chiều về NAT ngược cho đúng, datapath ghi một entry vào cilium_ct4_global (Bài 12) — kết nối này giờ được theo dõi, kèm SourceSecurityID của pod nguồn. Rồi cil_lxc_policy áp NetworkPolicy không theo IP mà theo identity (Bài 12 & 19): pod nguồn mang một security identity (số), CoreDNS mang 18203; chương trình tra map policy theo cặp identity để cho qua hay vứt. Ta đã xác nhận 18203 chính là CoreDNS:
$ cilium identity get 18203
18203 k8s:k8s-app=kube-dns k8s:io.kubernetes.pod.namespace=kube-system
Bước 5 — Giao gói: cùng node hay khác node
Sau DNAT + policy "cho qua", datapath giao gói tới backend:
- Cùng node: redirect thẳng từ veth nguồn sang veth pod đích (
tail_ipv4_to_endpoint), không lên network stack — nhanh nhất. - Khác node: đẩy ra
cil_to_netdev→ens5(sau lớp XDP của Bài 11) → tới node đích, nơicil_from_netdevnhận và chạy lại chuỗi tail call để giao vào veth pod CoreDNS.
Bước 6 — Quan sát: cilium_events
Trên đường đi, datapath gọi bpf_perf_event_output đẩy sự kiện vào perf ring cilium_events (Bài 19). cilium-agent đọc ra, Hubble giàu hóa identity số thành tên. Cả vòng đời kết nối hiện ra như một flow đọc được — chính là dòng ta đã thấy ở Bài 19:
10.200.0.64:54764 (host) -> kube-system/coredns-…:8080 (ID:18203) to-endpoint FORWARDED (SYN)
<- kube-system/coredns-…:8080 (ID:18203) to-stack FORWARDED (SYN,ACK)
-> kube-system/coredns-…:8080 (ID:18203) to-endpoint FORWARDED (ACK)
... (ACK,FIN)
Cùng một cỗ máy, nhiều vai
Điều đáng dừng lại: mọi bước trên đều là eBPF, cùng một công nghệ ta học từ Bài 1. Cân bằng tải, NAT, theo dõi kết nối, áp policy bảo mật, sinh sự kiện quan sát — không phải năm hệ thống riêng, mà là các chương trình eBPF gắn ở các hook khác nhau, chia sẻ trạng thái qua BPF map, nối nhau bằng tail call, tất cả trong nhân. Đó là lý do Cilium thay thế được cả một chồng công cụ cũ (kube-proxy + iptables + một agent quan sát + một agent policy) bằng một datapath thống nhất. 74 chương trình sched_cls và 56 map trên node (Bài 0) không phải mớ hỗn độn — chúng là cỗ máy này, mổ ra từng mảnh suốt cả series.
🧹 Dọn dẹp
Bài tổng hợp, chỉ tham chiếu trạng thái và dữ liệu đã thu ở các bài trước — không chạy gì mới, không có gì để dọn. Sơ đồ + tham chiếu lệnh ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 20-case-study.
Tổng kết
Một gói từ pod gọi Service DNS đi qua một chuỗi eBPF thống nhất, ghép trọn series: rời pod tại cil_from_container (tc/sched_cls, Bài 12-13, đã qua verifier Bài 2 + JIT Bài 1); tail call (Bài 4) qua cilium_call_policy sang tail_handle_ipv4→tail_ipv4_ct_ingress→cil_lxc_policy; cân bằng tải bằng tra cilium_lb4_services (BPF map, Bài 3/12) + DNAT 10.32.0.10:53→10.200.0.44:53 (kube-proxy-less); ghi conntrack cilium_ct4_global và áp policy theo identity 18203=CoreDNS (Bài 12/19); giao qua redirect veth (cùng node) hoặc cil_to_netdev→ens5→node đích; và phát sự kiện qua bpf_perf_event_output→cilium_events→Hubble (Bài 19). Tất cả là eBPF — một công nghệ, nhiều hook, chia sẻ map, nối bằng tail call — nên Cilium gộp được LB + NAT + conntrack + policy + observability vào một datapath trong nhân.
Bài 21 (bài cuối) đóng series bằng một việc thực hành đúng tinh thần "từ số không": tự viết một công cụ quan sát hoàn chỉnh — connmon, monitor kết nối TCP toàn node theo thời gian thực bằng kprobe + ring buffer + loader Go — rồi nhìn lại toàn bộ hành trình.