Skip to content

Conversation

filipedeschamps
Copy link
Owner

@filipedeschamps filipedeschamps commented Feb 23, 2022

Não sei até que ponto isso vai funcionar na vida real, mas subiu a estabilidade da aplicação para outro nível (mas só a vida real vai dizer se isso se sustenta 😂 ) e foi uma das coisas mais delicinha que eu programei até então.

Contexto: quem viu o último vídeo do TabNews, acompanhou que uma hora no meio do tutorial o banco engasgou, do nada, e isso "aparentemente" só estava acontecendo localmente e sem padrão algum (porque nunca aconteceu no CI). Pensei que era minha máquina e fiquei analisando o banco, número de conexões abertas e não encontrava nada... a aplicação do Next.js simplesmente engasgava, só que em paralelo eu conseguia conectar normalmente no banco usando um Client externo. No final das contas, era eu quem estava fechando por completo o Pool de conexões a todo momento, achando que eu estava apenas fechando a conexão de um Client específico e que foi aberta manualmente.

Então como funciona o pg nesse sentido (e pensando aqui toda estratégia de Pool com Postgres): sempre você está com um Client em mãos para trabalhar... mas que pode ser adquirido de duas formas: manualmente abrindo a conexão e com isso tendo em mãos um Client, ou pedindo para o Pool por um Client disponível que esteja lá na fila parado e aguardando ser utilizado. Eu pensei que você se conectava contra um Pool, mas não, ele só administra Clients que ele mesmo está abrindo e fechando.

E quando você precisa de um Client manual? Quando você faz uma transaction (que no Postgres deve sempre manter na mesma conexão) ou bibliotecas que pedem um Client conectado, como a node-pg-migrate que usamos, que por dentro usa uma transaction e passa pra você a responsabilidade de encerrar ela ao final.

Então antes eu pedia um Client e encerrava o Pool (porque ele estava vindo de lá). Agora não, você pode usar o getNewClient() para pegar um client e o database.query() vai continuar usando de forma transparente o Pool.

Mas o que trouxe "robustez" foi outra coisa

Parar de fechar o Pool foi um bug que eu introduzi por me confundir com a api do pg, mas deixar o database mais robusto quando você tem poucas conexões disponíveis foi feito através de uma estratégia de retry.

Tanto para pegar um Client manualmente ou do Pool, o código fica repetidamente pedindo por um novo Client, tentando, tentando, tentando (com máximo de 50 tentativas) e quando conseguir pegar um com sucesso, o código continua. E por continuar pode ser simplesmente retornar o Client, ou rodar uma query. Importante destacar que "rodar a query" fica fora das tentativas... a única coisa que se tenta é pegar um Client saudável.

import retry from 'async-retry';

...

async function tryToGetNewClientFromPool() {
  const clientFromPool = await retry(newClientFromPool, {
    retries: 50,
    minTimeout: 0,
    factor: 1.3,
  });

  return clientFromPool;

  async function newClientFromPool() {
    return await pool.connect();
  }
}

E isso pode ser usado no método de quey:

async function query(query, params) {
  let clientFromPool;

  try {
    clientFromPool = await tryToGetNewClientFromPool(); // isso só irá continuar quando pegar um Client saudável
    return await clientFromPool.query(query, params);
  } catch (error) {
    ...
  } finally {
    if (clientFromPool) {
      clientFromPool.release();
    }
  }
}

Testes de carga em localhost

Usei o ab pra tentar arrebentar aqui o serviço local tanto com 5 conexões disponíveis no Postgres, quanto 1 única conexão e a aplicação se comportou muito melhor do que eu esperava. A única forma que consegui estourar foi chegando no limite dos sockets do sistema operacional.

Fiquei muito feliz porque isso nos habilita a usar serviços mais baratos de Postgres (e que vem com uma quantidade pequena de conexões), com o agravante de estarmos num ambiente serverless e que pode abrir infinitas conexões. Mas novamente, só a vida real vai nos dizer o que vai acontecer.

Em paralelo, confere essa issue #196 onde o @andrefd17 deu duas dicas sensacionais de ferramentas pra teste de carga.

Número máximo de conexões

Agora o endpoint /api/v1/status retorna o número máximo de conexões do banco no campo max_connections. Vai ficar mais rápido e fácil saber o que de fato nosso provider de banco de dados está nos entregando:

{
  "updated_at": "2022-02-23T10:20:17-08:00",
  "dependencies": {
    "database": {
      "status": "healthy",
      "max_connections": 4,
      "opened_connections": 1
    }
  }
}

[edit]
Não pera, testando em staging está retornando "max_connections": 10000 😂 vou tentar isolar para o nosso banco, como fizemos em opened_connections.

[edit2]
Não tem jeito, não consigo isolar por user ou database, mas coloquei um TODO ali caso alguém saiba como pegar esse número em um banco de dados compartilhado.

[edit3]
Consegui:

    const maxConnectionsResult = await database.query(
      'SELECT rolconnlimit as max_connections FROM pg_roles WHERE rolname = $1;',
      [process.env.POSTGRES_USER]
    );

@vercel
Copy link

vercel bot commented Feb 23, 2022

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/tabnews/tabnews/CTenqbtidhwgG1wCpkR6xfDqJSUR
✅ Preview: https://tabnews-git-database-tabnews.vercel.app

@filipedeschamps
Copy link
Owner Author

Estou fazendo testes contra o ambiente de Preview (100 conexões em paralelo) e tanto o Pool quanto o Retry estão se comportando como esperado, porém algo não esperado obviamente aconteceu: por algum motivo, algumas queries ficam presas lá no serviço do banco de dados, olha só:

image

E quando isso acontece, fica ocupando as conexões e não tem tentativa que aguente, pois a Lambda é terminada depois de 60 segundos:

image

Se alguém tiver alguma sugestão do que fazer seria show 😍

@filipedeschamps
Copy link
Owner Author

Olha que massinha, eu consigo reproduzir o travamento das queries com 100 requests concorrentes, onde ao atingir 900 requests ele buga. Daí o Retry entrou em ação e nesse meio tempo eu fui lá e matei na mão uma query presa, e ele recuperou, pegou um Client e deu response adequado 🤝 👍

image

To tentando agora entender porque as queries travam quando o server fica atolado 👍

@filipedeschamps
Copy link
Owner Author

Saí agora pouco de um call com 3 pessoas fantásticas do Pagar.me: @gustavolivrare @lucianopf @grvcoelho

Esse call aconteceu da forma mais maluca e espontânea possível 😂 e a gente conseguiu melhorar esse PR para lidar com muito mais conexões do que estava antes e o segredo foi, por incrível que pareça, parar de usar o Pool e gerenciar manualmente o Client de conexão.

Num cenário serverless e distribuído como o da Vercel, junto com um banco que dá no máximo 5 conexões simultâneas, a pior escolha é usar o Pool, porque ele vai segurar as conexões para poder reaproveitar elas, (bloqueando conexões que poderia ser abertas por outras instâncias), e com o agravante de que a lamba em que tudo isto está sendo gerenciado é encerrada dentro de 60 segundos, aparentemente impedindo com que o Pool feche os clients abertos, deixando eles pendurados lá, como a gente viu no print abaixo. Então se o encerramento da lambda impede o Pool de fazer seu trabalho até ao final (que é desconectar os clients), você vai lotar suas conexões.

image

Então nesse cenário (e que não serve para quando você trabalha com instâncias menos efêmeras), acabou sendo muito melhor tentar abrir uma conexão, fazer o que for preciso, e fechar ela o mais rápido possível. Isso penaliza a performance, pois a todo momento precisamos abrir uma nova conexão, mas para esse cenário de conexões limitadas, ficou excelente. E como esse é o "worst-case scenario", daqui pra frente só melhora 👍

@filipedeschamps filipedeschamps merged commit 52f8b64 into main Feb 24, 2022
@filipedeschamps filipedeschamps deleted the database branch February 24, 2022 03:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant