دراسات حالة واقعية لتصميم الأنظمة

تصميم نظام الإشعارات

18 دقيقة الدرس 8 من 10

تصميم نظام الإشعارات

الإشعارات هي نبضة كل منتج حديث. تُخبر المستخدم أن سيارته وصلت، وطرده شُحن، وطلب إعادة تعيين كلمة المرور ينتظره. على نطاق واسع — مثل مليار إشعار دفع يوميًا في Meta، أو 4 مليارات بريد إلكتروني يوميًا في Gmail — النهج الساذج المتمثل في استدعاء SDK البائع مباشرةً أثناء الطلب كارثة على مستوى الموثوقية. تُصمَّم هذه الدرس خدمة إشعارات متعددة القنوات على مستوى الإنتاج من المبادئ الأولى.

المرحلة الأولى — المتطلبات

المتطلبات الوظيفية

  • إرسال الإشعارات عبر ثلاث قنوات: الدفع (iOS APNs / Android FCM)، البريد الإلكتروني (SendGrid / SES)، والرسائل القصيرة (Twilio / SNS).
  • تتيح للمتصلين (الخدمات الداخلية الأخرى) تشغيل الإشعارات عبر API — يحددون المستخدم والقالب وأولوية القناة وحمولة البيانات الاختيارية.
  • دعم إدارة تفضيلات المستخدم: يمكن للمستخدم إلغاء الاشتراك في قناة أو كتم فئة بالكامل.
  • دعم الإشعارات المجدولة (الإرسال في وقت محدد، مثل تذكير عند الساعة 9 صباحًا في المنطقة الزمنية للمستخدم).
  • توفير إيصال تسليم: يمكن للمتصلين الاستعلام عما إذا كان الإشعار قد سُلِّم أو فشل أو لا يزال معلقًا.

المتطلبات غير الوظيفية

  • الإنتاجية: 10 ملايين إشعار/يوم (~116/ثانية بالمتوسط، ~1,000/ثانية في الذروة).
  • وقت الاستجابة: يجب أن يصل الدفع والرسائل القصيرة إلى البائع خلال ثانية واحدة من التشغيل. يمكن أن يكون البريد الإلكتروني بذل أفضل جهد لمدة تصل إلى 30 ثانية.
  • التوافر: 99.9% وقت تشغيل — يجب ألا تصبح خدمة الإشعارات نفسها نقطة إخفاق واحدة للمتصلين.
  • التسليم مرة واحدة على الأقل: يجب إعادة محاولة الإرسال الفاشل تلقائيًا؛ التكرار مقبول (عاطف التأثير على مستوى البائع).
  • تحليلات شبه فورية: معدلات نجاح/فشل التسليم مرئية في لوحة تحكم العمليات خلال دقيقة واحدة.
قيد رئيسي: الإشعارات هي أثر جانبي لمنطق الأعمال، وليست الاستجابة الأساسية. واجهة API التي تُشغّل الإشعار (مثل POST /orders) يجب ألا تُعيق أبدًا الانتظار حتى يقبل البائع الرسالة. يجب أن تكون خط أنابيب الإشعارات غير متزامن تمامًا من منظور المتصل.

المرحلة الثانية — تقدير الحجم

  • 10 ملايين إشعار/يوم — موزعة تقريبًا: 60% دفع، 30% بريد إلكتروني، 10% رسائل قصيرة.
  • الدفع: 6 ملايين/يوم = 70/ثانية متوسط، 600/ثانية ذروة — ضمن نطاق مجموعة اتصال FCM واحدة ولكن تحتاج منطق fan-out.
  • البريد الإلكتروني: 3 ملايين/يوم = 35/ثانية — حد إنتاجية SES هو 14 بريدًا/ثانية افتراضيًا لكل حساب (يرفع عبر طلب حصة)؛ تحتاج هويات إرسال متعددة أو مجموعة.
  • الرسائل القصيرة: مليون/يوم = 12/ثانية — حد معدل Twilio هو 1 رسالة/ثانية لكل رقم هاتف؛ تحتاج مجموعة أرقام أو short code بهذا الحجم.
  • التخزين: كل صف سجل إشعار ~500 بايت؛ 10 مليون/يوم × 500 بايت × 90 يوم احتفاظ ≈ 450 جيجابايت. مثيل Postgres واحد يتحمل هذا؛ قسّم اليوم للتشذيب السريع.

المرحلة الثالثة — تصميم API

واجهة API الداخلية للتشغيل هي السطح العام الوحيد لهذه الخدمة. أبقها نحيفة:

POST /v1/notifications Authorization: Bearer <service-token> { "user_id": "u_8821", "template_id": "order_shipped", "channels": ["push", "email"], // الأفضلية بالترتيب؛ احتياطي إذا فشل الأول "priority": "high", // high | normal | low "data": { "order_id": "O-9912", "eta": "2 hours" }, "scheduled_at": null // ISO-8601 أو null للفوري } Response 202 Accepted: { "notification_id": "n_7fa3b2", "status": "queued" } GET /v1/notifications/{notification_id} Response: { "status": "delivered|failed|pending", "channel_results": [...] }

لاحظ 202 Accepted — وليس 200 OK. الخدمة تُقرّ بالاستلام وتسلّم المهمة إلى خط الأنابيب غير المتزامن. لا يُعاق المتصل أبدًا في انتظار استجابة البائع.

المرحلة الرابعة — البنية العامة

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

Multi-channel notification system architecture Internal Services / Apps Notification API Service validate + enqueue Preference Service + Cache Message Queue Kafka / SQS push | email | sms topics Scheduler delayed jobs (Redis) Push Worker FCM / APNs Email Worker SES / SendGrid SMS Worker Twilio / SNS FCM / APNs Vendor API SES / SendGrid Vendor API Twilio / SNS Vendor API Notification Log Postgres (partitioned) POST check prefs enqueue log receipt
بنية نظام الإشعارات متعدد القنوات: يضع المتصلون المهام في الطابور عبر API؛ ينفذ العمال الخاصون بكل قناة المهام ويستدعون واجهات API للبائعين؛ تُكتب النتائج في سجل الإشعارات.

المرحلة الخامسة — التعمق والمقايضات

1. تصميم موضوعات الطابور — طابور واحد أم متعدد؟

استخدم موضوعات منفصلة لكل قناة (مثل notifications.push، notifications.email، notifications.sms). هذا يتيح لك توسيع كل مجموعة مستهلكين بشكل مستقل: عمال الدفع يحتاجون إنتاجية عالية جدًا (600/ثانية ذروة)؛ عمال الرسائل القصيرة محدودون بمعدل البائع ويحتاجون تقنين السرعة. طابور مختلط واحد سيجبر القناة الأبطأ على تعطيل الأسرع.

أضف أيضًا موضوعًا ذا أولوية عالية لكل قناة (مثل notifications.push.high). لا ينبغي أبدًا أن تنتظر تنبيهات الأمان ورموز OTP وتأكيدات الدفع خلف إرسال تسويقي جماعي. دائمًا يستنزف العمال القسم .high أولًا (ترتيب أقسام Kafka، أو FIFO في SQS مع مجموعات الرسائل).

2. خدمة التفضيلات — التخزين المؤقت غير قابل للتفاوض

قبل وضع أي إشعار في الطابور، يجب على خدمة API التحقق مما إذا كان المستخدم قد ألغى الاشتراك في تلك القناة أو كتم تلك الفئة. عند ذروة 1,000 طلب/ثانية، سيضغط هذا التحقق على قاعدة بيانات التفضيلات. احفظ تفضيلات المستخدم في Redis بمدة حياة TTL تبلغ 5 دقائق. عندما يحدّث المستخدم تفضيلاته، اكتب للتو إلى كل من Postgres وRedis فورًا. نافذة البيانات القديمة لمدة 5 دقائق تعني أن مستخدمًا ألغى الاشتراك قد يتلقى بضعة إشعارات أخرى — مقبول، ومنصوص عليه في اتفاقية مستوى الخدمة الخاصة بك.

Preference check flow with cache Notification API Service Redis Cache prefs, TTL 5 min Pref DB Postgres Allowed? opted-in + not muted for this category Enqueue to Queue Suppress log + discard cache hit? cache miss yes no
تدفق التحقق من التفضيلات: يُتحقق من Redis أولًا؛ يعود فشل الإصابة بالذاكرة المؤقتة إلى Postgres ويملأ الذاكرة المؤقتة. يتم تجاهل المستخدمين الذين ألغوا الاشتراك بهدوء قبل أن يصل أي شيء إلى الطابور.

3. إعادة المحاولة وطابور الرسائل الميتة (DLQ)

واجهات API للبائعين تفشل. FCM يُعيد 503؛ SendGrid يُقيّد السرعة. ابنِ التراجع الأسي في كل عامل:

  • إعادة المحاولة 1 بعد 10 ثوانٍ، إعادة المحاولة 2 بعد 30 ثانية، إعادة المحاولة 3 بعد دقيقتين — ثم الانتقال إلى DLQ.
  • مستهلك DLQ منفصل يُنبّه مهندسي المناوبة، يكتب في سجل الإشعارات بـ status = failed، ويصعّد اختياريًا إلى القناة التالية (فشل الدفع → حاول البريد الإلكتروني).
  • اجعل إعادة المحاولات عاطفة التأثير: أدرج notification_id كمفتاح عاطفة التأثير للبائع حيث تدعم الواجهة ذلك (SES وTwilio يدعمان هذا).
أفضل ممارسة: أضف فحص قِدَم الرمز المميز للجهاز قبل إرسال الدفع. تُصبح رموز الأجهزة قديمة عندما يحذف المستخدم التطبيق. يُعيد FCM خطأ registration_not_registered — تعامل معه بحذف الرمز المميز من قاعدة بياناتك فورًا، لا في الدفعة التالية. وإلا ستُهدر السعة في إعادة المحاولة على رموز ميتة.

4. عرض القالب — أين يحدث؟

قدّم نص الإشعار في العامل، لا في خدمة API. تستلم خدمة API template_id وحمولة بيانات. يجلب العامل القالب (من مخزن قوالب مدعوم بالتخزين المؤقت)، يدمج البيانات، ويرسل النص المعروض إلى البائع. هذا يُبقي خدمة API نحيفة وسريعة، ويتيح لك تحديث القوالب دون إعادة نشر طبقة API.

5. التحليلات والرصد

كل انتقال حالة (مُدرج في الطابور ← مُرسَل ← مُسلَّم / فاشل) يكتب حدثًا في موضوع Kafka للتحليلات. تجميع وظيفة معالجة البث (Flink أو Kinesis Data Analytics) معدلات التسليم لكل قناة لكل قالب في نوافذ مدتها دقيقة واحدة وتكتب في مخزن سلاسل زمنية (InfluxDB أو CloudWatch Metrics). لوحة تحكم العمليات تستعلم عن هذا لمراقبة اتفاقيات مستوى الخدمة. هذا أيضًا هو المكان الذي تكتشف فيه أداء البائع المتدهور قبل أن يُثير شكاوى العملاء.

خطأ شائع — fan-out للأجهزة غير النشطة: إذا كان موضوع الدفع يحتوي على 10 ملايين رمز مميز مسجل للجهاز لكن 40% من المستخدمين لم يفتحوا التطبيق منذ 6 أشهر، فأنت تحرق سعة الطابور على رموز ستفشل بصمت. شغّل وظيفة تنظيف ليلية: استعلم عن نقاط نهاية تغذية FCM / APNs للرموز غير المسجلة وقلّص حجمها. يمكن أن يُقلص هذا حجم الدفع بنسبة 30–50% ويحسن مقياس نسبة التسليم بشكل كبير.

الخلاصة — قرارات التصميم الرئيسية

  • غير متزامن بالتصميم: المتصلون دائمًا يحصلون على 202؛ الطابور يفصل التشغيل عن التسليم.
  • موضوعات لكل قناة مع مسارات أولوية: يعزل حدود الإنتاجية؛ الرسائل عالية الأولوية لا تتأخر أبدًا بسبب الإرسال التسويقي الجماعي.
  • ذاكرة تخزين مؤقت للتفضيلات في Redis: فحوصات إلغاء الاشتراك لا تُبطئ المسار الحرج أبدًا؛ TTL لمدة 5 دقائق مقايضة اتساق مقبولة.
  • التراجع الأسي + DLQ + تصعيد القناة: يضمن التسليم مرة واحدة على الأقل دون حلقات إعادة محاولة لا نهائية.
  • عرض القالب في العامل: يُبقي طبقة API عديمة الحالة ويتيح التبديل السريع للقوالب.
  • التحليلات عبر معالجة البث: لوحات تسليم بأقل من دقيقة دون الاستعلام عن قاعدة البيانات الإجرائية.