---
title: 'تجميع اتصالات Postgres (Pooling) في بيئة الإنتاج: PgBouncer + Supavisor'
description: 'دليل عملي لسنة 2026: PgBouncer 1.25.1 أمام Postgres 18، ثم الانتقال إلى Supabase Supavisor — الـ transaction pooling، والـ prepared statements، والعقبات الشائعة.'
url: https://nerdleveltech.com/ar/production-postgres-pooling-pgbouncer-supabase-supavisor-tutorial
language: ar
date: '2026-05-07'
category: backend
tags:
  - postgres
  - postgresql
  - pgbouncer
  - supavisor
  - supabase
  - connection-pooling
  - nodejs
  - backend
  - architecture
markdown_url: https://nerdleveltech.com/ar/production-postgres-pooling-pgbouncer-supabase-supavisor-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/production-postgres-pooling-pgbouncer-supabase-supavisor-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# تجميع اتصالات 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-2025[^6]      |
| صورة PgBouncer            | `edoburu/pgbouncer:v1.25.1-p0` | توفر PgBouncer 1.25.1 على Alpine 3.22[^7]      |
| 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](https://nerdleveltech.com/postgres-listen-notify-realtime-presence-tutorial) للتعرف على نمط الحل البديل (وصلة مباشرة مخصصة للمستمع، مع تجميع بقية تطبيقك).

## الخطوة 1 — تشغيل Postgres 18 + PgBouncer باستخدام Docker Compose

قم بإنشاء دليل جديد وملف Compose. سنقوم بربط PgBouncer للتحدث مع Postgres عبر شبكة Docker خاصة، ونعرض الخدمتين للمضيف على منافذ مختلفة — بهذه الطريقة يمكن لاختبار التحميل في القسم الأخير استهداف أي منهما مباشرة.

```bash
mkdir pg-pooling-demo && cd pg-pooling-demo
mkdir pgbouncer
```

احفظ ما يلي باسم `Docker-compose.yaml`:

```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 فقط في الوقت الحالي:

```bash
Docker compose up -d postgres
Docker compose logs -f postgres   # انتظر ظهور رسالة "database system is ready to accept connections"
```

## الخطوة 2 — تكوين PgBouncer لتجميع المعاملات (transaction pooling)

احفظ ما يلي باسم `pgbouncer/pgbouncer.ini`:

```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.0[^17] والتي تحقق إنتاجية أعلى بنسبة 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 مخصص.

```bash
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] قم بتأمين الملف بحيث لا يكون قابلاً للقراءة من قبل الجميع:

```bash
chmod 600 pgbouncer/userlist.txt
```

الآن قم بتشغيل PgBouncer:

```bash
Docker compose up -d pgbouncer
Docker compose logs -f pgbouncer
```

أنت تبحث عن بدء تشغيل نظيف بدون أخطاء في `auth_file`. إذا رأيت `password authentication failed`، فإن السبب الأكثر شيوعاً هو نسخ ولصق سر SCRAM مع سطر جديد زائد أو بعلامات اقتباس ذكية من محرر نصوص غني — أعد اللصق من طرفية (terminal) عادية.

## الخطوة 4 — التحقق من المجمع باستخدام psql

اتصل *عبر PgBouncer* على المنفذ 6432 — وليس مباشرة بـ Postgres على 5432:

```bash
psql "host=127.0.0.1 port=6432 user=appuser dbname=appdb"
```

قم بتشغيل استعلام بسيط، ثم افصل الاتصال. الآن اتصل بوحدة تحكم إدارة PgBouncer — وهي قاعدة بيانات وهمية خاصة تسمى `pgbouncer`:

```bash
psql "host=127.0.0.1 port=6432 user=appuser dbname=pgbouncer"
```

بمجرد الدخول، قم بتشغيل أهم أمرين للتشخيص:

```sql
SHOW POOLS;
SHOW STATS;
```

يعرض `SHOW POOLS` صفاً واحداً لكل زوج `(database, user)` مع العدد المباشر لاتصالات العميل والخادم في كل حالة.[^20] الحقول التي تهمك فعلياً في بيئة الإنتاج هي:

- `cl_active` — العملاء المقترنون باتصال خادم، والذين يقومون بمعالجة استعلام.
- `cl_waiting` — العملاء الذين ينتظرون في الطابور للحصول على خادم. أي قيمة فوق الصفر لفترات مستمرة هي علامة تحذير لك.
- `cl_active_cancel_req` — العملاء الذين أصدروا أمراً بالإلغاء وينتظرون استجابة الخادم.[^21]
- `sv_active` — اتصالات الخادم المقترنة حالياً بعميل.
- `sv_idle` — اتصالات الخادم الموجودة في المجمع والجاهزة للتسليم.
- `sv_used` — اتصالات الخادم التي ظلت خاملة لفترة كافية تجعل PgBouncer يرغب في فحص سلامتها قبل إعادة استخدامها.

يعطيك `SHOW STATS` إجماليات ومتوسطات متحركة لكل قاعدة بيانات — إجمالي المعاملات، إجمالي الاستعلامات، متوسط مدة الاستعلام بالميكروثانية، متوسط وقت الانتظار، البايتات المستلمة والمرسلة — حتى تتمكن من رسم مخطط لإنتاجية المجمع بمرور الوقت دون الحاجة إلى إضافة أدوات قياس لتطبيقك.

## الخطوة 5 — ربط تطبيق Node.js عبر المجمع

العقد البرمجي للتطبيق هو "تظاهر بأن المجمع هو Postgres." مع استثناء واحد سنراه بعد قليل.

```bash
mkdir client && cd client
npm init -y
npm install pg@8.20.0 dotenv@17.4.2
```

احفظ ما يلي باسم `client/index.mjs`:

```JavaScript
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
```

قم بتشغيله:

```bash
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:

```bash
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:

```bash
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 التعليمي](https://nerdleveltech.com/postgres-listen-notify-realtime-presence-tutorial) الخاص بنا.

**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 المحيطة، فإن [دليل بنية قواعد البيانات](https://nerdleveltech.com/designing-robust-database-architectures-a-complete-guide) الخاص بنا يغطي مقايضات النسخ المتماثل والتقسيم التي تتفاعل مع حجم المجمع. إذا كنت تدمج Postgres المجمع في خلفية أكبر، فإن [دليل تطوير الويب للخلفية](https://nerdleveltech.com/backend-web-development-the-complete-2025-guide) يشرح بقية الطبقات — التخزين المؤقت، وقوائم انتظار الرسائل، و [هجرات قواعد البيانات](https://nerdleveltech.com/mastering-database-migration-strategies-zero-downtime-to-success) بدون توقف.

---

[^1]: وثائق استهلاك الموارد في 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/>

[^3]: مستودع كود Supavisor — "مجمع اتصالات Postgres سحابي ومتعدد المستأجرين." <https://GitHub.com/Supabase/supavisor>

[^4]: وثائق Supabase — "الاتصال بقاعدة بياناتك،" وتغطي روابط مجمع الاتصالات في وضعي الجلسة (session) والمعاملة (transaction). <https://Supabase.com/docs/guides/database/connecting-to-postgres>

[^5]: إعلان إصدار PgBouncer 1.21.0، "تمكنت هذه الميزة من زيادة إنتاجية الاستعلامات بنسبة تتراوح بين 15% إلى 250%، اعتمادًا على عبء العمل." <https://www.postgresql.org/about/news/pgbouncer-1210-released-now-with-prepared-statements-2735/>

[^6]: إعلان الإصدار العام لـ PostgreSQL 18، 25 سبتمبر 2025. <https://www.postgresql.org/about/news/postgresql-18-released-3142/>

[^7]: تاجات edoburu/pgbouncer على Docker Hub — الإصدار `v1.25.1-p0` يشحن PgBouncer 1.25.1 على Alpine 3.22. <https://hub.Docker.com/r/edoburu/pgbouncer/tags>

[^8]: وثائق عنوان IPv4 المخصص من Supabase — "0.0055 دولار في الساعة ... حوالي 4.00 دولار إذا تُرك مفعلاً لشهر كامل ... متاح للمؤسسات من فئة Pro وما فوق." <https://Supabase.com/docs/guides/platform/ipv4-address>

[^9]: ويكي PostgreSQL، ضبط خادم PostgreSQL الخاص بك — العبء الإضافي لذاكرة الاتصال وقاعدة `(CPU_cores × 2) + number_of_disks`. <https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server>

[^10]: نفس مصدر [^9]، المشار إليه مباشرة بواسطة دليل أفضل ممارسات Heroku PgBouncer. <https://devcenter.heroku.com/articles/best-practices-pgbouncer-configuration>

[^11]: وثائق ميزات PgBouncer — أوضاع التجميع (`session`، `transaction`، `statement`). <https://www.pgbouncer.org/features.html>

[^12]: وثائق ميزات PgBouncer — مصفوفة توافق الميزات، تسرد ما هو غير آمن في كل وضع. <https://www.pgbouncer.org/features.html>

[^13]: مشكلة PgBouncer رقم 655، "ميزة: دعم Listen/Notify مع تجميع المعاملات" — تؤكد أن `LISTEN` غير مدعوم في وضع المعاملة بينما يعمل `NOTIFY`. <https://GitHub.com/pgbouncer/pgbouncer/issues/655>

[^14]: وثائق إعدادات PgBouncer — القيمة الافتراضية لـ `default_pool_size` هي 20. <https://www.pgbouncer.org/config.html>

[^15]: وثائق إعدادات PgBouncer — القيمة الافتراضية لـ `query_wait_timeout` هي 120 ثانية. <https://www.pgbouncer.org/config.html>

[^16]: وثائق إعدادات PgBouncer — القيمة الافتراضية لـ `server_reset_query` هي `DISCARD ALL`. <https://www.pgbouncer.org/config.html>

[^17]: ملاحظات إصدار PgBouncer 1.21.0، "الإصدار الذي يحتوي على العبارات المُعدة (prepared statements)." <https://GitHub.com/pgbouncer/pgbouncer/releases/tag/pgbouncer_1_21_0>

[^18]: Crunchy Data، "كيفية استخدام SCRAM في Postgres مع pgBouncer،" يشرح كيفية نسخ `rolpassword` من `pg_authid` إلى `userlist.txt`. <https://www.crunchydata.com/blog/pgbouncer-scram-authentication-postgresql>

[^19]: وثائق إعداد PgBouncer — تنسيق `auth_file` الموصوف كـ `"username" "password"` مع خيارات SCRAM أو MD5 أو كلمة مرور نصية صريحة. <https://www.pgbouncer.org/config.html>

[^20]: استخدام سطر أوامر PgBouncer — `SHOW POOLS` و `SHOW STATS`. <https://www.pgbouncer.org/usage.html>

[^21]: Heap Engineering، "فك تشفير المعلومات التشخيصية لـ PgBouncer" — معاني حقول `cl_active_cancel_req` و `sv_used`. <https://www.heap.io/blog/decrypting-pgbouncers-diagnostic-information>

[^22]: pganalyze، "5 دقائق من Postgres" — أضاف إصدار PgBouncer 1.21 العبارات المُعدة (prepared statements) على مستوى البروتوكول؛ يجب أن يكون `max_prepared_statements` أكبر من 0 للتفعيل. <https://pganalyze.com/blog/5mins-postgres-pgbouncer-prepared-statements-transaction-mode>

[^23]: مناقشة Supabase رقم 32755 — مجمع اتصالات Supabase سيلغي وضع الجلسة (session mode) على المنفذ 6543 في 28 فبراير 2025. <https://GitHub.com/orgs/Supabase/discussions/32755>

[^24]: مشكلة Supavisor رقم 69 — "دعم العبارات المُعدة المسماة" تم دمجها في Supavisor 1.0. <https://GitHub.com/Supabase/supavisor/issues/69>

[^25]: NVD CVE-2025-12819 — مسار بحث (search\_path) غير موثوق في معالج اتصال `auth_query` في PgBouncer، تم إصلاحه في 1.25.1. <https://nvd.nist.gov/vuln/detail/CVE-2025-12819>
