SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-21

تجميع اتصالات Postgres على VPS بسعة 4 GB

كل اتصال في PostgreSQL عملية بذاكرة مستقلة، لذلك ينفد VPS بسعة 4 GB قبل بلوغ max_connections. تعرّف على ما يصلحه pooler وما يكسره.

لماذا ينفد RAM في VPS صغير قبل بلوغ max_connections

لا تُستخدم تجميعات اتصالات Postgres على VPS كحيلة لزيادة السرعة. بل تحافظ على عمل خادم بسعة 4 GB، لأن كل اتصال PostgreSQL هو عملية منفصلة في نظام التشغيل، وتحتفظ بذاكرة خاصة بها. يضع pooler عدداً صغيراً وثابتاً من عمليات backend الفعلية خلف عدد كبير ومنخفض التكلفة من اتصالات العملاء.

القيمة الافتراضية لـ max_connections هي 100. هذه القيمة حدّ وليست ميزانية. لا يتحقق PostgreSQL أبداً مما إذا كان جهازك يستطيع فعلياً تشغيل 100 عملية backend لتنفيذ استعلامات حقيقية. لذلك يتعطل الجهاز أولاً. تختار آلية kernel out of memory (OOM) killer إحدى العمليات. وعندما تختار عملية backend، يعيد PostgreSQL تشغيل المجموعة كاملة لحماية الذاكرة المشتركة. يعرض السجل server process (PID 1234) was terminated by signal 9: Killed، ثم terminating any other active server processes. وتموت كل الاتصالات المفتوحة، بما فيها الاتصالات السليمة.

ينفد الذاكرة في الجهاز لأن كل اتصال هو عملية، ولأن work_mem تُمنح لكل عملية فرز أو تجزئة، وليس لكل اتصال. ويؤدي هذان العاملان إلى تضاعف الاستهلاك.

كل اتصال هو عملية، وكل عملية تستهلك ذاكرة

يستخدم PostgreSQL عملية واحدة لكل اتصال. ينشئ postmaster عملية backend عند اتصال العميل، وتظل هذه العملية موجودة حتى يفصل العميل الاتصال. هذه ليست خيطاً تنفيذياً. لها جداول صفحات خاصة بها، وذاكرات تخزين مؤقت خاصة بها لفهارس النظام، وخطط استعلام مخزنة مؤقتاً خاصة بها. تزداد أحجام ذاكرات التخزين المؤقت هذه عندما يصل الاتصال إلى مزيد من الجداول وينفّذ استعلامات مختلفة أكثر، لذلك يستهلك الاتصال طويل الأمد في تطبيق ORM نشط ذاكرة أكبر من الاتصال الجديد.

الذاكرة المشتركة مشتركة فعلاً. shared_buffers هي عملية تخصيص واحدة للعُنقود بأكمله، وتُربط بكل عملية backend. أما الذاكرة الخاصة فلا تُشارك، ولهذا فإن top قد يضللك هنا: يتضمن حجم مجموعة الذاكرة المقيمة (RSS) لعملية backend الصفحات المشتركة التي وصلت إليها تلك العملية، لذلك يؤدي جمع RSS عبر 50 عملية backend إلى احتساب shared_buffers أكثر من 50 مرة.

قِس الجزء الخاص بدلاً من ذلك. يقسم PSS (حجم المجموعة المتناسب) كل صفحة مشتركة على عدد العمليات التي تربطها، بينما يحسب USS (حجم المجموعة الفريد) الصفحات التي تخص تلك العملية وحدها.

sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'

يمثل العمود USS الذاكرة التي ستُحرَّر إذا انتهت عملية backend تلك. هذه هي التكلفة الفعلية لكل اتصال. تضع الأرقام المنشورة عادةً حجم عملية backend الخاملة عند بضعة ميغابايت، وحجم عملية backend التي نفّذت استعلامات ORM واسعة عند عدة أضعاف ذلك. اعتبر هذه الأرقام المنشورة قيماً نموذجية، لا أرقاماً تخص نظامك. الرقم الوحيد الذي يستحق أن تبني التخطيط عليه هو الرقم المقاس على خادمك وتحت عبء العمل لديك.

من السهل إغفال تخصيص واحد لكل جلسة. يعيّن temp_buffers القيمة الافتراضية إلى 8MB، ويُخصَّص لكل جلسة في المرة الأولى التي تصل فيها تلك الجلسة إلى جدول مؤقت. ولا تُعاد هذه الذاكرة حتى تنتهي الجلسة.

يُخصَّص work_mem لكل عملية، وليس لكل اتصال

هنا يخطئ الناس في الحسابات. الإعداد الافتراضي لـwork_mem هو 4MB، وتوضح وثائق PostgreSQL مباشرةً ما يعنيه ذلك: «قد ينفّذ الاستعلام المعقّد عدة عمليات فرز وتجميع تجزئة في الوقت نفسه، ويُسمح لكل عملية عموماً باستخدام مقدار من الذاكرة يساوي هذه القيمة قبل أن تبدأ بكتابة البيانات إلى ملفات مؤقتة». يمكن لخطة تحتوي على ثلاث عقد فرز أن تستخدم ثلاثة أضعاف work_mem داخل backend واحد وفي اللحظة نفسها.

تستهلك عمليات التجزئة مقداراً أكبر. القيمة الافتراضية لـhash_mem_multiplier هي 2.0، لذلك قد تستخدم عملية hash join أو hash aggregate مقدار work_mem مضروباً في اثنين، أي 8MB في الإعدادات الافتراضية. ويؤدي الاستعلام المتوازي إلى مضاعفة الاستهلاك مرة أخرى، لأن كل parallel worker هو عملية أخرى لها الحد المسموح به الخاص بها.

أجرِ الحسابات على VPS بسعة 4 GB. اضبط shared_buffers على 1 GB، واترك work_mem على 4MB، ودع 100 اتصال ينفّذ كل منها استعلاماً واحداً يحتوي على عقدتي تجزئة. هذا يساوي 100 مضروبة في 16MB، أي 1.6 GB من الذاكرة الخاصة، بالإضافة إلى 1 GB من shared buffers، وقبل احتساب page cache وأي شيء آخر على الخادم. والآن ارفع work_mem إلى 64MB لأن الخادم يملك ذاكرة RAM فائضة، فستستهلك الاتصالات الـ100 نفسها 100 مضروبة في 256MB. لن يحذّرك النظام. ستعرف ذلك عندما ينفّذ OOM killer مهمته.

يمكنك التحقق مما إذا كانت قيمة work_mem صغيرة جداً بدلاً من التخمين. اضبط log_temp_files = 0 في postgresql.conf ثم أعد التحميل. ستكتب كل عملية تفريغ إلى القرص سطراً يذكر اسم الملف وحجمه، مثل temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520. تعني عمليات التفريغ المتكررة أن رفع قيمة work_mem قد يساعد. أما عدم حدوث أي تفريغ فيعني أن رفع القيمة لن يحقق فائدة، لكنه سيستهلك ذاكرة لا تملكها.

الحساب الفعلي لتجمّع الاتصالات الذي يسبب المشكلة

لا يخطط أحد لفتح 240 اتصالاً. يضبطون تجمّعاً يتسع لـ20 اتصالاً، ثم يشغّلون التطبيق في أكثر من مكان.

ChartBackends requested per app topology, pool size 20 per worker (arithmetic)
The data behind this chart
[
  {
    "config": "1 worker",
    "backends": 20
  },
  {
    "config": "4 web workers",
    "backends": 80
  },
  {
    "config": "4 web + 2 background",
    "backends": 120
  },
  {
    "config": "2 hosts x 4 workers",
    "backends": 160
  },
  {
    "config": "3 hosts x 4 workers",
    "backends": 240
  }
]

يطلب أربعة من عمليات Gunicorn العاملة، يحتفظ كل منها بتجمّع يتسع لـ20 اتصالاً، ما مجموعه 80 من الاتصالات الخلفية. أضف عاملَي مهام في الخلفية، فيصبح العدد 120. كبّر الإعداد إلى 3 hosts x 4 workers، وعندها يطلب التطبيق 240 من الاتصالات الخلفية مقابل max_connections سعته 100. لا يوجد أي خطأ في إعداد أي من هذه البيئات 5 عند فحصها منفردة. فالتجمّع خاص بكل عملية، ولا يستطيع أي جزء من التطبيق رؤية العدد الإجمالي.

تدفع القيم الافتراضية للمكتبات في الاتجاه نفسه. تضبط SQLAlchemy القيمة الافتراضية لـQueuePool على pool_size=5 مع max_overflow=10، أي 15 اتصالاً لكل عملية. والقيمة الافتراضية في HikariCP هي 10. لم يكن لدى Django تجمّع مضمّن قبل الإصدار 5.1، وكان يستخدم اتصالاً واحداً لكل عملية عاملة. لذلك تواجه تطبيقات Django هذه المشكلة في وقت لاحق، ثم تواجهها دفعة واحدة عندما يضبط أحدهم CONN_MAX_AGE أو يفعّل الخيار الأحدث "pool": True. إذا كنت تشغّل تطبيق Django خلف Gunicorn وnginx، فالعدد الذي يجب ضربه هو عدد العمليات العاملة في Gunicorn، وليس عدد خوادمك.

ما الذي يغيّره تجميع اتصالات Postgres فعلياً على VPS

المجمِّع هو عملية تتحدث إلى تطبيقك باستخدام بروتوكول PostgreSQL السلكي من جهة، وتحتفظ من الجهة الأخرى بمجموعة صغيرة من اتصالات الخادم الفعلية. لا يجعل أي استعلام أسرع. بل يغيّر الجهة التي تتحمل تكلفة الاتصال، وعدد العمليات الخلفية الفعلية الموجودة.

يتحسن أمران. لا يعود إنشاء الاتصال يتطلب عملية fork إضافةً إلى عمليات البحث في الكتالوج التي تملأ ذاكرة التخزين المؤقت الفارغة للعملية الخلفية، لأن المجمِّع يتولى الرد على اتصال العميل بنفسه. والأهم أن عدد العمليات الخلفية الفعلية لا يعود مرتبطاً بعدد اتصالات التطبيق، ولذلك يمكن لـ500 عميل مشاركة 20 عملية خلفية.

الانتظار هو الميزة، وهذه هي النقطة التي يقاومها البعض. من دون مجمِّع، تحصل جميع الاستعلامات المتزامنة البالغ عددها 500 على عملية خلفية وتعمل كلها في الوقت نفسه على نواتين لوحدة المعالجة المركزية، لذلك يكون كل استعلام منها بطيئاً، وتُستهلك الذاكرة كلها في اللحظة نفسها. مع وجود مجمِّع، يعمل 20 استعلاماً وينتظر الباقي بضع ميلي ثوانٍ، ولذلك يحصل كل استعلام قيد التنفيذ على حصة فعلية من وحدة المعالجة المركزية وينتهي بسرعة أكبر. يتفوق طابور أمام مجموعة صغيرة على غياب الطابور أمام مجموعة كبيرة.

لا يضع المجمِّع حداً لأي مورد آخر على الجهاز. إذا كان Postgres يشترك في VPS مع خادم تطبيق أو مع قاعدة بيانات متجهات على VPS نفسه، فإن المجمِّع يحمي Postgres من تطبيقك فقط، ولا يفعل شيئاً آخر. ضع سقفاً صارماً للموارد التي يستخدمها الجيران أيضاً: يمكنك تحديد الذاكرة ووحدة المعالجة المركزية التي يجوز لخدمة استخدامها باستخدام systemd حتى لا تتمكن عملية خارجة عن السيطرة من إسقاط قاعدة البيانات معها. يغيّر مكان تشغيل قاعدة البيانات طريقة ضبط هذه الحدود، وهذا هو الفرق العملي بين تشغيل Postgres في Docker أو مباشرة على المضيف.

التجميع حسب الجلسة مقابل التجميع حسب المعاملة

إعداد واحد يحدد كل ما عداه، وهو pool_mode.

في التجميع حسب الجلسة، يُخصَّص اتصال بالخادم لعميل طوال مدة اتصال ذلك العميل، ويُعاد إلى التجميع عند قطع العميل اتصاله. يعمل كل شيء لأن pooler ليس سوى proxy عادي. أنت توفّر تكلفة إنشاء الاتصالات ولا تحصل على شيء آخر. إذا فتح التطبيق 200 اتصال، فستظل بحاجة إلى 200 backend.

في التجميع حسب المعاملة، يُخصَّص اتصال بالخادم لعميل طوال مدة معاملة واحدة فقط. عند COMMIT أو ROLLBACK يُعاد الاتصال إلى التجميع، ويحصل عليه العميل التالي المنتظر. هذا ما يحوّل 500 عميلاً إلى 20 backend. وهو أيضاً ما يعطّل بعض الوظائف، وهذا التعطيل مقصود حسب التصميم: قد تُنفَّذ عبارتك التالية على backend مختلف عن الذي نفّذ عبارتك السابقة.

الإعداد الافتراضي في PgBouncer هو pool_mode = session. ثبّته ولا تغيّر شيئاً، وستحصل على الجانب منخفض التكلفة من دون الفائدة. يوجد وضع ثالث، هو statement، يعيد الاتصال بعد كل عبارة مفردة ويرفض المعاملات متعددة العبارات. اتركه كما هو ما لم تعرف بالضبط سبب حاجتك إليه.

ما الذي يتعطل في وضع المعاملات، ولماذا

يفشل كل ما يلي لسبب واحد. هذه حالة تعيش داخل backend واحد، ولا يضمن transaction pooling حصولك على backend نفسه مرتين.

  • SET وRESET على مستوى الجلسة. تعمل SET search_path وSET statement_timeout وSET TIME ZONE وSET ROLE على backend الذي نفّذ تلك العبارة، وتزول قبل معاملتك التالية. استخدم SET LOCAL داخل معاملة صريحة، إذ يقتصر نطاقه على تلك المعاملة، ولذلك يكون آمناً.
  • LISTEN. ينتمي تسليم الإشعارات إلى backend الذي نفّذ LISTEN، ويُسلَّم ذلك الـbackend إلى عميل آخر بمجرد انتهاء المعاملة. يظل NOTIFY يعمل في وضع المعاملات، ما يجعل هذا الفشل مربكاً: ينجح الإرسال، لكن الاستلام لا يحدث. إذا كنت تحتاج إلى LISTEN، فافتح اتصالاً إضافياً مباشرةً إلى المنفذ 5432، متجاوزاً pooler.
  • أقفال advisory على مستوى الجلسة. يحتفظ pg_advisory_lock() بالقفل طوال الجلسة، ويحرره عند انتهائها. في transaction pooling، تُنفّذ عملية فك القفل على backend مختلف، لذلك يظل القفل محجوزاً حتى يتقاعد اتصال الخادم في PgBouncer، ويحدث ذلك افتراضياً بعد server_lifetime، أي ساعة واحدة. استخدم pg_advisory_xact_lock()، إذ يحرره backend نفسه الذي أخذه عند نهاية المعاملة.
  • PREPARE وDEALLOCATE، وهما عبارتا SQL. لا يتوفران مطلقاً في وضع المعاملات.
  • مؤشرات WITH HOLD، وأي مؤشر على جانب الخادم يُفترض أن يستمر بعد انتهاء معاملته.
  • الجداول المؤقتة التي يُفترض أن تبقى بعد commit. يضع CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWS الجدول في المخطط المؤقت لأحد الـbackends، وقد لا تكون معاملتك التالية على ذلك الـbackend.
  • LOAD.

أما العبارات المجهزة على مستوى البروتوكول فهي العنصر الوحيد الذي تغيّر وضعه. أضاف PgBouncer 1.21.0 دعماً لها في وضع المعاملات، وفعّله الإصدار 1.24.0 افتراضياً بتعيين max_prepared_statements إلى 200. تترك الإصدارات الأقدم هذه القيمة عند 0، ما يعني أن الميزة معطلة. يوفّر Ubuntu 24.04 الإصدار PgBouncer 1.22.0، ولذلك تكون الميزة موجودة، لكن عليك تعيين max_prepared_statements بنفسك. إذا لم تكن متأكداً مما يفعله إصدارك، فالإعداد الآمن يكون على العميل: يوقف psycopg 3 استخدام العبارات المجهزة على جانب الخادم عند تعيين prepare_threshold إلى None.

لدى Django تسمية خاصة به لهذا الأمر. تنص الوثائق على أن "استخدام pooler للاتصالات في وضع transaction pooling، مثل PgBouncer، يتطلب تعطيل المؤشرات على جانب الخادم لهذا الاتصال"، لأن "المؤشرات على جانب الخادم لا يمكن الوصول إليها إلا من الاتصال الذي أُنشئت فيه". عيّن DISABLE_SERVER_SIDE_CURSORS إلى True في إدخال قاعدة البيانات المعنية، وإلا سيتحول كل استدعاء .iterator() إلى فشل متقطع لا يظهر إلا تحت الحمل.

يستحق وضع المعاملات الاعتماد عليه، لكنه يمثل عقداً يجب الالتزام به. اقرأ القائمة، وتحقق من توافق ORM ومكتبة مهام الخلفية لديك معها، ثم بدّل الوضع.

ثبّت PgBouncer ووجّه التطبيق إليه

الإعداد أدناه مخصّص للتنفيذ على خادمك: Ubuntu 24.04، مع PostgreSQL يستمع مسبقاً على 127.0.0.1 والمنفذ 5432.

sudo apt update
sudo apt install -y pgbouncer
pgbouncer --version

يوفّر Ubuntu 24.04 الحزمة PgBouncer 1.22.0. أما الإصدار upstream فهو 1.25.2 اعتباراً من August 2026. تحقّق من الإصدار المثبّت لديك، لأن سلوك prepared statement المذكور أعلاه يعتمد عليه.

أنشئ role تقتصر مهمته على تسجيل الدخول إلى وحدة تحكم إدارة PgBouncer، ثم أنشئ ملف كلمات المرور. يحتاج PgBouncer إلى أسرار SCRAM (salted challenge response authentication mechanism) من pg_authid، ولا يستطيع قراءة ذلك الجدول إلا superuser.

sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
  'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
  > /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txt

إن نسخ الأسرار بدلاً من إعادة كتابة كلمات المرور هو ما يجعل هذا الإعداد يعمل. لا يستطيع PgBouncer إعادة استخدام سر SCRAM لتسجيل الدخول إلى PostgreSQL إلا عندما تتم مصادقة العميل أيضاً باستخدام SCRAM، ويكون السر الموجود في الملف مطابقاً حرفياً للسر الموجود في pg_authid (بـsalt وعدد تكرارات متماثلين، وليس بمجرد تطابق كلمة المرور)، وألا يثبّت السطر [databases] قيمة user=. أضف user=appuser إلى ذلك السطر، وعندها يحتاج PgBouncer إلى كلمة مرور بنص صريح. أكّد أن مالك الملف يطابق الحساب الذي تعمل الخدمة به، باستخدام systemctl show pgbouncer -p User. يؤدي تغيير كلمة مرور في PostgreSQL إلى ضرورة إنشاء هذا الملف من جديد، وإلا فسيُرجع الاتصال التالي password authentication failed.

اكتب الآن /etc/pgbouncer/pgbouncer.ini.

[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digits

يُبقي listen_addr = 127.0.0.1 مجمّع الاتصالات بعيداً عن الإنترنت العام، وهذا مهم لأن مجمّع الاتصالات المتاح من الخارج يُعد نقطة مصادقة لم تكن تقصد نشرها. يحدّد max_client_conn عدد اتصالات التطبيق التي سيقبلها PgBouncer، وتكلفة هذه الاتصالات منخفضة، لذلك يمكن أن تكون القيمة كبيرة. يحدّد default_pool_size عدد الاتصالات الخلفية الفعلية التي يمكن لزوج واحد من قاعدة البيانات والمستخدم الاحتفاظ بها، وهو العدد الأعلى تكلفة. يحدّ max_db_connections إجمالي الاتصالات بقاعدة البيانات عند 80، مع ترك مساحة ضمن max_connections لـpsql والنسخ الاحتياطية والمراقبة. يمنع ignore_startup_parameters = extra_float_digits PgBouncer من رفض برامج التشغيل، ومنها JDBC driver، التي ترسل ذلك المعلَمة عند الاتصال.

sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pager

يسجّل التشغيل السليم سطراً يفيد بأن PgBouncer يستمع على 127.0.0.1:6432. أما التشغيل الذي يفشل بسبب ملف كلمات المرور فيسجّل المسار الذي تعذّر عليه قراءته. ويكون السبب في الغالب مشكلة في mode أو الملكية، لا في الصياغة. غيّر بعد ذلك سلسلة اتصال التطبيق من المنفذ 5432 إلى المنفذ 6432، ثم أعد تشغيله. لا يتغير أي شيء آخر في التطبيق.

كيفية التحقق من أن pool يؤدي وظيفته

يحتوي PgBouncer على وحدة تحكم إدارية يمكن الوصول إليها عبر قاعدة بيانات افتراضية تسمى pgbouncer.

psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncer
SHOW POOLS;
SHOW STATS;
SHOW CLIENTS;

SHOW POOLS هو المؤشر الذي يجب مراقبته. cl_active هو عدد العملاء المرتبطين حالياً باتصال خادم، وcl_waiting هو عدد العملاء المنتظرين لاتصال، أما sv_active وsv_idle فهما عدد الاتصالات الخلفية الفعلية قيد الاستخدام والمتاحة، وmaxwait فهو مدة انتظار العميل الموجود في مقدمة قائمة الانتظار، بالثواني. في ظل الحمل المعتاد، تعني الحالة السليمة أن تكون قيمة cl_waiting هي 0 وقيمة maxwait هي 0. إذا ارتفعت قيمة maxwait إلى أكثر من ثانية أو ثانيتين، فهذا يعني أن pool صغير جداً أو أن الاستعلامات بطيئة جداً. ولكل حالة من هاتين الحالتين معالجة مختلفة.

تحقق من الحالة الموجودة قبل زيادة default_pool_size.

SELECT state, count(*) FROM pg_stat_activity
  WHERE backend_type = 'client backend' GROUP BY state;

إذا كانت معظم الاتصالات الخلفية في حالة idle in transaction، فحجم pool ليس المشكلة. يفتح التطبيق معاملة ثم ينفذ داخلها عملية بطيئة، مثل استدعاء HTTP، ولذلك يظل كل اتصال خلفي محجوزاً من دون تنفيذ استعلام. ستُنهي idle_in_transaction_session_timeout هذه الاتصالات، لكن المعالجة الفعلية تكون في شيفرة التطبيق. أما إذا كانت جميع الاتصالات الخلفية في حالة active، فذلك يعني أن pool ممتلئ فعلاً، ويجب تحسين الاستعلامات عبر EXPLAIN (ANALYZE, BUFFERS) قبل إتاحة مزيد من الاتصالات.

أما بالنسبة إلى تحديد الحجم، فأكثر نقطة بداية استشهاداً بها هي قاعدة HikariCP التقريبية: ضعف عدد الأنوية مضافاً إليه 1. في VPS يحتوي على 2 من الأنوية، تكون النتيجة 5. تعامل مع ذلك باعتباره رقماً أولياً منشوراً، واضبط default_pool_size بالقرب منه، ثم غيّره بناءً على maxwait. قد تبدو pools الصغيرة غير كافية، لكنها عادةً تعطي نتائج قياس أفضل، لأن الاتصال الخلفي الموجود في قائمة الانتظار لا يستهلك شيئاً، بينما يستهلك الاتصال قيد التشغيل وقت CPU وذاكرة، ويسبب تنافساً على الأقفال.

المفاضلة بين PgBouncer وPgDog وPgpool-II

PgBouncer هو الخيار المناسب للحالة المعتادة: خادم PostgreSQL واحد، وVPS واحد، وتطبيق يفتح عدداً من الاتصالات أكبر مما يستطيع الخادم استيعابه. ينفّذ مهمة واحدة، ويستخدم ملف إعداد ini واحداً، وتوفّره حزم Debian وUbuntu. يعالج الاتصالات ضمن مؤشر ترابط واحد، وهذا يكفي لحِمل بحجم VPS، ولا يصبح حدّاً إلا على الأجهزة الأكبر بكثير.

يستحق PgDog الدراسة عندما يكون قرار التوجيه في قفزة الشبكة نفسها التي يجري فيها تجميع الاتصالات. يصف PgDog نفسه بأنه proxy لتوسيع PostgreSQL، وهو مكتوب بلغة Rust، ويوفّر تجميعاً على مستوى المعاملة والجلسة، وفصل القراءة والكتابة عبر تحليل الاستعلام، إضافة إلى التقسيم مع التوجيه إلى عدة shards والالتزام على مرحلتين. استخدمه عندما يكون لديك primary وreplica واحد أو أكثر، وتريد إرسال عمليات القراءة إلى replicas من دون تعريف التطبيق بوجودها. انتبه إلى نقطتين. ترخيصه AGPLv3، لذا يجب حسم سؤال بند الاستخدام عبر الشبكة مع الجهة التي تملك هذا القرار في شركتك قبل وصوله إلى الإنتاج؛ ويرى المشروع نفسه أن الاستخدام الداخلي والتعديلات الخاصة لا ينشئان التزاماً بتوفير المصدر. كما أنه مشروع حديث نسبياً، مع إصدارات أسبوعية وأرقام إصدارات 0.x، لذلك ثبّت release tag بدلاً من متابعة main.

git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.toml

يتطلب البناء من المصدر toolchain حالياً ومستقراً للغة Rust، وCMake، ومصرّف C/C++. تتوفر أيضاً ملفات Linux الثنائية المسبقة البناء وحزم Debian في صفحة الإصدارات، إضافة إلى container image في ghcr.io/pgdogdev/pgdog. ينقسم الإعداد بين ملفين. يحتوي الملف الأول على الإعدادات العامة وإدخال واحد لكل قاعدة بيانات، وتُكتب الإدخالات هنا على هيئة مصفوفة TOML من الجداول المضمّنة، كي يسهل التمييز بين الشكلين.

databases = [
  { name = "appdb", host = "127.0.0.1" },
]

[general]
port = 6432
default_pool_size = 10

يحتوي الملف الثاني على إدخال واحد لكل مستخدم، بالصيغة نفسها للمصفوفة.

users = [
  { name = "appuser", database = "appdb", password = "change-this" },
]

يستمع PgDog إلى المنفذ 6432 افتراضياً، وهو المنفذ نفسه الذي يستخدمه PgBouncer، لذلك لا يمكن للخدمتين استخدام الإعداد الافتراضي على المضيف نفسه.

يوفّر Pgpool-II، بالإصدار 4.7.2 حتى June 2026، تجميع الاتصالات إضافة إلى موازنة الحمل، مع watchdog لتنفيذ التحويل التلقائي عند الفشل. تؤدي الميزات الإضافية إلى أوضاع فشل إضافية، ونموذج التجميع فيه هو النقطة التي يجب فهمها قبل اختياره. ينشئ Pgpool-II مسبقاً num_init_children من العمليات الابنة، وتخزّن كل عملية ابنة ما يصل إلى max_pool من اتصالات الخادم، لذلك يكون الحد الأقصى للاتصالات الخلفية هو حاصل ضرب num_init_children في max_pool. تخدم كل عملية ابنة عميلاً واحداً في كل مرة، لذلك يساوي عدد العملاء الذين يمكنك قبولهم num_init_children، ويُحدَّد عند بدء التشغيل، كما يظل العميل الخامل يشغل عملية ابنة. اضبط num_init_children على 100 وmax_pool على 4، وبذلك تسمح بـ400 اتصال خلفي، وهي المشكلة نفسها التي ثبّتَّ pooler لحلها. اختر Pgpool-II عندما تريد ميزتي التحويل عند الفشل وتوجيه الاستعلامات، ثم احسب حاصل الضرب بعناية. أما إذا كان كل ما تريده هو تقليل عدد الاتصالات الخلفية، فهو يوفر آلية أكثر تعقيداً مما تتطلبه المهمة.

سؤال الوكيل المُدار، والبديل المستضاف ذاتياً

تطرح المنصات المُدارة هذا كمنتج مستقل. تضع AWS خدمة RDS Proxy أمام RDS، وتضع Supabase مجمّع الاتصالات الخاص بها، Supavisor، أمام Supabase Postgres. يؤدي كلاهما الوظيفة الموضحة هنا: يحتفظ باتصالات العملاء بتكلفة منخفضة، ويوزّع عدداً أقل من الاتصالات الفعلية بخوادم قاعدة البيانات. Supavisor مفتوح المصدر ويمكن استضافته ذاتياً، لذلك لا تقتصر المقارنة على خيار مملوك مقابل خيار مجاني.

البديل المستضاف ذاتياً للوكيل المُدار ليس فكرة مختلفة. إنّه الفكرة نفسها مع وجود ملف الإعدادات تحت سيطرتك: تشغيل PgBouncer في وضع transaction على نفس VPS الذي توجد عليه قاعدة البيانات، مع الاستماع على 127.0.0.1. هناك اختلافان فعليان. يوجد الوكيل المُدار على مسافة قفزة شبكية واحدة، لذلك يضيف زمن تأخير، ويواصل الاحتفاظ باتصالات العملاء أثناء إعادة تشغيل قاعدة البيانات تحته. أما PgBouncer على مضيف قاعدة البيانات فيضيف قفزة عبر loopback، وهي تكاد تكون مجانية، لكنه يتوقف عندما يتوقف ذلك المضيف. إذا أردت سلوك الاستمرار بعد إعادة التشغيل، فأنت تحتاج أيضاً إلى آلية failover، وهنا تبدأ آلية watchdog في Pgpool-II أو فحوصات الصحة في PgDog بتبرير تعقيدها.

يوجد خيار آخر يستحق الإضافة إلى القائمة. إذا كان عدد الاتصالات هو العامل الرئيسي الذي يجعل النشر معقداً، فلا توجد في قاعدة البيانات المضمّنة منظومة اتصالات تحتاج إلى التجميع، لأنها مكتبة داخل عمليتك وليست خادماً يستمع على منفذ. بالنسبة إلى خادم تطبيق واحد بحجم كتابة متوسط، فإن تشغيل SQLite في بيئة الإنتاج على VPS يزيل هذه المشكلة بالكامل بدلاً من إدارتها. وعندما تحتاج إلى خادم قاعدة بيانات فعلي، حدّد حجم pool قبل تحديد حجم الجهاز.

FAQ

هل ما زلت أحتاج إلى PgBouncer إذا كان تطبيقي يستخدم مسبح اتصالات بالفعل؟

غالباً نعم، لأن مسبح الاتصالات في التطبيق يكون خاصاً بكل عملية، ولا يستطيع رؤية مسابح العمليات الأخرى. إذا كان لديك أربعة عمال Gunicorn، ويحتفظ كل منهم بمسبح يضم 20 من الاتصالات إلى 80 backends، فإن إضافة عاملين للخلفية تجعل العدد 120. PgBouncer هو المكوّن الوحيد الذي يرى الإجمالي ويمكنه فرض حد أقصى له. الترتيب المناسب هو استخدام الاثنين معاً: مسبح صغير داخل كل عامل حتى لا تدفع الطلبات تكلفة إنشاء اتصال TCP، وPgBouncer في transaction mode لتحديد عدد الاتصالات الفعلية إلى backends خلف هذه المسابح.

ما الذي يتعطل تحديداً عند تحويل PgBouncer إلى transaction mode؟

يتعطل كل ما يحتفظ بالحالة في backend واحد عبر عدة معاملات. يشمل ذلك SET وRESET على مستوى الجلسة، والمؤشرات LISTEN وWITH HOLD، وتعليمتي SQL PREPARE وDEALLOCATE، وأقفال advisory على مستوى الجلسة، والجداول المؤقتة التي يجب أن تبقى بعد تنفيذ commit، وLOAD. يستمر NOTIFY في العمل، ولذلك قد يبدو تعطل LISTEN كأنه خلل في التسليم، لا في تجميع الاتصالات. في Django، اضبط DISABLE_SERVER_SIDE_CURSORS على True. وفي psycopg 3، اضبط prepare_threshold على None، أو شغّل PgBouncer 1.22 أو إصداراً أحدث مع ضبط max_prepared_statements على قيمة أكبر من 0. استبدل pg_advisory_lock() بـpg_advisory_xact_lock().

ما الحجم المناسب لـ default_pool_size على VPS بموَصِّلين؟

اجعله أصغر مما يبدو مناسباً. القاعدة الإرشادية الشائعة المنشورة لـHikariCP هي ضعف عدد الموصلات تقريباً زائد واحد، أي نحو 5 على موصلين، وهذا يمثل نقطة بداية لا إجابة نهائية. اضبط القيمة، ثم اقرأ maxwait وcl_waiting في SHOW POOLS أثناء تحميل فعلي. إذا كانت القيمتان صفراً، فهذا يعني أن حجم المسبح كافٍ. تشير زيادة maxwait إلى أن العملاء ينتظرون في طابور. وقبل زيادة العدد، افحص pg_stat_activity: فوجود backends عالقة في idle in transaction يمثل خطأ في التطبيق، ولن تؤدي زيادة الاتصالات إلا إلى إخفائه.

PgBouncer أم PgDog؟

استخدم PgBouncer مع خادم PostgreSQL واحد على VPS واحد، فهذا هو السيناريو الأكثر شيوعاً. يأتي PgBouncer ضمن حزم Ubuntu، وسلوكه موثق جيداً، وتهيئته كاملة في ملف ini واحد. استخدم PgDog عندما يكون تقسيم القراءة والكتابة بين النسخ المتماثلة أو التجزئة جزءاً من المسار نفسه الذي تتم فيه إدارة تجمع الاتصالات، حتى لا يضطر التطبيق إلى معرفة البنية. قبل اعتماد PgDog، احسم مسألة AGPLv3 مع الجهة المسؤولة عن التراخيص في مكان عملك، وثبّت إصداراً محدداً، لأن المشروع لا يزال يستخدم أرقام إصدارات 0.x ويصدر إصداراً جديداً كل أسبوع.

#postgres#pgbouncer#pgdog#connections#أداء