TL;DR
- ebpf_exporter là Prometheus exporter expose kernel-level metrics qua eBPF program. Không có sidecar, không có user-space sampling — observe trực tiếp trong kernel với overhead < 1% CPU.
- Cloudflare đang chạy ebpf_exporter trên hàng ngàn host trong production (theo blog post 2018 khi launch và follow-up 2024 khi maintain).
- Use case “kill app”: TCP retransmit per upstream IP (debug network issue), block I/O latency theo device (slow disk attribution), syscall latency theo PID/cgroup (detect noisy neighbor trong K8s).
- So với CloudWatch Container Insights ($1.5/GB ingest + agent overhead) và Datadog (agent + kernel module $$$): ebpf_exporter là một binary Go + một YAML config + một file
.bpf.c. Cost = compute trên host. Scale linear.- Trade-off: kernel version compatibility. eBPF feature set khác nhau giữa kernel 4.14 (CO-RE chưa stable) → 5.4 (production-ready) → 6.x (full feature). Pin kernel version trong AMI/image build.
- Configuration là YAML schema + libbpf-compiled BPF program. v2 dùng
libbpfthay vìbcc— không cần kernel header trên host, chạy được trong container minimal.- Đừng dùng eBPF cho metric mà metric đơn giản hơn solve được. CPU usage, memory, disk space → node_exporter. eBPF khi bạn cần kernel insight node_exporter không cover.
Tại sao eBPF, không phải agent thường
Câu hỏi đầu tiên: “Sao không xài node_exporter + cAdvisor cho mọi thứ?” Lý do — chúng đo cái mà Linux đã expose qua /proc, /sys. Đó là counter accumulated, không phải distribution.
Ví dụ: /proc/diskstats cho biết tổng số read I/O và tổng thời gian, không cho biết “p99 latency của read I/O trong 1 phút qua là bao nhiêu”. Một workload có 1000 read 1ms + 1 read 5 giây có cùng “average 6ms” với 1000 read 6ms cố định — nhưng latency profile rất khác.
eBPF observe ở entry/exit của kernel function. Cho mỗi I/O complete, BPF program đo latency, push vào histogram bucket. Prometheus scrape histogram → Grafana plot percentile thật:
biolatency_seconds_bucket{device="nvme0n1",le="0.001"} 9823
biolatency_seconds_bucket{device="nvme0n1",le="0.01"} 9991
biolatency_seconds_bucket{device="nvme0n1",le="0.1"} 9998
biolatency_seconds_bucket{device="nvme0n1",le="1"} 9999
biolatency_seconds_bucket{device="nvme0n1",le="+Inf"} 10000
p99 = 100ms ngay từ histogram. Không cần sampling, không cần aggregation client-side.
Architecture: BPF program + YAML config + Go binary
ebpf_exporter có 3 phần:
┌──────────────────────────────────────────────────────┐
│ User space │
│ ┌───────────────┐ HTTP /metrics ┌────────────┐ │
│ │ ebpf_exporter │ ──────────────► │ Prometheus │ │
│ └───────┬───────┘ └────────────┘ │
│ │ libbpf load │
│ │ │
│ ┌───────▼───────┐ │
│ │ BPF maps │ ◄─── kernel writes │
│ │ (histogram) │ │
│ └───────▲───────┘ │
└──────────┼───────────────────────────────────────────┘
│ BPF helper
│
┌──────────┴───────────────────────────────────────────┐
│ Kernel space │
│ ┌─────────────────────────────────────────────────┐ │
│ │ BPF program (verified, JIT compiled) │ │
│ │ Attach: kprobe/tracepoint/perf_event/uprobe │ │
│ │ Trigger: kernel event (I/O complete, syscall…) │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Workflow:
- Compile BPF program (
.bpf.c) với clang →.bpf.o(BPF bytecode) - ebpf_exporter load
.bpf.oqua libbpf, attach vào kernel hook - Kernel chạy program mỗi khi event fire, BPF program update map (histogram bucket counter)
- ebpf_exporter periodically read map, expose qua
/metricsHTTP - Prometheus scrape
Mỗi config (biolatency.yaml, tcp_retransmits.yaml) khai báo: BPF program file path, map name → metric name mapping, label decoder. Đó là tất cả config — không có sidecar, không có service mesh integration.
Example: TCP retransmit per remote IP
Use case thực tế: app team than “request lúc nhanh lúc chậm, không reproduce”. CloudWatch metrics cho ALB không cho biết. eBPF có thể đo TCP retransmit ở kernel để xác định network issue.
tcp_retransmits.yaml:
metrics:
counters:
- name: tcp_retransmits_total
help: Total TCP segments retransmitted
labels:
- name: saddr
size: 16
decoders:
- name: inet_ip
- name: daddr
size: 16
decoders:
- name: inet_ip
- name: dport
size: 2
decoders:
- name: uint
BPF program (tcp_retransmits.bpf.c, simplified):
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
struct key_t {
__u8 saddr[16];
__u8 daddr[16];
__u16 dport;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct key_t);
__type(value, __u64);
__uint(max_entries, 10240);
} tcp_retransmits_total SEC(".maps");
SEC("tracepoint/tcp/tcp_retransmit_skb")
int tcp_retransmit(struct trace_event_raw_tcp_event_sk_skb *ctx) {
struct key_t key = {};
struct sock *sk = (struct sock *)ctx->skaddr;
// Read addresses from socket struct
bpf_core_read(&key.saddr, 16, &sk->__sk_common.skc_v6_rcv_saddr);
bpf_core_read(&key.daddr, 16, &sk->__sk_common.skc_v6_daddr);
bpf_core_read(&key.dport, 2, &sk->__sk_common.skc_dport);
__u64 init = 1;
__u64 *count = bpf_map_lookup_elem(&tcp_retransmits_total, &key);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
bpf_map_update_elem(&tcp_retransmits_total, &key, &init, BPF_NOEXIST);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
Output Prometheus:
tcp_retransmits_total{saddr="10.0.1.5",daddr="10.0.2.10",dport="443"} 1247
tcp_retransmits_total{saddr="10.0.1.5",daddr="10.0.2.11",dport="443"} 8
tcp_retransmits_total{saddr="10.0.1.5",daddr="172.20.5.3",dport="3306"} 5
Grafana query: topk(10, rate(tcp_retransmits_total[5m])) cho biết upstream IP nào đang network issue. Hôm trước tôi debug câu hỏi “tại sao p99 latency RDS Aurora reader cao bất thường” — top retransmit pointed thẳng một IP reader instance. AWS support confirmed AZ network issue ở instance đó.
Example: Block I/O latency histogram
Use case: detect slow disk. node_exporter cho node_disk_io_time_seconds_total nhưng đó là total — không cho distribution.
biolatency.yaml:
metrics:
histograms:
- name: bio_latency_seconds
help: Block I/O latency distribution
bucket_type: exp2
bucket_min: 0
bucket_max: 26 # 2^26 ns = ~67s, đủ cover all
bucket_multiplier: 1e-9
labels:
- name: device
size: 32
decoders:
- name: string
- name: operation
size: 1
decoders:
- name: uint
- name: static_map
static_map:
0: read
1: write
BPF program đo từ block_rq_issue đến block_rq_complete:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, __u64); // request pointer
__type(value, __u64); // start time
__uint(max_entries, 10240);
} start_time SEC(".maps");
SEC("tracepoint/block/block_rq_issue")
int block_issue(struct trace_event_raw_block_rq *ctx) {
__u64 rq = (__u64)ctx->rwbs; // simplified
__u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start_time, &rq, &ts, BPF_ANY);
return 0;
}
SEC("tracepoint/block/block_rq_complete")
int block_complete(struct trace_event_raw_block_rq_completion *ctx) {
__u64 rq = (__u64)ctx->rwbs;
__u64 *tsp = bpf_map_lookup_elem(&start_time, &rq);
if (!tsp) return 0;
__u64 latency_ns = bpf_ktime_get_ns() - *tsp;
// Increment histogram bucket - log2(latency_ns)
__u64 slot = log2l(latency_ns);
// ... update bio_latency_seconds histogram
bpf_map_delete_elem(&start_time, &rq);
return 0;
}
Histogram cho histogram_quantile(0.99, rate(bio_latency_seconds_bucket[1m])). p99 > 50ms trên SSD = nghi vấn drive failing. Alert before crash, không sau.
Khi nào dùng ebpf_exporter
Quy tắc cá nhân:
| Metric cần | Tool |
|---|---|
| CPU/memory/disk usage | node_exporter |
| Container resource (CPU/mem/PID) | cAdvisor |
| HTTP/gRPC request rate, latency | App-level Prometheus client lib |
| TCP retransmit, listen drop, accept queue | ebpf_exporter |
| Block I/O latency distribution | ebpf_exporter |
| Syscall latency per cgroup/PID | ebpf_exporter |
| File system slow path attribution | ebpf_exporter |
| Network policy deny attribution (Cilium) | ebpf_exporter hoặc Cilium Hubble |
| Process exec/exit audit | ebpf_exporter (hoặc auditd) |
eBPF không thay node_exporter — bổ sung. Trên một host production, tôi chạy cả hai: node_exporter cho baseline + ebpf_exporter cho 5-10 metric đặc thù.
So sánh CloudWatch và Datadog
CloudWatch Container Insights:
- Agent (
cloudwatch-agenthoặc ADOT) chạy DaemonSet - Cost: $1.5/GB metric ingestion + $0.30/metric/tháng (custom metric)
- Granularity: 1 phút default, 1 giây paid premium
- Kernel insight: không sâu (cao nhất là
container_cpu_*,container_memory_*) - Latency: 1-3 phút delay khi metric appear
ebpf_exporter:
- Một binary trên host, scrape qua Prometheus
- Cost: chỉ compute (~10MB RAM, < 1% CPU)
- Granularity: scrape interval của Prometheus (15s default)
- Kernel insight: bất kỳ kprobe/tracepoint nào kernel cho
- Latency: scrape interval
Cluster 50 node, mỗi node 100 container, mỗi container 50 metric, scrape 1 phút = 50 × 100 × 50 × 60 × 24 × 30 = ~10.8 tỉ datapoint/tháng. CloudWatch ingest cost ~$1.5/GB × estimate 10GB = $15… nhưng đó là baseline. Custom metric (process-level, container-level deep) sẽ blow up cost vì $0.30/metric/tháng cộng dồn.
Datadog:
- Agent với kernel module hoặc eBPF program (Datadog cũng dùng eBPF)
- Cost: $15-$23/host/tháng tier base, thêm cho APM/logs
- 50 host = $750-$1150/tháng chỉ infra metric
ebpf_exporter self-host trên Prometheus:
- Prometheus storage local: ~1GB/tháng/host typical retention 14 ngày
- 50 host = 50GB Prometheus = 1 instance
m5.large($70/tháng) + EBS $5 - Tổng < $100/tháng cho 50 host vs Datadog $1000
Datadog có UI/alerting/correlation built-in — saving không free về effort. Pattern Cloudflare là Prometheus + Grafana + Alertmanager self-host. Saving khi đủ scale + có SRE capacity.
libbpf vs bcc — v2 vs legacy
ebpf_exporter v2 (2023+) dùng libbpf. Trước v2 dùng bcc — yêu cầu kernel header trên host để JIT compile BPF program tại runtime. Container minimal (distroless, Alpine) không có header → fail.
libbpf approach: compile BPF program ahead-of-time với CO-RE (Compile Once - Run Everywhere) trên build machine. Output là .bpf.o binary portable across kernel version >= 5.4. Khi deploy, chỉ cần copy .bpf.o + binary Go + YAML config. Container 50MB total.
Khuyến nghị: dùng v2 (releases/v2.x trên GitHub). Nếu kernel < 5.4 thì stuck với bcc — cân nhắc nâng cấp kernel hơn là maintain bcc setup.
Kernel version compatibility — table
| Kernel | Status | Note |
|---|---|---|
| < 4.14 | Don’t | eBPF infancy, không production-ready |
| 4.14 - 5.3 | Limited | CO-RE chưa stable, cần bcc + kernel header |
| 5.4 LTS | OK | Baseline cho production, minimal CO-RE support |
| 5.10 LTS | Good | Stable CO-RE, BTF widely available |
| 5.15 LTS | Good | Default Ubuntu 22.04 |
| 6.1 LTS | Best | Full feature set, kfunc support |
| 6.x | Latest | Cutting edge, một số tracepoint mới |
Production khuyến nghị: 5.15 LTS hoặc 6.1 LTS. Amazon Linux 2023 ships 6.1 LTS — không phải lý do migrate khỏi AL2, nhưng plus point.
Check BTF availability:
ls -la /sys/kernel/btf/vmlinux
# Có file = CO-RE work out of box
Nếu không có BTF, ebpf_exporter cần bundle BTF blob từ build machine hoặc fallback bcc.
Deployment trên Kubernetes
DaemonSet pattern (mỗi node 1 pod):
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ebpf-exporter
namespace: monitoring
spec:
selector:
matchLabels: { app: ebpf-exporter }
template:
metadata:
labels: { app: ebpf-exporter }
spec:
hostNetwork: true
hostPID: true
containers:
- name: ebpf-exporter
image: ghcr.io/cloudflare/ebpf_exporter:v2.4.0
args:
- --config.dir=/etc/ebpf_exporter
- --config.names=biolatency,tcp_retransmits,oomkill
ports:
- containerPort: 9435
hostPort: 9435
name: metrics
securityContext:
privileged: true # cần cho BPF load + attach
capabilities:
add: ["SYS_ADMIN", "SYS_RESOURCE", "BPF", "PERFMON"]
volumeMounts:
- name: config
mountPath: /etc/ebpf_exporter
- name: sys
mountPath: /sys
readOnly: true
- name: debugfs
mountPath: /sys/kernel/debug
readOnly: true
volumes:
- name: config
configMap: { name: ebpf-exporter-config }
- name: sys
hostPath: { path: /sys }
- name: debugfs
hostPath: { path: /sys/kernel/debug }
Lưu ý security: privileged: true + hostPID. Đó là requirement để load BPF program và xem PID toàn host. Pod này phải treat như infra-level — admission policy giới hạn chỉ namespace monitoring.
Prometheus scrape:
- job_name: ebpf-exporter
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: ebpf-exporter
action: keep
Operational trade-off
Pro:
- Visibility kernel-level mà không có solution khác cover
- Cost predictable (compute only, không per-metric pricing)
- Custom — viết BPF program cho metric riêng (đo function trong binary của bạn qua uprobe)
- Performance overhead < 1% CPU thực tế (Cloudflare benchmark)
Con:
- Cần kernel knowledge để debug BPF program (verifier error rất cryptic)
- Privileged container — security boundary giảm
- Kernel version pinning — không rolling kernel ad-hoc
- BPF program crash kernel thì khó hơn debug user-space (rare nhưng có)
Tôi maintain 3 BPF program custom cho stack (đo Postgres backend latency qua uprobe), tổng effort ~2 ngày setup + 0.5 ngày/quý maintain. Đáng — replace bằng commercial APM tốn $5k/tháng.
Bottom line
ebpf_exporter là tool cho team đã có Prometheus stack và cần insight kernel-level mà node_exporter không cover. Đừng deploy nó như default — deploy khi có câu hỏi cụ thể (debug TCP retransmit, đo bio latency, monitor syscall). Cloudflare chạy ở hàng ngàn host vì họ là Cloudflare — bạn không cần chạy 100 BPF program mỗi node. 3-5 metric đúng đắn cho stack của bạn là điểm sweet. Kernel ≥ 5.4, libbpf v2, DaemonSet privileged, scrape qua Prometheus — đó là blueprint.
Checklist trước production
- Kernel version >= 5.4 confirmed trên tất cả node
- BTF available (
/sys/kernel/btf/vmlinuxexist) hoặc fallback bundle BTF blob - BPF program compile với CO-RE flags, test trên kernel target version
- DaemonSet
privileged: true+ namespace isolated (admission policy bảo vệ) - Resource limit: CPU 100m, memory 100Mi mỗi pod (đủ cho 5-10 metric)
- Map size (
max_entries) đủ — quá nhỏ thì miss event, quá lớn thì waste memory - Scrape interval 30s default (không quá tải Prometheus)
- Cardinality control: label
daddrper IP có thể blow up Prometheus series. Limit hoặc aggregate - Alert rule cho
up{job="ebpf-exporter"} == 0(pod down trên node nào) - Grafana dashboard cho top metric: TCP retransmit, bio latency, syscall slow path
- Test BPF program không miss event dưới load (compare với strace/tcpdump baseline)
- DR plan: rollback BPF program version nếu verifier reject sau kernel upgrade
- Document mỗi config: why, who own, on-call runbook
Cạm bẫy thường gặp
1. Cardinality explosion. Metric tcp_retransmits_total{saddr,daddr,dport} với 10000 IP unique = 10000+ series. Prometheus storage blow up. Hoặc aggregate trước trong BPF (hash IP vào subnet), hoặc dùng recording rule.
2. BPF verifier reject loop. BPF program có loop unbounded → verifier reject. Phải dùng bpf_loop() helper (kernel >= 5.17) hoặc unroll manually.
3. Map full silent drop. Khi map đầy (max_entries reached), bpf_map_update_elem return -E2BIG — nếu không check return code, event miss silently. Set map size buffer 2-3× expected.
4. Tracepoint vs kprobe stability. Tracepoint là stable kernel ABI. Kprobe attach vào internal function — function rename giữa kernel version = program break. Prefer tracepoint khi có.
5. Privileged container security risk. privileged: true + hostPID = pod có thể read /proc của mọi process. Admission policy + RBAC giới hạn ai deploy ebpf_exporter.
6. Kernel upgrade rolling break BPF program. Function signature thay đổi giữa kernel version → CO-RE relocation fail. Test BPF program với kernel mới TRƯỚC khi rolling upgrade.
7. Quên fix LICENSE GPL trong BPF. Kernel reject BPF program không có GPL license (cho một số helper). char LICENSE[] SEC("license") = "GPL"; ở cuối file.