Pool de conexões PostgreSQL em um VPS
Em um VPS de 4 GB, o PostgreSQL pode ficar sem RAM antes de max_connections. Veja como o pooler reduz backends e quais problemas ele pode causar.
Por que um VPS fica sem RAM antes de atingir max_connections
O pool de conexões do Postgres num VPS não é um truque para aumentar a velocidade. É o que mantém um servidor com 4 GB operacional, porque cada conexão PostgreSQL é um processo separado do sistema operativo, com a sua própria memória privada. Um pooler coloca um número pequeno e fixo de processos backend reais por trás de um número grande e barato de conexões de cliente.
O valor predefinido de max_connections é 100. Esse valor é um limite, não um orçamento. O PostgreSQL não verifica se a máquina consegue realmente manter 100 backends a executar consultas reais. Por isso, a máquina falha primeiro. O kernel escolhe um processo através do mecanismo out of memory (OOM). Quando escolhe um backend, o PostgreSQL reinicia todo o cluster para voltar a garantir a segurança da memória partilhada. O log mostra server process (PID 1234) was terminated by signal 9: Killed e, em seguida, terminating any other active server processes. Todas as conexões abertas são terminadas, incluindo as que estavam saudáveis.
O servidor fica sem memória porque cada conexão é um processo e porque work_mem é atribuído por operação de ordenação ou hash, e não por conexão. Estes dois fatores multiplicam o consumo.
Cada ligação é um processo, e cada processo consome memória
O PostgreSQL usa um processo por ligação. O postmaster cria um backend quando um cliente se liga, e esse backend permanece ativo até o cliente terminar a ligação. Não é uma thread. Tem as suas próprias tabelas de páginas, as suas próprias caches de catálogos e os seus próprios planos de consulta em cache. Essas caches aumentam à medida que a ligação acede a mais tabelas e executa mais consultas distintas. Por isso, uma ligação de longa duração numa aplicação ORM ocupada consome mais memória do que uma ligação nova.
A memória partilhada é realmente partilhada. shared_buffers é uma alocação para todo o cluster, mapeada em cada backend. A memória privada não é partilhada. Por isso, top pode induzi-lo em erro neste caso: o resident set size (RSS) de um backend inclui as páginas partilhadas que esse backend acedeu. Somar o RSS de 50 backends conta shared_buffers 50 vezes.
Meça antes a parte privada. O PSS (proportional set size) divide cada página partilhada pelo número de processos que a mapeiam. O USS (unique set size) conta apenas as páginas que pertencem exclusivamente a esse processo.
sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'A coluna USS mostra a memória que seria libertada se esse backend terminasse. Esse é o custo real por ligação. Os valores publicados normalmente situam um backend inativo na ordem de poucos megabytes, e um backend que executou consultas ORM abrangentes pode consumir várias vezes esse valor. Considere esses valores como referências publicadas típicas, não como os valores do seu sistema. O único valor relevante para o planeamento é o que medir no seu servidor, com a sua carga de trabalho.
É fácil não detetar uma alocação por sessão. temp_buffers tem o valor predefinido de 8MB e é alocado por sessão na primeira vez que essa sessão acede a uma tabela temporária. A memória não é devolvida até a sessão terminar.
work_mem é concedido por operação, não por ligação
É aqui que as contas escapam a muitas pessoas. work_mem tem o valor predefinido de 4MB, e a documentação do PostgreSQL é clara sobre o que isso significa: "uma consulta complexa pode executar várias operações de ordenação e hash ao mesmo tempo, podendo cada operação usar, em geral, tanta memória quanto o valor especificado antes de começar a escrever dados em ficheiros temporários." Um plano com três nós de ordenação pode usar três vezes work_mem dentro do mesmo backend, ao mesmo tempo.
As operações de hash usam mais memória. hash_mem_multiplier tem o valor predefinido de 2.0, pelo que uma junção hash ou uma agregação hash pode usar work_mem vezes dois, o que corresponde a 8MB nas definições predefinidas. As consultas paralelas multiplicam novamente esse consumo, porque cada worker paralelo é outro processo com a sua própria alocação.
Faça as contas para uma VPS com 4 GB. Defina shared_buffers como 1 GB, mantenha work_mem em 4MB e permita que 100 ligações executem uma consulta com dois nós hash cada uma. Isso corresponde a 100 vezes 16MB, ou seja, 1.6 GB de memória privada além de 1 GB de buffers partilhados, antes da page cache e de qualquer outro consumo no sistema. Agora aumente work_mem para 64MB porque o servidor tem RAM disponível. As mesmas 100 ligações passam a representar 100 vezes 256MB. Nada gera um aviso. Só descobre o problema quando o OOM killer atua.
Pode verificar se work_mem é demasiado pequeno em vez de adivinhar. Defina log_temp_files = 0 em postgresql.conf e faça reload. Cada spill para o disco passa então a escrever uma linha com o nome e o tamanho do ficheiro, como temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520. Spills frequentes indicam que um valor mais alto de work_mem seria útil. Sem spills, aumentá-lo não traz qualquer benefício e consome memória que não tem.
A aritmética do pool que realmente causa problemas
Ninguém começa por abrir 240 ligações. Configura um pool de 20 e depois executa a aplicação em mais do que um local.
The data behind this chart
[
{
"config": "1 worker",
"backends": 20
},
{
"config": "4 web workers",
"backends": 80
},
{
"config": "4 web + 2 background",
"backends": 120
},
{
"config": "2 hosts x 4 workers",
"backends": 160
},
{
"config": "3 hosts x 4 workers",
"backends": 240
}
]Quatro workers do Gunicorn, cada um com um pool de 20, pedem 80 backends. Se adicionar dois workers para tarefas em segundo plano, o total passa a 120. Ao aumentar para 3 hosts x 4 workers, a aplicação passa a pedir 240 backends contra um max_connections de 100. Nenhuma dessas 5 configurações está mal configurada num único local. O pool é específico de cada processo, e nenhuma parte da aplicação consegue ver o total.
Os valores predefinidos das bibliotecas apontam na mesma direção. O SQLAlchemy define QueuePool como pool_size=5 por predefinição, com max_overflow=10, ou seja, 15 ligações por processo. O HikariCP usa 10 por predefinição. O Django não tinha um pool integrado antes da versão 5.1 e usava uma ligação por processo worker. Por isso, as aplicações Django encontram este problema mais tarde e depois encontram-no todo de uma vez quando alguém define CONN_MAX_AGE ou ativa a nova opção "pool": True. Se executar uma aplicação Django atrás do Gunicorn e do nginx, o número a multiplicar é a contagem de workers do Gunicorn, não a contagem de servidores.
O que o pool de conexões do Postgres realmente altera numa VPS
Um pooler é um processo que fala o protocolo de rede do PostgreSQL com a aplicação, de um lado, e mantém um conjunto pequeno de conexões reais ao servidor, do outro. Ele não torna nenhuma consulta mais rápida. Ele altera quem assume o custo de uma conexão e quantos backends reais existem.
Duas coisas melhoram. A abertura de uma conexão deixa de custar um fork e as consultas ao catálogo que preenchem a cache vazia do backend, porque o pooler responde diretamente à conexão do cliente. Mais importante, a quantidade de backends reais deixa de acompanhar a quantidade de conexões da aplicação. Assim, 500 clientes podem partilhar 20 backends.
A espera é a funcionalidade, e esta é a parte que muitas pessoas rejeitam. Sem um pooler, 500 consultas concorrentes recebem um backend e executam todas ao mesmo tempo em 2 cores de CPU. Por isso, todas ficam lentas e toda a memória é consumida no mesmo momento. Com um pooler, 20 consultas executam e as restantes esperam alguns milissegundos. Assim, cada consulta em execução recebe uma parcela real da CPU e termina mais depressa. Uma fila à frente de um pool pequeno é melhor do que não ter fila à frente de um pool grande.
O pooler não limita mais nada na máquina. Se o Postgres partilhar a VPS com um servidor de aplicações ou com uma base de dados vetorial na mesma VPS, o pooler protege o Postgres da aplicação e nada mais. Defina também um limite rígido para os outros serviços: pode limitar a memória e a CPU que um serviço pode usar com systemd para que um processo descontrolado não derrube a base de dados. O local onde a própria base de dados está alojada altera a forma como define esses limites. Essa é a diferença prática entre executar o Postgres no Docker ou diretamente no host.
Pooling de sessão versus pooling de transações
Uma definição determina todas as outras: pool_mode.
No pooling de sessão, uma ligação ao servidor é atribuída a um cliente durante toda a duração dessa ligação e libertada quando o cliente se desliga. Tudo funciona porque o pooler é um proxy simples. Poupa o custo de estabelecer ligações, mas não faz mais nada. Se a aplicação abrir 200 ligações, continuará a precisar de 200 backends.
No pooling de transações, uma ligação ao servidor é atribuída a um cliente apenas durante uma transação. Em COMMIT ou ROLLBACK, regressa ao pool e o cliente seguinte em espera recebe-a. É isto que transforma 500 clientes em 20 backends. Também é isto que causa problemas, por conceção: a instrução seguinte pode ser executada num backend diferente daquele que executou a anterior.
O modo predefinido do PgBouncer é pool_mode = session. Instale-o, não altere nada e obterá apenas a opção mais barata, sem a vantagem principal. Um terceiro modo, statement, devolve a ligação depois de cada instrução e rejeita transações com várias instruções. Não o utilize, a menos que saiba exatamente por que motivo precisa dele.
O que quebra no modo de transação e por quê
Tudo o que segue falha pelo mesmo motivo. É estado que fica dentro de um único backend, e o pool de transações não garante que você use o mesmo backend duas vezes.
SETeRESETno nível da sessão.SET search_path,SET statement_timeout,SET TIME ZONEeSET ROLEsão executados no backend que atender essa instrução e desaparecem antes da próxima transação. UseSET LOCALdentro de uma transação explícita. Ele fica limitado a essa transação e, portanto, é seguro.LISTEN. A entrega de notificações pertence ao backend que executouLISTEN, e esse backend é entregue a outro cliente assim que a transação termina.NOTIFYcontinua funcionando no modo de transação, o que torna essa falha confusa: o envio funciona, mas o recebimento nunca ocorre. Se você precisa deLISTEN, abra uma conexão adicional diretamente na porta 5432, sem passar pelo pooler.- Locks consultivos no nível da sessão.
pg_advisory_lock()é mantido pela sessão e liberado quando ela termina. No pool de transações, a chamada para liberar o lock é executada em outro backend. Por isso, o lock permanece mantido até o PgBouncer retirar essa conexão do servidor. Por padrão, isso ocorre depois deserver_lifetime, ou seja, uma hora. Usepg_advisory_xact_lock(). Ele é liberado no fim da transação pelo mesmo backend que o adquiriu. PREPAREeDEALLOCATE, as instruções SQL. Nunca ficam disponíveis no modo de transação.- Cursores
WITH HOLDe qualquer cursor no servidor que deva continuar existindo depois do fim da transação. - Tabelas temporárias que devem sobreviver a um commit.
CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWScoloca a tabela no schema temporário de um backend, e a próxima transação pode ser executada em outro backend. LOAD.
As instruções preparadas no nível do protocolo são o único item que mudou. O PgBouncer 1.21.0 adicionou suporte para elas no modo de transação, e a versão 1.24.0 ativou esse suporte por padrão, definindo max_prepared_statements como 200. Versões mais antigas deixam esse valor como 0, o que significa que o recurso está desativado. O Ubuntu 24.04 inclui o PgBouncer 1.22.0. Portanto, o recurso existe, mas você precisa definir max_prepared_statements manualmente. Se não tiver certeza do comportamento da sua versão, a configuração segura fica no cliente: o psycopg 3 deixa de usar instruções preparadas no servidor quando você define prepare_threshold como None.
O Django dá um nome próprio a esse comportamento. A documentação afirma que "usar um pooler de conexões no modo de pool de transações, como o PgBouncer, exige desativar os cursores no servidor para essa conexão", porque "os cursores no servidor só podem ser acessados na conexão em que foram criados". Defina DISABLE_SERVER_SIDE_CURSORS como True na entrada desse banco de dados. Caso contrário, cada chamada .iterator() se tornará uma falha intermitente que só aparece sob carga.
O modo de transação vale a pena, mas é um contrato. Leia a lista, verifique se o seu ORM e a sua biblioteca de tarefas em segundo plano são compatíveis com ela e só então faça a mudança.
Instale o PgBouncer e aponte a aplicação para ele
A configuração abaixo deve ser executada no seu próprio servidor: Ubuntu 24.04, com o PostgreSQL já a escutar em 127.0.0.1 na porta 5432.
sudo apt update
sudo apt install -y pgbouncer
pgbouncer --versionO Ubuntu 24.04 disponibiliza o PgBouncer 1.22.0. O upstream está na versão 1.25.2 em agosto de 2026. Verifique qual versão tem instalada, porque o comportamento das instruções preparadas acima depende dela.
Crie uma role cuja única função seja iniciar sessão na consola administrativa do PgBouncer e, em seguida, crie o ficheiro de palavras-passe. O PgBouncer precisa dos segredos SCRAM (mecanismo de autenticação por desafio e resposta com salt) de pg_authid, e apenas um superuser pode ler essa tabela.
sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
> /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txtCopiar os segredos, em vez de voltar a escrever as palavras-passe, é o que permite isto. O PgBouncer só pode reutilizar um segredo SCRAM para iniciar sessão no PostgreSQL quando o cliente também tiver sido autenticado com SCRAM, quando o segredo no ficheiro for byte a byte igual ao de pg_authid (com o mesmo salt e número de iterações, não apenas com a mesma palavra-passe) e quando a linha [databases] não fixar um user=. Adicione user=appuser a essa linha e o PgBouncer precisará de uma palavra-passe em texto simples. Confirme que o proprietário do ficheiro corresponde à conta com que o serviço é executado, usando systemctl show pgbouncer -p User. Alterar uma palavra-passe no PostgreSQL implica regenerar este ficheiro; caso contrário, a próxima ligação devolve password authentication failed.
Agora escreva /etc/pgbouncer/pgbouncer.ini.
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digitslisten_addr = 127.0.0.1 mantém o pooler fora da Internet pública, o que é importante porque um pooler acessível externamente é um endpoint de autenticação que não pretendia publicar. max_client_conn é o número de ligações da aplicação que o PgBouncer aceitará, e como cada uma consome poucos recursos, pode ser elevado. default_pool_size é o número de backends reais que um par de base de dados e utilizador pode manter, e este é o limite dispendioso. max_db_connections limita a base de dados inteira a 80, deixando margem abaixo de max_connections para psql, cópias de segurança e monitorização. ignore_startup_parameters = extra_float_digits impede que o PgBouncer rejeite drivers, incluindo o driver JDBC, que enviem esse parâmetro no momento da ligação.
sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pagerUm arranque saudável regista uma linha que indica que o PgBouncer está a escutar em 127.0.0.1:6432. Quando o arranque falha por causa do ficheiro de palavras-passe, o log indica o caminho que não conseguiu ler. Quase sempre é um problema de permissões ou de proprietário, não de sintaxe. Depois, altere a string de ligação da aplicação da porta 5432 para a porta 6432 e reinicie-a. Não é necessário alterar mais nada na aplicação.
Como verificar se o pool está a funcionar corretamente
O PgBouncer tem uma consola de administração acessível através de uma base de dados virtual chamada pgbouncer.
psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncerSHOW POOLS;
SHOW STATS;
SHOW CLIENTS;SHOW POOLS é o campo principal a monitorizar. cl_active são os clientes atualmente ligados a uma ligação de servidor, cl_waiting são os clientes à espera de uma ligação, sv_active e sv_idle são os backends reais em utilização e livres, e maxwait é o tempo, em segundos, que o cliente no início da fila está à espera. Em condições normais de carga, um sistema saudável mantém cl_waiting em 0 e maxwait em 0. Um maxwait que ultrapasse um ou dois segundos significa que o pool é demasiado pequeno ou que as consultas são demasiado lentas. Estas situações exigem correções diferentes.
Verifique qual dos dois casos se aplica antes de aumentar default_pool_size.
SELECT state, count(*) FROM pg_stat_activity
WHERE backend_type = 'client backend' GROUP BY state;Se a maioria dos backends permanecer em idle in transaction, o tamanho do pool não é o problema. A aplicação está a abrir uma transação e depois a executar uma operação lenta dentro dela, como uma chamada HTTP. Cada backend fica assim ocupado sem executar uma consulta. idle_in_transaction_session_timeout interrompe essas transações, mas a correção real deve ser feita no código da aplicação. Se, pelo contrário, todos os backends estiverem em active, o pool está realmente saturado e as consultas devem ser analisadas com EXPLAIN (ANALYZE, BUFFERS) antes de disponibilizar mais ligações.
Quanto ao dimensionamento, o ponto de partida mais citado é a heurística do HikariCP: aproximadamente o dobro do número de cores mais um. Num VPS com 2 cores, isso corresponde a 5. Use esse valor publicado como referência inicial, defina default_pool_size próximo dele e ajuste-o com base em maxwait. Pools pequenos podem parecer insuficientes, mas normalmente apresentam melhores resultados nas medições, porque um backend em espera não consome recursos, enquanto um backend em execução consome CPU, memória e capacidade de processamento de bloqueios.
Escolher entre PgBouncer, PgDog e Pgpool-II
PgBouncer é a escolha para o caso comum: um servidor PostgreSQL, um VPS e uma aplicação que abre mais ligações do que o sistema consegue suportar. Faz uma única tarefa, a configuração está num único ficheiro ini e é disponibilizado nos pacotes do Debian e do Ubuntu. Gere as ligações num único thread. Isso é suficiente para uma carga de trabalho do tamanho de um VPS e só se torna um limite em máquinas muito maiores.
Vale a pena considerar o PgDog quando a decisão de encaminhamento deve ocorrer no mesmo salto de rede que o pooling. O projeto descreve-o como um proxy para escalar o PostgreSQL. É escrito em Rust e suporta pooling de transações e de sessões, além de divisão de leitura/escrita através da análise da consulta. Também suporta sharding com encaminhamento para vários shards e commit em duas fases. Use-o quando tiver um primary e uma ou mais réplicas e quiser enviar as leituras para as réplicas sem ensinar à aplicação que elas existem. Há duas ressalvas. A licença é AGPLv3. Portanto, a cláusula relativa à utilização através da rede é uma questão de licenciamento que deve ser resolvida com a pessoa responsável por essa decisão na sua empresa antes de chegar à produção. A posição do próprio projeto é que a utilização interna e as modificações privadas não criam uma obrigação de disponibilizar o código-fonte. O projeto também é recente, com releases semanais e números de versão 0.x. Por isso, fixe uma release tag em vez de seguir main.
git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.tomlA compilação a partir do código-fonte requer uma toolchain Rust stable atual, CMake e um compilador C/C++. Também existem binários Linux pré-compilados e pacotes Debian na página de releases, além de uma imagem de container em ghcr.io/pgdogdev/pgdog. A configuração está dividida por dois ficheiros. O primeiro contém as definições gerais e uma entrada por base de dados, escrita aqui como um array TOML de tabelas inline, para distinguir facilmente as duas formas.
databases = [
{ name = "appdb", host = "127.0.0.1" },
]
[general]
port = 6432
default_pool_size = 10O segundo contém uma entrada por utilizador, com o mesmo formato de array.
users = [
{ name = "appuser", database = "appdb", password = "change-this" },
]Por predefinição, o PgDog escuta na porta 6432, a mesma porta do PgBouncer. Portanto, os dois não podem assumir simultaneamente a predefinição no mesmo host.
O Pgpool-II, na versão 4.7.2 em junho de 2026, oferece pooling e balanceamento de carga, com um watchdog para failover automático. As funcionalidades adicionais introduzem modos de falha adicionais, e é necessário compreender o seu modelo de pooling antes de o escolher. O Pgpool-II cria previamente num_init_children processos child, e cada child mantém em cache até max_pool ligações ao servidor. Assim, o limite de backends é num_init_children multiplicado por max_pool. Cada child atende um cliente de cada vez. Portanto, o número de clientes que pode aceitar é igual a num_init_children e fica definido no arranque. Um cliente inativo continua a ocupar um child. Defina num_init_children como 100 e max_pool como 4 e terá autorizado 400 backends. Esse é precisamente o problema que instalou um pooler para resolver. Escolha o Pgpool-II quando precisar do seu failover e do seu encaminhamento de consultas e faça essa multiplicação com atenção. Se tudo o que pretende é reduzir o número de backends, é uma solução mais complexa do que o necessário.
A questão do proxy gerido e o equivalente self-hosted
As plataformas geridas vendem isto como um produto separado. A AWS coloca o RDS Proxy à frente do RDS, e o Supabase coloca o seu próprio pooler, o Supavisor, à frente do Supabase Postgres. Ambos fazem o trabalho descrito aqui: manter as ligações dos clientes com baixo custo e distribuir um número menor de backends reais. O Supavisor é open source e pode ser self-hosted, portanto a escolha não é entre uma opção proprietária e uma opção gratuita.
O equivalente self-hosted de um proxy gerido não é uma ideia diferente. É a mesma ideia, com o ficheiro de configuração sob o seu controlo: PgBouncer em modo de transação, no mesmo VPS que a base de dados, a escutar em 127.0.0.1. Há duas diferenças reais. Um proxy gerido fica a um salto de rede de distância, por isso acrescenta latência e continua a manter as ligações dos clientes enquanto a base de dados reinicia por baixo dele. O PgBouncer no host da base de dados acrescenta um salto de loopback, com um custo praticamente nulo, e termina quando esse host termina. Se quiser o comportamento de sobreviver a um reinício, também precisa de mecanismos de failover. É nesse ponto que o watchdog do Pgpool-II ou as verificações de integridade do PgDog começam a justificar a sua complexidade.
Há mais uma opção a considerar. Se o número de ligações for o principal fator de complexidade da implementação, uma base de dados incorporada não tem um modelo de ligações para agrupar, porque é uma biblioteca dentro do processo, e não um servidor numa porta. Para um único servidor de aplicações com um volume de escrita moderado, executar SQLite em produção num VPS elimina todo este problema, em vez de o gerir. Quando precisar de um servidor real, dimensione primeiro o pool e só depois dimensione a máquina.
FAQ
Ainda preciso do PgBouncer se a minha aplicação já tiver um pool de ligações?
Normalmente, sim, porque o pool da aplicação é por processo e não consegue ver os restantes. Quatro workers do Gunicorn, cada um com um pool de 20 backends de pedidos 80, e mais dois workers em segundo plano perfazem 120. O PgBouncer é o único componente que vê o total e pode impor um limite. A configuração adequada combina os dois: um pool pequeno dentro de cada worker, para que os pedidos não tenham de pagar o custo de uma ligação TCP, e o PgBouncer em modo de transação, limitando os backends reais por trás desses pools.
O que falha exatamente quando mudo o PgBouncer para o modo de transação?
Tudo o que mantém estado num backend entre transações. SET e RESET ao nível da sessão, cursores LISTEN e WITH HOLD, as instruções SQL PREPARE e DEALLOCATE, bloqueios consultivos ao nível da sessão, tabelas temporárias que tenham de sobreviver a um commit e LOAD. NOTIFY continua a funcionar, o que faz com que LISTEN avariado pareça um problema de entrega, e não de pooling. No Django, defina DISABLE_SERVER_SIDE_CURSORS como True. No psycopg 3, defina prepare_threshold como None ou execute o PgBouncer 1.22 ou mais recente com max_prepared_statements acima de 0. Substitua pg_advisory_lock() por pg_advisory_xact_lock().
Qual deve ser o valor de default_pool_size numa VPS com 2 cores?
Menor do que parece adequado. A heurística do HikariCP, amplamente publicada, é aproximadamente o dobro do número de cores mais um, ou seja, cerca de 5 em dois cores. Isto é um ponto de partida, não uma resposta definitiva. Defina o valor e depois consulte maxwait e cl_waiting em SHOW POOLS sob carga real. O valor 0 em ambos significa que o pool tem tamanho suficiente. Um maxwait crescente indica que os clientes estão a aguardar na fila. Antes de aumentar o número, verifique pg_stat_activity: backends bloqueados em idle in transaction indicam um erro da aplicação que mais ligações apenas irá ocultar.
PgBouncer ou PgDog?
PgBouncer para um servidor PostgreSQL numa única VPS, que é o caso da maioria das implementações. Está disponível em pacotes para Ubuntu, o seu comportamento está bem documentado e toda a configuração fica num único ficheiro ini. Use PgDog quando a divisão de leituras e escritas entre réplicas ou o sharding tiver de ocorrer no mesmo salto que o pooling, para que a aplicação não tenha de conhecer a topologia. Antes de escolher o PgDog, esclareça a questão da licença AGPLv3 com a pessoa responsável pelo licenciamento no seu local de trabalho e fixe uma release específica, porque o projeto ainda usa números de versão 0.x e lança uma release por semana.