تجميع اتصالات Postgres (Pooling) في بيئة الإنتاج: PgBouncer + Supavisor
٧ مايو ٢٠٢٦
تستهلك وصلة Postgres الواحدة الخاملة حوالي 2-3 ميجابايت من الذاكرة قبل أن تقوم بتنفيذ أي استعلام.1 إذا دفعت بهذا الرقم إلى بضعة آلاف من استدعاءات serverless التي تفتح جميعها وصلاتها الخاصة، فستنهار قاعدة بياناتك بسبب ضغط الذاكرة قبل وقت طويل من أن يصبح المعالج (CPU) هو العائق. تجميع الوصلات (Connection pooling) هو الحل — ولكن بمجرد وضع مجمع (pooler) أمام Postgres، تتوقف نصف ميزاتك المفضلة عن العمل بهدوء.
يستعرض هذا الدليل إعداداً جاهزاً للإنتاج لكلا العالمين: PgBouncer 1.25.1 المستضاف ذاتياً أمام Postgres 18، ثم نفس التطبيق موجهاً إلى مجمع Supavisor المدار من Supabase. تم التحقق من كل قيمة إعداد، ومنفذ، وأمر مقابل ملاحظات إصدار PgBouncer 1.25.1 الرسمية،2 ومستودع كود Supavisor،3 ووثائق Supabase الخاصة4 في يوم الكتابة.
ملخص
ستقوم بتشغيل Postgres 18 و PgBouncer 1.25.1 باستخدام Docker Compose، وتهيئة تجميع المعاملات (transaction pooling) مع مصادقة SCRAM-SHA-256، وإنشاء قائمة مستخدمي SCRAM، وتوصيل تطبيق Node.js عبر المنفذ 6432. ستقوم بعد ذلك بتبديل سلسلة الاتصال إلى Supabase Supavisor (المنفذ 6543) ومشاهدة نفس الكود يعمل مقابل مجمع مدار. بنهاية الدليل، ستعرف ميزات Postgres التي تتعطل تحت تجميع المعاملات، وكيفية تمكين العبارات المعدة (prepared statements) على مستوى البروتوكول (مما يحقق زيادة في الإنتاجية بنسبة 15-250 بالمائة5)، وكيفية قراءة SHOW POOLS لاكتشاف المجمع المتعطش قبل أن يلاحظ المستخدمون.
ما ستتعلمه
- لماذا تعد وصلة Postgres الواحدة أغلى مما تعتقد، مع حسابات ذاكرة حقيقية.
- أوضاع تجميع PgBouncer الثلاثة — الجلسة (session)، المعاملة (transaction)، العبارة (statement) — وما الذي يعطله كل منها.
- كيفية تشغيل Postgres 18 + PgBouncer 1.25.1 باستخدام أمر واحد
Docker compose up. - كيفية تهيئة مصادقة SCRAM-SHA-256 وملف
userlist.txtحقيقي. - كيفية تمكين العبارات المعدة المسماة على مستوى البروتول (ميزة PgBouncer 1.21+).
- كيفية ربط تطبيق Node.js يستخدم
pg@8.20.0بالمجمع، وما الذي يتغير مقارنة بالتحدث إلى Postgres مباشرة. - كيفية توجيه نفس الكود إلى Supabase Supavisor عن طريق تغيير سلسلة الاتصال فقط.
- كيفية قراءة
SHOW POOLSوSHOW STATSكمصحح أخطاء (debugger)، وليس كمجرد لوحة بيانات. - إخفاقات الإنتاج الحقيقية الخمسة التي يواجهها كل فريق — LISTEN عبر مجمع معاملات، العبارات المعدة المعطلة، الأقفال الاستشارية (advisory locks)، SET على مستوى الجلسة، ومفاجآت IPv4 في اتصالات Supabase المباشرة.
المتطلبات الأساسية
| الأداة | الإصدار المحدد | السبب |
|---|---|---|
| Docker Desktop | 4.30+ (أو Docker Engine 27+) | استخدام صيغة Compose v2 أدناه |
| صورة Postgres | postgres:18-alpine | تم إطلاق Postgres 18 GA في 25-09-20256 |
| صورة PgBouncer | edoburu/pgbouncer:v1.25.1-p0 | توفر PgBouncer 1.25.1 على Alpine 3.227 |
| Node.js | 22 LTS | من أجل العميل التجريبي |
pg (npm) | 8.20.0 | أحدث إصدار مستقر في سجل npm |
dotenv (npm) | 17.4.2 | لتحميل ملفات البيئة (env-file) |
psql | 16+ | لوحدة تحكم المسؤول |
| مشروع Supabase (الخطوة 6) | الفئة المجانية تعمل للاختبار | فئة Pro+ إذا كنت بحاجة إلى عنوان IPv4 مخصص8 |
لماذا التجميع — حسابات الذاكرة التي لا يذكرها أحد
يعمل Postgres بنموذج "عملية لكل وصلة" (process-per-connection). القيمة الافتراضية لـ max_connections هي 100 و work_mem هي 4 ميجابايت.1 تعمل الوصلة الخاملة في عملية خلفية خاصة بها وتستهلك حوالي 2-3 ميجابايت من الذاكرة المقيمة قبل تشغيل أي استعلام9 — يعتمد الرقم الدقيق على الصفحات الضخمة (huge pages)، وسلوك "النسخ عند الكتابة" (copy-on-write) للنواة، وحمل العمل، لكن الترتيب الحسابي يظل ثابتاً. قد تستخدم الوصلة النشطة التي تشغل استعلاماً معقداً عدة مخازن مؤقتة لـ work_mem في وقت واحد: يمكن لعملية فرز (sort) وعملية ربط هاش (hash join) أن تأخذ كل منهما قطعة 4 ميجابايت خاصة بها فوق ذلك الأساس.
هذه الحسابات لها عواقب. تطبيق بأسلوب Lambda يقوم بتشغيل 1000 حاوية (container) باردة وتفتح كل منها وصلة، لا يطلب من قاعدة بياناتك "1000 مقبس" (sockets). بل يطلب حوالي 1000 × ~3 MB ≈ 3 جيجابايت من ذاكرة العمليات، بالإضافة إلى أي استعلامات تعمل في ذلك الوقت. على جهاز db.t4g.medium بذاكرة رام 4 جيجابايت، انتهت اللعبة بالفعل.
صيغة تحديد حجم مجمع Postgres الكلاسيكية هي (CPU_cores × 2) + number_of_disks من دليل ضبط Postgres wiki.10 بالنسبة لجهاز بـ 4 نواة مدعوم بقرص SSD، هذا يعني 9 وصلات. ومع ذلك، من المحتمل أن يحتاج تطبيقك للتعامل مع 500 طلب HTTP متزامن. التجميع هو الجسر بين هذين الرقمين.
أوضاع تجميع المجمع الثلاثة — وما الذي يعطله كل منها
يمكن تهيئة PgBouncer في ثلاثة أوضاع تجميع،11 ويحدد الوضع ميزات Postgres التي ستستمر في العمل.
| الوضع | يتم حجز الخادم لـ | هل تستمر ميزات الجلسة؟ | الاستخدام الواقعي |
|---|---|---|---|
session | عمر وصلة العميل | نعم — تكافؤ كامل مع Postgres | العمال (workers) طويلي الأمد، أدوات إدارة pgsql، مشغلات الهجرة (migrations) |
transaction | BEGIN ← COMMIT/ROLLBACK | لا: LISTEN، و SET للجلسة، والأقفال الاستشارية للجلسة، والجداول المؤقتة عبر المعاملات، ومؤشرات WITH HOLD | افتراضي لتطبيقات الويب عديمة الحالة (stateless) |
statement | عبارة واحدة فقط | يتم رفض المعاملات متعددة العبارات | أحمال العمل المعتمدة على البحث عن المفاتيح فقط في وضع autocommit |
تجميع المعاملات (Transaction pooling) هو ما تريده معظم تطبيقات الويب الإنتاجية — إنه الوضع العملي الذي يسمح لك بوضع 10,000 عميل ويب أمام 20 خلفية Postgres دون التخلي عن المعاملات متعددة العبارات (تجميع العبارات يمنع ذلك بحكم التعريف). المقايضة حادة: تتوقف ميزات حالة الجلسة (session-state) عن العمل لأن الاستعلام التالي قد يستقر على وصلة خلفية مختلفة.12 يعد LISTEN/NOTIFY بشكل خاص لغماً موثقاً — لا يعمل LISTEN في وضع المعاملة، على الرغم من أن NOTIFY يعمل.13 إذا كنت تكتب أيضاً تطبيقات في الوقت الفعلي، فراجع درس حضور Postgres LISTEN/NOTIFY للتعرف على نمط الحل البديل (وصلة مباشرة مخصصة للمستمع، مع تجميع بقية تطبيقك).
الخطوة 1 — تشغيل Postgres 18 + PgBouncer باستخدام Docker Compose
قم بإنشاء دليل جديد وملف Compose. سنقوم بربط PgBouncer للتحدث مع Postgres عبر شبكة Docker خاصة، ونعرض الخدمتين للمضيف على منافذ مختلفة — بهذه الطريقة يمكن لاختبار التحميل في القسم الأخير استهداف أي منهما مباشرة.
mkdir pg-pooling-demo && cd pg-pooling-demo
mkdir pgbouncer
احفظ ما يلي باسم Docker-compose.yaml:
services:
postgres:
image: postgres:18-alpine
container_name: demo-pg
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: app-secret-rotate-me
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256"
command:
- "postgres"
- "-c"
- "password_encryption=scram-sha-256"
- "-c"
- "max_connections=50"
ports:
- "5433:5432"
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- poolnet
pgbouncer:
image: edoburu/pgbouncer:v1.25.1-p0
container_name: demo-pgbouncer
depends_on:
- postgres
ports:
- "6432:5432"
volumes:
- ./pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
- ./pgbouncer/userlist.txt:/etc/pgbouncer/userlist.txt:ro
networks:
- poolnet
volumes:
pgdata:
networks:
poolnet:
هناك بعض الخيارات التي تستحق التوضيح. أولاً، قمنا عمداً بضبط max_connections=50 على Postgres — وهي قيمة صغيرة بما يكفي لتتمكن من إثبات أن التجميع (pooling) يعمل عن طريق إغراقه بالطلبات بدون مجمع. ثانياً، فرضنا password_encryption=scram-sha-256 حتى يعمل ملف قائمة المستخدمين (userlist) الذي سننشئه لاحقاً (Postgres 14+ يستخدم هذا افتراضياً على أي حال، ولكن من الجيد أن نكون صريحين). ثالثاً، قمنا بعرض Postgres على منفذ المضيف 5433 و PgBouncer على منفذ المضيف 6432، بحيث يمكن لاختبار التحميل في قسم التحقق استهداف كليهما مباشرة: 5433 = Postgres مباشر (بحد أقصى 50 اتصالاً)، 6432 = مجمع (يصطف حتى 1000 اتصال).
لن نقوم بتشغيل PgBouncer بعد لأنه يحتاج إلى pgbouncer.ini و userlist.txt. قم بتشغيل Postgres فقط في الوقت الحالي:
Docker compose up -d postgres
Docker compose logs -f postgres # انتظر ظهور رسالة "database system is ready to accept connections"
الخطوة 2 — تكوين PgBouncer لتجميع المعاملات (transaction pooling)
احفظ ما يلي باسم pgbouncer/pgbouncer.ini:
[databases]
appdb = host=postgres port=5432 dbname=appdb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 5432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
# Pooling
pool_mode = transaction
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
max_client_conn = 1000
max_db_connections = 40
# Prepared statements (protocol level — PgBouncer 1.21+)
max_prepared_statements = 200
# Hygiene
server_reset_query = DISCARD ALL
query_wait_timeout = 30
server_idle_timeout = 600
log_connections = 1
log_disconnections = 1
# Admin console
admin_users = appuser
stats_users = appuser
ignore_startup_parameters = extra_float_digits,application_name
القيم المذكورة أعلاه تم ضبطها بدقة وليست عشوائية. default_pool_size = 20 هي نقطة بداية معقولة مقابل Postgres بـ 50 اتصالاً،14 مما يترك مجالاً لعمليات النقل (migrations)، وفتحات النسخ الاحتياطي (replication slots)، وجلسات psql العارضة. max_client_conn = 1000 تسمح لألف عامل HTTP بالبقاء على المجمع في نفس الوقت. query_wait_timeout = 30 تتجاوز القيمة الافتراضية لـ PgBouncer البالغة 120 ثانية15 — فمعظم واجهات البرمجة (APIs) تفضل الفشل السريع بدلاً من الانتظار في الطابور لمدة دقيقتين. server_reset_query = DISCARD ALL هي أيضاً القيمة الافتراضية لـ PgBouncer؛16 وهي مدرجة صراحةً حتى تتذكر وجودها عندما تبدأ في استكشاف أخطاء "لماذا يرى هذا الاستعلام حالة متبقية من عميل آخر".
max_prepared_statements = 200 هو السطر الذي يفعل العبارات المعدة المسماة (named prepared statements) على مستوى البروتوكول في وضع المعاملات — وهي ميزة تمت إضافتها في PgBouncer 1.21.017 والتي تحقق إنتاجية أعلى بنسبة 15-250 بالمائة اعتماداً على عبء العمل.5 سنقوم بتجربتها من Node في الخطوة 5.
الخطوة 3 — إنشاء قائمة مستخدمين SCRAM-SHA-256
يتوقع PgBouncer 1.25.1 أسرار المصادقة في userlist.txt. مع auth_type = scram-sha-256 يمكنك تقديم إما نص عادي (وهو خطر أمني للصيانة) أو سر SCRAM المخزن في pg_authid.rolpassword.18 سنستخدم النهج الثاني.
اتصل بـ Postgres المشغل مباشرة واحصل على سر SCRAM. لاحظ أن pg_authid هو كتالوج متميز — فقط المستخدمين الخارقين (superusers) يمكنهم إجراء SELECT منه. تنشئ صورة postgres الرسمية في Docker مستخدم POSTGRES_USER كمستخدم خارق للتمهيد، ولهذا السبب يعمل الأمر التالي كـ appuser. في بيئة الإنتاج، ستقوم بتشغيل هذا كدور DBA مخصص.
Docker exec -it demo-pg psql -U appuser -d appdb -c \
"SELECT rolname, rolpassword FROM pg_authid WHERE rolname='appuser';"
يجب أن ترى شيئاً مثل:
rolname | rolpassword
---------+----------------------------------------------------------------------------------------------------------------------------
appuser | SCRAM-SHA-256$4096:abc123==$storedKeyBase64=:serverKeyBase64=
(1 row)
انسخ قيمة rolpassword حرفياً — بما في ذلك علامات الاقتباس وعلامات الدولار — واكتبها في pgbouncer/userlist.txt:
"appuser" "SCRAM-SHA-256$4096:abc123==$storedKeyBase64=:serverKeyBase64="
ضع كلا الحقلين بين علامتي اقتباس. التنسيق هو "username" "password-or-secret" وفقاً لوثائق تكوين PgBouncer.19 قم بتأمين الملف بحيث لا يكون قابلاً للقراءة من قبل الجميع:
chmod 600 pgbouncer/userlist.txt
الآن قم بتشغيل PgBouncer:
Docker compose up -d pgbouncer
Docker compose logs -f pgbouncer
أنت تبحث عن بدء تشغيل نظيف بدون أخطاء في auth_file. إذا رأيت password authentication failed، فإن السبب الأكثر شيوعاً هو نسخ ولصق سر SCRAM مع سطر جديد زائد أو بعلامات اقتباس ذكية من محرر نصوص غني — أعد اللصق من طرفية (terminal) عادية.
الخطوة 4 — التحقق من المجمع باستخدام psql
اتصل عبر PgBouncer على المنفذ 6432 — وليس مباشرة بـ Postgres على 5432:
psql "host=127.0.0.1 port=6432 user=appuser dbname=appdb"
قم بتشغيل استعلام بسيط، ثم افصل الاتصال. الآن اتصل بوحدة تحكم إدارة PgBouncer — وهي قاعدة بيانات وهمية خاصة تسمى pgbouncer:
psql "host=127.0.0.1 port=6432 user=appuser dbname=pgbouncer"
بمجرد الدخول، قم بتشغيل أهم أمرين للتشخيص:
SHOW POOLS;
SHOW STATS;
يعرض SHOW POOLS صفاً واحداً لكل زوج (database, user) مع العدد المباشر لاتصالات العميل والخادم في كل حالة.20 الحقول التي تهمك فعلياً في بيئة الإنتاج هي:
cl_active— العملاء المقترنون باتصال خادم، والذين يقومون بمعالجة استعلام.cl_waiting— العملاء الذين ينتظرون في الطابور للحصول على خادم. أي قيمة فوق الصفر لفترات مستمرة هي علامة تحذير لك.cl_active_cancel_req— العملاء الذين أصدروا أمراً بالإلغاء وينتظرون استجابة الخادم.21sv_active— اتصالات الخادم المقترنة حالياً بعميل.sv_idle— اتصالات الخادم الموجودة في المجمع والجاهزة للتسليم.sv_used— اتصالات الخادم التي ظلت خاملة لفترة كافية تجعل PgBouncer يرغب في فحص سلامتها قبل إعادة استخدامها.
يعطيك SHOW STATS إجماليات ومتوسطات متحركة لكل قاعدة بيانات — إجمالي المعاملات، إجمالي الاستعلامات، متوسط مدة الاستعلام بالميكروثانية، متوسط وقت الانتظار، البايتات المستلمة والمرسلة — حتى تتمكن من رسم مخطط لإنتاجية المجمع بمرور الوقت دون الحاجة إلى إضافة أدوات قياس لتطبيقك.
الخطوة 5 — ربط تطبيق Node.js عبر المجمع
العقد البرمجي للتطبيق هو "تظاهر بأن المجمع هو Postgres." مع استثناء واحد سنراه بعد قليل.
mkdir client && cd client
npm init -y
npm install pg@8.20.0 dotenv@17.4.2
احفظ ما يلي باسم client/index.mjs:
import 'dotenv/config';
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({
host: process.env.PGHOST,
port: Number(process.env.PGPORT),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
});
async function main() {
const start = Date.now();
const tasks = Array.from({ length: 200 }, (_, i) =>
pool.query({
name: 'select_const', // يعطي المحرك اسماً ثابتاً -> إعداد على مستوى البروتوكول
text: 'SELECT $1::int AS n, current_setting($2) AS app',
values: [i, 'application_name'],
}),
);
const results = await Promise.all(tasks);
console.log(`تم تشغيل ${results.length استعلام في ${Date.now() - start مللي ثانية`);
await pool.end();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
و client/.env:
PGHOST=127.0.0.1
PGPORT=6432
PGDATABASE=appdb
PGUSER=appuser
PGPASSWORD=app-secret-rotate-me
قم بتشغيله:
node index.mjs
يجب أن ترى شيئاً مثل ran 200 queries in 180 ms. الآن عد إلى وحدة تحكم الإدارة وقم بتشغيل SHOW POOLS; مرة أخرى — سترى sv_active يرتفع لفترة وجيزة نحو 10 (الحد الأقصى لـ Pool في Node)، وقد يظهر cl_waiting مدخلات غير صفرية أثناء الاندفاع، ثم يستقر كل شيء ويعود إلى sv_idle.
التفصيل المهم في تلك القطعة البرمجية هو حقل name في الاستعلام. عندما يتم ضبط name، يستخدم node-postgres بروتوكول الاستعلام الممتد — Parse بهذا الاسم، ثم Bind و Execute — مع تخزين الخطة المحللة مؤقتاً على أساس كل اتصال. مع max_prepared_statements = 200 في جانب PgBouncer، يتتبع المجمع تلك العبارات المعدة المسماة على مستوى البروتوكول ويعيد إعدادها بشفافية على أي خلفية (backend) يسلمها لك تالياً.22 بدون max_prepared_statements (التي تكون افتراضياً 0 — معطلة)، يرفض PgBouncer العبارات المعدة المسماة في وضع المعاملات وتتراجع معظم المحركات إلى الاستعلامات الممتدة غير المسماة، مما يضيع ميزة الأداء.
الخطوة 6 — التبديل إلى Supabase Supavisor
حتى الآن كنا نلعب دور مهندس SRE للمجمع (pooler) الخاص بنا. النصف الآخر من هذا الدليل هو المسار المدار بالكامل. توفر Supabase أداة Supavisor، وهو مجمع Postgres متعدد المستأجرين مكتوب بلغة Elixir ومتوافق مع بروتوكول Postgres-wire، أمام كل مشروع.3 لتبديل تطبيقك من PgBouncer إلى Supavisor، ستقوم بتغيير شيء واحد فقط — سلسلة الاتصال (connection string).
في لوحة تحكم Supabase، افتح Project Settings ← Database ← Connection string. ستظهر لك سلسلتا اتصال:4
# Session mode (port 5432) — full Postgres parity, persistent clients
postgres://postgres.<project-ref>:<password>@aws-0-<region>.pooler.Supabase.com:5432/postgres
# Transaction mode (port 6543) — default for serverless / web apps
postgres://postgres.<project-ref>:<password>@aws-0-<region>.pooler.Supabase.com:6543/postgres
قم بتحديث ملف client/.env ليشير إلى مجمع وضع المعاملات (transaction-mode):
PGHOST=aws-0-us-east-1.pooler.Supabase.com
PGPORT=6543
PGDATABASE=postgres
PGUSER=postgres.<your-project-ref>
PGPASSWORD=<your-db-password>
أعد تشغيل node index.mjs. سيعمل نفس الكود تماماً.
إليك بعض الحقائق الخاصة بـ Supavisor التي سيواجهها فريقك. المنفذ 5432 هو الآن لوضع الجلسة (session mode) والمنفذ 6543 هو لوضع المعاملات (transaction mode) — قامت Supabase بإيقاف دعم وضع الجلسة على المنفذ 6543 في 28 فبراير 2025.23 أضاف Supavisor 1.0 دعماً للبيانات المعدة المسماة (named prepared statements)؛24 كما بدأ العمل الأولي على البيانات المعدة في وضع المعاملات ولكنه كان يحتوي على أخطاء وقت كتابة هذا الدليل، لذا إذا كنت تستخدم البيانات المعدة مع تزامن عالٍ، فراجع إصدار Supavisor الخاص بك. وبالنسبة لبروتوكول IPv4: أسماء مضيفي قواعد البيانات المباشرة في Supabase تدعم IPv6 فقط في جميع الفئات، بينما اسم مضيف المجمع (pooler.Supabase.com) يدعم البروتوكولين معاً (dual-stack) — استخدام المجمع هو بحد ذاته الحل لـ IPv4. إذا كنت بحاجة تحديداً لعنوان IPv4 للاتصال المباشر، فإن إضافة Dedicated IPv4 تكلفتها 0.0055 دولار/ساعة ≈ 4 دولارات/شهر لكل قاعدة بيانات وتتطلب فئة Pro أو أعلى.8
التحقق — اختبار الحمل لمعرفة الفرق
الهدف الأساسي هو أن المجمع يسمح لخلفية Postgres واحدة بخدمة عدد أكبر بكثير من العملاء مما يسمح به max_connections. أثبت ذلك بنفسك.
لأننا قمنا بربط كل من 5433 → Postgres و 6432 → PgBouncer في ملف Compose، يمكنك توجيه الحمل مباشرة إلى كل منفذ من المضيف. قم بتشغيل 80 استعلام pg_sleep(0.5) متزامناً مباشرة ضد Postgres على المنفذ 5433:
for i in $(seq 1 80); do
PGPASSWORD=app-secret-rotate-me psql \
"host=127.0.0.1 port=5433 user=appuser dbname=appdb" \
-c "SELECT pg_sleep(0.5);" &
done
wait
ستظهر لك رسالة FATAL: sorry, too many clients already بمجرد أن تتجاوز الاتصالات المتزامنة حد الـ 50 اتصالاً الذي حددناه في ملف Compose. هذا هو Postgres يخبرك بوضوح أنه لا يمكنه خدمة العميل رقم 51 مباشرة.
الآن كرر نفس الحلقة ضد PgBouncer على المنفذ 6432:
for i in $(seq 1 80); do
PGPASSWORD=app-secret-rotate-me psql \
"host=127.0.0.1 port=6432 user=appuser dbname=appdb" \
-c "SELECT pg_sleep(0.5);" &
done
wait
تنجح جميع الـ 80 عميلاً. يقوم PgBouncer بوضع العملاء الذين يتجاوزون default_pool_size في قائمة انتظار ويخدمهم بمجرد تفرغ اتصالات الخلفية. أثناء تشغيل الحلقة، افتح نافذة أوامر ثانية، واتصل بوحدة تحكم المسؤول، وراقب ارتفاع وانخفاض cl_waiting أثناء ذروة العمل.
الأخطاء الشائعة — الإخفاقات الحقيقية الخمسة
هذه هي الإخفاقات التي ستواجهها فعلياً، مع التفسير الذي يوضح السبب الكامن وراءها.
1. LISTEN لا يعيد أي إشعارات. أنت تستخدم وضع المعاملات. LISTEN موثق كغير مدعوم في تجميع المعاملات — على الرغم من أن NOTIFY يعمل.13 انقل المستمع (listener) إلى اتصال مباشر مخصص (أو مجمع بوضع الجلسة) واترك بقية تطبيقك على وضع المعاملات. النمط مفصل في درس Postgres LISTEN/NOTIFY التعليمي الخاص بنا.
2. البيانات المعدة (Prepared statements) تتعطل برسالة "prepared statement does not exist." لقد نسيت ضبط max_prepared_statements. القيمة الافتراضية هي 0 — مما يعني تعطيل البيانات المعدة على مستوى البروتوكول.22 اضبطها على قيمة غير صفرية (100–200 قيمة معقولة)، وأعد تحميل PgBouncer، واترك المحرك (driver) يعيد الإعداد في الطلب التالي.
3. أمر SET على مستوى الجلسة ليس له تأثير بصمت. أمر مثل SET search_path (بدون LOCAL) يضبط متغير GUC للجلسة؛ تجميع المعاملات يمسحه بمجرد انتهاء المعاملة وهبوط المعاملة التالية على اتصال خلفية مختلف. استخدم SET LOCAL داخل المعاملة، أو أضف المعامل إلى track_extra_parameters إذا كنت بحاجة حقاً لنطاق الجلسة. لاحظ التبعات الأمنية: وجود search_path داخل track_extra_parameters هو جزء من الشروط المسبقة لثغرة CVE-2025-12819 في إصدارات PgBouncer الأقدم من 1.25.1،25 ولهذا السبب قمنا بتثبيت الإصدار 1.25.1-p0 في البداية.
4. الأقفال الاستشارية (Advisory locks) عبر الاستعلامات لا تصمد. ترتبط pg_advisory_lock (صيغة الجلسة) باتصال الخلفية. قد يهبط الاستعلام التالي في تطبيقك على اتصال مختلف في وضع المعاملات، لذا يتبخر القفل من منظور المتصل. استخدم المتغيرات ذات نطاق المعاملة (pg_advisory_xact_lock) أو انقل القفل إلى مجمع مخصص بوضع الجلسة.
5. فشل اتصال Supabase من جهازك المحمول / GitHub Actions / Vercel برسالة "no route to host." اسم مضيف قاعدة البيانات المباشر في Supabase (مثلاً db.<project>.Supabase.co) يدعم IPv6 فقط في جميع الفئات، بينما العديد من مزودي خدمة الإنترنت المنزليين ومشغلي CI يدعمون IPv4 فقط. الحل هو إما التوجيه عبر أسماء مضيفي مجمع Supavisor، والتي تدعم البروتوكولين معاً وهي ما استخدمناه في الخطوة 6، أو الترقية إلى Pro وتفعيل إضافة IPv4 المخصصة (~4 دولارات/شهر لكل قاعدة بيانات8).
أفضل الممارسات قبل الإطلاق
قائمة قصيرة من الإعدادات التي تستحق الضبط قبل أن يصبح التطبيق أمام المستخدمين.
- اختر الحجم المناسب لـ
default_pool_size. ابدأ بـ(CPU_cores × 2) + disks،10 ثم قم بالضبط من خلال مراقبةcl_waitingتحت حمل واقعي — وجود طابور انتظار مستمر وغير صفري يعني ضرورة زيادته (أو توسيع Postgres). - اضبط
query_wait_timeoutلفترة قصيرة (5–30 ثانية). القيمة الافتراضية لـ PgBouncer وهي 120 ثانية15 تخفي المشكلات لمدة دقيقتين قبل أن يدرك العميل وجود مشكلة. - ثبت
auth_type = scram-sha-256وقم بتدويرuserlist.txtبانتظام. تعامل معه كسرّ بنفس وتيرة تدوير كلمة مرور قاعدة البيانات الخاصة بك. - اضبط
max_db_connectionsليكون أقل بصرامة منmax_connectionsفي Postgres — اترك مساحة لعمليات الهجرة (migrations)، والنسخ الاحتياطي (replication)، وتسجيل دخول المسؤولين (superusers). - راقب
SHOW STATSبمرور الوقت. المقياسان اللذان يكشفان المشكلات مبكراً هما متوسط مدة الاستعلام وtotal_xact_count— أي قفزة غير مبررة في أي منهما تسبق تنبيهات APM بـ 30 ثانية عادةً.
الخطوات التالية
إذا كنت جديداً على بيئة Postgres المحيطة، فإن دليل بنية قواعد البيانات الخاص بنا يغطي مقايضات النسخ المتماثل والتقسيم التي تتفاعل مع حجم المجمع. إذا كنت تدمج Postgres المجمع في خلفية أكبر، فإن دليل تطوير الويب للخلفية يشرح بقية الطبقات — التخزين المؤقت، وقوائم انتظار الرسائل، و هجرات قواعد البيانات بدون توقف.
Footnotes
-
وثائق استهلاك الموارد في PostgreSQL 18، الافتراضي هو
max_connections = 100، والافتراضي هوwork_mem = 4MB. https://www.postgresql.org/docs/current/runtime-config-resource.html ↩ ↩2 -
إعلان إصدار PgBouncer 1.25.1 (3 ديسمبر 2025)، لإصلاح الثغرة CVE-2025-12819. https://www.postgresql.org/about/news/pgbouncer-1251-released-fixing-a-bunch-of-bugs-before-christmas-including-cve-2025-12819-3189/ ↩
-
مستودع كود Supavisor — "مجمع اتصالات Postgres سحابي ومتعدد المستأجرين." https://GitHub.com/Supabase/supavisor ↩ ↩2
-
وثائق Supabase — "الاتصال بقاعدة بياناتك،" وتغطي روابط مجمع الاتصالات في وضعي الجلسة (session) والمعاملة (transaction). https://Supabase.com/docs/guides/database/connecting-to-postgres ↩ ↩2
-
إعلان إصدار PgBouncer 1.21.0، "تمكنت هذه الميزة من زيادة إنتاجية الاستعلامات بنسبة تتراوح بين 15% إلى 250%، اعتمادًا على عبء العمل." https://www.postgresql.org/about/news/pgbouncer-1210-released-now-with-prepared-statements-2735/ ↩ ↩2
-
إعلان الإصدار العام لـ PostgreSQL 18، 25 سبتمبر 2025. https://www.postgresql.org/about/news/postgresql-18-released-3142/ ↩
-
تاجات edoburu/pgbouncer على Docker Hub — الإصدار
v1.25.1-p0يشحن PgBouncer 1.25.1 على Alpine 3.22. https://hub.Docker.com/r/edoburu/pgbouncer/tags ↩ -
وثائق عنوان IPv4 المخصص من Supabase — "0.0055 دولار في الساعة ... حوالي 4.00 دولار إذا تُرك مفعلاً لشهر كامل ... متاح للمؤسسات من فئة Pro وما فوق." https://Supabase.com/docs/guides/platform/ipv4-address ↩ ↩2 ↩3
-
ويكي PostgreSQL، ضبط خادم PostgreSQL الخاص بك — العبء الإضافي لذاكرة الاتصال وقاعدة
(CPU_cores × 2) + number_of_disks. https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server ↩ -
نفس مصدر 9، المشار إليه مباشرة بواسطة دليل أفضل ممارسات Heroku PgBouncer. https://devcenter.heroku.com/articles/best-practices-pgbouncer-configuration ↩ ↩2
-
وثائق ميزات PgBouncer — أوضاع التجميع (
session،transaction،statement). https://www.pgbouncer.org/features.html ↩ -
وثائق ميزات PgBouncer — مصفوفة توافق الميزات، تسرد ما هو غير آمن في كل وضع. https://www.pgbouncer.org/features.html ↩
-
مشكلة PgBouncer رقم 655، "ميزة: دعم Listen/Notify مع تجميع المعاملات" — تؤكد أن
LISTENغير مدعوم في وضع المعاملة بينما يعملNOTIFY. https://GitHub.com/pgbouncer/pgbouncer/issues/655 ↩ ↩2 -
وثائق إعدادات PgBouncer — القيمة الافتراضية لـ
default_pool_sizeهي 20. https://www.pgbouncer.org/config.html ↩ -
وثائق إعدادات PgBouncer — القيمة الافتراضية لـ
query_wait_timeoutهي 120 ثانية. https://www.pgbouncer.org/config.html ↩ ↩2 -
وثائق إعدادات PgBouncer — القيمة الافتراضية لـ
server_reset_queryهيDISCARD ALL. https://www.pgbouncer.org/config.html ↩ -
ملاحظات إصدار PgBouncer 1.21.0، "الإصدار الذي يحتوي على العبارات المُعدة (prepared statements)." https://GitHub.com/pgbouncer/pgbouncer/releases/tag/pgbouncer_1_21_0 ↩
-
Crunchy Data، "كيفية استخدام SCRAM في Postgres مع pgBouncer،" يشرح كيفية نسخ
rolpasswordمنpg_authidإلىuserlist.txt. https://www.crunchydata.com/blog/pgbouncer-scram-authentication-postgresql ↩
وثائق إعداد PgBouncer — تنسيق auth_file الموصوف كـ "username" "password" مع خيارات SCRAM أو MD5 أو كلمة مرور نصية صريحة. https://www.pgbouncer.org/config.html ↩
استخدام سطر أوامر PgBouncer — SHOW POOLS و SHOW STATS. https://www.pgbouncer.org/usage.html ↩
Heap Engineering، "فك تشفير المعلومات التشخيصية لـ PgBouncer" — معاني حقول cl_active_cancel_req و sv_used. https://www.heap.io/blog/decrypting-pgbouncers-diagnostic-information ↩
pganalyze، "5 دقائق من Postgres" — أضاف إصدار PgBouncer 1.21 العبارات المُعدة (prepared statements) على مستوى البروتوكول؛ يجب أن يكون max_prepared_statements أكبر من 0 للتفعيل. https://pganalyze.com/blog/5mins-postgres-pgbouncer-prepared-statements-transaction-mode ↩ ↩2
مناقشة Supabase رقم 32755 — مجمع اتصالات Supabase سيلغي وضع الجلسة (session mode) على المنفذ 6543 في 28 فبراير 2025. https://GitHub.com/orgs/Supabase/discussions/32755 ↩
مشكلة Supavisor رقم 69 — "دعم العبارات المُعدة المسماة" تم دمجها في Supavisor 1.0. https://GitHub.com/Supabase/supavisor/issues/69 ↩
NVD CVE-2025-12819 — مسار بحث (search_path) غير موثوق في معالج اتصال auth_query في PgBouncer، تم إصلاحه في 1.25.1. https://nvd.nist.gov/vuln/detail/CVE-2025-12819 ↩