هذا المكسب له تكلفة محددة: وضع المعاملة يكسر بصمت كل ما يفترض وجود اتصال Postgres مستقر من طلب إلى آخر. ضبط الجلسة، الاستماع/الإخطار، الأقفال الاستشارية، المؤشرات التي تنجو من المعاملة، البيانات المعدة مسبقًا. توضح هذه المقالة بالتفصيل الآلية، وتسرد هذه الحدود مع أعراضها الدقيقة، ثم توضح كيف تقوم الواجهة الخلفية في الإنتاج (التي تم التحقق منها مباشرة في مستودعها) بتكوينها دون الوقوع في شركها. للتعرف على طريقة القياس وراء أي أرقام أداء مذكورة هنا، راجع منهجية قياس الأداء .
الأساسيات
- وضع المعاملة: يتم تحرير اتصال الخادم في نهاية كل معاملة، وليس عند قطع اتصال العميل. هذا هو الوضع الأكثر فعالية لمشاركة اتصالات REST API القصيرة.
- غير متوافق من خلال الإنشاء مع: ضبط/إعادة تعيين الجلسة، والاستماع/الإخطار، وأقفال الجلسة الاستشارية، مع مؤشرات التعليق، والجداول المؤقتة التي يُعاد استخدامها من طلب إلى آخر.
- المأزق الأكثر شيوعًا في الممارسة العملية: البيانات المُعدة المُسماة، والتي يتم تنشيطها افتراضيًا بواسطة العديد من برامج التشغيل (sqlx، asyncpg، برنامج تشغيل JDBC pgjdbc)، يمكن إعادة تشغيلها على اتصال خادم مختلف وتسبب خطأ مثل
prepared statement does not existتحت التحميل. - منذ الإصدار 1.21، يمكن لـ PgBouncer متابعة البيانات المعدة للبروتوكول في وضع المعاملة (ذاكرة التخزين المؤقت LRU عبر اتصال الخادم). هذا لا يمنع تعطيل ذاكرة التخزين المؤقت من جانب العميل إذا تغير تطبيقك
search_pathفي كل طلب. - تم التحقق منه في كود Aurabase: تعمل مجموعات المستأجرين مع
statement_cache_capacity(0)وPgBouncer فيpool_mode=transaction، بينما يظل PostgREST طوعًا على اتصال مباشر لإعادة تحميل مخططه عبر LISTEN/NOTIFY.
أوضاع التجميع الثلاثة لـ PgBouncer
يقدم PgBouncer ثلاثة أوضاع، والتي تختلف فقط عندما يعود اتصال خادم Postgres إلى التجمع المشترك. تسميها الوثائق الرسمية sessionو transaction و statement (pgbouncer.org/features.html، قسم "أوضاع التجميع"، تم الوصول إليه في 24 أغسطس 2026).
| الموضة | اتصال الخادم فضفاض | توافق الجلسة |
|---|---|---|
| الجلسة (الافتراضية) | على انقطاع العميل | المجموع: ضبط، الاستماع، المؤشرات، كل شيء يعمل مثل البث المباشر |
| معاملة | في نهاية كل معاملة (COMMIT/ROLLBACK) | جزئي: فقط ما يبقى محليًا للمعاملة |
| بيان | بعد كل طلب على حدة | الحد الأدنى: المعاملات الصريحة متعددة الاستعلامات محظورة |
يعد وضع الجلسة هو الأكثر تساهلاً ولكنه الأقل فعالية في قابلية التوسع: يظل اتصال Postgres محجوزًا للعميل طالما ظل متصلاً، حتى لو لم يفعل شيئًا بين طلبين. يتم حجز وضع كشف الحساب لحالات محددة جدًا (وكيل للقراءة فقط، وفحوصات السلامة) وحتى كسر المعاملات الصريحة الكلاسيكية. وضع المعاملة هو الحل الوسط الذي يهيمن عمليًا على REST API: كل طلب HTTP يتوافق عمومًا مع معاملة Postgres قصيرة واحدة.
كيف يعمل وضع المعاملة، الاتصال عن طريق الاتصال
في وضع المعاملة، يقوم PgBouncer فقط بإرفاق اتصال الخادم بالعميل عندما يفتح الأخير معاملة، ويعيدها إلى المجمع عند الالتزام أو ROLLBACK. بين معاملتين، قد يجد نفس العميل نفسه قد تم إعادة تعيينه إلى اتصال خادم مختلف تمامًا.
بشكل ملموس، مع default_pool_size من 20، يمكن لـ PgBouncer استيعاب عدة مئات من العملاء المتزامنين الذين، في أي وقت، ليس لديهم سوى عدد قليل من المعاملات الجارية فعليًا. هذه هي النسبة التي تبرر وضع المعاملة لواجهة برمجة تطبيقات REST ذات حركة مرور عالية ولكن معاملات قصيرة: المورد النادر (اتصال Postgres، باهظ الثمن في الذاكرة من جانب الخادم) مشغول فقط للوقت الضروري تمامًا.
يلخص هذا التعليق الأخير الجوهر: يعمل وضع المعاملة لأنه يقطع الارتباط عمدًا بين "جلسة التطبيق الخاصة بي" و"اتصال Postgres الخاص بي". كل شيء يعتمد على هذا الارتباط فواصل. يسرد القسم التالي بالضبط ما.
ما فواصل في وضع تجميع المعاملات
تسرد وثائق PgBouncer الرسمية بوضوح ميزات PostgreSQL التي تفقد معناها بمجرد إعادة تدوير اتصال الخادم بين طلبين من نفس العميل.
| الوظيفة المتأثرة | لماذا ينكسر | أعراض نموذجية |
|---|---|---|
| ضبط / ضبط الجلسة | ينطبق الإعداد على الاتصال الذي يمكن إعادة تدويره مباشرة بعد ذلك | يبدو أن المعلمة قد تم نسيانها بشكل عشوائي بين طلبين |
| الاستماع / الإخطار | يفترض وجود اتصال مستمر لتلقي الإخطارات | لا يتم إخطار العميل أبدًا، أو بشكل متقطع فقط |
| الأقفال الاستشارية للجلسة | يتم الاحتفاظ بالقفل بواسطة اتصال الخادم، وليس العميل المنطقي | يتم تحرير القفل قبل الإكمال المتوقع، أو لا يتم تحريره مطلقًا |
| مع المتزلجون عقد | يجب أن يبقى بعد المعاملة التي فتحته | خطأ "المؤشر غير موجود" في التكرار التالي |
| الجداول المؤقتة | تتعلق بجلسة Postgres، وليس المعاملة | الجدول "يختفي" في الاستعلام التالي |
| البيانات المعدة اسمه | تم إعداده على اتصال خادم معين، وإعادة تشغيله على جهاز آخر | "البيان المعد ... غير موجود" تحت التحميل |
لا تظهر معظم هذه القيود في التنمية المحلية، حيث يخدم اتصال واحد عادةً كل حركة المرور. تظهر تحت التحميل الحقيقي، عندما يتشارك العديد من العملاء فعليًا في التجمع ويتغير اتصال الخادم فعليًا بين طلبين من نفس العميل المنطقي. اختبار الدخان لا يكشف عنها أبدًا.
التصريحات المعدة: الحد الأكثر سوء الفهم
تقوم معظم برامج تشغيل Postgres الحديثة بإعداد الطلبات المسماة من جانب البروتوكول بشكل افتراضي، دون أن يطلبها رمز التطبيق صراحةً. وهذا بالضبط ما يجعل من الصعب توقع هذا الفخ.
تتم تسمية بيان البروتوكول المُعد وتخزينه مؤقتًا على اتصال خادم محدد، في وقت Parse. في وضع المعاملة، يمكن إعادة تعيين هذا الاتصال إلى عميل آخر بين طلبين من نفس العميل المنطقي. إذا أعاد برنامج التشغيل بعد ذلك تشغيل نفس اسم العبارة على اتصال لم يتم إعداده مطلقًا، فسيستجيب Postgres بخطأ صريح، عادةً prepared statement "sqlx_s_N" does not exist لعميل sqlx. السلوك متقطع: فهو يعتمد على كيفية أداء الاتصالات تحت الحمل، وليس على خطأ حتمي يمكن تكراره في كل مكالمة.
التصحيح من جانب العميل هو نفسه بغض النظر عن اللغة: قم بتعطيل ذاكرة التخزين المؤقت للبيانات المُجهزة المُسماة، أو فرض استعلامات غير مسماة، لأي تجمع يتقاطع مع مُجمّع في وضع المعاملة. في Rust مع sqlx، يمر عبر statement_cache_capacity(0) في خيارات الاتصال.
منذ الإصدار 1.21، خفف PgBouncer جزءًا من المشكلة على جانب الخادم: يمكنه متابعة البيانات المعدة للبروتوكول في وضع المعاملة وإعدادها سريعًا على الاتصال المعين، باستخدام ذاكرة تخزين مؤقت LRU لكل اتصال يتم ضبط حجمها عبر max_prepared_statements. يؤدي هذا إلى تقليل عدد الأخطاء، لكنه لا يعفيك من تعطيل ذاكرة التخزين المؤقت للعميل في تجمع متعدد المستأجرين حيث يتغير search_path من طلب إلى آخر: تجمد الخطة المخزنة مؤقتًا المعرف الداخلي (OID) للجدول الذي تم حله في وقت Parse، وقد تؤدي إعادة تشغيله ضمن مخطط آخر إلى إرجاع بيانات من مستأجر خاطئ بدلاً من خطأ بسيط.
كيف يقوم Aurabase بتكوين PgBouncer في وضع المعاملة
ينشر مستودع Aurabase PgBouncer كـ pool_mode=transaction أمام مستوى البيانات المشتركة (deploy/helm/aurabase/templates/infra/pgbouncer.yaml)، وPooler CNPG الذي تم تكوينه بشكل مماثل أمام كل مثيل Postgres مخصص للمستأجر (deploy/cnpg/tenant-pooler.yaml). يطبق كلا المسارين نفس الانضباط الموصوف أعلاه.
يوثق الكود المصدري سببًا أمنيًا محددًا لهذا الاختيار، وليس مجرد سبب استقرار. تجمعات Postgres المشتركة بين المستأجرين تضع search_path مختلفًا لكل مشروع على الاتصالات المعاد استخدامها. يقوم البيان المجهز المخزن مؤقتًا بتجميد معرف الكائن (OID) الخاص بالجدول الذي تم حله في وقت Parse؛ سيؤدي إعادة تشغيله لمستأجر آخر على نفس الاتصال إلى تشغيل الاستعلام مقابل مخطط المستأجر الأول، وهو تجاوز عزل، وليس مجرد خطأ في التطبيق. ولذلك يتم تطبيق statement_cache_capacity(0) بدون استثناء، بما في ذلك على المثيلات المخصصة التي تمر أيضًا عبر مجمع CNPG في وضع المعاملة.
الإجراء الصحي للجلسة الثانية: عندما يعود كل اتصال إلى التجمع، يتم تنفيذ خطاف DISCARD ALL (إعادة ضبط الإعدادات، وإلغاء تخصيص البيانات المعدة من جانب الخادم، وتحرير الأقفال الاستشارية، وتطهير المؤشرات والجداول المؤقتة). بدون هذا الخطاف، يمكن أن تتسرب بقايا الجلسة الناتجة عن طلب سابق عند الطلب التالي من مستأجر مختلف يعيد استخدام نفس الاتصال المعاد تدويره.
تم قبول الاستثناء: تظل مثيلات PostgREST المخصصة على اتصال مباشر بالمثيل الأساسي، دون المرور عبر المجمع. تعتمد إعادة تحميل مخطط PostgREST على LISTEN/NOTIFY، الذي يفترض وجود اتصال مستمر، وهو بالضبط الوظيفة التي يكسرها وضع المعاملة (التفاصيل موثقة بالفعل في مقالتنا عن توافق PostgREST في Aurabase). يتم تمرير إعدادات RLS لكل طلب إلى SET LOCAL ضمن معاملة صريحة، وهي الطريقة الوحيدة للبقاء متوافقة مع مجموعة يمكنها تغيير اتصالات الخادم في أي COMMIT (راجع مقالتنا حولعزل RLS متعدد المستأجرين).
تمكين وضع المعاملة دون كسر التطبيق الخاص بك
قائمة مرجعية قصيرة تنطبق على أي واجهة خلفية تنتقل من اتصال Postgres المباشر إلى PgBouncer في وضع المعاملة.
- تدقيق كود التطبيق. ابحث عن
SETغير المتعلقة بالمعاملات،LISTEN/NOTIFY، الأقفال الاستشارية للجلسة،WITH HOLDالمؤشرات والجداول المؤقتة المعاد استخدامها بين الاستعلامات. - استبدل مجموعات الجلسة بمجموعات المحلية ضمن معاملة صريحة. هذا هو الإعداد الوحيد الذي ينجو بشكل صحيح من إعادة تدوير الاتصال، لأنه يتم تنظيفه في COMMIT/ROLLBACK بدلاً من التسرب في الاتصال التالي.
- قم بتعطيل ذاكرة التخزين المؤقت للبيانات المعدة من جانب برنامج التشغيل إذا كان مجموعتك تجتاز المجمع ويتغير المخطط أو الدور من طلب إلى آخر. إن تكلفة الأداء حقيقية ولكنها قابلة للقياس، وأقل بكثير من خطر التسرب بين المستأجرين.
- عزل الاتصالات التي تحتاج بالفعل إلى وضع جلسة (عمليات الترحيل، البرامج النصية للمسؤول، أي شيء يعتمد على الاستماع/الإخطار) إلى اتصال مباشر غير مجمّع، بدلاً من التخلي عن وضع المعاملة لجميع حركات المرور الأخرى.
- الحجم
default_pool_sizeوmax_client_connنسبة إلىmax_connectionsالفعلي لـ Postgres، وليس برقم عشوائي منسوخ من مشروع آخر. - اختبار تحت الحمل الحقيقي، وليس فقط اختبار الدخان. لا تظهر أخطاء البيانات المعدة وتسريبات إعدادات الجلسة أبدًا على اتصال محلي واحد.
- قم بمراقبة
SHOW POOLSوSHOW STATSمن وحدة تحكم إدارة PgBouncer مرة واحدة في الإنتاج، لاكتشاف تشبع التجمع قبل أن يصبح مرئيًا على جانب العميل.
هل يجب عليك دائمًا اختيار وضع المعاملة بدلاً من الجلسة؟
لا، ولكنه الخيار الافتراضي الصحيح للأغلبية العظمى من واجهات برمجة تطبيقات REST. يظل وضع الجلسة مفضلاً للتطبيق القديم الذي يعتمد بشكل كبير على وظائف الجلسة التي لا يمكنك إعادة هيكلتها بسرعة، أو لحركة المرور المنخفضة حيث لا يعوض كسب التجميع عن جهد الترحيل.
PgBouncer ليس التطبيق الوحيد لنموذج التجميع هذا أيضًا: Supavisor (Supabase) وPgCat هما بديلان حديثان، مع مقايضات مختلفة بشأن توزيع الأحمال والتجميع. راجع مقارنتنا التفصيلية، PgBouncer vs Supavisor vs PgCat، للاختيار من بين الثلاثة اعتمادًا على طوبولوجيتك.
الأسئلة الشائعة
الأسئلة التي تطرح في أغلب الأحيان بمجرد تنشيط وضع المعاملة في الإنتاج.