eBPF for Observability
eBPF is one of the most significant additions to the Linux kernel in the past decade. It lets you run sandboxed programs inside the kernel itself — attaching them to virtually any system event — without modifying kernel source code, loading a kernel module, or rebooting. The result is observability that was previously impossible or required invasive instrumentation: you can trace every system call, follow a network packet from NIC to socket, profile CPU usage at function-call granularity, and measure I/O latency histograms — all in production, with near-zero overhead.
This guide covers the eBPF landscape from first principles through practical tools: what eBPF is and how it works, the BCC toolkit for quick investigations, bpftrace for scriptable tracing, and Cilium for Kubernetes network observability.
What eBPF Is (and How It Works)
The name comes from “extended Berkeley Packet Filter” — the original BPF was a simple bytecode VM for filtering network packets in the kernel (used by tcpdump). Modern eBPF is a complete, general-purpose in-kernel execution environment with almost nothing in common with the original except the name.
Here’s how an eBPF program works:
- Write: You write an eBPF program in restricted C (or use a higher-level frontend like bpftrace).
- Compile: The compiler generates eBPF bytecode.
- Verify: The kernel’s verifier checks the bytecode — it proves the program terminates, doesn’t dereference bad pointers, doesn’t access unauthorized memory, and doesn’t call unsafe functions. If verification fails, the program is rejected.
- JIT compile: The kernel JIT-compiles verified bytecode to native machine code.
- Attach: The program is attached to a hook point — a kernel event like a system call, a network packet arriving, a kprobe on any kernel function, a tracepoint, or a user-space probe.
- Execute: Every time the hook fires, your program runs in kernel context. It can read kernel and user-space data, compute statistics, and write to eBPF maps.
- Communicate: eBPF maps are key-value stores shared between the eBPF program (kernel space) and a user-space controller. This is how metrics, traces, and events flow to your observability tooling.
The verifier is what makes eBPF safe. It’s not just “hope the program doesn’t crash the kernel” — it’s a formal proof that the program is safe to run. This is why eBPF can run production observability on live systems that can’t tolerate a kernel panic.
Hook Points
eBPF programs can attach to:
| Hook Type | What It Traces |
|---|---|
| kprobe / kretprobe | Any kernel function, entry and return |
| tracepoint | Stable kernel trace events (preferred over kprobes) |
| uprobe / uretprobe | Any user-space function in any binary |
| USDT | User-space statically defined tracepoints (in Go, Java, Python, etc.) |
| XDP | Network packets at driver level, before the kernel network stack |
| TC (Traffic Control) | Ingress/egress packets in the network stack |
| socket filter | Packets on a socket |
| LSM | Linux Security Module hooks (MAC policy enforcement) |
| perf events | CPU performance counters, sampling profiler |
| fentry / fexit | Fast alternatives to kprobes (BTF-based, lower overhead) |
The eBPF Tooling Ecosystem
There are several layers of tooling:
┌─────────────────────────────────────────┐
│ High-level tools (Cilium, Pixie, │
│ Parca, Pyroscope, Falco) │
├─────────────────────────────────────────┤
│ Scripting frontends │
│ (bpftrace, kubectl-trace) │
├─────────────────────────────────────────┤
│ Libraries (BCC, libbpf, cilium/ebpf) │
├─────────────────────────────────────────┤
│ eBPF bytecode / BPF CO-RE │
├─────────────────────────────────────────┤
│ Linux kernel eBPF subsystem │
└─────────────────────────────────────────┘
BCC (BPF Compiler Collection): A toolkit of pre-written eBPF tools for common tasks. Great for immediate investigation — tools like execsnoop, opensnoop, tcptracer, biolatency are single commands.
bpftrace: A high-level tracing language (think awk for eBPF). Write one-liners or short scripts to trace anything. The fastest path from “I need to understand what’s happening” to an answer.
libbpf / BPF CO-RE: A C library for writing eBPF programs that are portable across kernel versions. “CO-RE” (Compile Once – Run Everywhere) uses BTF (BPF Type Format) to relocate struct field offsets at load time rather than requiring kernel headers at compile time.
Cilium: A Kubernetes CNI plugin that uses eBPF for network policy enforcement, load balancing, and deep network observability. Hubble (Cilium’s observability layer) gives you per-flow visibility across your entire cluster.
Prerequisite: Kernel Version and BTF
Most modern eBPF features require a recent kernel. Check yours:
|
|
Minimum kernel versions for common features:
- kprobes: 4.1+
- tracepoints: 4.7+
- XDP: 4.8+
- BTF / CO-RE: 5.2+ (full support 5.8+)
- fentry/fexit: 5.5+
- LSM hooks: 5.7+
Ubuntu 22.04 (kernel 5.15) and later ship with everything you need.
BCC Tools: Instant Observability
Install the BCC toolkit:
|
|
execsnoop — Every New Process
|
|
Use this to audit what processes a container or service is spawning — useful for detecting unexpected subprocess execution.
opensnoop — Every File Open
|
|
biolatency — Block I/O Latency Histogram
|
|
This immediately shows whether your storage is the bottleneck — and where the tail latency lives.
tcptracer — TCP Connection Events
|
|
tcpconnect and tcpaccept
|
|
profile — CPU Flame Graph Data
|
|
The flame graph shows where CPU time is actually spent — much more accurate than top for finding bottlenecks in complex applications.
Other Useful BCC Tools
|
|
bpftrace: Scripting for Custom Tracing
bpftrace is a high-level language for one-off investigations. Its syntax is inspired by awk and DTrace.
|
|
One-Liners
|
|
bpftrace Scripts
For anything more complex, write a .bt script:
#!/usr/bin/env bpftrace
// File: slow-reads.bt
// Show read() calls slower than 1ms, with process and file descriptor
tracepoint:syscalls:sys_enter_read
{
@start[tid] = nsecs;
@fd[tid] = args->fd;
}
tracepoint:syscalls:sys_exit_read
/@start[tid]/
{
$latency_us = (nsecs - @start[tid]) / 1000;
if ($latency_us > 1000) {
printf("%-8d %-16s fd=%-4d latency=%d us\n",
pid, comm, @fd[tid], $latency_us);
}
delete(@start[tid]);
delete(@fd[tid]);
}
|
|
Another example — tracking memory allocations to find leaks:
#!/usr/bin/env bpftrace
// File: malloc-tracker.bt
// Track outstanding malloc calls in a specific process
// Usage: sudo bpftrace malloc-tracker.bt -p <PID>
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc
{
@allocs[tid] = nsecs;
@size[tid] = arg0;
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc
/@allocs[tid]/
{
@outstanding[retval] = @size[tid];
delete(@allocs[tid]);
delete(@size[tid]);
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:free
{
delete(@outstanding[arg0]);
}
interval:s:10
{
printf("\n=== Outstanding allocations (%d) ===\n",
count(@outstanding));
print(@outstanding);
}
Tracing User-Space Functions (uprobes)
bpftrace can trace any function in any binary:
|
|
Cilium: eBPF for Kubernetes Networking
Cilium is a Kubernetes CNI that replaces iptables-based networking with eBPF. Beyond the performance benefits, it provides deep observability via Hubble.
Installing Cilium on K3s
|
|
Hubble: Network Flow Visibility
Hubble gives you per-flow network visibility across all pods — who’s talking to whom, which connections are being dropped by policy, and latency between services.
|
|
Hubble UI
|
|
The Hubble UI shows a live service dependency graph: every service in your cluster, every connection, and whether flows are allowed or dropped. This alone is worth deploying Cilium — you’ll immediately see connections you didn’t know existed.
Network Policy with Cilium
Cilium extends Kubernetes NetworkPolicy with L7 (application-layer) awareness:
|
|
With standard Kubernetes NetworkPolicy you can only filter by IP and port. Cilium’s L7 policies filter by HTTP method, path, headers — and all enforcement decisions are visible in Hubble.
Practical Investigation Examples
Why Is My Service Slow?
|
|
What Is a Container Doing?
|
|
Detecting a Security Incident
|
|
Parca: Always-On CPU Profiling
Parca uses eBPF to continuously profile every process on a host — no instrumentation, no restarts, no overhead worth measuring.
|
|
Parca’s UI at http://localhost:7070 lets you query CPU profiles for any process over any time range. When you get a latency spike at 3 AM, you can look back and see exactly what code was running.
Key eBPF Concepts Summarized
Maps: Shared data structures between eBPF programs and user space. Types include hash maps, arrays, ring buffers, LRU maps, and per-CPU variants. Ring buffers (BPF_MAP_TYPE_RINGBUF) are the modern way to stream events to user space with minimal overhead.
BTF (BPF Type Format): Debug type information embedded in the kernel binary. Enables CO-RE — eBPF programs compiled on one machine can run on kernels with different struct layouts.
BPF CO-RE: Programs use bpf_core_read() macros instead of direct struct access. The loader applies relocations based on BTF, adjusting field offsets for the running kernel.
Verifier limits: eBPF programs have complexity limits — bounded loops only (since kernel 5.3, bounded loops are allowed), no unbounded recursion, limited stack depth (512 bytes). Very complex programs may be rejected.
Overhead: Well-written eBPF programs typically add < 1% CPU overhead. Poorly written ones (e.g., printing to user space in a hot path, complex maps in high-frequency hooks) can add more. Profile your eBPF programs the same way you’d profile any code.
eBPF has fundamentally changed what’s possible in production observability. Five years ago, getting a CPU flame graph required sampling with perf and hoping the stack unwinding worked. Getting per-flow network visibility required a service mesh. Tracking every file open by a container required strace with its prohibitive overhead.
Today, bpftrace one-liners answer questions that used to take hours, Cilium/Hubble gives you Kubernetes network visibility with no application changes, and tools like Parca profile your entire fleet continuously. The investment in learning eBPF pays off every time you open a bpftrace one-liner instead of staring at metrics trying to guess where the problem is.
Comments