Ingeniería

Prevención eficaz de duplicados para datos basados en eventos en Elasticsearch

El Elastic Stack se usa para muchos casos de uso diferentes. Uno de los más comunes tiene que ver con el almacenamiento y análisis de diferentes tipos de datos basados en eventos o series temporales; por ejemplo, los eventos, los logs y las métricas de seguridad. Estos eventos a menudo consisten en datos vinculados a una marca de tiempo específica que representa el momento en que el evento tuvo lugar o se recopiló, y a menudo no hay una clave natural para identificarlo de manera inequívoca.

Para algunos casos de uso, e incluso posiblemente tipos de datos en un caso de uso, es importante que los datos de Elasticsearch no se dupliquen: La duplicación de documentos puede derivar en análisis incorrectos y errores de búsqueda. Comenzamos a observar esto el año pasado en el blog de introducción al manejo de duplicados con Logstash, y en este profundizaremos un poco más y abordaremos algunas preguntas comunes.

Indexación en Elasticsearch

Al indexar datos en Elasticsearch, debes recibir la respuesta para asegurarte de que la indexación sea exitosa. Si un error (por ejemplo, un problema de conexión o una falla en un nodo) no te permite recibirla, no podrás estar seguro de que los datos se hayan indexado. Cuando los clientes se encuentran en una situación como esta, la alternativa estándar para garantizar la entrega es hacer un nuevo intento. Esto puede hacer que el mismo documento se indexe más de una vez.

Como se indicó en el blog sobre manejo de duplicados, es posible solucionar esto definiendo un ID único para cada documento en el cliente en lugar de hacer que Elasticsearch asigne uno automáticamente en el momento de la indexación. Cuando un documento duplicado se escribe en el mismo índice, el documento se actualiza en lugar de escribirse por segunda vez. Esto evita duplicados.

UUID vs. ID de documentos basados en hash

Cuando se trata de determinar el tipo de identificador que se usará, hay dos tipos.

Los identificadores únicos universales (UUID) son identificadores basados en números de 128 bits que pueden generarse en sistemas distribuidos, aunque por motivos prácticos son únicos. Este tipo de identificador generalmente no depende del contenido del evento con el que se asocia.

Para evitar duplicados mediante UUID, es fundamental que estos últimos se generen y se asignen al evento antes de que el evento exceda cualquier límite que garantice que se entregue exactamente una vez. En la práctica, esto a menudo significa que el UUID debe asignarse en el punto de origen. Si el sistema en el que se origina el evento no puede generar un UUID, posiblemente deba usarse un tipo de identificador diferente.

El otro tipo de identificador principal es aquel en el que se usa una función de hash para generar un hash numérico según el contenido del evento. La función de hash siempre generará el mismo valor para un contenido específico, pero no se garantiza que el valor generado sea único. La probabilidad de un conflicto de hashes, situación que se produce cuando dos eventos diferentes generan el mismo valor de hash, depende del número de eventos del índice, y también del tipo de función de hash empleado y de la extensión del valor que produce. Un hash de al menos 128 bits de extensión (por ejemplo, MD5 o SHA1) generalmente proporciona un buen equilibrio entre la extensión y bajas probabilidades de conflicto en muchísimas situaciones. Para garantizar todavía más la exclusividad se puede usar un hash aún más extenso, como el de cifrado SHA256.

Debido a que los identificadores basados en hash dependen del contenido de los eventos, es posible asignar esto en una etapa posterior del procesamiento porque el mismo valor se calculará donde se genere. Esto permite asignar este tipo de ID en cualquier punto antes de la indexación de los datos en Elasticsearch, lo que ofrece flexibilidad al designar un pipeline de ingestión.

Logstash ofrece compatibilidad con el cálculo de UUID, y con diferentes funciones de hash populares y comunes, a través del plugin de filtro de huellas digitales.

Elegir un ID de documento eficaz

Cuando tiene autorización para asignar el identificador de documentos en el momento de la indexación, Elasticsearch puede aplicar optimizaciones porque según sus registros el identificador generado no puede existir previamente en el índice. Esto mejora el rendimiento de la indexación. En el caso de los identificadores generados de manera externa y transmitidos con los documentos, Elasticsearch debe considerar esto como una posible actualización y verificar si los identificadores ya existen en segmentos de índice, proceso que requiere trabajo adicional y es más lento.

No todos los identificadores de documentos externos se crean de igual forma. Los identificadores que con el tiempo aumentan según el orden de clasificación generalmente ofrecen un mejor rendimiento de indexación que los identificadores totalmente aleatorios. Esto se debe a que Elasticsearch puede determinar rápidamente si un identificador existe en segmentos de índices más antiguos basándose únicamente en los valores mínimo y máximo de identificador de esos segmentos en lugar de tener que hacer búsquedas en ellos. Hay disponible una descripción en este blog, que sigue teniendo vigencia a pesar de ser un poco antigua.

Los identificadores basados en hash y muchos tipos de UUID generalmente son de naturaleza aleatoria. Al trabajar con un flujo de eventos en el que cada uno de estos tiene una marca de tiempo definida, se puede usar esta marca de tiempo como prefijo para el identificador para puedan clasificarse y, por lo tanto, aumentar el rendimiento de la indexación.

Crear un identificador con prefijo de una marca de tiempo también tiene el beneficio de reducir la probabilidad de conflicto de hash, ya que el valor de hash debe ser único por cada marca de tiempo. Esto permite usar valores de hash más cortos aun en situaciones de alto volumen de ingestión.

Es posible crear estos tipos de identificadores en Logstash usando el plugin de filtro de huellas digitales o generar un UUID o hash y un filtro Ruby para crear una representación de texto con codificación hexadecimal de la marca de tiempo. Suponiendo que hay un campo message al que podemos aplicar hash y que la marca de tiempo del evento ya se parseó en el campo @timestamp, se pueden crear los componentes del identificador y almacenarlo en los metadatos de la siguiente manera:

fingerprint {
  source => "message"
  target => "[@metadata][fingerprint]"
  method => "MD5"
  key => "test"
}
ruby {
  code => "event.set('@metadata[tsprefix]', event.get('@timestamp').to_i.to_s(16))"
}

Estos dos campos luego pueden usarse para generar un ID de documento en el plugin de salida de Elasticsearch:

elasticsearch {
  document_id => "%{[@metadata][tsprefix]}%{[@metadata][fingerprint]}"
}

Esto dará como resultado un ID de documento con codificación hexadecimal y 40 caracteres de extensión; por ejemplo, 4dad050215ca59aa1e3a26a222a9bbcaced23039. Se puede hallar un ejemplo de configuración completo en este caso.

Implicaciones para el rendimiento de la indexación

El impacto que tiene usar diferentes clases de identificadores dependerá muchísimo de tus datos, de tu hardware y del caso de uso. Si bien se pueden ofrecer algunas pautas generales, es importante hacer evaluaciones comparativas para determinar con exactitud el efecto que esto tiene en tu caso de uso.

Para que los resultados de indexación sean óptimos, la opción más eficaz siempre será usar identificadores generados automáticamente por Elasticsearch. Debido a que no se requieren verificaciones de actualización, el rendimiento de la indexación no se modifica demasiado a medida que aumenta el tamaño de los índices y los shards. Por lo tanto, se recomienda aplicar esto siempre que sea posible.

Para las verificaciones de actualización debidas al uso de un ID externo se requerirá acceso adicional al disco. El impacto de esto depende de la eficacia con que el sistema operativo pueda almacenar en caché los datos requeridos, de la velocidad del almacenamiento y del grado de efectividad con que pueda manejar lecturas aleatorias. La velocidad de indexación a menudo también disminuye a medida que el tamaño de los índices y los shards aumenta y que más segmentos deben verificarse.

Uso de la API de rollover

Los índices tradicionales basados en tiempo dependen de que cada índice abarque un período de tiempo establecido específico. Esto significa que los tamaños de los índices y los shards pueden variar muchísimo si los volúmenes de datos fluctúan con el tiempo. Los tamaños de shards irregulares no se recomiendan y pueden ocasionar problemas de rendimiento.

La API de índice de rollover se introdujo para ofrecer una manera flexible de administrar índices basados en tiempo según varios criterios, no solo el del tiempo. Permite hacer una implementación en un índice nuevo una vez que el existente alcanza un tamaño, una antigüedad o un número de documentos específicos. Esto hace posibles tamaños de shards e índices más predecibles.

Sin embargo, esto rompe el enlace entre la marca de tiempo de evento y el índice al que pertenece. Cuando los índices se basaban estrictamente en el tiempo, un evento siempre se destinaba al mismo índice, independientemente del momento en que llegaba. Este principio permite evitar duplicados usando identificadores externos. Por lo tanto, cuando se usa la API de rollover, ya no es posible evitar duplicados por completo, aunque la probabilidad se reduce. Es posible que dos eventos duplicados se reciban a ambos lados de una implementación y, por lo tanto, queden en diferentes índices aunque tengan la misma marca de tiempo. De esto no surgirá una actualización.

Por ello, no se recomienda usar una API de rollover si es un requisito estricto evitar duplicados.

Adaptación a volúmenes de tráfico impredecibles

Aun cuando no se pueda usar la API de rollover, hay maneras de adaptar y ajustar el tamaño de los shards si los volúmenes de tráfico fluctúan y generan índices basados en tiempo demasiado pequeños o grandes.

Si el tamaño final de los shards es demasiado grande debido, por ejemplo, a un aumento súbito del tráfico, es posible usar la API de índice dividido para dividir el índice en un número más grande de shards. Para esta API se debe aplicar un ajuste al crear el índice. Por ello, esto debe agregarse a través de una plantilla de índice.

Si los volúmenes, por el contrario, son demasiado bajos y el tamaño de los shards resultantes es anormalmente pequeño, se puede usar la API de índice de reducción para reducir el número de shards del índice.

Conclusiones

Como viste en este blog, es posible evitar duplicados en Elasticsearch especificando un identificador de documento de manera externa antes de indexar datos en Elasticsearch. El tipo de estructura del identificador puede tener un impacto considerable en el rendimiento de la indexación. Esto, sin embargo, variará según el caso de uso y por ello se recomienda hacer una evaluación comparativa para identificar la opción óptima para ti y tu situación específica.