Tecnologia

Como o ClickHouse escalou o PgBouncer para quadruplicar a vazão no Postgres

Com so_reuseport e peering, ClickHouse Managed Postgres supera gargalo de thread única do PgBouncer, elevando a vazão para 336k transações por segundo.

Compartilhar
Detalhe de servidores modernos com cabeamento de fibra óptica brilhante em rack de datacenter
Detalhe de servidores modernos com cabeamento de fibra óptica brilhante em rack de datacenter

O ClickHouse Managed Postgres implementou uma nova arquitetura de múltiplos processos para o PgBouncer que supera o gargalo histórico de execução em thread única desse utilitário, escalando a taxa de processamento para até 336.469 transações por segundo (TPS) em testes de estresse em instâncias de nuvem. Essa nova abordagem distribui de forma homogênea a carga de conexões entre múltiplos núcleos de processamento de servidores de alta performance, eliminando o ponto único de saturação de CPU que frequentemente limita o desempenho de bancos de dados Postgres sob cargas extremas de concorrência de clientes.

Detalhe de servidores modernos com cabeamento de fibra óptica brilhante em rack de datacenter
Foto: Hacker News

O problema central resolvido pela equipe liderada pelo engenheiro de software Kaushik Iska reside na natureza single-threaded do PgBouncer tradicional. Um único processo desse pooler de conexões consome integralmente apenas um único núcleo de CPU do servidor, independentemente de quantos núcleos estejam disponíveis na máquina física ou virtual hospedeira. Em um cenário com servidores modernos equipados com múltiplas vCPUs, essa restrição de thread única significa que um núcleo realiza todo o trabalho pesado de pooling de conexões enquanto todos os outros núcleos restantes permanecem completamente ociosos, limitando o rendimento muito antes que o próprio banco de dados Postgres esgote seus recursos de processamento.

O gargalo de núcleo único

A limitação do modelo single-threaded do PgBouncer manifesta-se de forma crítica quando o pooler é implantado em hardwares modernos de grande porte. Em uma máquina equipada com 16 vCPUs, por exemplo, o processo único do pooler é forçado a processar todas as conexões de entrada em um único core, deixando os outros quinze núcleos em estado de ociosidade absoluta. Sob esse modelo clássico, o gargalo operacional da infraestrutura de banco de dados deixa de ser o próprio mecanismo de armazenamento e execução de consultas do Postgres e passa a ser o próprio middleware de pooling de conexões, que atinge o seu teto de vazão de dados de forma precoce.

Para contornar essa barreira de hardware e maximizar o retorno sobre o investimento em servidores de alta capacidade, a engenharia do ClickHouse Managed Postgres optou por adotar uma arquitetura baseada em uma frota de processos individuais do PgBouncer executando simultaneamente de forma paralela. O tamanho dessa frota é dimensionado de maneira diretamente proporcional à quantidade de núcleos de processamento disponíveis na instância de máquina virtual do cliente, garantindo que toda a capacidade computacional contratada seja ativamente alocada para o gerenciamento de sessões do banco de dados.

Funcionamento do so_reuseport

O gerenciamento coordenado e unificado dessa frota de processos simultâneos exige um mecanismo de rede eficiente ao nível do sistema operacional Linux. Cada processo individual do PgBouncer na frota é configurado para fazer o bind exatamente na mesma porta lógica de rede do servidor hospedeiro utilizando a diretiva de socket conhecida como so_reuseport. Essa funcionalidade nativa do kernel do Linux possibilita que múltiplos processos independentes escutem e aceitem conexões de rede de forma simultânea e concorrente a partir de um mesmo ponto de entrada.

Com a diretiva so_reuseport ativa, o kernel do sistema operacional assume a responsabilidade de realizar o balanceamento de carga de rede, distribuindo de forma transparente todas as conexões TCP de clientes que chegam entre os múltiplos processos que compõem a frota do PgBouncer. Sob essa ótica de rede distribuída, os sistemas clientes e as aplicações externas continuam a se conectar a um único endpoint unificado de rede, sem nunca saberem que, por trás daquela porta lógica de comunicação, existe uma frota de múltiplos processos independentes gerenciando o tráfego de dados.

No entanto, a distribuição cega de conexões realizada pelo kernel do Linux utilizando so_reuseport introduz um problema técnico severo relacionado ao mecanismo de cancelamento de consultas do Postgres. Uma requisição de cancelamento de consulta enviada por um cliente chega ao servidor por meio de uma conexão de rede completamente nova e independente, que carrega consigo uma chave de cancelamento (cancel key) específica, totalmente separada da conexão TCP original que está executando a consulta ativa no banco de dados.

Devido à natureza do balanceamento aleatório promovido pelo so_reuseport, o kernel do Linux está livre para direcionar essa nova conexão contendo a requisição de cancelamento para qualquer um dos processos da frota do PgBouncer. Se a requisição de cancelamento for parar em um processo do pooler que não seja aquele que está mantendo a sessão original da consulta, esse processo receptor falhará em identificar a chave, o cancelamento da query não será executado e a operação original continuará consumindo recursos preciosos do banco de dados indefinidamente.

A solução por peering

Para contornar esse comportamento indesejado de cancelamentos órfãos de rede, a equipe de desenvolvimento implementou uma mecânica de pareamento (peering) entre os processos. Esse mecanismo de peering faz com que todos os processos que compõem a frota do PgBouncer passem a se comunicar de forma interna e coordenada, mantendo plena consciência do estado de sessões de seus processos vizinhos.

Com essa interconexão ativa, no momento em que uma requisição de cancelamento de query do Postgres é recebida pelo processo incorreto da frota, o processo receptor identifica que não é o proprietário daquela chave de cancelamento específica e, utilizando o canal de peering, encaminha a requisição de forma imediata ao processo correto que detém e controla a sessão correspondente. Esse redirecionamento garante que o controle de execução de queries continue funcionando perfeitamente em toda a frota de processos de forma uniforme.

O gerenciamento dos pools de conexões opera especificamente no modo de transação (transaction mode). Sob esse regime técnico de pooling, a conexão física com o servidor de banco de dados Postgres é imediatamente devolvida ao pool gerenciado no exato instante em que uma transação de cliente realiza o seu commit correspondente, o que maximiza o reaproveitamento de sessões entre as aplicações ativas.

Para garantir que a frota de múltiplos processos não sobrecarregue o banco de dados principal com excesso de requisições de rede concorrentes, o orçamento global de conexões é distribuído proporcionalmente. As diretivas de configuração clássicas conhecidas como max_client_conn e max_db_connections são divididas de forma exata pelo número de processos ativos da frota, garantindo que a soma agregada de conexões de todos os processos nunca ultrapasse os limites operacionais definidos como seguros para o servidor do banco de dados.

Detalhes dos testes técnicos

A validação científica e a comprovação prática dos ganhos de vazão de dados dessa nova arquitetura foram demonstradas por meio de uma bateria de testes exaustivos executados em instâncias idênticas da nuvem pública da AWS. Para o ambiente de pooling de conexões, foi implantada uma instância de computação do tipo c7i.4xlarge equipada com 16 vCPUs de processamento, conectada a um servidor secundário dedicado para o banco de dados Postgres, além de uma terceira máquina isolada responsável unicamente por gerar a carga de estresse computacional.

A simulação de carga de trabalho intensa foi executada pelo utilitário de benchmark padrão pgbench, configurado especificamente para operar no modo select-only com transações curtas de leitura. Os engenheiros testaram o mesmo cenário operacional, com o mesmo tipo de instância de nuvem, a mesma base de dados e o mesmo volume de dados, isolando apenas uma variável de infraestrutura: um cenário de teste rodou com um único processo tradicional do PgBouncer, enquanto o outro cenário rodou com uma frota paralela de 16 processos do pooler coordenados por peering e socket reutilizável.

Durante a execução do teste estressante, a carga de clientes simultâneos emulada pelo pgbench foi escalonada de forma geométrica de 8 conexões simultâneas até atingir o volume severo de 256 conexões de clientes em concorrência direta. Esse crescimento progressivo teve como finalidade rastrear a curva exata de desempenho de vazão transacional (TPS) e capturar a taxa de ocupação física de processamento de CPU do servidor hospedeiro sob as duas configurações arquitetônicas distintas.

Análise detalhada dos resultados

Os dados reais de desempenho levantados durante o benchmark demonstraram que, sob condições de baixa concorrência, o processo único do PgBouncer comporta-se de forma eficiente. Com uma carga inicial contendo apenas 8 clientes conectados, o processo único registrou 8.910 TPS demandando 0,8% de CPU da máquina, enquanto a frota registrou 6.450 TPS utilizando 2,9% de CPU, demonstrando que o overhead de coordenação interna da frota torna o processo único ligeiramente mais rápido quando não há necessidade de processamento paralelo.

No entanto, a divergência de desempenho torna-se massiva à medida que as aplicações exigem escala do banco de dados. Ao atingir o volume de 32 conexões ativas de clientes, o processo único do PgBouncer registrou 54.203 TPS utilizando 5,2% de CPU total do servidor hospedeiro, enquanto a frota multi-processo saltou para 64.244 TPS com 12,3% de consumo de processamento da máquina virtual.

O limite físico de thread única atinge seu ponto crítico de saturação quando a carga é elevada para 64 conexões concorrentes de clientes. Nesse nível de estresse, o processo único do pooler registrou 86.570 TPS com consumo de CPU do servidor estacionado em 8,3%, ao passo que a frota de múltiplos processos manteve sua curva de crescimento atingindo 219.439 TPS sob uma taxa de consumo de 31,9% da capacidade computacional global do host.

Sob concorrência de 128 conexões, o processo único do PgBouncer começou a demonstrar queda de performance devido à saturação de fila, caindo para uma taxa de 83.463 TPS com uma utilização de CPU global do host de 8,1%. Em contraste direto, a frota de 16 processos paralelos escalou com consistência para atingir 320.547 TPS, registrando uma taxa de utilização de CPU do servidor hospedeiro na casa dos 45,9%.

No cenário extremo de estresse simulando 256 conexões simultâneas concorrendo por recursos de processamento, a arquitetura de processo único sofreu uma severa degradação devido à disputa acirrada por tempo de execução do core do processador, recuando para uma vazão de 76.893 TPS. Simultaneamente, a frota de processos múltiplos do pooler atingiu o topo do seu desempenho entregando 336.469 transações por segundo (TPS) utilizando 48,9% da capacidade global de processamento do servidor, gerando uma melhoria de vazão transacional de aproximadamente quatro vezes em comparação direta com o modelo clássico de thread única.

Os dados internos de monitoramento de sistema coletados por meio da ferramenta pidstat confirmaram que, sob carga extrema, o processo tradicional único do PgBouncer permaneceu completamente travado e saturado em aproximadamente 97% de consumo de processamento, ou seja, esgotando integralmente a capacidade computacional daquele core individual, enquanto os outros 15 cores da máquina virtual permaneciam completamente ociosos abaixo de 10% de uso combinado.

A mesma discrepância de aproveitamento de infraestrutura paga foi registrada externamente pelo agente de monitoramento padrão de nuvem da AWS, o CloudWatch. Durante o período estável do benchmark sob carga severa de 256 conexões simultâneas de clientes, a métrica oficial de CPUUtilization reportou uma média de apenas 16% de uso geral da instância para o cenário rodando um único processo tradicional, enquanto o ambiente contendo a frota multi-processo coordenada por peering utilizou aproximadamente 60% dos recursos computacionais da instância do pooler.

O comportamento dos limites máximos de conexão de clientes também evidenciou a robustez desse arranjo multi-processo. Na arquitetura clássica, o processo do pooler aplica de forma isolada a restrição definida em max_client_conn, rejeitando sumariamente novas requisições com o erro de sistema FATAL: no more connections allowed (max_client_conn) no momento em que o teto configurado é ultrapassado, ao passo que a divisão coordenada do orçamento desse limite físico entre os diferentes membros da frota viabiliza a elevação do teto agregado de conexões de clientes sem expor o Postgres a riscos operacionais.

A arquitetura que unifica frotas dinâmicas do pooler ajustadas à contagem de núcleos físicos, aliada à partilha lógica de portas via so_reuseport e interconexão de dados via peering, converte o pooler de conexões de um perigoso e crônico ponto de saturação computacional em uma infraestrutura fluida e escalável. Devido a esses ganhos robustos de throughput e escalabilidade real provados sob testes rigorosos, o modelo de frota multi-processo do PgBouncer agora é distribuído de forma nativa e ativado por padrão em todos os servidores do serviço ClickHouse Managed Postgres.

#pgbouncer#postgres#clickhouse#so_reuseport#cloudwatch
Compartilhar

Artigos Relacionados