من فضلك تسجيل الدخول أو تسجيل لتفعل ذلك.

يتمتع Postgres LISTEN/NOTIFY بسمعة سيئة، ويرجع الفضل في ذلك جزئيًا إلى منشور مدونة شائع يدعي أنه لا يتوسع. إذا كان هذا صحيحًا، فسيكون أمرًا مؤسفًا، لأن LISTEN/NOTIFY أداة قوية، تتيح لك استخدام قاعدة بيانات Postgres الخاصة بك للإشعارات المستمرة ذات زمن الوصول المنخفض، والتدفقات، والنشر/الاشتراك. هذه الاتهامات ليست خاطئة تمامًا: فـ NOTIFY يتمتع بخصائص أداء غير بديهية وغير موثقة تنشأ عن استخدامه للقفل العام. لكن السلوك غير البديهي ليس مرادفًا لغير قابل للتطوير. في هذه المقالة، سنوضح كيف قمنا بتحسين التدفقات المدعومة بـ LISTEN/NOTIFY على نطاق واسع، محققين 60 ألف عملية كتابة في الثانية على خادم Postgres واحد مع زمن استجابة بمقياس مللي ثانية.

البث بزمن وصول منخفض باستخدام LISTEN/NOTIFY

التصميم الأساسي للتدفقات المدعومة من Postgres بسيط: أنشئ جدول تدفقات حيث تكون كل قطعة تدفق (على سبيل المثال، رمز استجابة LLM) عبارة عن صف جديد، ثم اكتب إلى التدفقات عن طريق إدراجها في الجدول.

الجزء الصعب هو القراءة من التدفق لأنك لا تعرف متى ستصل القطعة التالية. أحد الحلول هو الاستقصاء: اطلب من كل قارئ الاستقصاء في نهاية التدفق عن الأجزاء الجديدة. ومع ذلك، فإن هذا المستوى من الاستقصاء ضعيف. إذا تم تعيين الفاصل الزمني للاستقصاء على مستوى عالٍ جدًا، فسيكون زمن الوصول مرتفعًا جدًا لحالات الاستخدام التفاعلية (مثل الدردشات عبر الإنترنت). ولكن إذا تم تعيين الفاصل الزمني للاستقصاء على مستوى منخفض جدًا، فإن أدوات الاستقصاء المتزامنة ستغمر قاعدة البيانات.

الحل الأفضل هو LISTEN/NOTIFY. يتيح ذلك للقراء الحظر في انتظار إشعار من الكاتب بأنه قد تم نشر جزء جديد في التدفق. بهذه الطريقة، لا يهدر القراء الموارد في الاستقصاء، ولكنهم يستيقظون فورًا عند وصول جزء تدفق جديد.

في تطبيقنا الأولي للتدفقات المستندة إلى LISTEN/NOTIFY، أطلق مشغل في جدول التدفقات وظيفة ترسل إشعارًا في كل مرة تتم فيها كتابة مجموعة تدفق جديدة. انتظر القراء هذه الإشعارات واستيقظوا على مقطع تدفق جديد.

كان هذا التنفيذ صحيحًا وحقق زمن انتقال منخفضًا، لكن إنتاجيته كانت ضعيفة على نطاق واسع. حتى باستخدام قاعدة بيانات Postgres الكبيرة، لم يتمكن من الحفاظ على أكثر من 2.9 ألف عملية كتابة للتدفق في الثانية. ومن المثير للاهتمام أنه تم اختناقه دون استهلاك أي موارد Postgres بشكل واضح (وحدة المعالجة المركزية أو الذاكرة أو IOPS). وكما قد خمنت، كان السبب الجذري هو المشكلة الأصلية المعروفة بـ LISTEN/NOTIFY غير القابلة للتوسع: القفل العام الذي يأخذه Postgres أثناء NOTIFY. ولكن لماذا يفعل Postgres ذلك، وكيف يمكننا تحسينه دون فقدان فوائد إشعارات Postgres؟

قفل LISTEN/NOTIFY الحصري

لفهم المشكلة، سنحتاج إلى فحص كيفية عمل Postgres LISTEN/NOTIFY فعليًا.

السبب الجذري لضعف الأداء هو أنه في Postgres، يتطلب تنفيذ معاملة تستدعي NOTIFY الحصول على قفل حصري عالمي. يتم أخذ هذا القفل عندما تبدأ المعاملة في الالتزام، ولا يتم تحريره حتى يتم الالتزام بالمعاملة بالكامل ويتم مسح محتوياتها إلى القرص باستخدام fsync().

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

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

يشرح هذا القفل الحصري الأداء الضعيف الذي لاحظناه. نظرًا لأننا نستدعي NOTIFY من مشغل في جدول التدفقات، فإن كل كتابة للتدفق تتضمن استدعاء NOTIFY. من أجل الالتزام، تحتاج كل كتابة تدفق إلى أخذ القفل العام والاحتفاظ به طوال مدة التزامه بالكامل، بما في ذلك التدفق إلى القرص. هذا يعني أن عمليات الكتابة المتدفقة تحتاج إلى الالتزام بشكل تسلسلي، مما يحول دون تحسينات Postgres المعتادة مثل الالتزام الجماعي (الذي يرتكب العديد من المعاملات معًا في fsync() واحد). ونتيجة لذلك، لا يمكن إكمال عمليات كتابة التدفق بشكل أسرع من قدرة Postgres على تنفيذ المعاملات، مما يؤدي إلى هذا الاختناق. وهذا ما يفسر أيضًا سبب عدم رؤيتنا لاستهلاك كبير لأي من موارد Postgres مثل وحدة المعالجة المركزية أو القرص: لم يكن هناك أي استهلاك، لأن جميع المعاملات تم تسلسلها بواسطة قفل عام.

جانبًا، كانت هناك بعض المناقشات عبر الإنترنت حول تصحيح Postgres المتعلق بهذه المسألة. لا يؤدي هذا التصحيح (الذي سيتم إصداره في Postgres 19) إلى إزالة القفل العام أو إصلاح عنق الزجاجة الذي لاحظناه. وبدلاً من ذلك، فإنه يعمل على تحسين الحالة الأضيق حيث يوجد العديد من قنوات الإخطار وينتظر كل مستمع فقط على قناة معينة.

تحسين LISTEN/NOTIFY

لجعل عمليات البث المدعومة بـ LISTEN/NOTIFY أسرع، يتعين علينا التغلب على هذا الاختناق. الملاحظة الأساسية هي أنه بالنسبة للتدفقات، وبالنسبة للعديد من تطبيقات الاستماع/الإخطار الأخرى، فإن الإشعارات ليست في حد ذاتها مصدرًا للحقيقة. وبدلاً من ذلك، يقومون فقط بإرسال إشارة للقارئ للتحقق من جدول قاعدة البيانات (المصدر الحقيقي للحقيقة) بحثًا عن بيانات جديدة. ونتيجة لذلك، ليس من الضروري أن تكون الإشعارات مرتبة عالميًا أو متينة تمامًا، لذا يمكننا تحسين NOTIFY عن طريق تخزين الإشعارات مؤقتًا في الذاكرة ومسحها بشكل دوري في معاملة دفعة واحدة، مما يقلل بشكل كبير من التنافس على القفل العام.

يؤدي التخزين المؤقت والإشعارات المجمعة إلى تجنب الاختناق لأن القفل العام يجب أن يتم تنفيذه فقط عند مسح المخزن المؤقت، وليس لكل كتابة تدفق فردي. هذا يعني أن عمليات الكتابة الفردية للتدفق يمكن أن تستمر بسرعة، مع الاستفادة من تحسينات Postgres مثل الالتزام الجماعي للحصول على إنتاجية عالية، بينما يتدفق المخزن المؤقت في الخلفية.

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

من خلال قياس هذا الحل الأمثل، نرى أداءً محسنًا بشكل كبير: في ظل وجود قراء متزامنين، يمكننا إجراء ما يصل إلى 60 ألف عملية كتابة تدفق في الثانية (20 مرة أكثر من ذي قبل) مع الاستمرار في الحصول على زمن استجابة يتراوح بين 15 و100 مللي ثانية. عند الحد الأقصى من الإنتاجية، يتم استخدام وحدة المعالجة المركزية لـ Postgres بالكامل، مما يُظهر أن قاعدة البيانات مشبعة بالفعل بدلاً من اختناقها بسبب التنافس.

تعلم المزيد

جميع التعليمات البرمجية القياسية متاحة على GitHub: github.com/dbos-inc/dbos-postgres-benchmark

إذا كنت ترغب في بناء أنظمة موثوقة وقابلة للتوسع، فنحن نود أن نسمع منك. في DBOS، هدفنا هو جعل التنفيذ الدائم المدعوم من Postgres بسيطًا وعالي الأداء قدر الإمكان. تحقق من ذلك:

  • البداية السريعة: https://docs.dbos.dev/quickstart
  • جيثب: https://github.com/dbos-inc
  • مجتمع ديسكورد: https://discord.gg/eMUHrvbu67

اقرأ المزيد على مدونة DBOS.

Boox يثير الاهتمام بقارئ Picco الإلكتروني الصغير للغاية
التلاعب بسمة BGP ORIGIN وتأثيرها على الإنترنت

Reactions

0
0
0
0
0
0
بالفعل كان رد فعل لهذا المنصب.

ردود الفعل