Postgres LISTEN/NOTIFY لـ Job Queues: دليل
١١ يونيو ٢٠٢٦
استخدم Postgres LISTEN/NOTIFY فقط كإشارة تنبيه (wake-up signal) لزمام مهام (job queue)، ولا تستخدمها أبداً كزمام المهام نفسه. الإشعارات ليست مستمرة (not persisted)، وحمولات البيانات (payloads) بحد أقصى 8,000 بايت، وعملية NOTIFY تجعل عمليات الحفظ (commits) متسلسلة تحت الضغط. احتفظ بالمهام في جدول واستحوذ عليها باستخدام FOR UPDATE SKIP LOCKED.
ملخص
تعد LISTEN/NOTIFY آلية النشر/الاشتراك (pub/sub) المدمجة في Postgres، ومن المغري بناء زمام مهام عليها لأنها تلغي الحاجة للاستعلام الدوري (polling). المشكلة: الإشعارات تختفي إذا انقطع اتصال المستمع، وكل عملية حفظ (commit) تحمل NOTIFY تأخذ قفلاً عاماً (global lock) يجعل جميع عمليات الحفظ على المثيل (instance) متسلسلة، كما أن LISTEN تتعطل خلف PgBouncer في وضع المعاملات (transaction-mode). النمط الآمن للإنتاج في عام 2026 هو جدول مهام يتم تفريغه باستخدام SELECT ... FOR UPDATE SKIP LOCKED، مع استخدام NOTIFY فقط لتنبيه العمال الخاملين — أو استخدام إضافة جاهزة مثل pgmq.
ما ستتعلمه
- كيف تعمل LISTEN/NOTIFY فعلياً، بما في ذلك دلالات المعاملات والتسليم
- لماذا لا تتوسع LISTEN/NOTIFY تحت ضغط العديد من الكتاب المتزامنين (القفل العام "database 0")
- الحدود الصارمة: حمولات 8,000 بايت، وزمام إشعارات بحجم 8 جيجابايت، والرسائل المفقودة عند انقطاع الاتصال
- لماذا تفشل LISTEN خلف تجميع اتصالات PgBouncer، وماذا تفعل حيال ذلك
- نمط زمام المهام SKIP LOCKED والبدائل الجاهزة مثل pgmq
- متى تظل LISTEN/NOTIFY هي الأداة المناسبة
ما هي LISTEN/NOTIFY في Postgres وكيف تعمل؟
تعد LISTEN/NOTIFY آلية إشعار بين العمليات في Postgres: تقوم جلسة بتشغيل LISTEN channel، وأي جلسة تشغل NOTIFY channel, 'payload' في نفس قاعدة البيانات تدفع حدثاً إلى كل مستمع حالي على تلك القناة. إنها تعتمد على الدفع (push-based)، لذا لا يحتاج المستمعون للاستعلام الدوري.
هناك ثلاث دلالات تهم بناة زمام المهام. أولاً، يتم تسليم NOTIFY الصادر داخل معاملة (transaction) فقط إذا وعندما يتم حفظ تلك المعاملة — المعاملات الملغاة لا ترسل شيئاً.1 ثانياً، يتم دمج الإشعارات المتطابقة (القناة + الحمولة) داخل معاملة واحدة في حدث واحد يتم تسليمه، بينما يتم تسليم الحمولات المختلفة بشكل منفصل.1 ثالثاً، المعاملة التي نفذت NOTIFY لا يمكن تجهيزها للحفظ ثنائي المراحل (two-phase commit).1 بالنسبة لأسماء القنوات الديناميكية، فإن صيغة الدالة pg_notify(channel, payload) أسهل في الاستخدام من أمر SQL المجرد.1
-- worker session
LISTEN job_created;
-- producer session
INSERT INTO jobs (kind, args) VALUES ('send_email', '{"to": "..."}');
NOTIFY job_created, '42'; -- or: SELECT pg_notify('job_created', '42');
لماذا لا تتوسع LISTEN/NOTIFY في Postgres؟
لأن أي عملية حفظ (commit) تحتوي على NOTIFY تأخذ قفلاً عاماً من نوع AccessExclusiveLock على مثيل Postgres أثناء إضافتها إلى زمام الإشعارات، مما يجعل عمليات الحفظ تلك متسلسلة — ومع وجود عدد كافٍ من الكتاب المتزامنين، تتوقف قاعدة البيانات بأكملها. يظهر القفل في السجلات كـ AccessExclusiveLock on object 0 of class 1262 of database 0، ويوضح كود المصدر الخاص بـ Postgres أنه موجود لضمان ظهور مدخلات الزمام بترتيب الحفظ.2
هذا ليس أمراً نظرياً. فقد تتبع موقع Recall.ai، الذي يكتب بيانات اجتماعات منظمة من عشرات الآلاف من المنتجين المتزامنين، ثلاثة انقطاعات في الخدمة بين 19 مارس و22 مارس 2025 إلى هذا القفل تحديداً: ارتفع حمل قاعدة البيانات بينما انخفض استهلاك المعالج (CPU) ومدخلات/مخرجات القرص (disk I/O) بشكل حاد — وهي علامة مميزة لتنازع الأقفال (lock contention) بدلاً من العمل الحقيقي. أدى إزالة مسار الكود الوحيد الذي يحمل NOTIFY إلى حل المشكلة، وخلص تقريرهم بوضوح إلى أنه لا ينبغي استخدام LISTEN/NOTIFY مع العديد من الكتاب المتزامنين.2
هناك أخبار جيدة في الأفق: إصلاح جوهري (commit 282b1cde بواسطة Joel Jacobson، أرسله Tom Lane) يقضي على هذا الاختناق، لكنه مدرج في PostgreSQL 19 — الذي وصل إلى المرحلة التجريبية الأولى (Beta 1) في 4 يونيو 2026، مع توقع التوفر العام (GA) في سبتمبر 2026 تقريباً — وليس موجوداً في أي إصدار إنتاجي حالي. أحدث الإصدارات المستقرة اعتباراً من مايو 2026 هي 18.4، 17.10، 16.14، 15.18، و14.23.345 إذا كنت تقوم بتشغيل إصدار Postgres متاح اليوم، فإن القفل العام لا يزال واقعك.
ما هو حد حجم حمولة NOTIFY؟
8,000 بايت في الإعداد الافتراضي.1 هذا الحد هو السبب في النصيحة القياسية بعدم وضع بيانات المهمة في الحمولة أبداً: أرسل فقط معرفاً (أو لا شيء على الإطلاق) واحتفظ بصف المهمة الفعلي في جدول. تقول الوثائق الرسمية الشيء نفسه — بالنسبة للبيانات الكبيرة أو الثنائية، "ضعها في جدول قاعدة بيانات وأرسل مفتاح السجل."1
زمام الإشعارات الذي يخزن الأحداث غير المسلمة مؤقتاً محدود أيضاً: حوالي 8 جيجابايت في التثبيت القياسي، ويمكن تهيئته عبر max_notify_queue_pages منذ PostgreSQL 17. إذا امتلأ الزمام، تفشل المعاملات التي تستدعي NOTIFY عند الحفظ، ويبدأ Postgres في تسجيل تحذيرات بمجرد وصوله لنصف السعة — وعادة ما يشير ذلك إلى مستمع عالق في معاملة طويلة الأمد تمنع عملية التنظيف. يمكنك مراقبة الاستخدام باستخدام pg_notification_queue_usage().16
ماذا يحدث للإشعارات إذا انقطع اتصال المستمع؟
تُفقد بشكل دائم. يتم تسليم الإشعارات فقط للجلسات التي تستمع حالياً؛ لا يتم تخزين أي شيء لمستمع منقطع الاتصال، أو في حالة إعادة التشغيل، أو متعطل، ولا توجد آلية لإعادة التشغيل (replay).1 هذا هو أهم سبب يمنع LISTEN/NOTIFY من أن تكون هي زمام المهام نفسه: أي عملية نشر كود، أو خلل في الشبكة، أو نفاد ذاكرة (OOM) للعامل يعني فقدان المهام بصمت.
التخفيف القياسي هو جعل الجدول هو مصدر الحقيقة. يقوم العمال دائماً بإجراء استعلام استدراكي (catch-up query) عند بدء التشغيل (وبشكل دوري)، لذا فإن الإشعار المفقود يؤخر المهمة فقط حتى المسح التالي بدلاً من فقدانها.
هل تعمل LISTEN/NOTIFY مع PgBouncer أو تجميع الاتصالات؟
لا تعمل LISTEN من خلال PgBouncer في وضع تجميع المعاملات (transaction pooling) — وهو الوضع الذي تستخدمه معظم عمليات النشر الإنتاجية. يحتاج المستمع إلى اتصال خادم مستقر لمواصلة تلقي الأحداث، وتجميع المعاملات يعيد اتصال الخادم إلى المجمع بعد كل معاملة.7 أما الإرسال باستخدام NOTIFY فهو جيد في وضع المعاملات، لأنه مجرد أمر عادي.8
الحل البديل: امنح المستمعين مسارًا مخصصًا — إما إدخال منفصل لقاعدة بيانات PgBouncer مهيأ لوضع تجميع الجلسات (session pooling) أو اتصال مباشر بـ Postgres — واحتفظ باتصال استماع واحد لكل عملية عامل (worker process)، وليس لكل عميل مجمع. ينطبق نفس القيد على معظم أدوات التجميع في وضع المعاملات (transaction-mode)، ويظل دعم LISTEN في هذا الوضع طلب ميزة مفتوحًا في PgBouncer.8 للحصول على نظرة أعمق على أوضاع التجميع ومقايضاتها، راجع دليل تجميع Postgres للإنتاج مع PgBouncer و Supavisor.
ماذا يجب أن تستخدم بدلاً من LISTEN/NOTIFY لطابور المهام؟
استخدم جدول مهام يتم حجزه باستخدام FOR UPDATE SKIP LOCKED — المتاح منذ PostgreSQL 9.5 — كعمود فقري، وأضف NOTIFY فقط كتحسين للاستيقاظ إذا كان تأخير الاستطلاع (polling latency) يزعجك.9 يسمح SKIP LOCKED للعديد من العمال بالسحب من نفس الجدول دون حظر بعضهم البعض: الصفوف التي أغلقها عامل آخر يتم تخطيها ببساطة، وهو ما تشير إليه وثائق Postgres صراحةً كنمط لـ "مستهلكين متعددين يصلون إلى جدول يشبه الطابور."10
UPDATE jobs
SET status = 'running', started_at = now()
WHERE id = (
SELECT id FROM jobs
WHERE status = 'queued'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *;
تنجو المهام من الانهيارات لأنها صفوف عادية؛ وتعد عمليات إعادة المحاولة، والأولويات، ومعالجة الرسائل غير القابلة للتسليم مجرد أعمدة واستعلامات؛ وكل شيء يتم بشكل تعاملي (transactional) مع عمليات كتابة الأعمال الخاصة بك — وهي الخاصية التي تجعل طوابير Postgres جذابة في المقام الأول.
إذا كنت تفضل عدم بنائها يدويًا، فإن pgmq هي إضافة مفتوحة المصدر تنفذ طابورًا يشبه SQS (تسليم مرة واحدة بالضبط ضمن مهلة الرؤية) على Postgres من الإصدار 14 إلى 18، ويستخدمها Supabase و Tembo من بين آخرين.11 في Node، يوفر لك pg-boss طابور مهام Postgres جاهز للإنتاج مبنيًا على هذه الأساسيات بالضبط. وإذا كنت تحتاج حقًا إلى مئات الآلاف من الرسائل في الدقيقة، أو التوزيع على العديد من المستهلكين، أو التسليم عبر الخدمات، فهذه هي النقطة التي يستحق فيها الوسيط المخصص (SQS، RabbitMQ، Kafka، Redis Streams) تكلفته التشغيلية.
متى لا يزال LISTEN/NOTIFY هو الخيار الصحيح؟
عندما يكون حجم الكتابة متواضعًا، ويحتفظ المستمعون باتصالات مباشرة (أو مجمعة للجلسات)، ويكون فقدان إشعار غير ضار. تشمل الحالات المناسبة إشارات إبطال ذاكرة التخزين المؤقت (cache invalidation)، وتنبيهات "تغير التكوين، أعد التحميل"، وتحديثات لوحة البيانات المباشرة، وتنبيه مجموعة صغيرة من عمال الطابور حتى يقوموا بالاستطلاع فورًا بدلاً من الانتظار للدورة التالية. في كل هذه الحالات، الإشعار هو تلميح وليس البيانات — إذا فُقد، فإن المسح الدوري أو الاستطلاع التالي سيتدارك الأمر. نستعرض استخدامًا صحيًا لهذه الميزة في درس التواجد في الوقت الفعلي مع LISTEN/NOTIFY. إذا فشل أي من هذه الشروط الثلاثة — العديد من الكتاب المتزامنين الذين يحملون NOTIFY، أو اتصالات مجمعة للمعاملات، أو اعتماد الصحة على التسليم — فاستخدم النمط القائم على الجدول بدلاً من ذلك.
الخلاصة
LISTEN/NOTIFY هو أداة إشعار أولية، وليس طابورًا. احتفظ بالمهام في جدول، واحجزها باستخدام SKIP LOCKED، واترك NOTIFY يفعل الشيء الوحيد الذي يجيده: إخبار عامل خامل بالتحقق الآن بدلاً من الانتظار لثانية. إذا كنت مستعدًا للبناء، فابدأ بـ درس طابور مهام Postgres مع pg-boss، وتأكد من أن طبقة الاتصال لديك صلبة مع دليل تجميع PgBouncer و Supavisor.
Footnotes
-
PostgreSQL 18 official documentation, NOTIFY — https://www.postgresql.org/docs/current/sql-notify.html ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Recall.ai, "Postgres LISTEN/NOTIFY does not scale" (updated May 8, 2026) — https://www.recall.ai/blog/postgres-listen-notify-does-not-scale ↩ ↩2
-
Robins Tharakan, "Turbocharging LISTEN/NOTIFY with 40x Boost" (Jan 2026) — https://www.robins.in/2026/01/turbocharging-listennotify-with-40x.html ↩
-
PostgreSQL 18.4, 17.10, 16.14, 15.18, and 14.23 released (May 14, 2026) — https://www.postgresql.org/about/news/postgresql-184-1710-1614-1518-and-1423-released-3297/ ↩
-
إصدار PostgreSQL 19 Beta 1 (4 يونيو 2026) — https://www.postgresql.org/about/news/postgresql-19-beta-1-released-3313/ ↩
-
مرجع معامل max_notify_queue_pages — https://postgresqlco.nf/doc/en/param/max_notify_queue_pages/ ↩
-
مميزات PgBouncer — https://www.pgbouncer.org/features.html ↩
-
مشكلة PgBouncer رقم 655، "ميزة: دعم Listen/Notify مع Transaction Pooling" — https://GitHub.com/pgbouncer/pgbouncer/issues/655 ↩ ↩2
-
ويكي PostgreSQL، "ما الجديد في PostgreSQL 9.5" — https://wiki.postgresql.org/wiki/What's_new_in_PostgreSQL_9.5 ↩
-
وثائق PostgreSQL 18 الرسمية، SELECT (بند القفل) — https://www.postgresql.org/docs/current/sql-select.html ↩
-
pgmq — طابور رسائل خفيف الوزن. مثل AWS SQS و RSMQ ولكن على Postgres — https://GitHub.com/pgmq/pgmq ↩