5. Arquitectura de Jaeger
5. Arquitectura de Jaeger
Hasta ahora trataste a Jaeger como una caja negra: spans entran, UI los muestra. Para operar Jaeger en serio hay que entender qué hay dentro.
Este capítulo cubre la arquitectura clásica de Jaeger v1. Para la nueva basada en OpenTelemetry Collector ver capítulo 11.
Vista de alto nivel
flowchart LR
APP1[App con OTel SDK] -->|OTLP| C[Collector]
APP2[App con OTel SDK] -->|OTLP| C
APP3[App legacy con\nJaeger SDK deprecado] -->|UDP/Thrift| AG[Agent legado]
AG -->|gRPC| C
C --> S[(Storage:\nCassandra, ES,\nClickHouse, Badger)]
Q[Query Service] --> S
UI[UI Web :16686] --> Q
UI <-.HTTP API.-> Q
style C fill:#bfdbfe,stroke:#1e40af
style Q fill:#dcfce7,stroke:#166534
style S fill:#fef3c7,stroke:#92400e
Cuatro componentes: collector, query, storage y (legado) agent.
Collector
Punto de entrada de los spans. Los recibe, los valida, opcionalmente los procesa y los escribe al storage.
Responsabilidades:
- Aceptar tráfico OTLP (gRPC en
4317, HTTP en4318). - Aceptar formatos legados (Jaeger Thrift, Zipkin) si necesitás compatibilidad.
- Aplicar sampling del lado del servidor si está configurado.
- Validar el formato y rechazar spans corruptos.
- Buffer interno y batching antes de escribir al storage.
- Escribir al storage backend (Cassandra, Elasticsearch, etc.).
Configuración mínima
collector:
num-workers: 50
queue-size: 2000
http-server:
host-port: ":14268"
grpc-server:
host-port: ":14250"
otlp:
grpc:
host-port: ":4317"
http:
host-port: ":4318"
queue-size es el buffer interno antes de escribir al storage. Si el storage se pone lento y el buffer se llena, el collector empieza a descartar spans. La métrica jaeger_collector_spans_dropped_total te lo dice.
Escala horizontal
El collector es stateless y se escala horizontalmente. Detrás de un load balancer podés correr 5, 10, 50 réplicas. Lo único compartido es el storage.
Query
Servicio que lee del storage y expone una API HTTP/gRPC para la UI y consumidores externos.
Responsabilidades:
- Servir la UI estática (los assets de React).
- Exponer endpoints HTTP en
:16686para buscar trazas, obtener detalles, leer dependencias. - Exponer gRPC para integraciones programáticas.
- Calcular el grafo de dependencias (en algunos backends).
Como el collector, es stateless y horizontalmente escalable.
Endpoints HTTP útiles
| Endpoint | Para qué |
|---|---|
GET /api/services | Lista de servicios con trazas |
GET /api/services/{svc}/operations | Operaciones de un servicio |
GET /api/traces?service={svc}&limit=20 | Buscar trazas |
GET /api/traces/{traceID} | Detalle de un trace |
GET /api/dependencies | Grafo de dependencias |
GET /metrics | Métricas Prometheus internas de Jaeger |
# Listar servicios desde fuera de la UI
curl -s http://localhost:16686/api/services | jq -r '.data[]'
Storage backends
Jaeger no tiene storage propio: depende de un backend externo. Capítulo dedicado: 10-storage-backends.
| Backend | Cuándo usarlo |
|---|---|
| memory | Desarrollo local. Se pierde al reiniciar |
| badger | Embebido en disco. Single-node, OK para entornos chicos |
| cassandra | Storage histórico de Jaeger. Aguanta volumen alto |
| elasticsearch / opensearch | Búsqueda rica por atributos, agregaciones |
| clickhouse | Soporte oficial en Jaeger v2. Muy buen ratio costo/performance |
Agent (legado, en deprecación)
El agent es un proceso “side-car” que se desplegaba junto a las apps en Jaeger v1.
Su función original: las apps mandaban spans por UDP (rápido y barato) al agent local, y el agent los reenviaba al collector vía gRPC con buffering, retries y compresión.
flowchart LR
A[App] -->|UDP local| AG[Agent\nsidecar]
AG -->|gRPC red| C[Collector]
Por qué está deprecado:
- Los SDKs de OpenTelemetry se conectan directamente al collector vía OTLP.
- El collector ya hace batching y retries por sí mismo.
- Tener un sidecar adicional agrega complejidad sin valor claro.
- En Jaeger v2 el agent fue eliminado.
Si tu codebase todavía usa Jaeger SDKs deprecated, vas a ver agent. Migrá cuanto antes a OpenTelemetry SDKs.
Ingester (opcional, modo Kafka)
Para volúmenes altos, en lugar de que el collector escriba directo al storage:
flowchart LR
APP[Apps] --> C[Collector]
C --> K[(Kafka)]
K --> I[Ingester]
I --> S[(Storage)]
- Collector escribe los spans a un topic de Kafka.
- Ingester los consume y los escribe al storage a su ritmo.
Beneficios:
- Si el storage falla, los spans quedan en Kafka — no se pierden.
- Podés escalar collectors e ingesters independientemente.
- Aguanta picos de tráfico sin descartar spans.
Cuándo no: si todavía estás procesando < 50k spans/segundo, no necesitás Kafka todavía. Agregás complejidad sin valor.
Flujo completo de un span
sequenceDiagram
participant App
participant Collector
participant Storage
participant Query
participant UI
App->>App: span.start()
App->>App: span.end()
Note over App: SDK batchea<br/>spans por T ms
App->>Collector: OTLP gRPC<br/>(N spans)
Collector->>Collector: validar, samplear
Collector->>Storage: write batch
UI->>Query: GET /api/traces?service=X
Query->>Storage: read
Storage-->>Query: traces
Query-->>UI: JSON
UI->>UI: render timeline
Latencia típica end-to-end: span finalizado → visible en UI → 5-30 segundos (dominado por el batching del SDK + el período de flush del collector).
Si necesitás verlos más rápido en desarrollo, configurá el SDK con BatchSpanProcessor parámetros agresivos:
new BatchSpanProcessor(exporter, {
maxExportBatchSize: 10,
scheduledDelayMillis: 500,
});
¿Por qué all-in-one no sirve en producción?
| Problema | Razón |
|---|---|
| Storage en memoria | Se pierde al reiniciar |
| Sin separación de roles | No podés escalar collector independiente del query |
| Sin replicación | Single point of failure |
| Sin sampling de servidor | Te llenás de tráfico |
| Cap de trazas hardcoded | MEMORY_MAX_TRACES de a poco |
El all-in-one es perfecto para dev, demo, exploración y CI. Para producción se separa en componentes con storage real, capítulo 12.
Métricas internas de Jaeger
Cada componente expone métricas Prometheus en /metrics:
| Métrica | Qué mide |
|---|---|
jaeger_collector_spans_received_total | Spans aceptados |
jaeger_collector_spans_dropped_total | Spans descartados (cola llena) |
jaeger_collector_spans_serviceCacheSize | Tamaño del cache de servicios |
jaeger_query_requests_total | Requests servidas por query |
jaeger_storage_operations_duration_seconds | Latencia de storage |
Si no monitoreás spans_dropped_total, no estás monitoreando Jaeger. Es la primera métrica que te dice “estás perdiendo trazas”.
¿Qué viene?
Tenés el plano de la casa. En el próximo capítulo volvemos a la app: cómo instrumentar bien con OpenTelemetry, qué auto-instrumentaciones existen, cuándo escribir spans manuales y cómo no romper la cardinalidad.