Skip to content

minha solução para rinha de backend 2023 utilizando Nginx, Golang, Postgres, Redis, RabbitMQ, Docker e Vagrant

Notifications You must be signed in to change notification settings

DeveloperArthur/rinha-de-backend-2023

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Desafio

Fazer uma API com 2 instâncias sendo balanceada pelo Nginx (A estratégia de balanceamento para suas APIs pode ser do tipo round-robin ou fair distribution) e com banco de dados (MySQL, Postgres ou MongoDB), tudo isso deve rodar dentro de uma VM mínima de só 1.5 vCPU e 3GB de RAM, e deve suportar uma bateria de stress test de Gatling brutal em cima. Todos os componentes da solução devem rodar em containers Docker via docker-compose.

Link do desafio original: https://github.com/zanfranceschi/rinha-de-backend-2023-q3

Desenho da solução:

obj

Infraestrutura da solução

Todos os componentes da solução irão rodar dentro de uma VM do VirtualBox, provisionada pelo Vagrant:

obj

Dentro da VM, os componentes da solução irão rodar em containers Docker provisionados pelo docker-compose:

obj

Estratégias utilizadas para performance

Index

Problemas de desempenho, como lentidão, podem ser reduzidos em até 50% após criação de index, proporcionando um ganho significativo de desempenho.

Então como medida preventiva, para evitar problemas de desempenho, foram criados indexes no banco de dados na tabela pessoas para as colunas id e searchable, criei index nessas colunas pois são utilizadas em queries nas cláusuras WHERE

obj

Indexação de pesquisa textual

Eu tinha feito essa modelagem:

obj

Justamente pra poder executar essa query na busca por termo:

SELECT DISTINCT p.id, p.apelido, p.nome, p.nascimento
FROM pessoas p INNER JOIN stacks s
ON p.id = s.pessoa_foreign_key
WHERE p.apelido ILIKE '%termo%'
OR p.nome ILIKE '%termo%'
OR s.nome ILIKE '%termo%'

A query funciona mas não é a forma mais performática, muito pesada:

obj

Então segui a sugestão do Fabio Akita e do MrPowerGamerBR de utilizar indexação de pesquisa textual e refatorei meu código

Transformei stack em um array de string dentro da tabela Pessoa e removi a entidade Stack, criei um campo Searchable na model Pessoa e um método que popula esse campo:

func (pessoa *Pessoa) SetSearchable() {
    pessoa.Searchable = pessoa.Nome + pessoa.Apelido
    for _, s := range pessoa.Stack {
        pessoa.Searchable += s
    }
}

A performance melhorou consideravelmente:

obj

Fila & cache

Serviço de fila e cache costumam resolver 80% dos problemas de escalabilidade, portanto:

Foi utilizado serviço de fila com RabbitMQ nos endpoints de criação de pessoas, as instâncias irão enviar o payload para o serviço de fila, que irá enfileirar e gravar 1 pessoa de cada vez, evitando que o banco de dados seja sobrecarregado.

e foi utilizado serviço de cache com Redis nos endpoints de busca de pessoas por id e busca de pessoas por termo:

  • No caching por termo foi utilizado a estratégia Cache TTL, com expiração de 3 minutos, pois uma nova pessoa pode ser criada e não podemos retornar a lista com resultados inconsistentes/desatualizados.
  • No caching por id também foi utilizado a estratégia de Cache TTL, mas com expiração de 10 minutos, pois iremos gravar pessoas no caching logo após enfileirar, e o tempo de expiração não pode ser pequeno, pois isso pode gerar inconsistência/divergência de dados (explico melhor no próximo tópico).

Programação Paralela/Reativa não bloqueante

Como medida preventiva, para contornar a divergência de dados que será gerada devido a consistência eventual da fila, iremos gravar a pessoa no cache logo após enviar o payload para o serviço de fila, isso será feito para evitar que o usuário busque as informações da pessoa logo após cadastra-la e a API retorne 404 Pessoa não encontrada, isso pode acontecer se a pessoa ainda estiver na fila, ou seja, ainda não foi persistida no banco de dados.

E iremos utilizar programação paralela neste caso pois os processos de enfileirar pessoa e gravar pessoa no caching serão realizados ao mesmo tempo.

Least Connections Load Balancing

A estratégia de balanceamento utilizada no Nginx foi a "Least Connections", esta escolha foi feita visando melhorar a disponibilidade e performance das instâncias.

Essa estratégia busca distribuir solicitações para as instâncias de forma que a instância com o menor número de conexões ativas seja escolhida para receber a próxima solicitação.

Executando teste:

Para realizar a execução do teste, basta rodar os seguintes comandos:

cd rinha-de-backend-2023/app
vagrant init hashicorp/bionic64
vagrant up

O Vagrantfile vai iniciar a VM e realizar as configurações necessárias: criar imagem docker, executar o gatling etc

Quando o teste finalizar, execute os seguintes comandos:

vagrant ssh # para conferir qual foi o resultado
vagrant destroy # para destruir a VM

Resultado final:

Minha solução alcançou incríveis 386 inserções, com essa contagem eu ficaria em 51º lugar (penúltimo) na competição, eu ficaria abaixo do "leandronsp-bash" que fez 17 inserções, e acima de mim ficaria "wendryo" que fez 2.835 inserções.


Segunda chance

Decidi fazer algumas alterações na minha solução afim de conseguir um resultado melhor.

Desenho da nova solução

obj

Alterações para melhorar a performance

Remoção do serviço de caching Redis

Removi o Redis da solução pois no meu contexto não compensa utilizar caching, o tempo para gravar no Postgres é tão rápido quanto.

Configuração de campo como UNIQUE e remoção do serviço de fila RabbitMQ

Na primeira versão, toda vez que ia cadastrar um registro, eu fazia uma consulta no banco antes para ver se o apelido já estava em uso, realizei uma alteração para não fazer mais essas consultas toda vez antes do cadastro, alterei o campo apelido no banco para UNIQUE pra garantir a unicidade do apelido. Antes eu consultava o apelido no banco, e inseria assincronamente com RabbitMQ, e agora como não vou mais consultar no banco, não pode ser assíncrono... pois se tentar cadastrar um apelido já utilizado, o banco vai retornar erro, a API tem que tratar esse erro e retornar para o front na hora, então a inserção tem que ser síncrona, por esse motivo removi o RabbitMQ da solução, não será mais necessário.

Configuração de worker e pool de connections

Peguei a dica do @leandronsp nesse tweet: obj

E configurei o worker_connections do Nginx pra 256, na API deixei o connection pool de 15, e no Postgres max_connections pra 30:

Resultado final

Depois que fiz essas alterações na arquitetura, alcancei um resultado muito melhor do que o primeiro, a nova solução alcançou 26.586 inserções, com essa contagem eu ficaria em 16º lugar na competição, eu ficaria abaixo do "saiintbrisson" que fez 26.567 inserções, e acima de mim ficaria "rode" que fez 26.607 inserções.

Fui de 386 inserções para 26.586 inserções simplificando a arquitetura, isso economizou recursos e consequentemente melhorou o desempenho

obj obj obj obj

About

minha solução para rinha de backend 2023 utilizando Nginx, Golang, Postgres, Redis, RabbitMQ, Docker e Vagrant

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published