مستوى المقال: متوسط. مناسب لمن يشغّل PostgreSQL في الإنتاج ويعرف أساسيات psql والاتصال بقاعدة البيانات. لو لسه مبتدئ تمامًا، فيه مثال مبسّط في كل قسم قبل الشرح التقني.
حل مشكلة "too many clients" في PostgreSQL باستخدام PgBouncer
لو خدمتك بتقع فجأة تحت الضغط برسالة FATAL: sorry, too many clients already، المشكلة غالبًا مش في حجم السيرفر ولا في القرص. المشكلة إن تطبيقك بيفتح اتصالات أكتر مما تتحمّله القاعدة. هنا هتعرف ليه كل اتصال غالي، وإزاي تخدم آلاف العملاء بعشرين اتصال خلفي بس عبر PgBouncer.
المشكلة باختصار
PostgreSQL بيحط سقف على عدد الاتصالات المتزامنة في إعداد اسمه max_connections، وقيمته الافتراضية 100. أي طلب اتصال بعد ما توصل للسقف بيترفض فورًا بالرسالة اللي فوق. المشكلة إن التطبيقات الحديثة بتفتح اتصالات كتير: خيّل عندك 40 نسخة (instance) من خدمتك، وكل نسخة بتحتفظ بحوض 10 اتصالات. ده 400 اتصال محتمل على قاعدة سقفها 100. النتيجة: رفض اتصالات وأخطاء 500 عند أول موجة ترافيك.
ليه كل اتصال غالي فعلاً
خلّينا نبسّطها الأول. تخيّل بنك فيه 100 موظف على الشبابيك. كل عميل بيدخل بياخد موظفًا مخصصًا له طول ما هو واقف قدام الشبّاك، حتى وهو بيدوّر في أوراقه ومش بيكلّم الموظف. لو 400 عميل طلبوا شبّاك في نفس اللحظة، أول 100 بس هياخدوا موظف، والباقي هيترفض على الباب. ده بالظبط اللي بيحصل مع القاعدة.
الآن علميًا. PostgreSQL بيعمل عملية (process) مستقلة لكل اتصال، مش خيط (thread). كل عملية بتحجز ذاكرة أساسية وبتزوّد تكلفة تبديل السياق على المعالج. الاستهلاك التقديري بين 5 و10 ميجابايت لكل اتصال خامل، ويزيد تحت الحمل مع work_mem. يعني 400 اتصال مش بس بيكسروا السقف، كمان بياكلوا من 2 إلى 4 جيجابايت رام قبل ما تنفّذ أي شغل مفيد. الافتراض هنا إن اتصالاتك قصيرة العمر ومعظمها خامل بين الطلبات، وده الحال الطبيعي في تطبيقات الويب.
الحل: PgBouncer كوسيط تجميع اتصالات
بدل ما يتكلّم تطبيقك مع PostgreSQL مباشرة، تحطّ PgBouncer في النص. تطبيقك بيتصل بـ PgBouncer (المنفذ الافتراضي 6432)، وPgBouncer بيحتفظ بعدد صغير من الاتصالات الحقيقية للقاعدة ويعيد استخدامها. أهم إعداد هو pool_mode:
- transaction: الاتصال الخلفي بيتخصّص للعميل طول المعاملة بس، وبمجرد
COMMITأوROLLBACKيرجع للحوض. ده الوضع اللي بيدّي أعلى نسبة تجميع، والمناسب لمعظم تطبيقات الويب. - session: الاتصال بيفضل مخصّص للعميل طول جلسته. أأمن للميزات اللي بتعتمد على حالة الجلسة، لكن نسبة التجميع أقل بكتير.
ملف الإعداد pgbouncer.ini بسيط وقابل للنسخ:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 20
max_client_conn = 2000
التنصيب والتشغيل، ثم توصيل تطبيقك على المنفذ 6432 بدل 5432:
# على Debian/Ubuntu
sudo apt install pgbouncer
sudo systemctl enable --now pgbouncer
# اتصل عبر PgBouncer بدل الاتصال المباشر بالقاعدة
psql "host=127.0.0.1 port=6432 dbname=mydb user=app"
بكده 2000 عميل ممكن يتصلوا بـ PgBouncer (max_client_conn)، بينما القاعدة مش بتشوف غير 20 اتصال خلفي نشط (default_pool_size). سقف max_connections بقى بعيد عن الخطر، واستهلاك الرام على القاعدة ثابت ومتوقّع.
تحقّق إنه شغّال
PgBouncer عنده كونسول إدارة افتراضية اسمها pgbouncer. اتصل بيها وشوف حالة الأحواض:
psql -p 6432 -U pgbouncer pgbouncer -c "SHOW POOLS;"
هتشوف أعمدة زي cl_active (عملاء نشطين) وsv_active (اتصالات خلفية نشطة). لو cl_active بالمئات وsv_active حواليّ 20، يبقى التجميع شغّال بالظبط زي ما المفروض.
الـ trade-offs وما يجب الانتباه له
مفيش حل ببلاش. وضع transaction بيكسر أي ميزة معتمدة على حالة الجلسة، لأن العميل مش بيضمن نفس الاتصال الخلفي بين المعاملات. اللي بيتأثر: LISTEN/NOTIFY، الـ advisory locks على مستوى الجلسة، الـ SET على مستوى الجلسة، والـ cursors من نوع WITH HOLD. الـ prepared statements كانت مشكلة قديمًا، لكن PgBouncer من إصدار 1.21 بيدعمها في وضع transaction عبر max_prepared_statements.
يعني الـ trade-off بوضوح: بتكسب تقليل الاتصالات من مئات لعشرات وثبات في الرام، بتخسر بعض ميزات حالة الجلسة اللي ممكن تحتاج تعدّل كودك عشانها. لو تطبيقك بيعتمد عليها بكثافة ومش قادر تغيّرها، استخدم pool_mode = session واقبل نسبة تجميع أقل.
متى لا تستخدم هذه الطريقة
لو عدد اتصالاتك الحالية أقل من max_connections بمسافة مريحة (يعني مثلًا 30 اتصال على سقف 100 وثابت)، مفيش داعي تضيف طبقة PgBouncer وتعقيد التشغيل. راقب بس. كمان لو تطبيقك بيعتمد بشكل أساسي على LISTEN/NOTIFY أو session state ومش هتقدر تعدّله، وضع transaction مش ليك. ولو شغّال على منصة serverless زي Lambda بتتوسّع أفقيًا بسرعة، فكّر في pooler مُدار قريب من القاعدة بدل تشغيل PgBouncer بنفسك.
الخطوة التالية
افتح psql على قاعدتك ونفّذ SELECT count(*) FROM pg_stat_activity; وقارن الرقم بـ SHOW max_connections;. لو الرقم بيقرب من السقف وقت الذروة، نصّب PgBouncer بوضع transaction وdefault_pool_size = 20، وبدّل سلسلة اتصال تطبيقك للمنفذ 6432، وقيس الفرق في عدد الاتصالات الخلفية. لو sv_active فضل صغير تحت الحمل، يبقى المشكلة اتحلّت من جذورها.
المصادر
- PgBouncer — Configuration (pool_mode, default_pool_size, max_client_conn): pgbouncer.org/config.html
- PgBouncer — Features وأوضاع التجميع ودعم prepared statements: pgbouncer.org/features.html
- PostgreSQL — Connection settings و
max_connections: postgresql.org/docs/current/runtime-config-connection.html - PostgreSQL Wiki — Number Of Database Connections (تكلفة الاتصال الواحد): wiki.postgresql.org/wiki/Number_Of_Database_Connections