انتقل للمحتوى
علي هيثم·TECH·يردّ خلال أقل من ساعة
  • الرئيسيةابدأ من هنا
  • الأعمالدراسات حالة، مشاريع
  • الخدماتما الذي سأبنيه لك
  • المدونةمقالات، سلاسل
  • التدريبدورات، مختبرات، دورات
  • عنّيالمهندس وراء هذا الموقع
تسجيل الدخولابدأ مشروع
علي هيثم · TECH
  • الرئيسية↗
  • الأعمال↗
  • الخدمات↗
  • المدونة↗
  • التدريب↗
  • عنّي↗
تسجيل الدخولابدأ مشروع
متاح
علي هيثم·TECH

استوديو هندسي. دمشق، GMT+3.

aliyosef.online

الاستوديو

  • الأعمال
  • المدونة
  • التدريب
  • عنّي
  • تواصل معي

البوابة

  • تسجيل الدخول
  • فتح تذكرة دعم
  • متابعة مشروع
  • حسابي

مصادر

  • الوثائق
  • حالة الأنظمة
  • سجلّ التغييرات
  • حزمة العلامة
  • الخصوصية
  • الشروط

النشرة

ملاحظات تقنية وأخبار، أسبوعياً. بدون حشو.

مجاني. إلغاء الاشتراك متى ما حبيت.

تابعني على
© 2026 علي هيثم يوسف. جميع الحقوق محفوظة.مبنيّ يدويًا بـ React 19.آخر نشر · 2026-05-08كل الأنظمة تعمل
  1. الرئيسية/
  2. المدوّنة/
  3. محرّكات المزامنة
محرّكات المزامنة

مشكلة الـ reconnect storm

لما 800 موبايل يرجعوا متّصلين بنفس الوقت، شو بصير للسيرفر. الأنماط اللي بتصمد.

١٢ نيسان ٢٠٢٦·7 دقائق·بقلم علي يوسف

جاري تحميل المقال…

شاركXLinkedInRSS
استلم المقال التالي في بريدك.مقال جديد كل أحد. هندسة بلغة واضحة. مجاني، يمكنك إلغاء الاشتراك في أي وقت.اشترك→
AY
علي هيثم يوسف

مهندس وكاتب. أبني منصات متعدّدة المستأجرين، محرّكات مزامنة، وأنظمة موثوقة بهدوء. أكثر من ٦٠٠ مشروع مُسلَّم منذ ٢٠١٥.

اشتركابدأ مشروعاً→

اقرأ بعدها

  • PB
    ٥ أيار ٢٠٢٦·محرّكات المزامنة

    متى الـ polling أفضل من الـ websockets

    سنة من الـ trade-offs بمحاسب. ليش الجواب الواضح طلع غلط على ظروف الشبكة عندنا.

    8 دقائق
  • CR
    ٢٨ نيسان ٢٠٢٦·محرّكات المزامنة

    حلّ التعارضات في تطبيقات offline-first

    Last-write-wins غالباً غلط. هاد اللي بيشتغل لما اثنين clients يختلفوا.

    10 دقائق
  • SE
    Series
    ٢٠ نيسان ٢٠٢٦·محرّكات المزامنة

    بناء محرّك مزامنة، الجزء الأول: الـ queue

    افتتاح السلسلة. شكل الـ queue الصادر، ليش الترتيب مهم، والـ test harness اللي بيمسك كل شي.

    14 دقيقة
←العودة إلى كل المقالات

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

هذا المقال عمّا فعلناه في المرّة الأولى التي كسرتنا فيها، والأنماط التي عملت.

شكل العاصفة#

يستحقّ التدقيق فيما يحدث. العملاء ليسوا خبثاء؛ يفعلون ما قالت لهم الوثائق أن يفعلوه. عند إعادة الاتّصال:

  1. افتح websocket جديد.
  2. صادق.
  3. اسحب التغييرات التي فاتتهم بينما كانوا غير متّصلين (طلب HTTP لكلّ نوع كيان، في حالتنا أصلاً ٤٨ منها).
  4. ادفع كتاباتهم في الـ queue (طلب لكلّ عنصر، أو batch صغيرة).
  5. اشترك في قنوات المستأجر للحصول على تحديثات حيّة.

في التشغيل العاديّ هذه الخطوات منتشرة عبر المستخدمين والوقت، فيرى الخادم تدفّقاً ثابتاً. في العاصفة، يطلق بضعة آلاف جهاز الخطوات الخمس كلّها في نفس نافذة الخمس ثوانٍ. الخادم يقضي تلك الخمس ثوانٍ في خدمة المصافحات؛ قاعدة البيانات تقضيها في خدمة ٥٠٠٠٠ استعلام صغير مع تنازع عالٍ على الأقفال؛ طبقة الـ websocket تقضيها في إنشاء ٥٠٠٠ اتّصال جديد. الخطوة الأبطأ هي بوّابة كلّ شيء.

في المرّة الأولى التي حدث فيها لنا، بقي الخادم قائماً لكنّ الاستعلامات تكدّست في الـ queue؛ بدأ العملاء يحصلون على timeout، بدأوا بإعادة المحاولة، وإعادة المحاولة جعلت العاصفة أسوأ. بحلول الدقيقة الثالثة كنّا نشاهد الـ queue ينمو أسرع ممّا نستطيع تصريفه. بحلول الدقيقة الخامسة صنعنا kill switch بأيدينا.

خمسة إصلاحات، بترتيب أهمّيتها#

لم نحصل عليها كلّها دفعة واحدة. حصلنا على الأوّل في ذعر ذلك العصر والبقيّة على مدى الشهر التالي.

١. إعادة اتّصال عشوائيّة، على العميل#

أكبر فوز منفرد. كلّ عميل، عند فقدان الاتّصال، يختار تأخيراً عشوائيّاً في [0, jitter_max] قبل أوّل محاولة إعادة اتّصال؛ في الإخفاقات اللاحقة ينمو التأخير أُسّياً مع jitter صغير آخر. هذا يكسر التزامن. ألف عميل لم يعودوا يعيدون الاتّصال في الثانية ٠؛ ينتشرون عبر [0, 30]، ويرى الخادم منحدراً بدلاً من جدار.

Duration nextReconnectDelay({required int attempt}) {
  final base = math.min(30000, 500 * math.pow(1.7, attempt).toInt());
  final jitterMs = math.Random().nextInt(base ~/ 4);
  return Duration(milliseconds: base + jitterMs);
}

ملاحظة: الـ jitter يجب أن يكون مضاعفاً تقريباً، لا جمعيّاً. إضافة ±٢٠٠ مللي ثانية إلى تأخير ٣٠ ثانية بالكاد ينعّم المنحنى؛ المضاعفة أو إضافة كسر ملحوظ يفعل.

٢. سحب موحّد واحد، لا ٤٨#

ورثنا نقاط نهاية مزامنة لكلّ كيان (/products/list، /orders/list، /contacts/list، …). عند إعادة الاتّصال يُصدر العميل الواحد ٤٨ من هذه. نقطة نهاية سحب موحّدة تأخذ cursor الـ since عبر كلّ أنواع الكيانات في طلب واحد تطوي ٤٨ طلباً إلى ١. اتّصالات أقلّ، استعلامات أقلّ، إجراءات أقلّ.

POST /api/v1/sync/pull
{
  "cursors": {
    "contacts":     "2026-05-08T14:22:01.123Z",
    "invoices":     "2026-05-08T14:22:01.500Z",
    "products":     "2026-05-08T14:22:00.000Z"
  },
  "limits": { "perEntity": 200 }
}

الخادم يعيد حزمة واحدة مفهرسة بنوع الكيان. العميل يمشي في الحزمة ويطبّق بترتيب التبعيّة (جهات الاتّصال قبل الفواتير، المنتجات قبل بنود الفاتورة، إلخ).

٣. queues لكلّ مستأجر مع إنصاف#

إن كان مستأجر واحد يسيء التصرّف (أو لديه ألف جهاز)، يجب ألّا يُجوّع الباقين. بدلاً من queue Horizon عالميّة واحدة، نقسّم حسب المستأجر ونقوم بـ round-robin بين المستأجرين النشطين. مستأجر صاخب يستغرق وقتاً أطول لتصريف queue الخاصّ به؛ ضوضاؤه لا تنزف عبر.

المقايضة هي أنّك لا تستطيع استخدام autoscaling Horizon لكلّ «حمل عمل» بعد الآن، لأنّ «حمل العمل» الآن لكلّ مستأجر. يستحقّ.

٤. circuit breaker على المسار البارد#

نقطة نهاية السحب هي المسار البارد. إن بدأت تفشل بأكثر من ~٥٪ على دقيقة منزلقة، نُسقط breaker على جانب الخادم ونعيد «حاول ثانية بعد ٣٠ ثانية» مهذّباً مع header Retry-After. العملاء يحترمونه. الخادم يستعيد أنفاسه. بدون الـ breaker، كلّ سحب فاشل يُحفّز إعادة محاولة، إعادة المحاولة تضيف حملاً، الحمل يحفّز فشلاً أكثر.

الـ breakers تنتمي إلى الخادم، لا إلى العميل

breaker على جانب العميل جيّد، لكنّ breaker على جانب الخادم هو الذي يحمي قاعدة البيانات. حين تكون قاعدة البيانات مضغوطة، الطريقة الوحيدة لإيقاف العاصفة هي أن يرفض الخادم العمل، لا أن يتطوّع العميل للانتظار.

٥. حواجز connection pool#

طبقة الـ websocket (Reverb في حالتنا، لكن يمكن أن تكون أيّ طبقة) لديها ميزانيّة اتّصال محدودة لكلّ عمليّة. بدون حدود، تملأ العاصفة الـ pool، تُرفض الاتّصالات الجديدة على مستوى نظام التشغيل، ورسالة الخطأ في طرف العميل تقول «Connection refused» — عديمة الفائدة. تعيين حدود اتّصال صريحة لكلّ مستأجر ومستوى أقصى لكلّ عمليّة يعني أنّ الخادم يبدأ بـ shedding بنظافة، مع header يخبر العميل لماذا.

ما نقيسه الآن#

لوحة معلومات صحّة العاصفة صغيرة:

المقياسصحّيّتحذير
إعادات الاتّصال في الثانية< ٥٠/ث> ٢٠٠/ث
زمن استجابة السحب p99< ٨٠٠ مللي ثانية> ٣ ثوانٍ
معدّل تصريف الـ queue> ١× التدفّق الداخل< ٠٫٧× التدفّق
أحداث فتح breaker / ساعة٠≥ ١
استخدام الـ pool< ٦٠٪> ٨٥٪

المثير للاهتمام هو «معدّل تصريف الـ queue ÷ معدّل التدفّق الداخل». إن كان أقلّ من ١، أنت تخسر أرضاً. إن بقي أقلّ من ١ لأكثر من دقيقة أو اثنتين، العاصفة تنتصر. الـ kill switch يُسقط في مكان ما حول ٠٫٦.

لماذا يستحقّ هذا الإتقان#

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

إن كانت محرّكات المزامنة تثير اهتمامك، صفحة السلسلة تستكمل من حيث ينتهي هذا.