Como escalar a taxa de transferência do PgBouncer em 4x
(clickhouse.com)- O ClickHouse Managed Postgres opera por padrão com uma frota de múltiplos processos proporcional ao número de núcleos para evitar a limitação de um PgBouncer single-thread ficar preso a um único core de CPU
- Todos os processos fazem bind na mesma porta com
so_reuseport, e o kernel distribui as novas conexões, então para o cliente isso parece um único endpoint - Como requisições de cancelamento do Postgres entram por uma conexão separada e podem chegar a um processo que não mantém a sessão, usam pareamento entre processos para encaminhá-las ao verdadeiro dono da sessão
- Em um
c7i.4xlargede 16 vCPUs, um único processo atingiu pico de cerca de 87.000 TPS, mas uma frota de 16 processos chegou a cerca de 336.000 TPS, registrando aproximadamente 4x mais taxa de transferência - Com poucas conexões, um único processo pode ser equivalente ou um pouco mais rápido, mas em alta concorrência um core vira gargalo, então é preciso dividir o orçamento de conexões entre toda a frota para escalar a taxa de transferência e o limite de conexões sem sobreconectar o Postgres
O gargalo de CPU criado por um único processo
- O PgBouncer é single-thread, então independentemente do número de CPUs do sistema, um processo usa apenas um único core
- Mesmo em um servidor com 16 vCPUs, o trabalho de connection pooling fica concentrado em um core e os outros 15 ficam ociosos
- Por isso, o pooler limita a taxa de transferência total antes que a capacidade de processamento do Postgres se esgote
- O ClickHouse Managed Postgres opera uma frota de processos que define o número de processos do PgBouncer proporcionalmente ao número de cores disponíveis
Distribuição de conexões com so_reuseport
- Todos os processos da frota ativam
so_reuseporte fazem bind na mesma porta - O kernel distribui as conexões recebidas entre os processos, então o cliente se conecta a um único endpoint sem precisar saber que vários PgBouncers estão rodando por trás
- A documentação do PgBouncer também recomenda
so_reuseportcomo forma de usar vários cores com múltiplos processos single-thread
Pareamento entre processos para garantir cancelamento de consultas
- As requisições de cancelamento do Postgres chegam com a chave de cancelamento por uma nova conexão separada da conexão da consulta em execução
- Ao usar
so_reuseport, o kernel pode entregar a nova conexão de cancelamento a um processo diferente daquele que mantém a sessão correspondente- Se o processo que recebeu a requisição não conhecer aquela consulta, o cancelamento não será executado
- Ao fazer pareamento entre os processos do PgBouncer, é possível encaminhar requisições que chegaram ao processo errado para o processo que realmente detém a sessão
- Com isso, o cancelamento de consultas funciona corretamente em toda a frota, independentemente de qual processo receba a requisição
Pooling de transações e orçamento de conexões
- O pooling opera em modo de transação, e assim que a transação é confirmada, a conexão com o servidor volta ao pool
max_client_connemax_db_connectionssão divididos pelo número de processos e atribuídos a cada processo- Ao dividir o orçamento de conexões, é possível manter o total de conexões da frota dentro do limite seguro do Postgres enquanto se aumenta o teto agregado de conexões
- Um único processo rejeita novos clientes que excedam seu próprio
max_client_conncom o seguinte erroFATAL: no more connections allowed (max_client_conn)
Benchmark no mesmo hardware
- As duas configurações foram medidas no mesmo ambiente AWS EC2
- Servidor do pooler:
c7i.4xlargecom 16 vCPUs - Postgres: servidor separado
- Gerador de carga:
pgbenchem um terceiro servidor - Carga de trabalho: somente leitura (select-only), em modo de transaction pooling
- Servidor do pooler:
- A comparação foi entre o PgBouncer em processo único e uma frota de 16 processos, com o mesmo tipo de instância, o mesmo Postgres e a mesma carga de trabalho
- O número de conexões cliente foi aumentado de 8 para 256, medindo a taxa de transferência e o uso de CPU do servidor de 16 cores
| Clientes | TPS processo único | CPU do servidor processo único | TPS da frota | CPU do servidor da frota |
|---|---|---|---|---|
| 8 | 8,910 | 0.8% | 6,450 | 2.9% |
| 32 | 54,203 | 5.2% | 64,244 | 12.3% |
| 64 | 86,570 | 8.3% | 219,439 | 31.9% |
| 128 | 83,463 | 8.1% | 320,547 | 45.9% |
| 256 | 76,893 | 7.7% | 336,469 | 48.9% |
Resultados de taxa de transferência e uso de CPU
- O processo único atingiu pico em cerca de 87.000 TPS e depois perdeu desempenho conforme a carga aumentou, registrando cerca de 77.000 TPS com 256 clientes
- A frota de 16 processos aumentou até cerca de 336.000 TPS ao aproveitar mais cores, mostrando aproximadamente 4x mais taxa de transferência que o processo único
- Em medições com
pidstat, um único processo do PgBouncer usou cerca de 97% de CPU sob carga, preenchendo praticamente um core inteiro, enquanto o uso total do servidor de 16 vCPUs ficou abaixo de 10% - A frota distribuiu o trabalho pelo servidor inteiro e usou cerca de 8 cores, ainda com folga restante quando o Postgres e o gerador de carga atingiram seus limites
- Na medição mantendo 256 clientes, o servidor com processo único sustentou cerca de 9% de CPU, enquanto o servidor da frota sustentou cerca de 52% de CPU
- Em medições externas do EC2 CloudWatch, a CPUUtilization média da instância de processo único ficou em cerca de 16%, enquanto a da frota ficou em cerca de 60%
- Os números do CloudWatch são um pouco mais altos que as medições internas ao guest, mas a diferença continua a mesma: um único PgBouncer não consegue aproveitar a maior parte das 16 vCPUs
Escolha da configuração conforme a concorrência
- Quando o número de conexões é baixo, não há trabalho suficiente para paralelizar e as conexões da frota ficam distribuídas entre vários processos, então um único processo pode ser suficiente ou até ligeiramente mais rápido
- Com 8 clientes, o processo único registrou 8.910 TPS, contra 6.450 TPS da frota
- À medida que a concorrência aumenta, o único core ocupado pelo processo único vira a parede de taxa de transferência, e a diferença de desempenho para a frota cresce
- Em ambientes onde o Postgres atinge seu limite antes do PgBouncer, um único processo também é um padrão adequado
- Quando o pooler começa a limitar a taxa de transferência, é preciso combinar uma frota de processos alinhada ao número de cores, compartilhamento de porta com
so_reuseporte pareamento entre processos para eliminar o gargalo - Todos os servidores do ClickHouse Managed Postgres oferecem essa configuração por padrão
1 comentários
Opiniões no Hacker News
É só usar https://github.com/yandex/odyssey. Odyssey é um PgBouncer escalável
Temos muitos clientes que escalaram conexões do PostgreSQL para mais de 10 mil, e pretendemos avaliar alternativas como Odyssey ou pgdog no futuro. Dito isso, pessoalmente não gosto da abordagem de ter mais de 10 mil conexões no PostgreSQL, e acho que dá para escalar bem com apenas algumas centenas
Ao operar o PgBouncer no Kubernetes, é fácil tanto subir vários processos em uma máquina quanto distribuí-los por várias máquinas
Como o Azure costuma causar falhas sequenciais em todo o conjunto de servidores durante a manutenção de VMs, uma configuração com múltiplas máquinas é especialmente útil
Entendo o conceito de peering mencionado aqui, mas nunca o usei no PostgreSQL
A) Existe algum modo ou configuração no PostgreSQL para montar isso facilmente? Imagino algo como encaminhar solicitações de cancelamento sequencialmente para os peers até não dar erro, ou a solicitação de cancelamento conter metadados que permitam ao processo que a recebeu por engano identificar o processo correto
B) Se todos os processos do PostgreSQL recebem solicitações de clientes com
so_reuseport, parece que seria necessário algum meio separado de comunicação entre processos (IPC) para comunicação entre peers; que mecanismo é usado na prática?Parece relativamente simples de implementar incluindo o ID do processo do PgBouncer na chave de cancelamento
Dá para consultar a partir do slide 26: https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...
Também há uma apresentação em vídeo do Jelte, mantenedor do PgBouncer: https://www.youtube.com/watch?v=X-nCHcZ6vQU
Estou usando pgdog e ele atende muito bem ao que preciso
Fico me perguntando por que o PgBouncer precisa se preocupar até com solicitações de cancelamento. Não bastaria repassá-las ao PostgreSQL e, quando o PostgreSQL retornasse um erro em vez de uma resposta normal para a consulta cancelada, o PgBouncer responsável por aquela conexão trataria disso?
O PgBouncer fornece ao cliente um PID de cancelamento e um valor secreto falsos, mas rastreáveis; quando a solicitação de cancelamento volta, ele encontra e encaminha o PID e o valor secreto reais do servidor/processo. Além disso, pode haver situações em que o cliente acredita que a consulta ainda está em execução, mas o PgBouncer sabe que ela já terminou e a conexão foi reutilizada; portanto, é preciso verificar se aquela conexão e aquele PID ainda estão executando aquela consulta
Isso é para controlar o acesso ao servidor PostgreSQL por meio de um pool de conexões em microsserviços? Para um backend monolítico, não parece necessário
A maioria dos bons frameworks de backend já tem pool de conexões embutido, e isso resolve 98% dos casos de uso em que microsserviços não são necessários nem recomendados
O PgBouncer é um excelente software. Uso bastante e ele simplifica muito a operação de bancos de dados
Acabei de conhecer
so_reuseporte achei interessante. O ponto central da configuração parece ser isso e o peering; o peering é embutido no PgBouncer e fácil de configurar?Também dá para usar peering no Kubernetes? Nesse ambiente não deveria ser necessário reutilizar portas, e também fico curioso se cada Pod passa a ter seu próprio pool de conexões e operar de forma independente
Tenho 46 anos agora, e ainda me lembro de ter ficado chocado, aos 23, com o modelo pesado de conexões do PostgreSQL. Isso não melhorou desde então?
Ainda assim, sem usar pool de conexões, o PostgreSQL precisa fazer fork de processos, então criar uma nova conexão sempre custa dezenas de milissegundos ou mais. Aplicações escritas sem pool de conexões também não são ideais, mas existem com frequência
Os frameworks de aplicação também mudaram, e arquiteturas serverless podem criar dezenas de milhares de conexões, ponto em que o PostgreSQL começa a sofrer. Pessoalmente, não gosto de ter mais que algumas centenas de conexões, mas hoje isso é uma situação bastante realista
No processo de garantir compatibilidade com Java, muitas empresas tiveram que mudar bastante sua forma de lidar com concorrência. PostgreSQL e SQLite também foram projetados em uma época parecida, mas eram sistemas criados por veteranos da indústria que já implantavam sistemas de alta carga antes dessas discussões sobre threading surgirem, e que ainda precisavam dar suporte a usuários de hardware antigo