Latencia en Elasticsearch: Arreglando Shards y Heap con ILM (Caso Real)

La semana pasada, nuestro cluster de producción comenzó a arrojar excepciones de CircuitBreakerException bajo una carga moderada. El síntoma era claro pero aterrador: consultas de agregación simples que solían tardar 200ms estaban disparándose a 12 segundos, y el uso de Heap Memory en los nodos de datos oscilaba peligrosamente cerca del 95%. No era un ataque DDoS ni un pico de tráfico inesperado; era una deuda técnica silenciosa acumulada en nuestra Estrategia de Sharding.

En este artículo, desglosaré cómo diagnosticamos un problema de "oversharding" (exceso de fragmentación) en un entorno ELK Stack procesando 2TB de logs diarios, y cómo la implementación correcta de la Gestión de ciclo de vida de índices (ILM) redujo nuestra latencia en un 600%.

Análisis de Causa Raíz: El Costo Oculto de los Shards

Nuestro entorno consistía en un cluster de 6 nodos (AWS r6g.2xlarge) ejecutando Elasticsearch 8.4. Inicialmente, configuramos índices diarios (`logs-2024-01-01`) con 5 shards primarios cada uno. Esto parece una configuración estándar en muchos tutoriales de Elasticsearch, pero bajo un microscopio de rendimiento, fue desastroso.

El Error en los Logs: [2024-12-20T10:00:00] Data too large, data for [<transport_request>] would be [16gb/15.8gb], which is larger than the limit of [15.8gb]

El problema fundamental no era la falta de RAM, sino cómo Lucene gestiona los recursos. Cada shard consume recursos del Heap para mantener sus segmentos en memoria, independientemente de si tiene datos o no. Al rotar índices diariamente, independientemente del volumen de datos, generamos miles de shards pequeños (algunos de solo 100MB). Esta fragmentación excesiva obligaba al cluster a mantener metadatos masivos en el estado del cluster (cluster state), lo que ralentizaba las actualizaciones de los nodos maestros y las operaciones de búsqueda que tenían que "tocar" cientos de shards para una sola consulta.

Por qué aumentar la memoria no funcionó

Nuestra primera reacción de "bombero" fue duplicar la memoria RAM de los nodos. Inyectamos más dinero en AWS esperando que el problema desapareciera. Si bien esto alivió los síntomas del CircuitBreaker temporalmente, la latencia de búsqueda seguía siendo alta. ¿Por qué? Porque el problema era de concurrencia y sobrecarga de gestión, no puramente de capacidad. Tener 10,000 shards activos en un cluster de 6 nodos significa que cada hilo de búsqueda debe coordinar resultados de miles de fuentes, provocando un cuello de botella en la CPU y en la cola de hilos de gestión (`management thread pool`).

Solución: Index Rollover y Sharding Dinámico

La solución definitiva para el Tuning Elasticsearch no es adivinar el número de shards, sino dejar que el tamaño de los datos dicte cuándo crear un nuevo índice. Para esto, migramos de índices diarios estáticos a una política de Index Lifecycle Management (ILM) con Rollover basado en tamaño.

El objetivo: Mantener el tamaño de cada shard entre 30GB y 50GB, que es el "punto dulce" recomendado por Elastic para balancear la velocidad de recuperación y el rendimiento de búsqueda.

1. Definición de la Política ILM (Hot-Warm-Delete)

Esta política mueve los datos activamente escritos (Hot) a nodos optimizados para lectura (Warm) una vez que alcanzan cierto tamaño o edad, y finalmente los elimina.

// PUT _ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            // Rotar si el shard alcanza 50GB o si el índice tiene 7 días
            "max_primary_shard_size": "50gb",
            "max_age": "7d"
          }
        }
      },
      "warm": {
        "min_age": "7d", // Mover a nodos warm después de 7 días
        "actions": {
          "forcemerge": {
            "max_num_segments": 1 // CRÍTICO para rendimiento de lectura
          },
          "allocate": {
            "require": {
              "data": "warm"
            }
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {} // Eliminar después de 30 días
        }
      }
    }
  }
}

Noten el uso de forcemerge en la fase "warm". Esto es crucial para la Optimización de motores de búsqueda interna. Al reducir el número de segmentos a 1, liberamos una cantidad significativa de Heap y hacemos que las búsquedas en datos históricos sean extremadamente rápidas, ya que Lucene no tiene que verificar múltiples segmentos.

2. Configuración del Index Template

Debemos asociar esta política a nuestros índices futuros y asegurarnos de que el alias de escritura esté configurado.

// PUT _index_template/logs_template
{
  "index_patterns": ["logs-prod-*"],
  "template": {
    "settings": {
      "number_of_shards": 2, // Empezamos con 2, escalará con Rollover
      "number_of_replicas": 1,
      "index.lifecycle.name": "logs_policy",
      "index.lifecycle.rollover_alias": "logs-prod-write"
    }
  }
}
Métrica Antes (Índices Diarios) Después (ILM + Rollover)
Total Shards 4,200 450
Heap Usage Promedio 75-85% 40-50%
Latencia P99 (Búsqueda) 12,500ms 320ms
Cluster State Update 2.5s 80ms

La tabla anterior muestra el impacto directo. Al reducir el conteo de shards en casi un 90%, liberamos recursos que el cluster utilizaba solo para "existir", redirigiéndolos a procesar consultas reales. La drástica reducción en el tiempo de actualización del estado del cluster también eliminó los tiempos de espera (timeouts) al crear nuevos índices o modificar mapeos.

Ver Documentación Oficial de ILM

Riesgos y Casos Borde

Aunque ILM es potente, tiene trampas que pueden causar pérdida de datos temporal o bloqueos si no se manejan con cuidado.

Advertencia sobre Force Merge: Nunca ejecutes forcemerge en un índice que todavía está recibiendo escrituras (fase Hot). Esto creará segmentos muy grandes que luego tendrán que ser reescritos si hay actualizaciones, disparando el I/O de disco. Asegúrate de que la acción de force merge solo ocurra en la fase Warm o Cold, donde el índice es de solo lectura.

Otro punto a considerar es la configuración de los nodos. Para que la asignación "data": "warm" funcione, debes etiquetar tus nodos en el archivo elasticsearch.yml:

# En nodos Hot (SSDs rápidos, alta CPU)
node.attr.data: hot

# En nodos Warm (HDDs o almacenamiento denso, menor CPU)
node.attr.data: warm

Si olvidas etiquetar los nodos, los shards no podrán moverse y el ciclo de vida se detendrá, dejando tus índices llenando el disco de los nodos Hot.

Resultado Final: Logramos reducir nuestra factura de infraestructura en un 30% al mover datos antiguos a nodos Warm más baratos, manteniendo la velocidad de búsqueda para datos recientes.

Conclusión

La optimización de un cluster no se trata de lanzar hardware al problema. Una correcta Gestión de ciclo de vida de índices y una Estrategia de Sharding basada en el tamaño real de los datos son fundamentales para la estabilidad a largo plazo. Al pasar de una rotación basada en tiempo a una basada en tamaño y utilizar arquitecturas Hot-Warm, convertimos un sistema frágil en una plataforma robusta capaz de escalar sin intervención manual constante. Si estás lidiando con consultas lentas, deja de mirar el código de tu aplicación por un momento y revisa cuántos shards está intentando coordinar tu cluster.

Post a Comment