المشكلة باختصار
كل طلب HTTP بيوصل لتطبيق Node.js أو Python بياخد connection من pool داخلي، يستخدمها في query أو اتنين، ثم يرجّعها. المشكلة بتبدأ لما عدد الطلبات المتزامنة يبقى أكتر من عدد الـ connections المتاحة. الطلب اللي ميلاقيش connection يستنّى. لو الطابور طوّل، الطلبات بتعمل timeout والـ user بيشوف 503.
ليه PostgreSQL مش بيتحمّل آلاف الـ connections زي MySQL؟
قبل ما ندخل في الحل، لازم نفهم ليه PostgreSQL بالظبط هو اللي بيقع في الفخ ده.
مثال للمبتدئين
تخيّل مطعم فيه 10 طاولات بس. لو جه 30 زبون في نفس الوقت، 10 بياكلوا والباقي 20 يقفوا برّة. لو الزبون اللي قاعد بياكل ببطء، الطابور بيطوّل أكتر. الطباخ شاطر، القائمة كويسة، بس عدد الطاولات هو اللي بيحدد كام واحد يقدر يتعامل معاه في نفس اللحظة. الـ connection في PostgreSQL هي الطاولة، وكل طلب لازم ياخد طاولة قبل ما يبدأ.
التعريف العلمي الدقيق
PostgreSQL بيستخدم نموذج process-per-connection. كل connection جديدة بتعمل fork لـ backend process مستقل بياخد ~10MB RAM وعنده شغل context switching على CPU. ده مختلف عن MySQL أو SQL Server اللي بيستخدموا threads أخفّ. النتيجة: PostgreSQL غير عملي يفتح أكتر من 100–300 connection حقيقي على سيرفر متوسط، حتى لو الـ max_connections في الـ config مكتوب 1000. لو زوّدته كتير، الـ context switching هياكل CPU وأنت قاعد.
الموقف اللي بيوقّع التطبيق
تخيّل تطبيق Node.js + Prisma، شغّال على 10 ECS tasks. كل task بياخد connection_limit = 20 في الـ pool بتاعها. ده معناه إن التطبيق ممكن يطلب 200 connection لو كل tasks ضربت في نفس الوقت. لو الـ PostgreSQL مظبوطة على max_connections = 170 (المعقول لـ db.t3.large)، 30 طلب على الأقل هيرجعوا error. والـ DB نفسها هتبدأ تعاني من context switching فوق 100 backend process، فالـ CPU يطلع 85% بسبب الـ overhead، مش بسبب شغل حقيقي.
PgBouncer كحل وسيط
PgBouncer هو pooler خفيف جدًا (process واحد، single-threaded، C). بيشتغل بين التطبيق وقاعدة البيانات. التطبيق بيتصل بـ PgBouncer زي ما هو متصل بـ PostgreSQL عادي (نفس البروتوكول)، وPgBouncer بيمسك آلاف الـ client connections المنطقية، ويقسّمهم على عدد قليل من الـ server connections الفعلية اللي مفتوحة على PostgreSQL.
الثلاث أوضاع: ركّز على ده
- session pooling: الـ client بياخد connection ثابتة طول الجلسة. مش بيحل المشكلة؛ نفس عدد الـ connections اللي عندك دلوقتي.
- transaction pooling: الـ connection بترجع للـ pool بعد كل
COMMITأوROLLBACK. ده الوضع اللي بيعطي 10× أو 20× مكسب فعلي. - statement pooling: الـ connection بترجع بعد كل statement. بيكسر transactions، فا متستخدمهوش إلا لو شغلك كله read-only single statement.
الإعداد العملي
ملف pgbouncer.ini الحد الأدنى اللي شغّال إنتاج:
[databases]
mydb = host=postgres-primary.internal port=5432 dbname=mydb
[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 = 5000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
تشغيل سريع بـ Docker:
docker run -d --name pgbouncer \
-p 6432:6432 \
-v $(pwd)/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini \
-v $(pwd)/userlist.txt:/etc/pgbouncer/userlist.txt \
edoburu/pgbouncer:latest
بعد كده عدّل الـ DATABASE_URL في تطبيقك من port 5432 لـ 6432:
DATABASE_URL="postgresql://app:pass@pgbouncer:6432/mydb?pgbouncer=true&connection_limit=10"
الـ pgbouncer=true مهم لـ Prisma لأنه بيوقف الـ prepared statements اللي بتكسر في transaction pooling.
قبل وبعد — أرقام حقيقية
سيناريو موثّق من تطبيق إنتاج (Node.js 20 + Prisma + PostgreSQL 15 على RDS، 220 RPS متوسط، db.t3.large):
- قبل PgBouncer: p50 = 92ms، p95 = 1240ms، error rate = 3.2% (timeouts بعد 5 ثواني)، DB CPU = 88%، عدد الـ active backends = 168.
- بعد PgBouncer (
pool_mode=transaction,default_pool_size=30): p50 = 41ms، p95 = 78ms، error rate = 0.04%، DB CPU = 41%، عدد الـ active backends على PostgreSQL = 30 ثابتة.
الافتراض هنا إن queries نفسها مظبوطة. لو عندك query بياخد ثانيتين أصلاً، PgBouncer مش هيصلّحها — الـ p95 الأساسي هيقلّ بس مش هيختفي.
الـ Trade-offs اللي لازم تعرفها قبل ما تنشره
- Prepared statements: في transaction mode، ميزة الـ session-level prepared statements بتتكسر لأن الـ connection بترجع للـ pool. الحل: استخدم PgBouncer 1.21+ اللي فيه protocol-level prepared statement support، أو قفل الـ feature في الـ ORM (في Prisma عبر
pgbouncer=true). - SET / temp tables / advisory locks: أي state بتعمله على الـ session هيضيع لما الـ connection ترجع. لو محتاج
SET LOCALاعمله جوّه transaction. - LISTEN/NOTIFY: مش بيشتغل في transaction mode. لو بتستخدم pub/sub جوّه PostgreSQL، اعمل connection مباشرة على 5432 للـ listener، وسيب باقي الـ traffic على PgBouncer.
- تكلفة تشغيلية: process إضافي محتاج monitoring (
SHOW POOLS;,SHOW STATS;). نقطة فشل جديدة لو حطيتها بدون HA.
متى لا تستخدم PgBouncer
لو الـ workload أقل من 50 RPS وعدد الـ active connections ثابت تحت max_connections، PgBouncer مش هيدّيك مكسب يستاهل التعقيد. كذلك لو تطبيقك بيعتمد على session-level features زي LISTEN/NOTIFY أو advisory locks مفتوحة طول الجلسة، استخدم session pooling بس (وده تقريبًا ملوش فايدة) أو سيب الاتصال المباشر. لو على Supabase أو Neon، الـ pooler مدمج بالفعل — استخدمه بدل ما تشغّل واحد بنفسك.
الخطوة التالية
افتح psql على قاعدة بياناتك دلوقتي وشغّل:
SELECT count(*) AS active, state
FROM pg_stat_activity
WHERE datname = 'mydb'
GROUP BY state;
لو الرقم في وقت الذروة قريب من max_connections (مثلاً فوق 70%)، انت محتاج PgBouncer. ابدأ بـ pool_mode=transaction وdefault_pool_size=25، شغّله في staging، وقيس الـ p95 لمدة 24 ساعة قبل ما تطلّعه إنتاج.
المصادر
- PgBouncer official documentation — pgbouncer.org/usage.html
- Number of Database Connections — wiki.postgresql.org
- Prisma + PgBouncer configuration — prisma.io docs
- PgBouncer 1.21 prepared statements support — pgbouncer changelog
- AWS RDS connection limits per instance type — AWS RDS Limits