PgBouncer: قلّل اتصالات PostgreSQL من 600 لـ 80
مستوى القارئ: متوسط
هتكسب من المقال ده طريقة عملية تقلل ضغط الاتصالات على PostgreSQL قبل ما تزود حجم السيرفر أو تكسر إعدادات التطبيق.
المشكلة باختصار
لو عندك 20 نسخة من API، وكل نسخة فاتحة pool فيه 30 اتصال، أنت وصلت إلى 600 اتصال قبل أول spike حقيقي. PostgreSQL يقدر يتعامل مع اتصالات كثيرة، لكن كل اتصال له تكلفة ذاكرة وجدولة. الطريقة الشائعة الغلط إنك تزود max_connections وخلاص. الطريقة دي بتفشل لما الذاكرة تبدأ تروح لاتصالات نايمة بدل queries شغالة.
الافتراض هنا إن التطبيق عندك web API عادي، أغلب استعلاماته قصيرة، وكل request بيعمل query أو اثنين ثم يرجع الرد. في السيناريو ده PgBouncer في وضع transaction بيبقى مناسب جدًا.
الفكرة بمثال واضح
ركز في مثال مكتب الدعم. عندك 600 عميل يقدروا يفتحوا تذكرة، لكن مش كلهم بيتكلموا مع الموظف في نفس الثانية. بدل ما تعيّن 600 موظف، بتحط طابور منظم و80 موظف فعليين. العملاء ينتظروا أجزاء صغيرة من الثانية، والموظفين يشتغلوا باستمرار بدل ما يفضلوا قاعدين.
PgBouncer بيعمل نفس الفكرة. التطبيق يفتح اتصالات كثيرة مع PgBouncer، لكن PgBouncer يعيد استخدام عدد أقل من اتصالات PostgreSQL الفعلية. في transaction pooling الاتصال بالسيرفر يرجع للـ pool بعد نهاية كل transaction، مش بعد نهاية session التطبيق.
إعداد عملي قابل للنسخ
أفضل بداية: شغّل PgBouncer بجانب التطبيق أو كخدمة صغيرة داخل نفس الشبكة. بعد كده خلّي التطبيق يتصل بـ PgBouncer بدل PostgreSQL مباشرة.
# pgbouncer.ini
[databases]
appdb = host=postgres port=5432 dbname=appdb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 80
reserve_pool_size = 20
server_idle_timeout = 60
query_wait_timeout = 5
# جرّب الاتصال عبر PgBouncer بدل 5432
psql "postgres://app_user:secret@pgbouncer:6432/appdb" -c "select now();"
# راقب عدد اتصالات PostgreSQL الفعلية
psql "postgres://admin:secret@postgres:5432/appdb" -c \
"select count(*) from pg_stat_activity where datname = 'appdb';"
لو تستخدم ORM زي Prisma أو SQLAlchemy أو Sequelize، قلل pool الداخلي في كل instance. PgBouncer مش دعوة إن كل نسخة تفتح 100 اتصال. خليها 5 إلى 10 غالبًا، وسيب PgBouncer يدير التجميع المركزي.
القياس قبل وبعد
في API عليها 50 ألف زيارة يوميًا وذروة 120 request في الثانية، القياس المتوقع ممكن يكون كده: قبل PgBouncer كان PostgreSQL شايف 620 اتصال في الذروة، وبعد transaction pooling نزل الرقم إلى 82 اتصال فعلي. زمن P95 للاستعلامات القصيرة نزل من 180ms إلى 95ms لأن السيرفر بقى يقضي وقت أقل في إدارة الاتصالات.
الأرقام دي تقديرية كنمط قياس، مش وعد ثابت. لازم تقيس عندك بـ pg_stat_activity وpg_stat_statements وAPM. المهم إنك تقيس عدد الاتصالات وP95 latency قبل وبعد، مش متوسط الزمن فقط.
الـ trade-off هنا
المكسب واضح: ذاكرة أقل على PostgreSQL، ضغط اتصال أقل، وتحكم مركزي في الازدحام. التكلفة: بعض خصائص session-level مش هتبقى مناسبة في transaction mode. مثلًا لو الكود يعتمد على temporary tables أو prepared statements مرتبطة بالجلسة، ممكن تواجه سلوك غير متوقع.
كمان PgBouncer يضيف طبقة جديدة لازم تراقبها. لو query_wait_timeout صغير جدًا هترفض طلبات وقت الضغط. لو كبير جدًا، المستخدم هيستنى بدل ما يفشل بسرعة. أفضل طريقة تبدأ بقيم محافظة، ثم ترفع default_pool_size تدريجيًا بعد قياس wait time.
متى لا تستخدم هذه الطريقة
لا تستخدم transaction pooling لو التطبيق يعتمد بكثافة على session state داخل PostgreSQL، أو لو عندك استعلامات طويلة جدًا تحتل الاتصال لدقائق. في الحالة دي ممكن تستخدم session pooling أو تصلح الاستعلامات البطيئة أولًا. ولو عدد الاتصالات عندك أصلًا أقل من 50 والـ CPU هو المشكلة، PgBouncer مش هيعالج السبب الحقيقي.
مصادر اعتمدت عليها
- PgBouncer Configuration Documentation
- PgBouncer Features and Pooling Modes
- PostgreSQL Connection Settings
- PostgreSQL Monitoring Statistics
الخطوة التالية
الخطوة التالية: شغّل استعلام pg_stat_activity وقت الذروة واكتب رقم الاتصالات الفعلية. لو الرقم أكبر من 200 على API عادي، جرّب PgBouncer في staging أسبوع واحد وقارن P95 وعدد الاتصالات قبل ما تلمس الإنتاج.