5. Arquitectura de Jaeger

Por: Artiko
jaegerarquitecturacollectorquerystorage

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:

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:

Como el collector, es stateless y horizontalmente escalable.

Endpoints HTTP útiles

EndpointPara qué
GET /api/servicesLista de servicios con trazas
GET /api/services/{svc}/operationsOperaciones de un servicio
GET /api/traces?service={svc}&limit=20Buscar trazas
GET /api/traces/{traceID}Detalle de un trace
GET /api/dependenciesGrafo de dependencias
GET /metricsMé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.

BackendCuándo usarlo
memoryDesarrollo local. Se pierde al reiniciar
badgerEmbebido en disco. Single-node, OK para entornos chicos
cassandraStorage histórico de Jaeger. Aguanta volumen alto
elasticsearch / opensearchBúsqueda rica por atributos, agregaciones
clickhouseSoporte 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:

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)]

Beneficios:

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?

ProblemaRazón
Storage en memoriaSe pierde al reiniciar
Sin separación de rolesNo podés escalar collector independiente del query
Sin replicaciónSingle point of failure
Sin sampling de servidorTe llenás de tráfico
Cap de trazas hardcodedMEMORY_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étricaQué mide
jaeger_collector_spans_received_totalSpans aceptados
jaeger_collector_spans_dropped_totalSpans descartados (cola llena)
jaeger_collector_spans_serviceCacheSizeTamaño del cache de servicios
jaeger_query_requests_totalRequests servidas por query
jaeger_storage_operations_duration_secondsLatencia 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.