يكون تجميع اتصالات PostgreSQL أكثر أمانًا عندما يعمل كـAdmission Control، لا كتصريح لفتح Sessions أكثر. ضع PgBouncer بين عمال التطبيق المرنين وقاعدة البيانات، وحدد اتصالات الخادم الفعلية تحت السقف التشغيلي الآمن، واسمح بطابور Client محدود، وارفض العمل قبل انتهاء مهلة المستدعي. يقدم Transaction Pooling أعلى استفادة غالبًا لطلبات الويب القصيرة، لكن بعد إثبات أن التطبيق لا يعتمد على حالة مرتبطة بالـSession.
هذا دليل عملي مبني على وثائق PgBouncer وPostgreSQL 18 الرسمية التي راجعتها في ٥ أكتوبر ٢٠٢٦. الأرقام في الإعداد مثال توضيحي وليست توصية عامة. استخرج قيم الإنتاج من مدة المعاملة وإشارات تشبع القاعدة وأولوية الأحمال واختبار ضغط مضبوط.
`max_connections` سقف وليس هدف Throughput
ينشئ PostgreSQL عملية Backend لكل اتصال مقبول. وتوضح وثائقه أن بعض الموارد تُحجّم مباشرة حسب `max_connections`، لذلك تزيد الذاكرة المشتركة وموارد أخرى عند رفع القيمة. قد يمنع الحد الأكبر خطأ “too many connections” مؤقتًا، لكنه قد يسمح بعمل متزامن أكبر مما تستطيع CPU أو Storage أو مسارات Locks إكماله.
ابدأ من التزامن النشط الآمن لقاعدة البيانات، لا من عدد Replicas في التطبيق. خدمة فيها ٤٠ حاوية وPool محلي حجمه ٢٠ يمكنها نظريًا طلب ٨٠٠ اتصال. هذا الحساب لا يثبت أن PostgreSQL يشغل ٨٠٠ Query بكفاءة. ميزانية السعة تخص Failure Domain قاعدة البيانات، ويجب أن تترك مساحة للترحيلات والمراقبة والإدارة والReplication والتعافي أثناء الحوادث.
حافظ على منافذ الطوارئ في PostgreSQL. يدعم الخادم `reserved_connections` للأدوار التي تحمل `pg_use_reserved_connections`، ويحتفظ بـ`superuser_reserved_connections` كاحتياط أخير. لا تمنح Traffic التطبيق العادي هذه الأدوار. إذا استهلك Pool كل المنافذ العادية فسيصبح فحص القاعدة أصعب في اللحظة التي تحتاج فيها إليه.
اختر حد التجميع قبل ضبط الأرقام
يقدم PgBouncer ثلاثة حدود لإعادة استخدام الاتصال. Session Pooling يحتفظ باتصال خادم واحد طوال اتصال Client ويدعم كل خصائص PostgreSQL. Transaction Pooling يعيد الاتصال بعد نهاية كل Transaction. أما Statement Pooling فيعيده بعد كل Statement ويمنع Transactions متعددة الجمل.
في HTTP APIs والWorkers والمهام Serverless ذات المعاملات القصيرة، يكون Transaction Pooling فرضية بداية مفيدة غالبًا. يمكن لألف اتصال تطبيق خامل في معظم الوقت مشاركة مجموعة أصغر من اتصالات قاعدة البيانات العاملة. لكنه ليس شفافًا؛ توثق PgBouncer أن هذا الوضع لا يحافظ على `SET`/`RESET` العادي و`LISTEN` وSQL-level `PREPARE` وSession advisory locks أو الجداول المؤقتة التي تحفظ صفوفها بين المعاملات.
استخدم Session Pooling للحمل الذي يحتاج هذه الدلالات فعلًا، أو افصله في Database Entry بميزانية مستقلة. لا تضعف Pool واجهة API كلها لأن Worker واحدًا يستخدم `LISTEN`. ويمكن لاتصال مباشر بـPostgreSQL أن يكون منطقيًا لعدد صغير ومعروف من العمال الإداريين الطويلين إذا حُجزت سعته صراحة.
ابنِ ميزانية اتصالات لا Pool عالميًا واحدًا
ينشئ PgBouncer Pool منفصلًا لكل زوج `(database, user)`. هذه النقطة تغيّر الحساب. قيمة `default_pool_size=30` لا تعني بالضرورة ٣٠ اتصال خادم للعملية كلها؛ يمكن لكل قاعدة ومستخدم الحصول على Pool مستقل. استخدم `max_db_connections` و`max_user_connections` لفرض ميزانية Failure Domain الأوسع بدل الاعتماد على Default لكل Pool فقط.
قد يبدو إعداد توضيحي هكذا:
[databases]
app = host=postgres.internal dbname=app \
pool_mode=transaction \
pool_size=32 \
max_db_connections=40 \
max_db_client_connections=240 \
query_wait_timeout=3
[pgbouncer]
listen_port=6432
max_client_conn=600
default_pool_size=20
reserve_pool_size=0
max_prepared_statements=100
server_idle_timeout=600هذه قيم شرح لا قيم إنتاج جاهزة. يحمي سقف قاعدة البيانات PostgreSQL عبر Pools الخاصة بالمستخدمين، بينما يحد سقف Clients حجم العمل الذي يمكنه الانتظار لتلك القاعدة. تصف وثائق PgBouncer الفرق بين `max_db_client_connections` و`max_db_connections` بأنه حجم Queue تصوري. أما `max_client_conn` على مستوى العملية فهو حارس إضافي وليس بديلًا للحدود الخاصة بكل قاعدة ومستخدم.
تحتاج File Descriptors إلى حساب مستقل. تحذر PgBouncer أن العدد المحتمل يشمل Clients المقبولين وPools الخادم معًا، ويكبر نظريًا مع عدد القواعد والمستخدمين. رفع Client Limit من دون رفع حد نظام التشغيل قد ينقل الفشل من PostgreSQL إلى Pooler فقط.
اجعل الطابور مقصودًا وافشل قبل انتهاء المهلة
يمتص Queue دفعة قصيرة؛ لكنه لا يصنع سعة في قاعدة البيانات. عندما تكون كل اتصالات الخادم مشغولة تنتظر المعاملات الجديدة داخل PgBouncer. وإذا بقي معدل الوصول أعلى من معدل الإنجاز، يكبر الطابور ويرتفع التأخير وقد تنتهي مهلة الطلب الخارجي قبل أن تبدأ Query أصلًا.
حدد مهلة حصول التطبيق على اتصال أو مهلة الطلب بوعي، ثم اجعل `query_wait_timeout` داخل هذه الميزانية. تفصل PgBouncer العميل إذا لم يحصل طلبه على Server خلال المهلة؛ والقيمة صفر تسمح بالانتظار بلا نهاية. الفشل المحدود أسهل في إعادة المحاولة أو Load Shedding أو عرضه بوضوح من طلب يحتل Worker حتى يغلقه Proxy آخر.
لا تعامل `reserve_pool_size` كسعة دائمة. قد يساعد Reserve صغيرًا عند Burst استثنائي بعد `reserve_pool_timeout`، لكن تشغيله باستمرار يعني أن الميزانية العادية خاطئة أو أن القاعدة متشبعة. اجعله صغيرًا أو معطلًا، وقِس سبب تفعيله.
يجب أن يصعد Backpressure إلى التطبيق أيضًا. حد العمليات المتزامنة لكل Instance، واستخدم Retry مع Jitter للقراءات الآمنة أو الكتابات Idempotent فقط، وأوقف Batch منخفض الأولوية عندما تتجاوز مدة انتظار Pool هدفها. وجود Queue في كل طبقة يخفي التشبع ويضاعف Tail Latency.
قصّر المعاملة حتى تعيد الاتصال بسرعة
لا يستطيع Transaction Pooling إعادة استخدام Server قبل نهاية Transaction. عميل `idle in transaction` يحتفظ بسعة نادرة من دون عمل مفيد، وقد يبقي Locks أو Snapshot قديمة. افتح المعاملة مباشرة قبل الجمل المترابطة، واعمل Commit قبل استدعاء HTTP خارجي، ولا تنتظر إدخال المستخدم وأنت تحتفظ بها.
Transaction ينشئها ORM حول Rendering واستدعاءات API ونشر الرسائل تهزم Pool حتى لو كانت كل SQL سريعة. افصل المسار: تحقق أولًا، ونفذ أصغر تغيير ذري في القاعدة، ثم Commit، وبعدها نفذ العمل الخارجي القابل لإعادة المحاولة عبر Outbox أو Durable Job. قِس مدة Transaction منفصلة عن مدة كل Query.
اضبط Timeouts حسب معناها بدل نسخ رقم واحد. `statement_timeout` في PostgreSQL يحد Statement، و`lock_timeout` يحد انتظار Lock، و`query_wait_timeout` في PgBouncer يحد انتظار اتصال خادم، وHTTP deadline يغطي الطلب كله. كل واحد يحمي مرحلة مختلفة.
اختبر Prepared Statements وحالة Session صراحة
يستطيع PgBouncer الحديث تتبع Protocol-level named prepared statements في Transaction Pooling عندما تكون `max_prepared_statements` أكبر من صفر. يربط أسماء Client بعبارات داخلية ويجهزها على اتصال الخادم الذي يُسند لاحقًا. أوامر SQL مثل `PREPARE` و`EXECUTE` و`DEALLOCATE` لا تحصل على المعالجة الشفافة نفسها.
لهذه الميزة كلفة Memory وتوافق. تتحكم القيمة في LRU Cache لكل اتصال خادم؛ ورفعها يبقي عبارات أكثر جاهزة. مرّر Driver وORM الحقيقيين عبر PgBouncer في Transaction Mode، واختبر تغييرات Schema وإعادة الاتصال، وتأكد أن أخطاء Prepared Plans لا تظهر أثناء Rolling Deployment.
يستحق PHP فحصًا مقصودًا. تقول FAQ الخاصة بـPgBouncer إن توافق PHP/PDO مع تتبع Prepared Statements يعتمد على PHP 8.4+ مع libpq 17؛ وللتركيبات الأقدم توصي بالترقية أو تعطيل Server-side prepares. يغيّر Emulated Prepare مكان Parsing وBinding، لذلك اعتبر اختيار Driver قرار توافق مختبرًا لا Environment Toggle مخفيًا.
راجع الاعتماد على Session أيضًا. ابحث في الكود عن `SET` و`LISTEN` وAdvisory Locks والجداول المؤقتة وSQL-level prepared statements. أضف Contract Test ينفذ معاملات متتالية يُحتمل إسنادها إلى اتصالات خادم مختلفة. نجاح Query بسيطة لا يثبت أمان Transaction Pooling.
راقب PgBouncer وPostgreSQL كنظامين
صحة Pooler وصحة قاعدة البيانات تجيبان عن سؤالين مختلفين. يعرض `SHOW POOLS` قيم `cl_waiting` وعدد اتصالات الخادم النشطة والخاملة و`maxwait`، أي عمر أقدم Client ينتظر. ويعرض `SHOW STATS` معدلات المعاملات والQueries ومددها و`avg_wait_time`، أي متوسط وقت انتظار Clients الذين حصلوا على Backend خلال فترة الإحصاء.
في PostgreSQL افحص `pg_stat_activity` حسب Application والحالة وعمر Transaction وWait Event. يوضح دليل المراقبة الرسمي كيف تكشف `wait_event_type` و`wait_event` الجلسات المنتظرة على Lock أو I/O أو تزامن داخلي. اجعل `application_name` محدودًا ومفيدًا حتى يميز عرض الخادم بين API والWorkers والترحيلات والإدارة.
فسر الطبقتين معًا:
- ارتفاع `cl_waiting` و`maxwait` مع تشبع CPU أو I/O في PostgreSQL يعني أن Pool يحمي قاعدة متشبعة؛ إضافة اتصالات خادم قد تزيدها سوءًا. - ارتفاع انتظار Pool مع وجود سعة خاملة في PostgreSQL قد يعني ميزانية صغيرة أو Pools مجزأة حسب `(database, user)` أو معاملات طويلة أو مشكلة في مسار الاتصال. - كثرة Backends بحالة `idle in transaction` عيب في Lifecycle التطبيق، وليست سببًا لرفع Pool Size. - متوسط انتظار منخفض مع p99 سيئ في التطبيق قد يخفي Endpoint أو Tenant ساخنًا؛ احفظ سياق المسار وفئة الحمل خارج Metric Labels عالية Cardinality.
أنشئ Alert على عمر Queue المستمر لا على عدد Clients فقط. قد تكون مئات الاتصالات الخاملة غير مؤذية في Transaction Pooling، بينما خمسة Clients ينتظرون وDeadline الخاصة بهم ثانية واحدة حادث بدأ بالفعل.
اختبر Burst والفشل
أعد تكوين Topology الاتصال في Staging: العدد نفسه من Replicas وسلوك Driver Pool ووضع PgBouncer وحدود PostgreSQL. شغّل Baseline ثابتًا ثم Burst قصيرًا ثم Overload مستمرًا. سجل المعاملات المكتملة وتوزيع انتظار PgBouncer والجلسات النشطة وWait Events وCPU وI/O وانتظار Locks وتصنيفات أخطاء التطبيق.
بعدها أدخل الفشل عمدًا. أوقف PostgreSQL لحظات، وأعد تشغيل PgBouncer بأمان، واستنفد ميزانية Tenant أو Workload واحدة، واترك Transaction مفتوحة، وانشر Schema change بينما Prepared Statements فعالة. تأكد أن الطوابير تبقى محدودة، وأن المستدعين يستقبلون أخطاء قابلة للتصنيف، وأن Retry لا يكرر الأثر، وأن وصول الطوارئ يبقى متاحًا.
غيّر حدًا واحدًا في كل تجربة. رفع `pool_size` وتزامن التطبيق وعدد Retry معًا يجعل النتيجة غير قابلة للتفسير. يجب أن تحدد Release Gate أقصى عمر Queue وError Budget تحت Burst المستهدف، مع إعداد Rollback واضح.
اعرف متى لا يكون PgBouncer هو الحل
لا يصلح PgBouncer Query بطيئة أو Index ناقصًا أو تنافس Locks أو Storage متشبعًا أو ORM يفتح معاملات أوسع من اللازم. أصلح العمل المكلف أولًا. وفي خدمة قليلة الاتصالات على قاعدة Managed تقدم Pooler متوافقًا أصلًا، قد يضيف Proxy آخر كلفة تشغيل بلا حماية مؤثرة.
Transaction Pooling ليس Default مناسبًا للأحمال المبنية على Session semantics. أبقها على Session Pooling أو اتصالات مباشرة محجوزة. وفي الاتجاه المقابل، يقدم Session Pooling Multiplexing ضعيفًا عندما ينشئ Serverless أو Autoscaling عددًا كبيرًا من الاتصالات الطويلة؛ عندها يجب تغيير سلوك التطبيق وPool Mode معًا.
الـInvariant الإنتاجي بسيط: يرى PostgreSQL عددًا محدودًا من Sessions الهادفة، وينتظر الطلب الزائد داخل Deadline صريحة فقط، ويمكن مراقبة كل Queue وTransaction. هكذا يصبح PgBouncer Capacity Firewall بدل مكان تختفي فيه ضغوط الاتصال. ولضوابط مرتبطة، اقرأ عزل العملاء في PostgreSQL ومراقبة انحراف Logical Replication ومراقبة عمليات Backend.
المراجع الرسمية
تدعم هذه المراجع سلوك الأدوات المذكورة. الأمثلة وقرارات التصميم توضيحية، ويجب تكييفها مع متطلبات المشروع وإصداراته.
إعداد: Noor Yasser
تعمل على تحدٍ تقني مشابه؟
أساعد الفرق على تحويل القرار المعماري إلى نطاق واضح وتنفيذ يمكن تشغيله ومراجعته بثقة.




