البنية المدفوعة بالأحداث دون فوضى
تَعِد البنية المدفوعة بالأحداث بالاقتران المرن وقابلية التوسّع، لكنها تُدخل بصمت أنماط أعطال جديدة. إليك كيف تحصل على المزايا دون حوادث الثالثة فجرًا.
بقلم Innovation T Team
تُسوَّق البنية المدفوعة بالأحداث بوصفها العلاج للاقتران الوثيق، وقد تكون كذلك بالفعل. لكنها قد تحوّل أيضًا قاعدة شيفرة نظيفة إلى رواية بوليسية موزّعة لا يستطيع فيها أحد أن يشرح لماذا فُوترت طلبية مرتين. الفرق ليس في الـ broker الذي تختاره، بل في حفنة القرارات التي تتخذها بشأن الأحداث وضمانات التسليم وقابلية المراقبة قبل أن تتدفق أول رسالة على الإطلاق.
ما الذي يمنحك إياه النهج المدفوع بالأحداث فعليًا
في جوهرها، تعني البنية المدفوعة بالأحداث (EDA) أن الخدمات تتواصل عبر نشر وقائع عمّا حدث، بدلًا من أن يستدعي بعضها بعضًا مباشرةً وتنتظر. تُصدر خدمة الدفع الحدث PaymentCaptured. ومن يعنيه الأمر (إتمام الطلب، التحليلات، الإشعارات) يتفاعل وفق جدوله الخاص. لا أحد يعيق أحدًا.
تظهر المكاسب الحقيقية في ثلاثة مواضع:
- فك الاقتران في الزمن. يمكن أن يتعطّل مستهلك لعشر دقائق ثم يلحق بالركب لاحقًا. أما في سلسلة متزامنة، فإن العطل ذاته يتتالى نحو الأعلى.
- فك الاقتران في ملكية الفرق. إضافة مستهلك جديد لا تتطلب المساس بالمُنتِج. تصبح الميزات الجديدة "اشترك وتفاعل" بدلًا من "عدّل وأعِد نشر النواة".
- التوزيع الطبيعي المتشعّب. يمكن لحدث واحد أن يقود خمسة تفاعلات دون أن يعلم المُنتِج بوجود أيٍّ منها.
بحسب خبرتنا، تلجأ الفرق إلى EDA في اللحظة نفسها التي يبدأ فيها مخطط استدعاءاتها المتزامنة يشبه طبقًا من المعكرونة. هذا الحدس صائب عادةً. لكن الخطأ هو افتراض أن طبقة المراسلة تزيل التعقيد. إنها تنقله. أنت تستبدل التعقيد المرئي لاستدعاءات الدوال بالتعقيد الخفي لدلالات التسليم.
الأحداث مقابل الأوامر: التمييز الذي يمنع نصف علاتك البرمجية
هذا هو النموذج الذهني الأكثر فائدة، ومعظم الأنظمة الفوضوية تخطئ فيه.
- الحدث هو تقرير عن واقعة ماضية.
InvoiceIssued. لقد وقع بالفعل. وهو يُبَثّ. لا يبالي المُنتِج بمن يستمع ولا يتوقع ردًّا. صفر أو واحد أو عشرة مستهلكين، كل ذلك مقبول. - الأمر هو طلب لأن يحدث شيء في المستقبل.
IssueInvoice. وهو موجّه إلى معالِج واحد بالضبط ويمكن رفضه.
حين تطمس هذا الفرق، تحصل على مُنتِجين يعتمدون سرًّا على تشغيل مستهلك بعينه، وهذا اقتران يرتدي زيّ الحدث. قاعدة جيدة: سمِّ الأحداث بصيغة الماضي، ولا تُرمّز فيها أبدًا توقّعًا لردّ. فإن نشرت خدمة OrderPlaced ثم أخذت تستطلع بحثًا عن InvoiceIssued قبل أن تتمكن من المتابعة، فأنت لم تبنِ EDA. لقد بنيت استدعاء إجراء بعيد (remote procedure call) بطيئًا وعسير التنقيح.
دقّة تحبيب الأحداث وشكل الحمولة
خياران إضافيان يوقعان الفرق في المتاعب لاحقًا:
- الأحداث النحيفة مقابل السمينة. يحمل الحدث النحيف معرّفًا ويتوقع من المستهلكين أن يجلبوا التفاصيل. أما الحدث السمين فيحمل الحالة الكاملة. تُبقي الأحداث النحيفة الحمولات صغيرة لكنها تُحدث تدفقًا كثيفًا من النداءات المرتدة نحو المُنتِج، فتعيد إدخال الاقتران. والأحداث السمينة مكتفية بذاتها لكنها قد تُسرّب المخطط الداخلي. نحن نفضّل عادةً أحداثًا سمينة باعتدال تحمل البيانات التي يحتاجها مستهلك معقول، مع حقل للإصدار.
- إصدار المخطط منذ اليوم الأول. أضف حقل
versionوعامل الحمولة بوصفها عقدًا عموميًا. التغييرات الإضافية (حقول اختيارية جديدة) آمنة. أما حذف الحقول أو إعادة تسميتها فهو تغيير كاسر يستلزم إصدارًا جديدًا ونافذة ترحيل.
ضمان التسليم الذي لا يقرأ أحد بنوده الدقيقة
يمنحك كل broker حديث تقريبًا (Kafka، RabbitMQ، AWS SNS/SQS، Google Pub/Sub، NATS) تسليمًا من نوع at-least-once بشكل افتراضي. اقرأ ذلك مجددًا. at-least-once يعني أن التكرارات ليست حالة نادرة. إنها أمر يومي. انقطاعات الشبكة، وإعادة تشغيل المستهلكين، وعمليات إعادة التوازن، كلها تتسبب في وصول الرسالة نفسها مرتين.
النتيجة العملية: يجب أن يكون كل مستهلك عديم الأثر (idempotent). معالجة PaymentCaptured مرتين يجب ألّا تُفوتر العميل مرتين ولا أن تُرسل إيصالين. هذا ليس تحصينًا اختياريًا تضيفه لاحقًا. إنه الجدار الحامل.
الطرق التي ننفّذ بها الخاصية العديمة الأثر في أنظمة حقيقية:
- امنح كل حدث معرّفًا ثابتًا وفريدًا عند المُنتِج.
- عند المستهلك، سجّل المعرّفات المُعالَجة في جدول لإزالة التكرار (أو ذاكرة تخزين مؤقت مفهرسة بـ TTL معقول) ضمن المعاملة نفسها التي تُنجز العمل.
- قبل التنفيذ، تحقق مما إذا كان المعرّف قد عُولج بالفعل. فإن كان كذلك، فأقرّ باستلامه وامضِ قدمًا.
- لتحديثات الحالة، فضّل العمليات العديمة الأثر بطبيعتها (تعيين الرصيد إلى X، لا إضافة X إلى الرصيد).
التسليم من نوع exactly-once هو في معظمه عبارة تسويقية. ما يمكنك تحقيقه هو تسليم at-least-once مقرونًا بمعالجة عديمة الأثر، وهو ما يُرى من الخارج بمثابة effectively-once. صمّم على هذا الأساس تكفّ عن مطاردة الأشباح.
الترتيب وفخ الكتابة المزدوجة
نمطا عطل خفيّان يتسببان في حصة غير متناسبة من حوادث الإنتاج.
الترتيب. معظم الـ brokers لا تضمن الترتيب إلا داخل partition واحدة أو طابور واحد، لا عالميًا. فإذا وصل AccountCreated وAccountDeleted إلى partitions مختلفة، فقد يرى المستهلك الحذف أولًا. الحل هو التقسيم بمفتاح ثابت (عادةً معرّف الكيان المجمَّع) بحيث تحتفظ كل أحداث كيان واحد بترتيبها. اقبل أنك تحصل على ترتيب على مستوى الكيان الواحد لا على المستوى العالمي، وصمّم مستهلكين يتحمّلون إعادة الترتيب بين الكيانات.
مشكلة الكتابة المزدوجة. تُحدّث خدمتك قاعدة بياناتها ثم تنشر حدثًا. فإن انهارت العملية بين هاتين الخطوتين، فأنت إما التزمت بالحالة لكنك فقدت الحدث، أو نشرت الحدث لتغيّر حالة جرى التراجع عنه. لا يمكنك جعل نظامين منفصلين يلتزمان بشكل ذرّي عبر try/catch ساذجة.
الحل المعياري هو نمط outbox المعامَلاتي:
- في معاملة قاعدة بيانات محلية واحدة، اكتب تغييرك في منطق الأعمال وأدرِج الحدث في جدول
outbox. - تقرأ عملية ترحيل منفصلة (أو أداة change-data-capture تقرأ سجل المعاملات) جدول outbox وتنشر إلى الـ broker.
- علّم الصفوف بأنها نُشرت، مع إعادة المحاولة بأمان لأن المستهلكين عديمو الأثر على أي حال.
يحوّل هذا كتابةً مزدوجة غير موثوقة إلى التزام محلي موثوق واحد يتبعه نشر متسق في نهاية المطاف. إنه ممل، والممل هو تمامًا ما تريده في مساراتك التي تحرّك الأموال. الفرق المنتقلة من قاعدة بيانات وحيدة نحو الخدمات تصطدم بهذا الجدار باستمرار، ولذلك نتناول الانتقال الأوسع في دليلنا حول الانتقال من الأحادية إلى الخدمات المصغّرة.
قابلية المراقبة، وإلا فستطير أعمى
الأنظمة المتزامنة تفشل بصخب مع تتبّع للمكدس (stack trace). أما الأنظمة المدفوعة بالأحداث فتفشل بصمت. تختفي رسالة، ويتخلّف مستهلك بهدوء، فتعرف بالأمر من عميل غاضب. لا يمكنك إطلاق EDA دون قابلية للمراقبة مصمّمة منذ البداية.
الأمور غير القابلة للتفاوض:
- معرّفات الربط والسببية (correlation and causation IDs) على كل حدث، منتشرة من الطرف إلى الطرف، كي تتمكن من إعادة بناء تدفق عمل كامل عبر الخدمات.
- التتبّع الموزّع (distributed tracing) (يُعدّ OpenTelemetry المعيار الفعلي في 2026) بحيث يمتد تتبّع واحد عبر المُنتِج والـ broker وكل مستهلك.
- مقاييس تخلّف المستهلك (lag). نبّه على التخلّف، لا على الأخطاء فحسب. مستهلك يعمل لكنه متخلّف بمقدار 40٬000 رسالة هو عطل لن يلتقطه أي health check.
- طوابير الرسائل الميتة (DLQs). الرسائل التي تفشل مرارًا يجب أن تستقر في مكان مرئي، مع أدوات لفحصها وإصلاحها وإعادة تشغيلها. طابور DLQ لا يراقبه أحد ليس سوى طريقة أبطأ لفقدان البيانات.
النظافة الجيدة للمخطط ترتبط بهذا. فإذا كانت أحداثك عقودًا نظيفة ومُصدَرة، صار التتبّع وإعادة التشغيل أسهل بكثير، وهو المبدأ ذاته الذي نصفه في تصميم واجهات API يحبها المطورون.
متى لا تلجأ إلى النهج المدفوع بالأحداث
الهندسة الرفيعة هي أن تعرف متى تقول لا. يكون EDA الخيار الافتراضي الخاطئ حين:
- يكون التفاعل بحق من نوع طلب/استجابة ويحتاج المستدعي إلى إجابة الآن (فحص تسجيل دخول، أو عرض سعر عند الدفع). إقحام ذلك في الأحداث يضيف تأخّرًا وتعقيدًا دون طائل.
- لم يسبق لفريقك أن شغّل broker في الإنتاج والموعد النهائي هو الشهر القادم. منحنى التعلّم التشغيلي حقيقي.
- لديك ثلاث خدمات ولا معاناة توسّع. قد تكون تشتري مشكلات الأنظمة الموزّعة لتحل مشكلة اقتران لا تعانيها بعد.
مسار وسط عملي كثيرًا ما نوصي به: أبقِ الاستدعاءات المتزامنة للقراءات الموجّهة للمستخدم التي تتطلب إجابة فورية، واستخدم الأحداث للآثار الجانبية (الإشعارات، التحليلات، التزويد اللاحق) التي يمكن أن تحدث بعيد ذلك. النهج الهجين ليس فشلًا. إنه عادةً البنية الصحيحة.
قائمة تحقق قبل الإطلاق
قبل أن يصل أول حدث لديك إلى الإنتاج، تأكّد من كل بند من هذه البنود:
- لكل حدث معرّف فريد وإصدار مخطط.
- كل مستهلك عديم الأثر ومختبَر في مواجهة التسليم المكرر.
- يستخدم المُنتِجون نمط outbox (أو CDC) بدلًا من الكتابات المزدوجة.
- تضمن مفاتيح التقسيم الترتيب لكل كيان يحتاج إليه.
- تتدفق معرّفات الربط عبر كل قفزة وتظهر في السجلات والتتبّعات.
- توجد طوابير DLQ، وهي مراقَبة، ولها إجراء موثّق لإعادة التشغيل.
- لتخلّف المستهلك عتبة تنبيه، لا مجرد تنبيه أخطاء.
- لتغييرات المخطط سياسة مكتوبة للإصدار والإهمال (deprecation).
إن لم تستطع تعليم البنود الثمانية جميعها، فأنت لست مستعدًا لتوسيع النمط. أنت مستعد لعمل نموذج أولي منه.
كيف تستطيع Innovation T مساعدتك
تكافئ البنية المدفوعة بالأحداث الفرق التي تُتقن الأجزاء غير البرّاقة: الخاصية العديمة الأثر، وoutbox، والترتيب، وقابلية المراقبة. وهذا بالضبط حيث نصرف وقتنا. في Innovation T، تصمّم فرقنا لهندسة البرمجيات والسحابة أنظمة مدفوعة بالأحداث مبنيّة لتُنقّح، لا لتُعرض في عرض ترويجي فحسب. نساعدك على اختيار الـ broker المناسب لحِمل عملك، وتنفيذ outbox معامَلاتية، وتوصيل تتبّع OpenTelemetry، وإعداد إعادة تشغيل DLQ بحيث يصبح النشر السيئ تعافيًا في خمس دقائق بدلًا من عطلة نهاية أسبوع ضائعة.
إن كنت تزن الانتقال إلى المراسلة، أو تفكيك أحادية، أو محاولة ترويض نظام غدا فوضويًا بالفعل، فبإمكاننا مساعدتك على فعل ذلك بتأنٍّ. استكشف خدماتنا أو تواصل معنا ودعنا نرسم أنظف مسار لبنيتك.
جاهز للبناء مع Innovation T؟
سواء كان الأمر يتعلق بالأمن أو النمو أو الهندسة، يمكن لفريقنا مساعدتك على تنفيذه بإتقان.