PgBouncer
Every named tenant has a database of its own, and in each broker process a connection pool of its own. Without a pooler, the connections a broker may hold grow with every tenant it keeps loaded, and that keeps tenancy.residency.max_resident small. PgBouncer in transaction mode breaks the link. A loaded tenant that isn't serving requests holds almost no PostgreSQL connections, so one broker can keep hundreds of tenants loaded.
- AndromedaA pool of 2 for each loaded tenant
- requests and background work of every tenant
- PgBouncerTransaction mode, one pool per tenant database
- a server connection for each transaction in flight
- PostgreSQLAlso reached directly, for migrations, maintenance and the platform database
Pointing the broker at PgBouncer
[database]
connection = "postgres://andromeda:secret@postgres:5432/andromeda"
[database.pooler]
connection = "postgres://andromeda:secret@pgbouncer:6432/andromeda"
max_connections_per_tenant = 2
The pooler forwards to the same PostgreSQL, so the two connections normally differ only in host and port. Either one can also be a table of its parts, host, port, user, password, database and parameters. The passwords then come from ANDROMEDA__DATABASE__CONNECTION__PASSWORD and ANDROMEDA__DATABASE__POOLER__CONNECTION__PASSWORD, without being escaped into a URL.
database.connection names PostgreSQL itselfNever point database.connection at the pooler. The work that stays on it keeps session state across transactions, which a transaction pooler breaks.
With the section present, the traffic splits in two.
The login
The login the broker uses through PgBouncer must be the role in database.connection. Snapshot schemas are created over the serving pool, so the role that logs in through PgBouncer owns them, while the role in database.connection upgrades and maintains them. With two different roles, the schemas belong to a role other than the one working on them, and only a superuser in database.connection can get past that. That role also needs CREATE on each tenant database. PostgreSQL lists every privilege.
Configuring PgBouncer
This pgbouncer.ini is a starting point.
[databases]
* = host=postgres port=5432
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
max_client_conn = 5000
default_pool_size = 2
min_pool_size = 0
reserve_pool_size = 1
reserve_pool_timeout = 3
server_idle_timeout = 5
max_user_connections = 120
max_db_connections = 20
ignore_startup_parameters = extra_float_digits,IntervalStyle,DateStyle,TimeZone,client_encoding
query_wait_timeout = 15
These are the settings that matter most.
Sizing
With a pooler, PostgreSQL sees roughly as many connections as the active tenants hold, plus the direct ones.
Here is the number of active tenants, is default_pool_size, and is the direct connections, the ones that never go through the pooler.
- The platform pool,
database.platform_max_connections - Two for each tenant that may be loading at once,
tenancy.build.max_concurrent - Maintenance, three on the platform database and one in the tenant it is working on
- Two for a repack, in the tenant whose table it copies
- Creating new tenant databases
- Checking for due work on tenants still being created, one at a time
PgBouncer's max_user_connections caps the first term for the whole fleet, so PostgreSQL's max_connections has to cover that cap and the direct connections, with room to spare.
Here is PostgreSQL's max_connections and is max_user_connections. Database connections explains the same sum without a pooler. From there:
Why quiet tenants hold no connections
A loaded tenant has background work on its database, such as expiry sweeps, the notification and history queues, periodic notifications and Snapshot recovery. Each runs only when something is due. The broker reads the tenant's earliest due work once, and every task rests until then, or until a request reaches the tenant. A tenant with nothing due makes one small read every 16 expiry.sweep_interval periods, and nothing else. Its pool goes quiet, and PgBouncer closes the server connections after server_idle_timeout.
So the connections PostgreSQL sees follow the tenants that are active, not the tenants that are loaded.
When PgBouncer or PostgreSQL is down
While PgBouncer can't reach PostgreSQL, it fails the broker's transactions, and it remembers a failed login for server_login_retry. A PgBouncer restart cuts the broker's connections in the middle of their transactions. Either way, clients get 503 with Retry-After: 5, never 500, and the broker's background work resumes once the database is back. Keep server_login_retry short, so a restarted PostgreSQL is picked up within a few retries. Store outages describes the rest.
What pooling doesn't fix
PgBouncer bounds what many tenants cost in connections. The other costs of one database per tenant remain.
Checking it works
Connect to PgBouncer's admin console as a user listed in its admin_users, and list the pools.
psql -h pgbouncer -p 6432 -U pgbouncer_admin pgbouncer -c 'SHOW POOLS'
After a few requests to different tenants, there is one pool per tenant database, each named andromeda_tenant_.... Leave the broker idle for longer than server_idle_timeout, and the sv_idle and sv_active counts of the quiet tenants drop to zero.
Then ask the broker whether its direct and pooled connections fit the server.
curl -s http://localhost:1056/ngsi-ld/v1/version
The connections object of the postgresql component should say "verdict": "sufficient". Connection capacity explains the numbers.