Pontos Principais
- Elasticsearch é uma solução fantástica: rápido, escalável e open-source. Mas é como um bom carro: não adianta um motor poderoso se não sabemos pilotar. Neste artigo vou compartilhar um pouco do aprendizado que tive ao usar o produto em escala;
- Filtros e ranqueamento de documentos funcionam maravilhosamente bem. É um belo cenário de "it just works"! A dificuldade aparece quando tentamos unir cenários diferentes sem conhecer as particularidades de sua configuração. Um bom tuning inicial é extremamente importante para evitar problemas;
- Adotar e manter um cluster de Elasticsearch pode ser uma tarefa custosa se seus requisitos não estiverem alinhados. Cenários como deep-pagination e analytics podem cobrar um custo alto de manutenção;
- A configuração de shards é essencial para que seu cluster fique saudável. Ter o conhecimento sobre seus requisitos de leitura e escrita podem antecipar uma série de surpresas desagradáveis;
Antes de tudo, sobre o Elasticsearch
O Elasticsearch foi construído em cima do Lucene, uma biblioteca Java para indexação e pesquisa de textos mantida pela Apache Software Foundation. A Elastic (empresa) incorporou essa biblioteca e a transformou num produto escalável, praticamente um banco noSQL. E como todo produto nascido a partir de algo open-source, possui versões gratuita e paga. Também há todo um ecossistema para trabalhar com o Elasticsearch, uma suíte conhecida como ELK ⎯ Elasticsearch, Logstash e Kibana.
Como exemplo, assumindo que a estrutura de dados seja um cenário com um banco não-relacional, o que deve ser considerado então para escolhermos o Elasticsearch?
O que levar em consideração
Quantos documentos uma pesquisa pode retornar? Isso será paginado?
Esta é uma das maiores dificuldades que já tive com o Elasticsearch. Para explicar, primeiro precisamos saber como é formado.
Cada node do Elasticsearch é uma instância do Lucene e cada instância possui um ou mais índices. Por sua vez, cada índice possui um ou mais shards primários ou réplicas. Podemos ver os shards como partições de dados que garantem a alta-disponibilidade do Elasticsearch. Cada shard possui um conjunto de documentos (o ID do documento geralmente possui um hash que permite localizar diretamente o shard onde está armazenado), com o Elasticsearch garantindo que estes são copiados para todas as réplicas que existem.
Todo cluster Elasticsearch possui ao menos 1 master-node (os próprios nodes elegem 1 deles para ser o master, até que este falhe e seja substituído). Para executar uma pesquisa, o master-node envia uma requisição para todos os shards que possam atender a query e cada um deles retorna seus "X" documentos, ordenados pelo ranking. Por fim, o master-node unifica o resultado e executa seu próprio ranqueamento, retornando os "X" documentos finais.
Mas por que a paginação é um problema? Imagine um índice com 5 shards primários. Se requisitamos a primeira página com os 10 documentos melhor ranqueados, o master-node receberá 50 documentos para selecionar os 10 finais ⎯ é o responsável pela paginação, na prática. Mas se requisitarmos a página 1.000, os resultados esperados serão os documentos nas posições 10.001 a 10.010 após o ranqueamento final (somados os resultados de todos os shards). Neste caso, o master-node receberá 50.050 documentos de onde selecionará apenas 10!
Se precisarmos fazer um "deep pagination", provavelmente teremos um problema. Existem alternativas como a "Scroll API", mas mesmo esta não é indicada para o usuário final: cada página gerada cria um "scroll" em memória para que o cliente possa partir dele. O problema é que geralmente são bastante custosos e a própria Elastic só o recomenda para uso controlado (como em algum batch job), não para o usuário final.
Outra alternativa seria o "search after", que funciona de forma similar à "Scroll API" mas sem manter estado no Elasticsearch. Seu ponto negativo é não fazer uma consulta diretamente na página solicitada, mas sim criar diversas consultas em paralelo para recriar a página original. E exatamente por isso, a ordem do ranqueamento pode mudar, fazendo com que tenhamos páginas inconsistentes.
A paginação com Elasticsearch é um problema sem solução simples. Mesmo a quantidade de documentos retornados por uma pesquisa tem um limite definido pela propriedade index.max_result_window (o valor padrão são 10000 documentos). É possível aumentar este número mas o impacto gerado nas consultas aumenta conjuntamente (lembre-se da conta que fizemos nos parágrafos anteriores). A Elastic sugere que faça a paginação em memória e se, caso o seu cenário passe muito disso, pode ser que seja muito custoso manter o sistema.
Que tipo de consulta será feita?
O "cenário padrão" do Elasticsearch é quando fazemos uma query esperando um ranqueamento dos documentos retornados, aí perguntamos: "O quão próximo da minha pesquisa estão meus documentos?". E é perfeitamente possível efetuar uma query sem o ranking, retornando apenas documentos que atendam (ou não) a uma pesquisa. Ambos os cenários funcionam maravilhosamente bem.
As coisas se complicam um pouco quando precisamos fazer agregações nos resultados. Este cenário é muito comum quando o usamos em conjunto com o Kibana para montar Dashboards, mas como fica para o usuário final? As agregações predefinidas transformam o Elasticsearch numa ferramenta poderosa para Analytics. Porém, dependendo do volume e formato de seus documentos, podem ter um tempo de resposta bem alto. Isso é aceitável para seu usuário?
Considere o cenário abaixo, onde uma livraria contabiliza diariamente as visitas a um determinado produto e nossa intenção é retornar cada livro visitado com a quantidade total de visitas que tiveram; como seria essa query?
titulo_livro |
data |
visitas |
The Godfather |
27/06/2019 |
10 |
The Godfather |
28/06/2019 |
5 |
Sherlock Holmes |
27/06/2019 |
15 |
Nesta consulta, seriam necessárias duas agregações. Primeiro, uma agregação de tipo "bucket", usadas para agrupar dados. Criaríamos um bucket para cada livro e estes seriam preenchidos com todos os documentos que atendem a um campo "agrupador" (que poderia ser o título). Feito isto, teremos uma agregação do tipo "sum" para sumarizar todos os campos "visitas" de cada bucket.
Este tipo de cenário funciona perfeitamente com algumas centenas de milhares de documentos, entretanto, se houver um volume considerável de buckets, isso tenderá a demorar mais. O problema é que para atender a este volume grande, o master-node precisa receber todos os documentos que atendem à consulta (vide item sobre paginação). Após isso, serão executadas as agregações de bucket e soma (sum), gerando o resultado para responder à requisição. Dependendo do volume de dados, pode ser preferível receber todos os documentos que atendam à query e unificá-los em memória.
Qual a previsão de uso? Write-intensive ou read-intensive?
E voltamos aos shards. O caso de uso de seus índices terá influência direta em como serão configurados. Para um cenário de muita leitura e pouca escrita, talvez deseje manter apenas poucos shards primários e muitas, muitas réplicas. Como o master-node sabe onde os dados estão armazenados, ele não envia a mesma query para o mesmo shard. Então, por exemplo, se tiver apenas 1 shard primário, apenas ele ou uma de suas réplicas receberá esta requisição. Mas se tiver 15 shards primários, todos eles (1 primário ou réplica de cada) podem receber esta mesma consulta para retornar os resultados para o master-node.
Bem, se isso é verdade, então o certo seria sempre manter apenas 1 shard primário e várias réplicas, correto? Infelizmente, não. Imagine um cenário onde esteja usando o Elasticsearch para armazenar o resultado de um batch job que gere milhões de documentos. Provavelmente, você usará a "batch API" para inserir lotes simultaneamente e se tiver uma configuração como esta, precisará de muita paciência para acabar o processo de indexação. Isso acontece porque todos os documentos precisam ser indexados primeiro no shard primário e depois replicados. Este é um processo custoso (tanto de CPU, quanto de I/O e memória) e que pode acabar levando seu cluster ao limite. Neste caso, a replicação é uma grande vilã.
Um fato relevante é que a quantidade de shards primários não pode ser alterada após o índice ser criado (réplicas podem ser adicionadas ou removidas conforme a necessidade). Uma solução comum para acelerar o processo de indexação é remover as réplicas de um shard e executar apenas no primário, adicionando as réplicas após o processo. O problema com esta abordagem é que não teremos alta disponibilidade durante este processo e corremos um grande risco de perda de dados. É preciso achar o meio termo entre o processo de indexação e as requisições de leitura.
Conclusão
O Elasticsearch é versátil, poderoso e rápido. Se estiver bem configurado, certamente será a vantagem que seu serviço precisa! Só é preciso um certo cuidado, principalmente durante a primeira configuração. Adotar um produto top de linha como esse sem ter o conhecimento necessário pode custar muitas noites em claro até chegar a um ponto aceitável. Falo por experiência própria!
Sobre o autor
César Lawall é um entusiasta de tecnologia que sempre procura aprender coisas novas. Com 17 anos de carreira, sendo 5 deles trabalhando como Arquiteto de sistemas, dirigiu seu foco para a segurança de aplicações. Hoje atua como desenvolvedor na OLX. Além de desenvolvedor, também é instrutor de tecnologia, dando aula sobre diversos assuntos como Node, Android, iOS, React, React-native, JavaScript, HTML5, CSS3, etc.