skip to content

You own the eBPF-based tracing tools for a fleet of Linux servers spanning several kernel versions and distributions. How would you decide between shipping bcc-based tools and libbpf CO-RE binaries, and what would you standardise in the build and distribution pipeline?

level: principalimportance: should knowfreq 26%

answer

  1. what each model installs on production
  2. baseline first, exceptions counted
  3. builds once, loads many times
  4. a build gate is not a load gate
  5. exploration versus shipped software

basics

~20 s

Standardise on libbpf CO-RE binaries for anything shipped, because they need no compiler or headers on production hosts, and keep bcc for ad-hoc work on lab machines. Then fix a minimum kernel and BTF baseline, a pinned build toolchain, and a load-test matrix covering every kernel in the fleet.

solid answer

~60 s

The decision follows from what each model demands of a production host. bcc needs LLVM and matching kernel headers on every box and pays seconds of startup and hundreds of megabytes to compile; that is unacceptable as a fleet-wide dependency, but perfectly fine for exploration on a lab machine. So the default is libbpf CO-RE: compile once, ship one static binary, no toolchain on target. What that buys must be paid for with discipline elsewhere. I would declare a minimum kernel version and a hard requirement that hosts expose BTF, inventory the exceptions, and decide explicitly whether legacy hosts get externally supplied BTF or are simply out of scope. I would pin the clang and libbpf versions in a container image so builds are reproducible, and add a CI matrix that actually loads each binary on every kernel version in the fleet — because verifier acceptance and available hooks vary by kernel, and a binary that builds is not a binary that loads. Distribution is then an ordinary signed-package problem, which is a large part of the appeal.

go deeper

for a junior

Know that the same eBPF tool can be built two ways, and that one needs a compiler on the machine where it runs while the other does not.

for a middle

Articulate the concrete dependencies each model imposes — LLVM plus kernel headers versus kernel BTF — and why that alone usually settles the choice for software you distribute.

for a senior

Design the pipeline: pinned build container, generated skeletons, static linking, and a per-kernel load test, plus a plan for hosts that lack BTF instead of discovering them in production.

for a principal

Own the whole position — the baseline you declare, the exceptions you fund or decline, the overhead and memory budget tools must meet, attribution and a fleet-wide kill switch, and the line between exploration and shipped software.

## Frame the decision as a fleet dependency, not a preference The question is not which toolchain is nicer to write. It is what each one obliges you to install and maintain on every production host, forever. bcc puts a compiler in production. That means LLVM and Clang packages, kernel headers that must track every kernel upgrade, seconds of startup latency, and a large transient memory footprint on machines that are already sized for their application. It also means the failure mode during an incident is a compile error. As a fleet-wide standard that is a bad trade. libbpf CO-RE moves all of that to build time. The artifact is a single binary; the host needs only a kernel that exposes BTF. Startup is milliseconds, the memory cost is the program and its maps, and distribution is the same signed-package pipeline you already run for everything else. So the default is clear. The interesting work is in the constraints that choice creates. ## Declare a baseline and inventory the exceptions Write down the minimum kernel version and the requirement that `/sys/kernel/btf/vmlinux` exists. Then measure it, rather than assuming — a one-line check across the fleet tells you what fraction is actually eligible: ```bash test -e /sys/kernel/btf/vmlinux && echo ok || echo no-btf ``` For hosts that fail, there are three honest positions and you must pick one per host class: upgrade the kernel; supply an externally generated BTF for that exact build and load it via `btf_custom_path`; or declare the host out of scope for shipped tooling and handle it by hand. What you must not do is leave it undecided, because the discovery then happens during an incident. ## Standardise the build - **Pin the toolchain.** Clang version, libbpf version and bpftool version live in a build container image, not on developers' machines. eBPF codegen is version-sensitive and "works on my laptop" is a real failure here. - **Generate, do not commit.** `vmlinux.h` and the skeleton headers are build artifacts, regenerated every build. - **Link statically where you can.** The point of CO-RE is one artifact; a binary that needs a particular libbpf on the host has given some of that back. - **One repository, one release train.** Tools should ship as versioned packages with the same signing and rollout controls as any other production software — including the ability to roll back. ## Test where it actually runs This is the part teams skip. A CO-RE binary compiles once, but its *acceptance* depends on the target kernel: which hooks exist, which helpers are available, and what the kernel's verifier will pass. A build-only CI gate proves nothing. So the pipeline should boot a VM per kernel version present in the fleet and, for each, load and attach the binary and assert it produces output. `bpftool feature probe` on each image gives you a machine-readable capability inventory to compare against. When a hook is missing on an older kernel, handle it in code — disable that program before load with `bpf_program__set_autoload(..., false)` and degrade the tool's output — rather than shipping a binary that fails to load on a third of the estate. ## Operate what you ship - **Attribution.** Agree a pin naming convention under `/sys/fs/bpf` so an engineer who finds an unfamiliar program with `bpftool prog show` can trace it to an owner in seconds. - **Resource budget.** Map `memlock` is real kernel memory and a badly sized `max_entries` is charged whether or not the map is populated. Give tools a budget and check it in review. - **Overhead honesty.** A tracing program runs in a hot kernel path. Measure the cost of the hooks you ship on a representative workload before fleet rollout, not after. - **A kill switch.** Because pinned or hook-attached programs outlive their loader, define how a tool is disabled fleet-wide without a reboot. ## Where bcc keeps its place None of this makes bcc worthless. On a lab box, or on a single host during a deep investigation where a toolchain is already present, the iteration speed of editing a Python file and rerunning is genuinely faster than a compile-and-deploy loop. The rule I would state is: exploration may use anything; anything that lands on a production host is a CO-RE binary from the release pipeline. That keeps the fast path fast without letting a compiler leak into the fleet.

  • A binary builds cleanly but fails to load on a third of the fleet. What would you have caught that in CI, and how?
    A load gate, not a build gate. Boot a VM for each kernel version in the fleet, load and attach the binary, and assert it emits output; combine that with a `bpftool feature probe` inventory per image so missing hooks and helpers are visible as data. Where a hook is genuinely absent, disable that program before load and degrade output rather than failing the whole tool.
  • How would you decide whether legacy hosts without kernel BTF are worth supporting?
    Count them and price both sides. Supporting them means generating and distributing per-build BTF and keeping that matched to every kernel respin — real ongoing work. If they are a handful of machines heading for decommission, exclude them and handle incidents by hand. If they are a large or long-lived class, either fund the external-BTF path properly or make the kernel upgrade the project instead.
  • What guardrails would you put on the kernel-side cost of tools you ship fleet-wide?
    A memory budget checked in review, since map `memlock` is charged for `max_entries` up front, and a measured overhead number on a representative workload before rollout — a probe on a hot path is not free. Plus a documented way to disable a tool everywhere without a reboot, because pinned or hook-attached programs outlive the process that loaded them.

saying these in an interview costs you the question

  • Standardising on bcc because it is easier to write
  • Assuming a successful build implies the binary will load
  • Leaving BTF-less hosts as an undecided exception
  • Treating clang and libbpf versions as developer-local details
  • Ignoring map memlock when sizing tools for a whole fleet

context