1 pontos por GN⁺ 4 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • 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.4xlarge de 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_reuseport e 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_reuseport como 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_conn e max_db_connections sã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_conn com o seguinte erro
    • FATAL: 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.4xlarge com 16 vCPUs
    • Postgres: servidor separado
    • Gerador de carga: pgbench em um terceiro servidor
    • Carga de trabalho: somente leitura (select-only), em modo de transaction pooling
  • 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_reuseport e pareamento entre processos para eliminar o gargalo
  • Todos os servidores do ClickHouse Managed Postgres oferecem essa configuração por padrão

1 comentários

 
GN⁺ 4 시간 전
Opiniões no Hacker News
  • É só usar https://github.com/yandex/odyssey. Odyssey é um PgBouncer escalável

    • Começamos ajustando e usando direito o PgBouncer, que é o mais comprovado e se encaixa naturalmente no PostgreSQL; ele também resolveu problemas antigos, como suporte a prepared statements, e vem funcionando muito bem
      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
    • Um fato interessante relacionado: o ClickHouse também foi originalmente desenvolvido pela Yandex
    • A Yandex é uma empresa russa, então acho melhor não usar
  • 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

    • Fico curioso sobre o impacto no throughput quando há mais um salto de rede entre o PgBouncer e o PostgreSQL. Imagino que a diferença seja pequena, mas vale verificar
  • 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?

  • Estou usando pgdog e ele atende muito bem ao que preciso

    • O Pgdog foi criado para resolver pontos fracos do PgBouncer que encontramos há muito tempo na Instacart e para oferecer uma base mais sólida para escalar o PostgreSQL horizontalmente, ou seja, sharding
    • Também pretendemos avaliar oferecer pgdog no futuro. Expliquei melhor por que escolhemos o PgBouncer em https://news.ycombinator.com/item?id=48873867
    • Ultimamente vêm aparecendo, um após o outro, projetos de proxy reverso para PostgreSQL. O motivo de termos criado o Pgdog pode ser visto aqui: https://news.ycombinator.com/item?id=48819308
  • 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 cancelamento de consulta é enviado separadamente da conexão da consulta existente, então o PgBouncer precisa mandar a solicitação para o servidor e a conexão corretos
      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

    • Se em cada host a aplicação de backend roda como um único binário grande, sim. Mas, se o backend é composto por muitos processos forkados que não compartilham estado, então isso é claramente necessário
  • O PgBouncer é um excelente software. Uso bastante e ele simplifica muito a operação de bancos de dados

  • Acabei de conhecer so_reuseport e 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?

    • Melhorou bastante, e hoje ele lida razoavelmente bem até com milhares de conexões: https://techcommunity.microsoft.com/blog/adforpostgresql/imp...
      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
    • Naquela época, várias empresas estavam finalmente apresentando respostas utilizáveis ao modelo de threading proposto pelo Java. Era uma forma do modelo do Windows com recursos adicionais, e até o Solaris tinha dificuldade para lidar com Java; o HP-UX era ainda pior, e não me lembro se a SGI era melhor que a HP
      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