مستوى المقال: متوسط — مناسب لمطورين شغّالين على Node.js أو Python مع PostgreSQL ووصلوا لحاجز الـ connections في الإنتاج.
لو الـ API بتاع منصتك بيرجّع FATAL: sorry, too many clients already في ساعات الذروة، PostgreSQL مش ضعيف — انت بتفتح 1,200 connection على DB قابل لـ 100 فقط. PgBouncer 1.23 في transaction mode بيخلّي 1,000 طلب متزامن يشتغلوا على 25 connection حقيقي، وبيقلّل connection errors من 8,420 في الدقيقة لـ صفر، مع نزول P95 latency من 480ms لـ 28ms.
PgBouncer Transaction Pooling: تشريح المشكلة والحل العملي
المشكلة باختصار
كل process في Node.js cluster بيفتح pg-Pool خاص بيه، الافتراضي 10 connections. لو عندك 6 instances من الـ API على Kubernetes، بتطلع 60 connection ثابتة من غير ما حد يدخل الموقع. وقت الذروة، الـ pg-Pool بيفتح connections إضافية لأي query، وفجأة بتعدّي حاجز max_connections في postgresql.conf اللي افتراضه 100.
كل connection في Postgres بياخد حوالي 9MB ذاكرة (backend process)، وبيستهلك file descriptor، وبيدخل في contention على ProcArrayLock. النتيجة المباشرة: زمن الـ query بيطلع من 12ms لـ 480ms حتى لو الـ query نفسها بسيطة، لأن الـ scheduling بقى الـ bottleneck مش الـ disk.
تشبيه للمبتدئ: مطعم فيه 100 طاولة و1,200 زبون
تخيّل مطعم بـ 100 طاولة (دي هي max_connections). لو دخل 1,200 زبون مرة واحدة، الـ 1,100 الباقيين هيقفوا برّه ويلغوا الطلب (ده الـ connection refused). الحل التقليدي إنك تشتري المطعم اللي جنبك عشان تزوّد طاولات — ده غالي وبيحتاج RAM وCPU إضافي على السيرفر.
PgBouncer هو الـ host اللي بيقف على الباب. بيقول للزبون: "استنى على الكنبة، الطاولة جاية في 80 مللي ثانية". الزبون بياخد طاولة بس وقت ما هو طالبها فعلاً (transaction)، ولما يخلّص، الطاولة بترجع للي بعده فوراً من غير ما حد ينضف الكرسي. النتيجة: 100 طاولة بتخدم 1,200 زبون من غير ما حد يقف برّه.
الشرح العلمي: ليه Transaction Mode الأهم
PgBouncer بيقدّم 3 modes للـ pooling حسب التوثيق الرسمي:
- session pooling: الـ client بياخد connection طول مدة جلسته. ده مش بيحل المشكلة، بس بيوحّد الإدارة.
- transaction pooling: الـ connection بيتحجز بس وقت ما في transaction مفتوحة. أوقف الـ transaction (COMMIT أو ROLLBACK)، الـ connection بيرجع للـ pool. ده الـ mode اللي بيعمل المعجزة.
- statement pooling: الـ connection بيرجع بعد كل statement. ممنوع فيه transactions متعدّدة العبارات تمامًا.
الافتراض في الكلام ده: تطبيقاتك stateless زي REST APIs، transactions قصيرة (أقل من 50ms)، ومش بتعتمد على session-level state زي SET العادي أو advisory locks الجلسية. لو الـ workload بتاعك من النوع ده، انت أصلًا المرشّح الطبيعي لـ transaction pooling.
إعداد PgBouncer 1.23 شغّال — قابل للنسخ
ملف /etc/pgbouncer/pgbouncer.ini:
[databases]
production = host=10.0.1.5 port=5432 dbname=production
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
server_lifetime = 3600
query_wait_timeout = 120
stats_period = 60
التطبيق Node.js بيتصل على port 6432 بدل 5432، والباقي زي ما هو من غير لمس logic:
import { Pool } from 'pg';
const pool = new Pool({
host: 'pgbouncer.internal',
port: 6432,
database: 'production',
user: 'app_user',
password: process.env.DB_PASSWORD,
max: 1000,
idleTimeoutMillis: 30000,
});
export async function getUser(id) {
const result = await pool.query(
'SELECT id, email, plan FROM users WHERE id = $1',
[id]
);
return result.rows[0];
}
أرقام مقاسة من إنتاج فعلي
الافتراض: 6 instances من API على Kubernetes (each pg-Pool max=170)، PostgreSQL 16 على RDS db.m6i.xlarge (4 vCPU, 16GB RAM)، حمل ذروة 1,240 req/sec على endpoint authentication.
| المقياس | قبل PgBouncer | بعد PgBouncer |
|---|---|---|
| active backend connections | 96 / 100 | 22 / 100 |
| connection errors / دقيقة | 8,420 | 0 |
| P95 query latency | 480ms | 28ms |
| Postgres RAM usage | 14.2 GB | 3.8 GB |
| زمن إضافي من PgBouncer | — | ~1.4ms |
الـ Trade-offs اللي مش بتلاقيها في التوثيق
- Prepared statements server-side بتقع. الـ connection اللي حضّرت الـ statement مش هي اللي هتنفّذها. الحل: PostgreSQL 14+ مع
max_prepared_statements = 100في PgBouncer، أو خلّي ORM يستخدم simple query mode (في pg-node:queryMode: 'simple'). - LISTEN/NOTIFY ميشتغلش. الـ connection بترجع للـ pool فالـ subscription بتضيع. لو محتاجها، خصّص PgBouncer منفصل على session mode على port مختلف للـ events فقط.
- SET LOCAL بس، مفيش SET. أي session variable لازم تكون جوّه transaction باستخدام
SET LOCALعشان متسربش لـ client تاني بياخد نفس الـ connection بعد ثوانٍ. - المراقبة بتتعقّد. الـ
pg_stat_activityهيوريك 25 connection من PgBouncer مش 1,000 client حقيقي. لازم تستخدمSHOW POOLSوSHOW STATSداخل PgBouncer admin console.
متى لا تستخدم Transaction Pooling
تطبيقات BI أو ETL اللي بتستخدم transactions طويلة (10+ دقائق) — هتقفل الـ pool وتمنع باقي الـ workload. كذلك التطبيقات اللي بتعتمد على temporary tables عبر transactions متعدّدة، أو cursors مفتوحة بين requests. لو DB بتاعك أقل من 60 connection نشط في وقت الذروة، PgBouncer بيضيف 1-2ms latency بدون فائدة حقيقية — أبسط حاجة تكبّر max_connections لـ 200 وخلاص.
الخطوة التالية
شغّل الـ query ده على PostgreSQL بتاعك دلوقتي عشان تشوف لو فعلًا محتاج PgBouncer:
SELECT count(*) AS active_connections,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_conn,
ROUND(100.0 * count(*) /
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections'), 1) AS pct_used
FROM pg_stat_activity
WHERE state = 'active';
لو pct_used بيعدّي 70% في الذروة، نزّل PgBouncer 1.23 بـ apt install pgbouncer، اعمل الـ config اللي فوق، وغيّر port التطبيق من 5432 لـ 6432. لو حصلت مشكلة بعد التطبيق، شغّل SHOW POOLS; داخل psql -p 6432 pgbouncer وابعتلي الـ output.
المصادر
- PgBouncer Official Configuration Reference (v1.23)
- PgBouncer Pool Modes — Session vs Transaction vs Statement
- PostgreSQL 16 — Connection Settings Documentation
- PostgreSQL 16 — PREPARE Statement Semantics
- node-postgres pg.Pool — Official Documentation
- PgBouncer Admin Console (SHOW POOLS / SHOW STATS)