Engenharia

Eficiente bloqueio de duplicatas para dados baseados em eventos no Elasticsearch

O Elastic Stack é empregado para muitos casos de uso diferentes. Um dos mais comuns é armazenar e analisar diferentes tipos de dados baseados em eventos ou em séries de tempo, por exemplo, eventos de segurança, logs e métricas. Esses eventos com frequência consistem em dados vinculados a uma marca de tempo específica representando quando o evento ocorreu ou foi coletado e normalmente não há uma chave natural disponível para identificar exclusivamente o evento.

Para alguns casos de uso, e talvez até mesmo para tipos de dados contidos em um caso de uso, é importante que os dados no Elasticsearch não estejam duplicados: Documentos duplicados podem resultar em análise incorreta e erros de pesquisa. Começamos olhando no último ano na postagem de blog de introdução ao manuseio de duplicatas usando o Logstash, e nela vamos aprofundar mais e tratar de algumas questões comuns.

Indexação no Elasticsearch

Ao indexar dados no Elasticsearch, você precisa receber a resposta para garantir que os dados tenham sido indexados com êxito. Se um erro, como um erro de conexão ou uma falha no nó, bloquear o recebimento da resposta, não haverá garantia de que algum dos dados foi indexado ou não. Quando os clientes encontram esse tipo de cenário, o método padrão para garantir a entrega é repetir o processo, o que pode fazer com que o mesmo documento seja indexado mais de uma vez.

Conforme descrito na postagem de blog sobre manuseio de duplicatas, é possível contornar essa situação definindo um ID exclusivo para cada documento no cliente, em vez de fazer com que o Elasticsearch atribua automaticamente um ID no momento da indexação. Quando um documento duplicado é gravado no mesmo índice, isso resulta em uma atualização, em vez de uma segunda gravação do documento, o que bloqueia as duplicatas.

Comparativo entre UUIDs e ids de documento baseados em hash

É possível escolher entre dois tipos principais de identificador que será usado.

Universally Unique Identifiers (UUIDs) são identificadores baseados em números de 128 bits que podem ser gerados entre sistemas distribuídos, ao mesmo tempo sendo exclusivos para efeitos práticos. Esse tipo de identificador geralmente não tem dependência do conteúdo do evento ao qual está associado.

Para usar UUIDs para evitar duplicatas, é essencial que o UUID seja gerado e atribuído ao evento antes que ele ultrapasse qualquer limite que realmente garanta que o evento está sendo entregue exatamente uma vez. Na prática, isso geralmente significa que o UUID deve ser atribuído no ponto de origem. Se o sistema de origem do evento não puder gerar um UUID, poderá ser necessário usar um tipo diferente de identificador.

O outro tipo principal de identificador é aquele em que uma função hash é usada para gerar um hash numérico baseado no conteúdo do evento. A função hash sempre gerará o mesmo valor para uma parte específica do conteúdo, mas não há garantia de que o valor gerado será exclusivo. A probabilidade de uma colisão de hash, que é quando dois eventos diferentes resultam no mesmo valor de hash, depende do número de eventos no índice, além do tipo de função hash usado e da extensão do valor que ela produz. Um hash de pelo menos 128 bits de extensão, por exemplo, MD5 ou SHA1, geralmente permite um bom equilíbrio entre extensão e baixa probabilidade de colisão para muitos cenários. Para ter garantias ainda melhores de exclusividade, pode ser usado um hash ainda mais extenso, como SHA256.

Como um identificador baseado em hash depende do conteúdo do evento, é possível atribuir isso em uma etapa de processamento posterior porque o mesmo valor será calculado onde for gerado. Isso possibilita atribuir esse tipo de IDs em qualquer ponto antes que os dados sejam indexados no Elasticsearch, o que permite flexibilidade ao criar um pipeline ingest.

O Logstash oferece suporte para calcular UUIDs e inúmeras funções hash conhecidas e comuns através do plugin de filtro de impressão digital.

Escolha de um id de documento eficiente

Quando o Elasticsearch obtém a permissão de atribuir o identificador do documento no momento da indexação, ele pode executar otimizações porque sabe que o identificador gerado não pode preexistir no índice. Isso melhora o desempenho da indexação. Para identificadores gerados externamente e inseridos com o documento, o Elasticsearch deve tratar isso como uma possível atualização e verificar se o identificador do documento já está nos segmentos de índice existentes, o que exige trabalho adicional e portanto é mais lento.

Nem todos os identificadores de documento externos são criados por igual. Os identificadores que gradativamente aumentam ao longo do tempo com base na ordem de classificação geralmente resultam em melhor desempenho de indexação do que os identificadores completamente aleatórios. O motivo disso é que o Elasticsearch consegue determinar rapidamente se um identificador existe em segmentos de índice mais antigos com base exclusivamente no valor de identificador mínimo e máximo em vez de ter de pesquisar nele. Isso está descrito nesta postagem de blog, que ainda é relevante apesar de já estar ficando meio obsoleta.

Os identificadores baseados em hash e muitos tipos de UUIDs geralmente são aleatórios por natureza. Ao lidar com um fluxo de eventos em que cada um tem uma marca de tempo definida, podemos usar essa marca de tempo como prefixo do identificador para torná-lo classificável e assim aumentar o desempenho da indexação.

A criação de um identificador prefixado por uma marca de tempo também tem o benefício de reduzir a probabilidade de colisão de hash, porque o valor do hash só precisa ser exclusivo por marca de tempo. Isso possibilita usar valores de hash mais curtos mesmo em cenários de alto volume de ingest.

Podemos criar esses tipos identificadores no Logstash usando o plugin de filtro de impressão digital para gerar um UUID ou um hash e um filtro Ruby para criar uma representação de sequência de caracteres com codificação hexadecimal da marca de tempo. Se supusermos que temos um campo de mensagem para o qual podemos criar um hash e que a marca de tempo no evento já foi analisada no campo @timestamp, poderemos criar os componentes do identificador e armazená-lo nos metadados desta maneira:

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

Esses dois campos podem ser usados para gerar um id de documento no plugin de saída do Elasticsearch:

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

O resultado será um id de documento com codificação hexadecimal e 40 caracteres de extensão, por exemplo: 4dad050215ca59aa1e3a26a222a9bbcaced23039. Um exemplo de configuração completa pode ser encontrado neste gist.

Implicações no desempenho da indexação

O impacto de usar diferentes tipos de identificadores dependerá muito dos dados, do hardware e do caso de uso. Podemos fornecer algumas diretrizes gerais, mas é importante executar benchmarks para determinar exatamente como isso afeta o caso de uso.

Para obter um throughput de indexação ideal, o uso de identificadores autogerados pelo Elasticsearch sempre será a opção mais eficiente. Como não são necessárias verificações de atualização, o desempenho da indexação não se altera muito à medida que os índices e shards aumentam de tamanho. Portanto, é recomendável usar isso sempre que possível.

As verificações de atualização resultantes de um ID externo exigirão acessos adicionais ao disco. O impacto disso dependerá do grau de eficiência com que os dados necessários poderão ser armazenados em cache pelo sistema operacional, da velocidade do armazenamento e do desempenho no manuseio de leituras aleatórias. A velocidade de indexação também normalmente cai à medida que os índices e shards aumentam e cada vez mais segmentos precisam ser verificados.

Uso da API de rollover

Os índices baseados em tempo tradicionais dependem do fato de cada índice cobrir um período de tempo definido específico. Isso significa que os tamanhos dos índices e shards poderão acabar variando muito se os volumes de dados flutuarem com o tempo. Tamanhos de shard irregulares não são desejáveis e podem ocasionar problemas no desempenho.

A API de índice de rollover foi introduzida para oferecer uma maneira flexível de gerenciar índices baseados no tempo a partir de vários critérios, e não apenas do tempo. Ela permite executar o rollover em um novo índice logo depois que um existente atinge um tamanho, uma contagem de documento e/ou uma idade específica, resultando em tamanhos de shard e índice muito mais previsíveis.

Isso entretanto rompe o vínculo entre a marca de tempo do evento e o índice ao qual ela pertence. Quando os índices se baseavam estritamente no tempo, um evento sempre acessava o mesmo índice independentemente de ele ter chegado tarde. É esse princípio que possibilita bloquear duplicatas usando identificadores externos. Durante o uso da API de rollover, portanto, não é mais possível bloquear completamente as duplicadas, mesmo que a probabilidade seja reduzida. É possível que dois eventos duplicados cheguem a algum dos lados de um rollover e assim acabem em índices diferentes apesar de terem a mesma marca de tempo, o que não resultará em uma atualização.

Portanto, não será recomendável usar a API de rollover se o bloqueio de duplicatas for um requisito absoluto.

Adaptação a volumes de tráfego imprevisíveis

Mesmo se não for possível usar a API de rollover, ainda haverá maneiras de adaptar e ajustar o tamanho de shards se os volumes de tráfego flutuarem e resultarem em índices baseados no tempo que sejam muito pequenos ou muito grandes.

Se os shards acabaram ficando muito grandes devido a, por exemplo, falha no tráfego, é possível usar a API de divisão de índices para dividir o índice em um número maior de shards. Essa API exige que uma configuração seja aplicada na criação do índice, por isso precisa ser adicionada através de um modelo de índice.

Se em contrapartida os volumes de tráfego forem muito baixos, resultando em shards incomumente pequenos, a API de encolhimento de índices poderá ser usada para reduzir o número de shards no índice.

Conclusões

Como você viu nesta postagem de blog, é possível bloquear duplicatas no Elasticsearch especificando um identificador de documento externamente antes de indexar dados no Elasticsearch. O tipo e a estrutura do identificador podem ter um impacto significativo no desempenho da indexação. Entretanto, isso vai variar dependendo do caso de uso, por isso é recomendável executar benchmark para identificar o que é ideal para você e seu cenário específico.