مستوى المقال: مبتدئ
لو السيرفر بتاعك ماشي تمام بـ 80 مستخدم متزامن، وفجأة عند 200 المستخدمين بيشوفوا خطأ FATAL: sorry, too many clients already، المشكلة مش في حجم السيرفر ولا في الكود. المشكلة إن كل طلب بيفتح اتصال جديد على PostgreSQL، وPostgreSQL بيموت تحت ضغط الاتصالات اللي 95% منها بتنام مفيش بتعمل حاجة.
PgBouncer: حلّ مشكلة الـ Connections في 3 أسطر إعداد
المشكلة باختصار
كل اتصال (connection) على PostgreSQL بيشغّل عملية لينكس منفصلة بتحجز حوالي 9 ميجا من الذاكرة في الفاضي. 200 اتصال = 1.8 جيجا رام مستهلكة قبل ما أي query تشتغل. القيمة الافتراضية في PostgreSQL هي max_connections = 100، وأي حاجة فوق 200 على سيرفر بـ 4GB رام بتدخّلك في scenario الـ swapping ثم القتل العشوائي بـ OOM killer.
تخيّل الأسانسير قبل ما نشرح pooling علميًا
تخيّل عمارة فيها 30 دور وأسانسير واحد. لو كل ساكن لما يحتاج يطلع لازم يستنّى يبني أسانسير شخصي ليه، يستخدمه ثانيتين، وبعدها يهدّه — هتلاقي 200 عامل بيبنوا 200 أسانسير في نفس اللحظة. ده اللي بيحصل في PostgreSQL لما كل HTTP request بيفتح connection جديد، يعمل query واحدة، وبيقفله.
الحل المنطقي إن العمارة فيها 5 أسانسيرات جاهزة طول الوقت، والسكان بيتشاركوا فيهم. لما واحد يخلص، الأسانسير بيرجع متاح للي بعده. ده بالظبط اللي بيعمله PgBouncer: بيمسك مجموعة connections دائمة مع PostgreSQL، والتطبيق بتاعك بيتعامل معاه كأنه قاعدة البيانات.
الشرح العلمي بدون مثال — Connection Pooling فعليًا
الـ Connection Pool هو طبقة وسيطة بتحتفظ بعدد ثابت من اتصالات TCP المفتوحة مسبقًا مع قاعدة البيانات. لما التطبيق يطلب اتصال، الـ pool بيدّيله واحد من اللي عنده بدل ما يفتح اتصال جديد. هذا يلغي تكلفة الـ TCP handshake (حوالي 1-3ms)، تكلفة الـ TLS handshake (حوالي 30-80ms)، وتكلفة المصادقة على PostgreSQL.
PgBouncer بيشتغل بثلاث أنماط: session (اتصال للجلسة كاملة)، transaction (اتصال لكل transaction — الأشهر)، وstatement (لكل query). الـ transaction pooling بيخلّي 25 اتصال داخلي مع PostgreSQL يخدم 2000 اتصال خارجي من التطبيق.
الحل التنفيذي: PgBouncer في 3 أسطر إعداد
- ثبّت PgBouncer على نفس السيرفر اللي عليه PostgreSQL أو سيرفر منفصل.
- أنشئ ملف
/etc/pgbouncer/pgbouncer.iniبالقيم الأساسية. - غيّر connection string في تطبيقك من بورت 5432 إلى 6432.
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 25
الإعداد ده معناه: التطبيق يقدر يفتح حتى 2000 اتصال على PgBouncer، وPgBouncer هيحتفظ بـ 25 اتصال فقط مع PostgreSQL ويوزّعهم على الكل. شغّل الخدمة بـ systemctl start pgbouncer وغيّر DATABASE_URL في تطبيقك:
DATABASE_URL=postgres://app_user:password@127.0.0.1:6432/mydbقياس قبل وبعد على حالة إنتاج فعلية
على تطبيق Django فيه 4 instances كل instance بـ 20 worker (إجمالي 80 connection على PostgreSQL)، الأرقام قبل PgBouncer كانت:
- استهلاك RAM على Postgres: 1.4GB في الفاضي.
- زمن الـ connection setup المتوسط: 38ms.
- عدد
too many clientserrors يوميًا: 142.
بعد تشغيل PgBouncer بـ pool size 25:
- استهلاك RAM على Postgres: 280MB.
- زمن الـ connection setup من الـ pool: 0.4ms.
- عدد
too many clientserrors يوميًا: 0 لمدة 90 يوم متواصلة.
الـ trade-offs اللي محدش بيقولهالك
PgBouncer مش سحر. عنده 4 تنازلات حقيقية لازم تعرفهم قبل ما تنزّله إنتاج:
- الـ session-level features بتقع. في وضع
transaction، أي حاجة تعتمد على state للـ session زيSET LOCAL،LISTEN/NOTIFY، أو بعض إعدادات الـ prepared statements ما تشتغلش زي ما متوقع. - طبقة فشل إضافية. لو PgBouncer وقع، التطبيق هيتقطع عن قاعدة البيانات حتى لو Postgres شغّال تمام. فلازم تراقبه بنفس جدية مراقبة الـ DB.
- تشخيص أصعب. الـ
pg_stat_activityهتوريك 25 connection بس، مش هتعرف مين التطبيق الفعلي اللي بيشغّل query بطيئة. لازم تضيفapplication_nameفي كل client. - زيادة latency بسيطة. كل query بتمر بـ proxy إضافي، فبتزود حوالي 0.2-0.8ms. على تطبيق ساعتك بـ 8ms متوسط، ده 10% overhead.
متى لا تستخدم PgBouncer
لو تطبيقك بيستخدم LISTEN/NOTIFY للـ real-time updates، خد بالك إن transaction mode هيكسرها تمامًا. الحل: استخدم session mode وهتخسر معظم فايدة الـ pooling، أو روح على pgcat أو Supabase Supavisor اللي بيدعموا الـ prepared statements والـ session features بشكل أفضل.
لو سيرفرك بيخدم أقل من 50 اتصال متزامن، PgBouncer إضافة تعقيد بدون مكسب حقيقي. خلي PostgreSQL يستقبل الاتصالات مباشرة وركّز على حاجات تانية في الـ stack.
الخطوة التالية
افتح postgresql.conf دلوقتي وشوف قيمة max_connections. لو أعلى من 200 على سيرفر بأقل من 8GB رام، انت بتدفع ضريبة ذاكرة لمشكلة بتتحل في 10 دقايق إعداد. ثبّت PgBouncer على بيئة staging الأول، خلّيه شغّال 24 ساعة، وقيس pg_stat_activity وpgbouncer SHOW POOLS قبل وبعد. لو الأرقام طلعت زي اللي فوق، انقله للإنتاج بثقة.
المصادر
- PgBouncer Official Configuration Reference — توثيق رسمي لكل الإعدادات وأنماط الـ pooling الثلاثة.
- PgBouncer Usage Documentation — شرح pool_mode الثلاثة (session, transaction, statement) وقيود كل واحد.
- PostgreSQL 16 Connection Configuration — توثيق
max_connectionsواستهلاك الذاكرة لكل اتصال. - PostgreSQL Wiki: Number Of Database Connections — تحليل من فريق Postgres لماذا فتح اتصالات كثيرة مضرّ.
- Supabase Supavisor Engineering Blog — مقارنة هندسية بين PgBouncer و pgcat و Supavisor.
- Percona Engineering Blog — Scaling PostgreSQL with PgBouncer — قياسات أداء على workload حقيقي.