Escolhendo DB pool_size para um aplicativo Flask-SQLAlchemy em execução no Gunicorn

8

Eu tenho um aplicativo Flask-SQLAlchmey em execução no Gunicorn conectado a um banco de dados PostgreSQL, e estou tendo problemas para descobrir qual pool_sizedeve ser o valor e quantas conexões de banco de dados devo esperar.

Esta é minha compreensão de como as coisas funcionam:

  • Os processos no Python 3.7 NÃO compartilham memória
  • Cada trabalhador Gunicorn é seu próprio processo
  • Portanto, cada trabalhador Gunicorn obterá sua própria cópia do pool de conexão com o banco de dados e não será compartilhado com nenhum outro trabalhador
  • Threads no Python compartilham memória
  • Portanto, quaisquer threads em um trabalhador do Gunicorn compartilharão um pool de conexão com o banco de dados

Isso está correto até agora? Se isso estiver correto, para um aplicativo Flask síncrono em execução no Gunicorn:

  • O número máximo de conexões com o banco de dados é = (número de trabalhadores) * (número de threads por trabalhador)?
  • E dentro de um trabalhador, ele utilizará mais conexões de um pool do que os trabalhadores?

Existe uma razão pela qual pool_sizedeve ser maior que o número de threads? Então, para um aplicativo gunicorn lançado com gunicorn --workers=5 --threads=2 main:appdeveria pool_sizeser 2? E se eu estiver usando apenas trabalhadores, e não usando threads, existe algum motivo para ter um pool_sizemaior que 1?

Joshmaker
fonte

Respostas:

3

Adicionando meus 2 centavos. Sua compreensão está correta, mas algumas considerações a serem consideradas:

  • caso seu aplicativo esteja vinculado a IO (por exemplo, conversando com o banco de dados), você realmente deseja ter mais de 1 thread. Caso contrário, sua CPU nunca alcançará 100% de utilização. Você precisa experimentar o número de threads para obter o valor correto, geralmente com a ferramenta de teste de carga e comparando solicitações por segundo e utilização da CPU.

  • Tendo em mente a relação entre número de trabalhadores e conexões, você pode ver que, ao alterar o número de trabalhadores, será necessário ajustar o tamanho máximo do pool. Isso pode ser fácil de esquecer, então talvez uma boa idéia seja definir o tamanho da piscina um pouco acima do número de trabalhadores, por exemplo, duas vezes esse número.

  • O postgresql cria um processo por conexão e pode não ter uma boa escala, quando você terá muitos processos gunicorn. Eu iria com algum pool de conexão que fica entre o aplicativo e o banco de dados (o pgbouncer é o mais popular, eu acho).

matino
fonte
3

Apenas adicionando algumas das minhas experiências recentes à resposta do @ matino . Os aplicativos WSGI também podem se beneficiar de trabalhadores assíncronos. Vou acrescentar alguns pontos sobre async workerse connection poolsaqui.

Recentemente, enfrentamos alguns problemas semelhantes em nossa produção. Nosso tráfego aumentou rapidamente em 1-2 dias e todos os pedidos foram obstruídos por algum motivo. Estávamos usando gunicorn comgevent trabalhadores assíncronos para nossa djangoaplicação. Acabou que as conexões psql estavam sendo o motivo de muitas solicitações ficarem paradas (e eventualmente atingirem o tempo limite).

O número sugerido de solicitações simultâneas é(2*CPU)+1 . Portanto, em um cenário de sincronização, seus cálculos seriam como:(workers_num * threads_num) <= (2 * cores_num) + 1

E você obterá o (workers_num * threads_num)máximo de conexões com seu banco de dados. (digamos, todos os pedidos têm consultas de banco de dados). Portanto, você precisará definir sua pool_sizeconfiguração psql para algo maior que esse número. Mas quando você usa trabalhadores assíncronos, os cálculos serão um pouco diferentes. Veja este comando gunicorn:

gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 django:app

Nesse caso, o número máximo de solicitações simultâneas pode receber 3000solicitações. Portanto, você deve definir seupool_size para algo maior que 3000. Se seu aplicativo estiver vinculado a E / S, você obterá um melhor desempenho com trabalhadores assíncronos. Dessa forma, você poderá utilizar sua CPU com mais eficiência.

E sobre o pool de conexões, quando você usa uma solução como essa PgBouncer, está se livrando da sobrecarga de abrir e fechar conexões o tempo todo. Portanto, isso não afetará sua decisão de definir sua pool_size. Os efeitos podem não ser perceptíveis em baixos tráfegos, mas serão necessários para lidar com taxas de tráfego mais altas.

nima
fonte
2

Eu diria que seu entendimento é muito bom. Os encadeamentos em um único trabalhador WSGI realmente compartilharão um conjunto de conexões; teoricamente, o número máximo de conexões com o banco de dados é (number of workers) * Nonde N = pool_size + max_overflow. (Não sei ao que Flask-SQLAlchemy define max_overflow, mas é uma parte importante da equação aqui - consulte a documentação do QueuePool para que isso significa.)

Na prática, se você apenas usar a Sessão com escopo de encadeamento fornecida pelo Flask-SQLAlchemy, terá no máximo uma conexão por encadeamento; portanto, se sua contagem de threads for menor que N, seu limite superior será realmente (number of workers) * (number of threads per worker).

Anthony Carapetis
fonte