<!-- Canonical: https://lucioduran.com/es/blog/ebpf-observability-production-kubernetes
Language: es
Content-SHA256: 9eada725f2cc0061e71a8b175ef82560778e2c74b9f9693270bd5f61e44665b5
-->

Infrastructure

# eBPF en Producción: Observabilidad a Nivel de Kernel en Kubernetes Sin el Overhead

2025-10-259 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/ebpf-observability-production-kubernetes>)

## Qué es eBPF: Visión Técnica

Eliminar el hype y eBPF es bastante directo: es una máquina virtual dentro del kernel de Linux que te permite correr programas sandboxeados en puntos de enganche específicos sin modificar el código fuente del kernel ni cargar módulos de kernel. El "extended" en eBPF (originalmente Berkeley Packet Filter) se refiere a que fue expandido mucho más allá del filtrado de paquetes hacia un framework de instrumentación de kernel de propósito general.

Las restricciones clave que lo hacen seguro:

```
1. Los programas deben pasar el verificador BPF (prueba terminación, seguridad de memoria)
2. Sin loops no acotados (el verificador debe probar que todos los loops terminan)
3. Tamaño de stack limitado (512 bytes)
4. Sin sleep ni blocking en la mayoría de tipos de programa
5. Las funciones helper son la única forma de interactuar con estado del kernel
```

Estas restricciones suenan limitantes, pero garantizan algo crucial: **un programa eBPF no puede crashear tu kernel**. Punto. El verificador rechaza cualquier cosa que pueda causar comportamiento indefinido. Esto es lo que hace viable a eBPF para producción, a diferencia de kprobes o scripts de SystemTap que pueden tirar abajo una máquina.

## El Stack de Observabilidad en 2026

A continuación se muestra cómo se ve un stack de observabilidad moderno basado en eBPF en Kubernetes de producción:

```
┌─────────────────────────────────────┐
│ Grafana / UI │
├─────────────────────────────────────┤
│ Prometheus / OpenTelemetry │
├──────────┬──────────┬───────────────┤
│ Hubble │ Tetragon │ Probes Custom │
├──────────┴──────────┴───────────────┤
│ Cilium (CNI + eBPF) │
├─────────────────────────────────────┤
│ Linux Kernel (5.10+) │
└─────────────────────────────────────┘
```

### Cilium: La Capa de Red

[Cilium](<https://cilium.io/>) reemplaza kube-proxy e iptables con programas eBPF enganchados a hooks TC (traffic control). En vez de recorrer una cadena de reglas iptables por cada paquete, Cilium hace un solo lookup en un mapa BPF. La diferencia de performance es dramática:

```
# kube-proxy basado en iptables (5,000 servicios):
# ~20ms por setup de conexión (recorrido de reglas)
# ~15% overhead de CPU en nodo proxy

# Cilium eBPF (5,000 servicios):
# ~0.2ms por setup de conexión (lookup en hash map)
# ~2% overhead de CPU en nodo proxy
```

Pero la verdadera ganancia no es el throughput crudo, es la observabilidad. El componente Hubble de Cilium te da visibilidad L3/L4/L7 en cada paquete fluyendo por el cluster, con cero instrumentación en tu código de aplicación.

```bash
# Ver todas las requests HTTP entre servicios en tiempo real
hubble observe --namespace production \
 --protocol http \
 --verdict FORWARDED \
 -o json | jq '{src: .source.labels, dst: .destination.labels, http: .l7.http}'
```

Esto está observando el flujo de paquetes real a nivel de kernel, sin sampling. Cada paquete individual. Y porque es eBPF, el overhead es medible en microsegundos por paquete.

### Tetragon: Observabilidad de Seguridad

[Tetragon](<https://tetragon.io/>) es donde las cosas se ponen realmente interesantes desde una perspectiva de monitoreo de seguridad. Engancha programas eBPF a funciones del kernel (kprobes, tracepoints, hooks LSM) y te da una interfaz declarativa YAML para definir qué se desea observar.

A continuación se presenta una TracingPolicy que monitorea accese es sospechose es a archivos:

```yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
 name: sensitive-file-access
spec:
 kprobes:
 - call: "security_file_open"
 syscall: false
 args:
 - index: 0
 type: "file"
 selectors:
 - matchArgs:
 - index: 0
 operator: "Prefix"
 values:
 - "/etc/shadow"
 - "/etc/passwd"
 - "/root/.ssh"
 - "/var/run/secrets/kubernetes.io"
 matchActions:
 - action: Sigkill # Matar el proceso inmediatamente
 - action: Post # También mandar evento a userspace
```

Esta policy corre enteramente en kernel space. Cuando un proceso abre ` /etc/shadow `, el programa eBPF se dispara, checkea el path, y puede **matar el proceso antes de que la syscall ` open() ` retorne**. Sin roundtrip a userspace. Sin condición de carrera TOCTOU. El enforcement pasa a nivel de kernel.

En un escenario de deployment, un cluster corriendo un container de un vendor tercero sospechoso de exfiltrar secretos fue monitoreado con esta policy. En una hora, Tetragon lo enganchó intentando leer tokens de service account de Kubernetes del volumen de secrets montado. El proceso fue matado antes de que pudiera leer el primer byte.

## Escribiendo Probes Custom

A veces las herramientas out-of-the-box no alcanzan. Así se construyen probes eBPF custom para necesidades específicas de observabilidad.

### Ejemplo: Histograma de Latencia de Conexión TCP

Esta probe mide el tiempo entre que se llama a ` connect() ` y el handshake TCP se completa:

```c
// tcp_connect_latency.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

struct {
 __uint(type, BPF_MAP_TYPE_HASH);
 __uint(max_entries, 65536);
 __type(key, u64); // pid_tgid
 __type(value, u64); // timestamp de inicio
} connect_start SEC(".maps");

struct {
 __uint(type, BPF_MAP_TYPE_RINGBUF);
 __uint(max_entries, 256 * 1024);
} events SEC(".maps");

struct event {
 u32 pid;
 u32 dport;
 u64 latency_ns;
 u32 daddr;
};

SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(tcp_v4_connect_entry, struct sock *sk)
{
 u64 pid_tgid = bpf_get_current_pid_tgid();
 u64 ts = bpf_ktime_get_ns();
 bpf_map_update_elem(&connect_start, &pid_tgid, &ts, BPF_ANY);
 return 0;
}

SEC("kprobe/tcp_finish_connect")
int BPF_KPROBE(tcp_finish_connect_entry, struct sock *sk)
{
 u64 pid_tgid = bpf_get_current_pid_tgid();
 u64 *start_ts = bpf_map_lookup_elem(&connect_start, &pid_tgid);
 if (!start_ts)
 return 0;

 u64 latency = bpf_ktime_get_ns() - *start_ts;
 bpf_map_delete_elem(&connect_start, &pid_tgid);

 struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
 if (!e)
 return 0;

 e->pid = pid_tgid >> 32;
 e->latency_ns = latency;
 e->dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
 e->daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);

 bpf_ringbuf_submit(e, 0);
 return 0;
}

char LICENSE[] SEC("license") = "GPL";
```

El componente Go de userspace que consume estos eventos:

```go
package main

import (
 "bytes"
 "encoding/binary"
 "fmt"
 "log"
 "net"

 "github.com/cilium/ebpf/link"
 "github.com/cilium/ebpf/ringbuf"
 "github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang tcpLatency tcp_connect_latency.bpf.c

type Event struct {
 Pid uint32
 Dport uint32
 LatencyNs uint64
 Daddr uint32
}

func main() {
 if err := rlimit.RemoveMemlock(); err != nil {
 log.Fatal(err)
 }

 objs := tcpLatencyObjects{}
 if err := loadTcpLatencyObjects(&objs, nil); err != nil {
 log.Fatalf("loading objects: %v", err)
 }
 defer objs.Close()

 kp1, err := link.Kprobe("tcp_v4_connect", objs.TcpV4ConnectEntry, nil)
 if err != nil {
 log.Fatalf("opening kprobe: %v", err)
 }
 defer kp1.Close()

 kp2, err := link.Kprobe("tcp_finish_connect", objs.TcpFinishConnectEntry, nil)
 if err != nil {
 log.Fatalf("opening kprobe: %v", err)
 }
 defer kp2.Close()

 rd, err := ringbuf.NewReader(objs.Events)
 if err != nil {
 log.Fatalf("opening ringbuf reader: %v", err)
 }
 defer rd.Close()

 fmt.Println("Tracing TCP connect latency... Ctrl+C to stop")

 for {
 record, err := rd.Read()
 if err != nil {
 log.Printf("reading ringbuf: %v", err)
 continue
 }

 var event Event
 if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
 continue
 }

 addr := make(net.IP, 4)
 binary.LittleEndian.PutUint32(addr, event.Daddr)
 fmt.Printf("pid=%d dst=%s:%d latency=%.2fms\n",
 event.Pid, addr, ntohs(uint16(event.Dport)),
 float64(event.LatencyNs)/1e6)
 }
}
```

### Deployando Probes Custom en Kubernetes

La parte complicada no es escribir la probe, es desplegarla de forma segura en toda la flota. Este es un patrón de deployment probado:

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: tcp-latency-probe
 namespace: observability
spec:
 selector:
 matchLabels:
 app: tcp-latency-probe
 template:
 metadata:
 labels:
 app: tcp-latency-probe
 spec:
 hostPID: true
 hostNetwork: true
 containers:
 - name: probe
 image: ghcr.io/myorg/tcp-latency-probe:v1.2.0
 securityContext:
 privileged: false
 capabilities:
 add:
 - BPF
 - PERFMON
 - SYS_RESOURCE
 volumeMounts:
 - name: bpf-fs
 mountPath: /sys/fs/bpf
 - name: debugfs
 mountPath: /sys/kernel/debug
 readOnly: true
 volumes:
 - name: bpf-fs
 hostPath:
 path: /sys/fs/bpf
 - name: debugfs
 hostPath:
 path: /sys/kernel/debug
```

Observar que no estamos usando ` privileged: true `. Las capabilities ` BPF `, ` PERFMON ` y ` SYS_RESOURCE ` son suficientes. Correr probes eBPF como containers privilegiados es un anti-pattern común que le da a la probe acceso innecesario.

## Características de Performance

Números reales de un cluster de producción (150 nodos, ~3000 pods):

```
Baseline (sin observabilidad eBPF):
 Utilización CPU de nodo: 62% promedio
 Latencia p99 de requests: 45ms

Con Cilium + Hubble (visibilidad L7):
 Utilización CPU de nodo: 63.8% promedio (+1.8%)
 Latencia p99 de requests: 45.2ms (+0.4%)

Con Tetragon (monitoreo de seguridad, 12 TracingPolicies):
 Utilización CPU de nodo: 64.1% promedio (+0.3% adicional)
 Latencia p99 de requests: 45.2ms (sin cambio)

Con probe TCP latency custom (todas las conexiones):
 Utilización CPU de nodo: 64.3% promedio (+0.2% adicional)
 Latencia p99 de requests: 45.3ms (+0.1%)

Overhead total: ~2.3% CPU, ~0.3ms latencia p99
```

Compará esto con un service mesh tradicional basado en sidecars (Istio con Envoy):

```
Con sidecar Istio (visibilidad L7):
 Utilización CPU de nodo: 71% promedio (+9%)
 Latencia p99 de requests: 52ms (+7ms)
 Overhead de memoria: ~50MB por pod (sidecar)
```

La diferencia es abismal. eBPF opera a nivel de kernel, evitando los context switches y copias de memoria que los proxies sidecar requieren. Obteners mejor observabilidad con un orden de magnitud menos de overhead.

## Errores Comunes

Después de desplegar eBPF en múltiples ambientes de producción, estos son los errores que se repiten:

**1. Copiar demasiados datos a userspace.** Cada ` bpf_ringbuf_submit() ` o ` bpf_perf_event_output() ` involucra una copia de memoria. Si se está traceando cada syscall y copiando buffers de argumentos completos, se va a quemar CPU. Filtrá y agregar in-kernel siempre que sea posible.

**2. No usar CO-RE.** Si se está compilando programas eBPF contra headers de kernel específicos y mandando binarios diferentes por versión de kernel, lo se está haciendo mal. BPF CO-RE (Compile Once, Run Everywhere) con BTF elimina esto completamente.

**3. Ignorar el dimensionamiento de maps.** Los mapas BPF se alocan al momento de carga. Un ` BPF_MAP_TYPE_HASH ` con ` max_entries=1000000 ` aloca memoria de entrada en cada CPU. Dimensioná tus mapas basándote en cardinalidad real, no en el peor caso.

**4. Correr como privileged.** Como se mencionó arriba, no se necesitan containers privilegiados. ` CAP_BPF ` + ` CAP_PERFMON ` es suficiente para probes de observabilidad. ` CAP_NET_ADMIN ` si se está haciendo cosas de red. Eso es todo.

## Hacia Dónde Va Esto

El ecosistema eBPF está madurando rápido. Los grandes cambios a observar:

- **eBPF en Windows.** Microsoft viene construyendo soporte de eBPF para Windows, y está llegando a production readiness. Las probes eBPF cross-platform se están volviendo viables.
- **BPF Arena.** Nuevo tipo de mapa que le da a los programas eBPF un arena de memoria compartida, habilitando estructuras de datos más complejas (árboles, grafos) en kernel space.
- **Programas de clase de scheduling.** Programas BPF que pueden influenciar el scheduler de CPU del kernel. sched\_ext de Google ya está usando esto en producción.

Para equipos de plataforma, el mensaje es claro: si todavía se está corriendo service meshes basados en sidecars principalmente para observabilidad, se está pagando un impuesto de overhead de 5-10x. eBPF te da visibilidad más profunda a una fracción del costo. El tooling maduró al punto donde no se necesita desarrolladores de kernel en el equipo para desplegarlo. Cilium y Tetragon cubren el 90% de los case es de uso out of the box.

El 10% restante, probes custom para tu dominio específico, es donde está la verdadera ventaja competitiva. Y escribir eBPF no es tan difícil una vez que se internalizan las restricciones. El verificador es tu amigo, no tu enemigo. Es la razón por la que es posible desplegar estas cosas en producción sin miedo.

## Preguntas frecuentes

### ¿eBPF agrega overhead significativo a workloads de producción?

Cuando se implementa correctamente, eBPF agrega overhead despreciable, típicamente menos del 1% de CPU. La clave es filtrar eventos in-kernel en vez de mandar todo a userspace. Una probe mal escrita que copia buffers grandes en cada syscall puede agregar 5-10% de overhead.

### ¿Puedo usar eBPF sin escribir código C?

Sí. Herramientas como Cilium, Tetragon y bpftrace proveen abstracciones de alto nivel. Tetragon usa TracingPolicies en YAML, y bpftrace tiene un lenguaje de scripting estilo awk. Para necesidades custom, libbpf-rs (Rust) y cilium/ebpf (Go) proveen wrappers seguros.

### ¿Qué versión de kernel necesito para observabilidad con eBPF?

Mínimo kernel 5.4 para soporte BTF (portabilidad CO-RE). Kernel 5.8+ para ring buffers. Kernel 5.11+ para bpf\_snprintf y soporte de timers. La mayoría de las ofertas de Kubernetes managed (EKS, GKE, AKS) corren 5.10+ desde 2025.

## Tips

1. **BPF ring buffer vs perf buffer:** Utilizar siempre ` BPF_MAP_TYPE_RINGBUF ` en vez de ` BPF_MAP_TYPE_PERF_EVENT_ARRAY ` para programas nuevos. Los ring buffers comparten memoria entre CPUs, reducen el overhead de memoria ~50%, y soportan registros de largo variable. La única excepción es cuando se necesita ordenamiento estricto por CPU.
2. **Tail call chaining para probes complejas:** Los programas eBPF individuales tienen un límite de 1M instrucciones (antes era 4096). Para lógica de observabilidad compleja, utilizar ` bpf_tail_call() ` para encadenar hasta 33 programas via ` BPF_MAP_TYPE_PROG_ARRAY `. Cada tail call resetea el contador de instrucciones, proporcionando tamaño de programa efectivamente ilimitado.
3. **Portabilidad CO-RE con BTF:** Compilar las probes eBPF con ` -g -target bpf ` y utilizar wrappers ` __builtin_preserve_access_index() ` en accese es a structs. Esto embebe relocaciones BTF para que el mismo .o compilado corra en kernels 5.4+ sin recompilación. Verificar con ` bpftool btf dump file /sys/kernel/btf/vmlinux `.
4. **Selectores de TracingPolicy en Tetragon:** No engancharse a cada syscall. Utilizar los selectores ` matchArgs ` y ` matchReturnArgs ` de Tetragon para filtrar in-kernel: ` matchArgs: [{index: 0, operator: Equal, values: ["/etc/shadow"]}] `. Esto descarta eventos irrelevantes antes de que lleguen a userspace, reduciendo overhead de CPU de ~5% a \<0.1%.

- ebpf
- kubernetes
- cilium
- tetragon
- observability
- linux-kernel
- security-monitoring

[Volver al Blog](<https://lucioduran.com/es/blog>)
