مستوى المقال: محترف. الشرح موجّه لمن يدير خدمة إنتاج على PostgreSQL ويصطدم بسقف الاتصالات تحت الحمل. وضعنا مثالًا تشبيهيًا بسيطًا في البداية كي يلحق المبتدئ، ثم نزلنا للتفاصيل الدقيقة.
PgBouncer: استوعب 500 عامل تطبيق على 20 اتصال PostgreSQL فقط
لو خدمتك بترمي الخطأ FATAL: sorry, too many clients already تحت الحمل رغم إن الـ CPU والـ RAM لسه فاضيين، المشكلة مش في حجم السيرفر. المشكلة في عدد الاتصالات المفتوحة على PostgreSQL مباشرة. الحل اسمه connection pooling، وأشهر أداة ليه هي PgBouncer.
المشكلة باختصار
كل اتصال على PostgreSQL هو عملية (process) منفصلة على مستوى نظام التشغيل، وبتستهلك ذاكرة تتراوح عادةً بين 5 و10 ميجابايت لكل اتصال حسب الإعدادات والـ workload. القيمة الافتراضية لـ max_connections هي 100، وزيادتها مش مجانية: كل اتصال إضافي بياكل ذاكرة وبيزود تكلفة الـ context switching على المعالج.
الافتراض اللي بنبني عليه هنا: عندك خدمة بـ 500 عامل تطبيق (workers/threads) موزّعين على عدة instances، وقاعدة بيانات على سيرفر متواضع 4 vCPU و8 جيجا RAM. لو كل عامل فتح اتصاله الخاص، هتطلب 500 اتصال متزامن — والسيرفر ده عمليًا بيختنق قبل ما يوصلها.
المفهوم بمثال أولًا
تخيّل مطعم فيه 20 طاولة فقط، لكن 500 زبون بيوصلوا على مدار الليلة. لو خصّصت لكل زبون طاولة من ساعة ما يدخل لحد ما يمشي — حتى وهو بيتكلم في التليفون مش بياكل — هتقفل الأبواب بسرعة وترفض الباقي. الأذكى: الزبون ياخد طاولة وقت ما ياكل بس، وأول ما يخلّص يسيبها للي بعده. الـ 20 طاولة بتكفي 500 زبون لأن مفيش حد بيحجز طاولة وهو قاعد فاضي.
علميًا: PgBouncer وسيط خفيف (proxy) بيقعد بين تطبيقك وقاعدة البيانات. التطبيق بيفتح اتصالاته على PgBouncer (رخيصة)، وPgBouncer بيحتفظ بعدد صغير ثابت من الاتصالات الحقيقية على PostgreSQL (غالية) ويعيد استخدامها. الاتصال الخامل عند العميل لا يحجز اتصالًا حقيقيًا على القاعدة.
لماذا 20 اتصالًا قد تكون أسرع من 500
الحدس بيقول إن اتصالات أكثر = إنتاجية أعلى. ده غلط بعد نقطة معينة. لما عدد الاتصالات النشطة يتجاوز عدد الأنوية المتاحة بكتير، PostgreSQL بيقضي وقت أطول في التنافس على الـ locks والـ CPU بدل ما ينفّذ شغل فعلي. القاعدة العملية الشائعة: لو محتاج أكثر من 200 اتصال، غالبًا انت محتاج pooling مش سيرفر أكبر. حجم الـ pool المنطقي يبدأ من حوالي (عدد الأنوية × 2) + عدد الأقراص كنقطة انطلاق، يعني هنا حوالي 10 إلى 20.
الحل: إعداد PgBouncer
الخطوات قابلة للنسخ على Ubuntu 22.04 وPgBouncer 1.21:
# /etc/pgbouncer/pgbouncer.ini
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb
[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 = 1000
default_pool_size = 20
reserve_pool_size = 5
server_idle_timeout = 60