eBPF metrics cho production — Cloudflare's ebpf_exporter pattern

ebpf_exporter expose kernel metrics qua Prometheus, không cần sidecar. So sánh CloudWatch agent và Datadog kernel module. Pattern Cloudflare dùng cho hàng ngàn host.

· 8 phút đọc

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 libbpf thay 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:

  1. Compile BPF program (.bpf.c) với clang → .bpf.o (BPF bytecode)
  2. ebpf_exporter load .bpf.o qua libbpf, attach vào kernel hook
  3. Kernel chạy program mỗi khi event fire, BPF program update map (histogram bucket counter)
  4. ebpf_exporter periodically read map, expose qua /metrics HTTP
  5. 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ầnTool
CPU/memory/disk usagenode_exporter
Container resource (CPU/mem/PID)cAdvisor
HTTP/gRPC request rate, latencyApp-level Prometheus client lib
TCP retransmit, listen drop, accept queueebpf_exporter
Block I/O latency distributionebpf_exporter
Syscall latency per cgroup/PIDebpf_exporter
File system slow path attributionebpf_exporter
Network policy deny attribution (Cilium)ebpf_exporter hoặc Cilium Hubble
Process exec/exit auditebpf_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-agent hoặ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

KernelStatusNote
< 4.14Don’teBPF infancy, không production-ready
4.14 - 5.3LimitedCO-RE chưa stable, cần bcc + kernel header
5.4 LTSOKBaseline cho production, minimal CO-RE support
5.10 LTSGoodStable CO-RE, BTF widely available
5.15 LTSGoodDefault Ubuntu 22.04
6.1 LTSBestFull feature set, kfunc support
6.xLatestCutting 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/vmlinux exist) 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 daddr per 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.

Tham chiếu