TL;DR — eBPF lets you safely run sandboxed kernel code to trace function entry/exit with microsecond-scale overhead, enabling real-time latency breakdowns and hotpath profiling without perturbing production workloads. Combined with map-driven data aggregation and userspace readers, eBPF transforms the kernel into a programmable observability platform, revealing bottlenecks from syscall latency to lock contention across diverse architectures. When paired with tools like bpftrace or Cilium, teams can instrument production services at granularity that traditional perf or strace simply cannot match.

eBPF has become the de facto standard for high‑performance tracing in modern Linux environments. Where traditional tools like strace or perf introduce measurable overhead and require privileged contexts, eBPF programs attach safely at kernel entry points, execute compiled bytecode verified by the BPF verifier, and exit without compromising system stability. This capability has unlocked a new class of observability patterns: per‑function latency heatmaps, kernel‑space deadlock detection, and real‑time traffic‑aware routing decisions. In this post, we walk through the mechanics of tracing kernel functions with eBPF, the architecture of a typical tracing pipeline, and production‑grade patterns for safely instrumenting running services.

Why eBPF Matters for Performance Engineering

Classical profiling tools rely on sampling interrupt vectors or user‑space syscall wrappers. perf can attribute cycles to functions, but it does so at the cost of enabling kernel preemption and, in some configurations, perturbing the very workloads you’re trying to optimize. strace intercepts every system call, which quickly becomes infeasible for high‑throughput services.

eBPF changes the equation by offering three properties:

  1. Safety – The BPF verifier proves at load time that the program won’t crash the kernel, access out‑of‑bounds memory, or loop forever.
  2. Performance – Attach points such as kprobes, uprobes, and tracepoints incur sub‑microsecond overhead when the program is a no‑op.
  3. Programmability – You write the tracing logic once (in C, Rust, or Python via bpftrace) and the kernel executes it in‑place.

For performance engineers, this means you can pinpoint a hot function, capture argument values, and emit metrics—all without restarting the process or deploying a separate agent. The result is a feedback loop that spans from “something looks slow” to “here’s the exact line and kernel path eating cycles,” often within minutes.

Core eBPF Tracing Mechanics

Tracing a kernel function with eBPF involves a well‑defined flow: attachment, execution, data egress, and consumption. Let’s unpack each stage.

Program Types: kprobe, uprobe, tracepoints

  • kprobes attach to any kernel address. You can set a probe at the beginning (kprobe) or return path (kretprobe) of a function. This is the most generic mechanism and works for any symbol the kernel exports.
  • uprobes operate similarly but target user‑space functions. Useful when you want to correlate a kernel action with the argument that triggered it.
  • tracepoints are statically defined in the kernel (e.g., sched:sched_switch, block:request_complete). They follow a fixed format and are often more efficient than kprobes because the probe point already exists; you merely register a handler.

In practice, a performance analysis workflow might start with a kprobe on do_fork to capture process‑creation latency, then switch to a tracepoint for tcp:tcp_connect when investigating network‑bound services.

Map Types and Data Flow

Maps are the primary mechanism for eBPF programs to persist and share data with userspace. Choosing the right map type depends on your access pattern:

Map TypeTypical Use Case
BPF_MAP_TYPE_PERF_EVENT_ARRAYReal‑time streaming to userspace via perf buffers
BPF_MAP_TYPE_HASHKey‑value stores for counting, aggregation
BPF_MAP_TYPE_ARRAYFixed‑size indexed data, e.g., per‑CPU counters
BPF_MAP_TYPE_DEDUPRemoving duplicate entries (Cilium usecase)

A common pattern is the hash map keyed by function address, where each kprobe increments a counter or records a timestamp. A periodic userspace reader then aggregates the data into a latency distribution. Because maps are per‑CPU or global, you can design for scalability: a BPF_MAP_TYPE_PERCPU_HASH eliminates lock contention when many CPUs are tracing the same function.

Architecture of a Typical eBPF Tracing Pipeline

When you load an eBPF program, the kernel verifies it, attaches it to the chosen probe, and on every hit, executes the bytecode. The program’s output typically flows through one of three paths:

  1. Perf‑buffer streaming – The program writes to a perf buffer, and a userspace library (libbpf‑tools, bpftrace) reads events in real time. This is ideal for latency‑sensitive monitoring where you need to react within milliseconds.
  2. Periodic userspace polling – The eBPF program fills a ring buffer or hash map; a userspace process reads the data every few seconds, aggregates it, and emits metrics to Prometheus or Grafana.
  3. Kernel‑internal consumption – The program updates internal kernel data structures (e.g., cgroup stats, bpf_lsm hooks). This path is less about end‑user observability and more about enforcing policies or augmenting kernel subsystems.

A production‑ready pipeline often layers multiple maps: a per‑CPU hash for hot‑path counters, a perf buffer for alert‑triggering events, and a BPF_MAP_TYPE_DEDUP to suppress noisy duplicate entries.

Production Tracing Patterns

Understanding the mechanics is table stakes; the real value comes from applying patterns that balance observability with overhead control.

Selective Tracing and Overhead Control

Tracing every function in a production binary is rarely advisable. The most effective approach is probabilistic sampling or function‑level filtering. With bpftrace, you can write:

#!/usr/bin/bpftrace
kprobe:do_fork { 
    @forks[comm] = timestamp;
}
kretprobe:do_fork {
    @forks[comm] = nsecs(@forks[comm]);
}
profile:cpu-99 { ... }

The profile probe samples the CPU at a fixed rate (99 Hz here) and can be combined with speculative execution to minimize impact. In C‑based eBPF, you often guard probes with a runtime flag or a per‑CPU counter so that tracing activates only during a debugging window.

Another technique is argument filtering: instead of capturing the full stack, you capture only the fields you need. For instance, tracing kmalloc can be limited to recording the size argument and the returning pointer, reducing the amount of data written to maps and lowering the chance of verifier rejection.

Cross‑Subsystem Correlation

Modern services are rarely bottlenecked by a single subsystem. An eBPF tracing pipeline can correlate kernel‑space events with userspace metrics. A practical pattern:

  1. eBPF program fires on sys_enter_read, extracts the file descriptor and byte count, and updates a hash map keyed by pid.
  2. Userspace reads the map every second and joins the data with application‑level request traces (e.g., from OpenTelemetry).
  3. The combined view surfaces that “read latency spikes correlate with a specific cache‑miss pattern in the application heap,” guiding a code‑level fix rather than a kernel‑tuning exercise.

Tools like Cilium exemplify this pattern: their eBPF agents instrument both kernel networking paths and expose metrics to a central observability stack, enabling “why is this pod slow?” dashboards that drill from a TCP retransmission event all the way to the application’s database query.

Key Takeaways

  • eBPF provides a safe, low‑overhead mechanism to trace kernel functions and correlate events across subsystems.
  • Choosing the right program type (kprobe/uprobe/tracepoint) and map type (hash/perf array/percpu) dictates both the granularity and scalability of your tracing solution.
  • Production deployments should employ selective tracing, probabilistic sampling, and argument filtering to keep overhead bounded.
  • A well‑designed pipeline layers perf‑buffer streaming for real‑time alerts with periodic map polling for long‑term capacity planning.
  • Cross‑subsystem correlation—joining kernel eBPF data with application metrics—is where eBPF delivers its greatest operational value.

Further Reading