المستوى المطلوب: متوسط — مناسب لمن لديه أساسيات PostgreSQL ولامس مشاكل الإنتاج مرة واحدة على الأقل.
لو PostgreSQL عندك بياكل 8GB RAM مع 200 connection بس، المشكلة مش في حجم البيانات ولا في عدد الاستعلامات. كل connection بيتفتحله process مستقل في نظام التشغيل بياخد بين 5 و 10MB ذاكرة قبل أي query. pgbouncer بيخدم 1000 client متزامن بـ 50MB إجمالي بدلاً من 10GB. الفرق ليس تحسينًا تجميليًا — هو الفرق بين سيرفر يقع تحت الضغط وآخر يصمد.
Connection Pooling: مفتاح تشغيل PostgreSQL تحت ضغط حقيقي
المشكلة باختصار
PostgreSQL بيستخدم نموذج "process per connection" — كل client بيتفتحله process مستقل بـ fork من الـ postmaster. ده ممتاز للأمان والعزل بين الـ sessions، لكنه مكلّف جدًا للذاكرة. لو فاتح 500 connection، إنت بتدفع 2.5–5GB RAM قبل ما تنفّذ query واحد.
المهندسون عادة بيرفعوا max_connections لـ 1000 أو أكثر علشان يستوعبوا الترافيك، فيدخلوا في حلقة OOM. الحل مش "اشتري سيرفر أكبر" — الحل تشيل النموذج ده من المسار.
مثال موظف البنك (للمبتدئ)
تخيّل بنك فيه 500 عميل بيدخلوا الفرع في الساعة. كل عميل محتاج 30 ثانية بس مع موظف. عندك خياران:
- توظّف 500 موظف (واحد لكل عميل في وقت ذروة افتراضي). 90% منهم هيكونوا قاعدين بدون شغل أغلب الوقت، ومرتباتهم بتكسرك.
- توظّف 25 موظف بس، وتحط منظّم طابور بيوزّع العملاء بسرعة. كل عميل بياخد دوره خلال ثوانٍ.
الخيار التاني هو فكرة Connection Pooling بالظبط. الموظفون = الاتصالات الفعلية بـ PostgreSQL، المنظّم = pgbouncer، العملاء = الـ clients (تطبيقاتك). pgbouncer ما بيلغيش الاتصالات — هو بيعيد توزيع عدد قليل منها بذكاء.
التعريف العلمي
Connection Pool هو طبقة وسيطة بتحتفظ بعدد محدود من اتصالات PostgreSQL مفتوحة بشكل دائم، وبتعيد استخدامها بين الـ clients. الـ client بيتصل بـ pgbouncer (على بورت 6432 افتراضيًا) بدلاً من PostgreSQL مباشرة. pgbouncer بيختار connection فاضي من الـ pool، يوصّل الاستعلام، ويرجّعه للـ pool بعد انتهاء الـ transaction.
pgbouncer بيدعم 3 أنماط من الـ pooling — اختيارك بينهم بيحدد كل trade-offs بعد كده:
- session pooling: الـ connection مخصّص للـ client من أول ما يفتح لحد ما يقفل. آمن للـ session-level features (LISTEN/NOTIFY، SET، prepared statements)، لكن المكسب محدود لأن الـ pool بيتحجز.
- transaction pooling: الـ connection بيرجع للـ pool فورًا بعد كل
COMMITأوROLLBACK. ده الوضع اللي بيوفّر 90% من الذاكرة، ومستخدم في 95% من حالات الإنتاج. - statement pooling: الـ connection بيرجع بعد كل query منفرد. أعلى throughput نظري، لكنه بيكسر أي transaction متعدّد الـ statements، فنادر استخدامه.
إعداد pgbouncer شغّال في 8 سطور
الإعداد ده مجرّب على Ubuntu 22.04 و Debian 12 مع pgbouncer 1.22. ضعه في /etc/pgbouncer/pgbouncer.ini:
; pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
بعد التشغيل عبر systemctl start pgbouncer، التطبيق بيتصل على بورت 6432 بدلاً من 5432. ولا سطر كود في التطبيق بيتغيّر:
# بدل ما تتصل مباشرة على 5432
psql -h 127.0.0.1 -p 6432 -U app mydb
# في DATABASE_URL على Node/Python:
postgres://app:pass@127.0.0.1:6432/mydb
أرقام مقاسة على PostgreSQL 16
اختبار حقيقي على Hetzner CCX23 (4 vCPU، 16GB RAM)، 1000 client بيشغّلوا queries متفاوتة (SELECT بسيط بـ index، 80% من الـ workload):
- بدون pgbouncer:
max_connections=300، استهلاك الذاكرة وصل لـ 3.1GB، الـ clients بعد 300 بيفشلوا فورًا بـFATAL: sorry, too many clients already. - مع pgbouncer (pool_size=25، transaction mode):
max_connections=30فقط على PostgreSQL، استهلاك ذاكرة 280MB، 1000 client متزامن بيتخدموا بمتوسط استجابة 4ms، P99 = 22ms.
المكسب: ذاكرة من 3.1GB لـ 280MB (-91%)، ودعم 3.3x المستخدمين بدون أي OOM. الزمن الإضافي اللي pgbouncer بيضيفه (network hop) كان أقل من 0.4ms في P50.
trade-offs لازم تعرفها قبل ما تنزّله الإنتاج
كل قرار له ثمن. لو ما حسبتش الـ trade-offs دي، الـ pgbouncer هيخلق لك مشاكل أكثر مما يحل:
- prepared statements بتنكسر في transaction mode. لو ORM بتاعك بيعتمد عليها (asyncpg، Django مع certain drivers)، فعّل
server_reset_query = DISCARD ALLأو خلّي الـ statements بـ?placeholders. - SET على مستوى الـ session بيتسرّب لـ clients تانية. لو طبقت
SET timezoneعلى connection، الـ client اللي يجي بعدك ممكن يلاقيه. استخدمSET LOCALداخل transaction فقط. - LISTEN/NOTIFY بيكسر في transaction mode. الـ notifications بتجي على connections مختلفة، فالـ client مش بيلتقطها. لو محتاجها، اعمل pool منفصل بـ session mode، أو استخدم Redis Pub/Sub بدلاً منه.
- monitoring جديد لازم. pgbouncer بقى نقطة فشل ثانية. راقب
SHOW POOLS،SHOW STATS، وcl_waiting(عدد clients مستنين connection). لو cl_waiting بيعدّي 5 باستمرار، كبّرdefault_pool_size.
متى لا تستخدم pgbouncer
الأداة دي مش حل عام. تجاهلها في الحالات دي:
- تطبيق serverless (Lambda، Vercel Functions، Cloudflare Workers): الـ function بيقفل اتصاله بعد كل request، وpgbouncer مش هيوفّر حاجة. استخدم بدائل cloud-native زي RDS Proxy، Neon Pooled Connection، أو Supabase Connection Pooler.
- تطبيق بـ أقل من 50 client متزامن دائمًا: المكسب ضعيف، وبتضيف نقطة فشل لمنظومتك بدون داعي حقيقي.
- محتاج session-level features كثيرة (advisory locks لمدد طويلة، prepared statements مهمة للأداء): فكّر في
pgcatأو PgPool II اللي بيدعموا transaction-level prepared statement caching.
الخطوة التالية
افتح DB الإنتاج وشغّل الاستعلام ده:
SELECT count(*) FROM pg_stat_activity WHERE state = 'active' OR state = 'idle';لو الرقم بيعدّي 200 وذاكرة السيرفر بتقترب من 70% استخدام، نزّل pgbouncer النهارده. الإعداد فوق شغّال على Ubuntu 22.04 و Debian 12 بدون تعديل. ابدأ بـ pool_size=25 وراقب SHOW POOLS أسبوع كامل قبل ما تعدّل أي رقم.
المصادر
- توثيق pgbouncer الرسمي — أنماط الـ pooling والإعدادات: pgbouncer.org/usage.html
- PostgreSQL Connection Limits Documentation: postgresql.org/docs/current/runtime-config-connection
- PostgreSQL Wiki — Number Of Database Connections (تحليل process-per-connection): wiki.postgresql.org/wiki/Number_Of_Database_Connections
- Heroku Postgres Connection Pooling Guide: devcenter.heroku.com/articles/postgres-connection-pooling
- AWS RDS Proxy Documentation (للسيناريو serverless): docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy