Engenharia

Suporte para controle de versão do Elasticsearch

Um dos principais princípios por trás do Elasticsearch é permitir que você aproveite ao máximo seus dados. Historicamente, a busca era um empreendimento somente leitura no qual um mecanismo de busca era carregado com dados de uma única fonte. À medida que o uso cresce e o Elasticsearch se torna mais central para sua aplicação, os dados precisam ser atualizados por vários componentes. Vários componentes levam à simultaneidade, e a simultaneidade leva a conflitos. O sistema de controle de versão do Elasticsearch existe para ajudar a lidar com esses conflitos.

A necessidade de controle de versão — um exemplo

Para ilustrar a situação, vamos supor que temos um site que as pessoas usam para classificar o design de camisetas. O site é simples. Ele lista todos os designs e permite que os usuários aprovem ou reprovem um design usando ícones de polegar para cima e polegar para baixo. Para cada camiseta, o site mostra o saldo atual de votos positivos e votos negativos.

Um registro para cada mecanismo de busca tem a seguinte aparência:

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 999
}'

Como você pode ver, cada design de camiseta tem um nome e um contador de votos para controle do saldo atual.

Para manter as coisas simples e escaláveis, o site é completamente sem estado. Quando alguém olha para uma página e clica no botão de votação, ele envia uma solicitação AJAX para o servidor, que deve indicar ao Elasticsearch que o contador deve ser atualizado. Para fazer isso, uma implementação ingênua pegará o valor atual dos votos, incrementará em um e enviará para o Elasticsearch:

curl -XPOST 'http://localhost:9200/designs/shirt/1' -d'
{
"name": "elasticsearch",
"votes": 1000
}'

Essa abordagem tem uma falha grave — ela pode perder votos. Digamos que Adão e Eva estejam olhando para a mesma página ao mesmo tempo. No momento, a página mostra 999 votos. Como ambos são fãs, ambos clicam no botão de votação. Agora o Elasticsearch recebe duas cópias idênticas da solicitação acima para atualizar o documento, o que ele faz com satisfação. Isso significa que, em vez de ter uma contagem total de votos de 1.001, a contagem de votos agora é de 1.000. Opa.

Claro, a API de atualização permite que você seja mais inteligente e comunique o fato de que o voto pode ser incrementado em vez de definido com um valor específico:

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update' -d'
{
"script" : "ctx._source.votes += 1"
}'

Fazendo desta forma, significa que o Elasticsearch primeiro recupera o documento internamente, executa a atualização e o indexa novamente. Embora isso aumente a probabilidade de sucesso, ainda carrega o mesmo problema potencial de antes. Durante a pequena janela entre a recuperação e a indexação dos documentos novamente, algo pode dar errado.

Para lidar com o cenário acima e ajudar com outros mais complexos, o Elasticsearch vem com um sistema de controle de versão integrado.

O sistema de controle de versão do Elasticsearch

Cada documento armazenado no Elasticsearch tem um número de versão associado. Esse número de versão é um número positivo entre 1 e 2 63-1 (inclusive). Quando você indexa um documento pela primeira vez, ele recebe a versão 1, e você pode ver isso na resposta que o Elasticsearch retorna. Este é, por exemplo, o resultado do primeiro comando cURL neste post do blog:

{
"ok": true,
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 1
}

A cada operação de gravação neste documento, seja uma indexação, uma atualização ou uma exclusão, o Elasticsearch incrementará a versão em 1. Esse incremento é atômico e é garantido que acontecerá se a operação for retornada com sucesso.

O Elasticsearch também retornará a versão atual dos documentos com a resposta das operações get (lembre-se de que são em tempo real) e também pode ser instruído a retorná-lo com cada resultado de busca.

Bloqueio otimista

Então, de volta ao nosso exemplo, precisávamos de uma solução para um cenário em que potencialmente dois usuários tentassem atualizar o mesmo documento ao mesmo tempo. Tradicionalmente, isso se resolve com o bloqueio: antes de atualizar um documento, alguém adquire um bloqueio nele, faz a atualização e libera o bloqueio. Quando há um bloqueio em um documento, você tem a garantia de que ninguém poderá alterá-lo.

Em muitas aplicações, isso também significa que, se alguém estiver modificando um documento, ninguém mais poderá lê-lo até que a modificação seja concluída. Esse tipo de bloqueio funciona, mas tem um preço. No contexto de sistemas de alto rendimento, tem duas desvantagens principais:

  • Em muitos casos, simplesmente não é necessário. Se o sistema é bem feito, as colisões são raras. Claro, elas vão acontecer, mas isso será apenas para uma fração das operações que o sistema faz.
  • O bloqueio pressupõe que você realmente se importa. Se você quer apenas renderizar uma página da web, provavelmente não terá problema em obter um valor ligeiramente desatualizado, mas consistente, mesmo que o sistema saiba que isso mudará em um instante. As leituras nem sempre precisam esperar que as gravações em andamento sejam concluídas.

O sistema de controle de versão do Elasticsearch permite que você use facilmente outro padrão chamado bloqueio otimista. Em vez de adquirir um bloqueio todas as vezes, você informa ao Elasticsearch qual versão do documento espera encontrar. Se o documento não foi alterado nesse meio tempo, sua operação foi bem-sucedida, sem bloqueio. Se algo tiver mudou no documento e ele tiver uma versão mais recente, o Elasticsearch o sinalizará para que você possa lidar com isso de forma adequada.

Voltando ao exemplo de votação do mecanismo de busca acima, é assim que funciona. Quando renderizamos uma página sobre o design de uma camiseta, anotamos a versão atual do documento. Isso é retornado com a resposta da solicitação get que fazemos para a página:

curl -XGET 'http://localhost:9200/designs/shirt/1'
{
"_index": "designs",
"_type": "shirt",
"_id": "1",
"_version": 4,
"exists": true,
"_source": {
"name": "elasticsearch",
"votes": 1002
}
}

Depois que o usuário tiver votado, poderemos instruir o Elasticsearch a indexar apenas o novo valor (1003) se nada tiver mudado nesse meio tempo: (observe o parâmetro da string de consulta version extra)

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=4' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

Internamente, tudo o que o Elasticsearch precisa fazer é comparar os dois números de versão. Isso é muito mais leve do que adquirir e liberar um bloqueio. Se ninguém tiver alterado o documento, a operação será bem-sucedida com um código de status de 200 OK. No entanto, se alguém tiver alterado o documento (aumentando assim seu número de versão interno), a operação falhará com um código de status de 409 Conflict. Nosso site agora pode responder corretamente. Ele recuperará o novo documento, aumentará a contagem de votos e tentará novamente usando o novo valor da versão. É bem provável que isso terá sucesso. Se não, simplesmente repetiremos o procedimento.

Esse padrão é tão comum que o endpoint de atualização do Elasticsearch pode fazer isso por você. Você pode definir o parâmetro retry_on_conflict para dizer a ele para repetir a operação em caso de conflitos de versão. É especialmente útil em combinação com uma atualização com script. Por exemplo, este cURL dirá ao Elasticsearch para tentar atualizar o documento até cinco vezes antes de falhar:

curl -XPOST 'http://localhost:9200/designs/shirt/1/_update?retry_on_conflict=5' -d'
{
"script" : "ctx._source.votes += 1"
}'

Observe que a verificação de controle de versão é totalmente opcional. Você pode escolher aplicá-la ao atualizar determinados campos (como votes) e ignorá-la quando atualizar outros (normalmente campos de texto, como name). Tudo depende dos requisitos da sua aplicação e de suas compensações.

Já tem um sistema de controle de versão instalado?

Ao lado de seu suporte interno, o Elasticsearch funciona bem com versões de documentos mantidas por outros sistemas. Por exemplo, você pode ter seus dados armazenados em outro banco de dados que mantém o controle de versão para você ou pode ter alguma lógica específica da aplicação que determina como você quer que o controle de versão se comporte. Nessas situações, você ainda pode usar o suporte para controle de versão do Elasticsearch, instruindo-o a usar um tipo de versão external. O Elasticsearch funcionará com qualquer sistema de controle de versão numérico (no intervalo 1:263-1), desde que seja garantido que ele aumentará a cada alteração no documento.

Para dizer ao Elasticssearch para usar um controle de versão externo, adicione um parâmetro version_type juntamente com o parâmetro version em todas as requisições que alterarem os dados. Por exemplo:

curl -XPOST 'http://localhost:9200/designs/shirt/1?version=526&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 1003
}'

Manter o controle de versão em outro lugar significa que o Elasticsearch não necessariamente sabe sobre cada alteração nele. Isso tem implicações sutis em como o controle de versão é implementado.

Considere o comando de indexação acima. Com controle de versão internal, significa “só indexar esta atualização de documento se sua versão atual for igual a 526”. Se a versão coincidir, o Elasticsearch aumentará em um e armazenará o documento. No entanto, com um sistema de controle de versão externo, esse será um requisito que não podemos impor. Talvez esse sistema de controle de versão não seja incrementado em um a cada vez. Talvez salte com números arbitrários (pense em um controle de versão baseado no tempo). Ou talvez seja difícil comunicar cada alteração de versão ao Elasticsearch. Por todas essas razões, o suporte de controle de versão external se comporta de maneira ligeiramente diferente.

Com version_type definido como external, o Elasticsearch armazenará o número da versão conforme fornecido e não o incrementará. Além disso, em vez de verificar se há uma correspondência exata, o Elasticsearch só retornará um erro de colisão de versão se a versão atualmente armazenada for maior ou igual à do comando de indexação. Isso significa efetivamente “somente armazene essas informações se ninguém mais tiver fornecido a mesma versão ou uma versão mais recente nesse meio tempo”. Concretamente, a solicitação acima será bem-sucedida se o número da versão armazenada for menor que 526. 526 e acima farão com que a solicitação falhe.

Importante: ao usar o controle de versão external, lembre-se de sempre adicionar a version atual (e version_type ) a qualquer chamada de indexação, atualização ou exclusão. Se você esquecer, o Elasticsearch usará seu sistema interno para processar essa solicitação, o que fará com que a versão seja incrementada erroneamente.

Algumas palavras finais sobre exclusões.

A exclusão de dados é problemática para um sistema de controle de versão. Depois que os dados desaparecem, não há como o sistema saber corretamente se as novas solicitações são datadas ou realmente contêm novas informações. Por exemplo, digamos que executamos o seguinte para excluir um registro:

curl -XDELETE 'http://localhost:9200/designs/shirt/1?version=1000&version_type=external'

Essa operação de exclusão era a versão 1000 do documento. Se simplesmente jogarmos fora tudo o que sabemos sobre isso, uma solicitação a seguir fora de sincronia fará a coisa errada:

curl -XPOST 'http://localhost:9200/designs/shirt/1/?version=999&version_type=external' -d'
{
"name": "elasticsearch",
"votes": 3001
}'

Se esquecêssemos que o documento existiu, simplesmente aceitaríamos essa chamada e criaríamos um novo documento. No entanto, a versão da operação (999) na verdade nos diz que esta é uma notícia antiga e o documento deve permanecer excluído.

Fácil, você pode dizer — não exclua realmente tudo, mas continue lembrando as operações de exclusão, os ids de documentos a que se referem e sua versão. Embora isso realmente resolva o problema, vem com um preço. Em breve esgotaremos os recursos se as pessoas indexarem documentos repetidamente e depois excluí-los.

A busca do Elasticsearch encontra um equilíbrio entre os dois. Ela mantém registros de exclusões, mas os esquece depois de um minuto. Isso é chamado de coleta de lixo de exclusão. Para a maioria dos casos de uso prático, 60 segundos são suficientes para o sistema recuperar o atraso e para que as solicitações atrasadas cheguem. Se isso não funcionar para você, você poderá alterar definindo index.gc_deletes no seu índice com algum outro intervalo de tempo.