Tarantool Cartridge: particionamento do backend Lua em três linhas

Tarantool Cartridge: particionamento do backend Lua em três linhas

Na Mail.ru Group, temos o Tarantool — um servidor de aplicativos em Lua que também funciona como um banco de dados (ou vice-versa?). Ele é rápido e excelente, mas as capacidades de um único servidor ainda não são ilimitadas. A escalabilidade vertical também não é a panaceia, portanto, o Tarantool possui ferramentas para escalabilidade horizontal — o módulo vshard. [1]. Ele permite shardificar dados em vários servidores, mas você terá que se esforçar para configurá-lo e integrar a lógica de negócios.

Boas notícias: nós enfrentamos desafios (por exemplo, [2], [3]) e criamos um novo framework que simplifica muito a resolução desse problema.

Tarantool Cartridge — é um novo framework para o desenvolvimento de sistemas distribuídos complexos. Ele permite focar na escrita da lógica de negócios em vez de resolver problemas de infraestrutura. Abaixo, vou explicar como esse framework funciona e como usá-lo para escrever serviços distribuídos.

Mas qual é, na verdade, o problema?

Temos o Tarantool, temos o vshard — o que mais poderíamos desejar?

Primeiramente, trata-se de conveniência. A configuração do vshard é ajustada por meio de tabelas Lua. Para que um sistema distribuído de vários processos Tarantool funcione corretamente, a configuração deve ser idêntica em todos os lugares. Ninguém quer fazer isso manualmente. Portanto, entram em cena scripts de diversos tipos, Ansible, sistemas de implantação.

O Cartridge gerencia automaticamente a configuração do vshard, fazendo isso com base em sua própria configuração distribuída.Essencialmente, trata-se de um simples arquivo YAML, cuja cópia é armazenada em cada instância do Tarantool. A simplificação está no fato de que o framework monitora sua configuração e garante que ela seja consistente em todos os lugares.

Em segundo lugar, novamente, trata-se de conveniência. A configuração do vshard não tem relação alguma com o desenvolvimento da lógica de negócios e apenas distrai o programador de seu trabalho. Quando discutimos a arquitetura de um determinado projeto, geralmente falamos sobre componentes individuais e suas interações. É cedo demais para pensar na implantação de um cluster em 3 data centers.

Enfrentamos esses problemas repetidamente e, em algum momento, conseguimos desenvolver uma abordagem que simplifica o trabalho com o aplicativo durante todo o seu ciclo de vida: criação, desenvolvimento, teste, CI/CD, manutenção.

O Cartridge introduz o conceito de papéis para cada processo do Tarantool. Papéis são uma ideia que permite ao desenvolvedor focar na escrita de código. Todos os papéis existentes em um projeto podem ser executados em uma única instância do Tarantool, e para testes, isso é suficiente.

Principais recursos do Tarantool Cartridge:

  • orquestração automatizada de clusters;
  • expansão da funcionalidade do aplicativo com novos papéis;
  • modelo de aplicativo para desenvolvimento e implantação;
  • particionamento automático embutido;
  • integração com o framework de testes Luatest;
  • gerenciamento de cluster através do WebUI e API;
  • ferramentas de empacotamento e deploy.

Olá, Mundo!

Mal posso esperar para mostrar o framework, então vamos deixar a explicação sobre a arquitetura para depois e começar com o básico. Supondo que o Tarantool já esteja instalado, resta apenas fazer

$ tarantoolctl rocks install cartridge-cli
$ export PATH=$PWD/.rocks/bin/:$PATH

Esses dois comandos instalarão as utilitários de linha de comando e permitirão criar seu primeiro aplicativo a partir do modelo:

$ cartridge create --name myapp

E aqui está o que iremos obter:

myapp/
├── .git/
├── .gitignore
├── app/roles/custom.lua
├── deps.sh
├── init.lua
├── myapp-scm-1.rockspec
├── test
│   ├── helper
│   │   ├── integration.lua
│   │   └── unit.lua
│   ├── helper.lua
│   ├── integration/api_test.lua
│   └── unit/sample_test.lua
└── tmp/

Este é um repositório git com um aplicativo "Hello, World!" pronto. Vamos tentar executá-lo imediatamente, instalando as dependências (incluindo o próprio framework):

$ tarantoolctl rocks make
$ ./init.lua --http-port 8080

Assim, temos uma node do futuro aplicativo shardado em execução. Um curioso pode imediatamente abrir a interface web, configurar o cluster de um único nó com o mouse e desfrutar do resultado, mas a alegria deve ser contida por enquanto. Por enquanto, o aplicativo não consegue fazer nada útil, então falarei sobre o deploy depois, e agora é hora de escrever código.

Desenvolvimento de aplicativos

Imagine que estamos projetando um sistema que deve receber dados, armazená-los e construir um relatório diariamente.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Começamos a desenhar o esquema e colocamos três componentes nele: gateway, storage e scheduler. Vamos aprofundar na arquitetura. Como estamos usando o vshard como armazenamento, adicionamos ao esquema o vshard-router e o vshard-storage. Nem o gateway, nem o scheduler se conectarão ao armazenamento diretamente; para isso, temos o roteador, que foi criado para esse propósito.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Este esquema ainda não reflete com precisão o que vamos criar no projeto, pois os componentes parecem abstratos. Precisamos analisar como isso será projetado no Tarantool real — agruparemos nossos componentes por processos.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Manter o vshard-router e o gateway em instâncias separadas não faz muito sentido. Por que precisamos percorrer a rede, se isso já faz parte das responsabilidades do roteador? Eles devem ser executados dentro de um único processo. Ou seja, no mesmo processo serão inicializados tanto o gateway quanto o vshard.router.cfg, permitindo que eles interajam localmente.

Na fase de design, trabalhar com três componentes era conveniente, mas, como desenvolvedor, enquanto escrevo o código, não quero me preocupar em executar três instâncias do Tarantool. Preciso rodar testes e verificar se escrevi o gateway corretamente. Ou, talvez, eu queira demonstrar uma funcionalidade para colegas. Por que devo me incomodar em implantar três instâncias? Assim nasceu o conceito de papéis. Um papel é um módulo Lua comum, cujo ciclo de vida é gerenciado pelo Cartridge. Neste exemplo, existem quatro — gateway, router, storage, scheduler. Em outro projeto, pode haver mais. Todos os papéis podem ser executados em um único processo, e isso será suficiente.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Quando se trata de implantações em staging ou em produção, atribuíremos a cada processo do Tarantool seu próprio conjunto de papéis, dependendo das capacidades de hardware:

Tarantool Cartridge: particionamento do backend Lua em três linhas

Gerenciamento de topologia

As informações sobre onde quais papéis estão em execução precisam ser armazenadas em algum lugar. E esse 'algum lugar' é a configuração distribuída que já mencionei acima. O mais importante nela é a topologia do cluster. Aqui estão mostrados 3 grupos de replicação de 5 processos Tarantool:

Tarantool Cartridge: particionamento do backend Lua em três linhas

Não queremos perder dados, por isso lidamos com cautela com as informações sobre os processos em execução. O Cartridge monitora a configuração usando um commit de duas fases. Assim que queremos atualizar a configuração, ele primeiro verifica a disponibilidade de todas as instâncias e sua prontidão para aceitar a nova configuração. Depois disso, na segunda fase, a configuração é aplicada. Dessa forma, mesmo que uma instância fique temporariamente indisponível, nada de grave acontecerá. A configuração simplesmente não será aplicada e você verá um erro antecipadamente.

Na seção de topologia também é indicado um parâmetro importante, que é o líder de cada grupo de replicação. Normalmente, este é a instância na qual as gravações são feitas. As outras são geralmente somente leitura, embora possam haver exceções. Às vezes, desenvolvedores ousados não têm medo de conflitos e podem gravar dados em várias réplicas simultaneamente, mas há algumas operações que, independentemente das circunstâncias, não devem ser executadas duas vezes. Para isso, existe a designação de líder.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Vida das funções

Para que uma função abstrata possa existir em tal arquitetura, o framework deve de alguma forma gerenciá-la. Claro, a gestão ocorre sem reiniciar o processo Tarantool. Existem 4 callbacks para gerenciar as funções. O Cartridge irá chamá-los dependendo do que está escrito na configuração distribuída, aplicando assim a configuração a funções específicas.

function init()
function validate_config()
function apply_config()
function stop()

Cada função tem uma função init. Ela é chamada uma vez, seja ao ligar a função ou ao reiniciar o Tarantool. É conveniente, por exemplo, inicializar box.space.create, ou o scheduler pode iniciar algum fiber em segundo plano, que realizará trabalho em determinados intervalos de tempo.

Uma função init pode não ser suficiente. O Cartridge permite que as funções utilizem a configuração distribuída que ele usa para armazenar a topologia. Podemos declarar uma nova seção nessa mesma configuração e armazenar nela um fragmento da configuração de negócios. No meu exemplo, pode ser um esquema de dados ou configurações de agendamento para a função scheduler.

O cluster chama validate_config e apply_config sempre que a configuração distribuída é alterada. Quando a configuração é aplicada através de um commit de duas fases, o cluster verifica se cada função está pronta para aceitar essa nova configuração e, se necessário, informa ao usuário sobre erros. Quando todos concordam que a configuração está correta, é executada apply_config.

As funções também têm um método stop, que é necessário para limpar os resultados da atividade da função. Se dissermos que o scheduler neste servidor não é mais necessário, ele pode parar os fibers que iniciou. init.

Os papéis podem interagir entre si. Estamos acostumados a escrever chamadas de funções em Lua, mas pode acontecer de não haver o papel necessário neste processo. Para facilitar as chamadas pela rede, usamos um módulo auxiliar rpc (chamada de procedimento remoto), que é baseado no netbox padrão, integrado ao Tarantool. Isso pode ser útil, por exemplo, se o seu gateway quiser solicitar diretamente ao scheduler que execute uma tarefa imediatamente, em vez de esperar um dia.

Outro ponto importante é garantir a tolerância a falhas. Para monitorar a saúde, o Cartridge utiliza o protocolo SWIM [4]. Resumindo, os processos trocam "rumores" entre si via UDP — cada processo informa seus vizinhos sobre as últimas novidades, e eles respondem. Se, por acaso, a resposta não chegar, o Tarantool começa a suspeitar de algo errado e, após algum tempo, decreta a morte e começa a informar a todos ao redor sobre essa notícia.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Com base nesse protocolo, o Cartridge organiza o tratamento automático de falhas. Cada processo monitora seu ambiente, e se o líder de repente parar de responder, a réplica pode assumir seu papel, enquanto o Cartridge configura adequadamente os papéis em execução.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Aqui é preciso ter cuidado, pois a mudança frequente de papéis pode levar a conflitos de dados durante a replicação. Ativar o failover automático aleatoriamente, evidentemente, não é uma boa ideia. É necessário entender claramente o que está acontecendo e ter certeza de que a replicação não falhará após a recuperação do líder, quando ele reassumir seu papel.

De tudo o que foi dito, pode-se ter a impressão de que os papéis são semelhantes a microsserviços. De certa forma, eles são, mas como módulos dentro dos processos do Tarantool. No entanto, existem algumas diferenças fundamentais. Primeiro, todos os papéis do projeto devem viver na mesma base de código. E todos os processos do Tarantool devem ser iniciados a partir de uma única base de código, para evitar surpresas, como quando tentamos inicializar um scheduler, mas ele simplesmente não existe. Também não se deve permitir diferenças nas versões do código, pois o comportamento do sistema em tal situação é muito difícil de prever e depurar.

Diferente do Docker, não podemos simplesmente pegar a "imagem" de um papel, levá-la para outra máquina e executá-la lá. Nossos papéis não são tão isolados quanto os contêineres Docker. Além disso, não podemos executar duas instâncias do mesmo papel em uma única instância. Um papel ou existe ou não existe; de certa forma, é um singleton. Por último, dentro de todo o grupo de replicação, os papéis devem ser idênticos, porque, caso contrário, seria absurdo — os dados são os mesmos, mas a configuração é diferente.

Ferramentas de implantação

Eu prometi mostrar como o Cartridge ajuda a implantar aplicações. Para facilitar a vida dos outros, o framework empacota pacotes RPM:

$ cartridge pack rpm myapp -- empacota para nós .\/myapp-0.1.0-1.rpm
$ sudo yum install .\/myapp-0.1.0-1.rpm

O pacote instalado contém quase tudo o que é necessário: tanto a aplicação quanto as dependências Lua instaladas. O Tarantool no servidor também será incluído como dependência do pacote RPM, e nosso serviço estará pronto para iniciar. Isso é feito através do systemd, mas primeiro é preciso escrever um pouco de configuração. Pelo menos, devemos especificar o URI de cada processo. Três de exemplo são suficientes.

$ sudo tee \/etc\/tarantool\/conf.d\/demo.yml <<CONFIG
myapp.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
myapp.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
myapp.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
CONFIG

Aqui há um detalhe interessante. Em vez de especificar apenas a porta do protocolo binário, especificamos o endereço público do processo completo, incluindo o hostname. Isso é necessário para que os nós do cluster saibam como se conectar uns aos outros. É uma má ideia usar o endereço 0.0.0.0 como advertise_uri; deve ser o endereço IP externo, e não um socket de bind. Sem isso, nada funcionará, então o Cartridge simplesmente não permitirá que um nó seja iniciado com um advertise_uri incorreto.

Agora que a configuração está pronta, podemos iniciar os processos. Como uma unidade systemd comum não permite iniciar mais de um processo, as aplicações no Cartridge utilizam o que são chamados de unidades instanciadas, que funcionam assim:

$ sudo systemctl start myapp@router
$ sudo systemctl start myapp@storage_A
$ sudo systemctl start myapp@storage_B

Na configuração, especificamos a porta HTTP na qual o Cartridge serve a interface web — 8080. Vamos acessá-la e dar uma olhada:

Tarantool Cartridge: particionamento do backend Lua em três linhas

Observamos que os processos estão em execução, mas ainda não estão configurados. O cartucho ainda não sabe quem deve replicar com quem e não pode tomar a decisão sozinho, então aguarda nossas ações. E nossa escolha não é grande: a vida do novo cluster começa com a configuração do primeiro nó. Depois adicionaremos os outros ao cluster, designaremos seus papéis, e a implantação pode ser considerada concluída com sucesso.

Vamos servir uma caneca da nossa bebida favorita e relaxar após uma longa semana de trabalho. O aplicativo pode ser utilizado.

Tarantool Cartridge: particionamento do backend Lua em três linhas

Resultados

E quais são as conclusões? Experimente, use, deixe seu feedback e abra tickets no GitHub.

Links

[1] Tarantool » 2.2 » Referência » Referência de Rocks » Módulo vshard

[2] Como implementamos o núcleo do negócio de investimentos do Alfa-Bank com base no Tarantool

[3] Arquitetura de billing de nova geração: transformação com a migração para Tarantool

[4] SWIM — protocolo de construção de cluster

[5] GitHub — tarantool/cartridge-cli

[6] GitHub — tarantool/cartridge

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster