تحجيم Kamailio إلى 1,000 CPS و60,000 مكالمة متزامنة — رحلة عبر 13 عائقاً

خلاصة سريعة: نادراً ما يكون Kamailio نفسه هو الحدّ الأعلى. وصلنا إلى 1,000 CPS من الإشارة المستقرّة و60 ألف حوار (dialog) متزامن فقط بعد إصلاح 13 عائقاً متفرّقة عبر SHM، وطبقة TM، وفحص المُوزّع (dispatcher)، ومجمع العاملين (workers)، و RTPEngine، ومسار قاعدة البيانات الساخن، و Redis. ولم يكن أيٌّ من هذه العوائق تقريباً «بطء Kamailio نفسه» — كانت إعدادات افتراضية لا تصمد أمام الحمل الحقيقي.

هذا تدوين تفصيلي لمشروع توسعة سعة استمرّ أسابيع على منصّة VoIP إنتاجية. الأسماء وتفاصيل البنية التحتية بقيت عامّة؛ التقنيات لا.


الهدف بصيغته الصادقة

كان الهدف المُعلَن 1,000 CPS مستدامة، مع 60,000 مكالمة متزامنة قيد التشغيل في لحظة واحدة. رقمان، وعدٌ واحد: على زوج SBC أن يحمل ساعة الذروة دون أن يصطفّ، أو يُسقط، أو يضرّ بـ PDD.

ما لم يُذكَر خلاف ذلك، فإنّ أرقام الـCPS أدناه تشير إلى عُقدة الـSBC النشطة قيد الاختبار. هامش الإنتاج بعد ذلك يعتمد على ما إذا كان الزوج يعمل بنمط active/standby أم active/active.

قبل أن نمضي، أمران لا بدّ من توضيحهما:

  • CPS = عدد INVITEs الجديدة المقبولة في الثانية. يضغط على مسار الإشارة: TM، اختيار dispatcher، منطق routing، عمليات htable، وأحياناً عدد قليل من زيارات SQL لكل مكالمة.
  • Concurrent = عدد الـdialogs الفعّالة في أيّ لحظة. يضغط على الحالة (state): SHM، وحدة dialog، جدول dispatcher، فتحات جلسات RTPEngine، حدود UAS التحتانية، والأهمّ — انضباط مؤقّتات الجلسة (session timers).

منصّة تُحقّق 1,000 CPS لعشر دقائق، ومنصّة تستديم 1,000 CPS عند 60 ألف متزامنة، آلتان مختلفتان. الثانية هي المطلوبة.

الرقم خلف الرقم

الدرس الأول، تعلّمناه بمشقّة:

البنية CPS مستديمة الحدّ المُقيِّد
Kamailio بمسار الرفض فقط (دون backend ودون وسيط ميديا) 1,000+ لا شيء — Kamailio بمفرده على ما يُرام
المسار الكامل مع وسيط ميديا و UAS ثقيل 200–300 ng-protocol في وسيط الميديا (مقبس UDP وحيد)
ميديا مباشرة مع UAS ثقيل ~500 استهلاك CPU في UAS
ميديا مباشرة مع UAS خفيف 500–600 مقبس UDP في النواة على Kamailio (لا يوجد SO_REUSEPORT في 5.8)

اقرأ الجدول من أعلى لأسفل: معالجة Kamailio لـ SIP ليست هي القيد. كلّ طبقة أخرى هي القيد. أكبر خطأ ترتكبه فِرَقٌ كثيرة هو معاملة «CPS الخاصّ بـ Kamailio» على أنّه CPS المنصّة — ليس كذلك، وستُضيِّع أسابيع في ضبط العملية الخطأ.

الطريق إلى 1,000 CPS عبر المسار الكامل هو إذن تمرين على مستوى المكدّس بأكمله: نُسَخ متوازية من وسيط الميديا، UAS خفيف حيث يصحّ، listeners متعدّدة الـIP، وتخزين مؤقّت عدواني لكلّ استعلام SQL يقع في المسار الساخن.


العوائق الثلاثة عشر، ملخّصة

ظهرت تقريباً بهذا الترتيب مع تصاعد الـCPS. كلّ من يُحجِّم Kamailio سيصطدم بمجموعة فرعيّة منها — وغالباً تبدأ بعائق رقم 1 و4.

1. استنزاف SHM من htables الخاصّة بكلّ مكالمة

htable حالة واحد لكلّ مكالمة (مدخلٌ لكلّ Call-ID، ~30 مفتاحاً) راكَم المداخل لأنّ تنظيفها كان فقط في dialog:end. المكالمات الفاشلة — CANCEL، أخطاء الـbackend، استنفاد dispatcher — لم تصل إلى dialog:end فأبقت الحالة. عند 50 CPS انهرنا بعد ~70 ثانية.

الإصلاح: تنظيف صريح لكلّ مفتاح خاصّ بالمكالمة في كلٍّ من event_route[dialog:end] و event_route[dialog:failed]. لا تعتمد على dialog:failed كشبكة الأمان الوحيدة: حسب موضع الفشل وما إذا كان تتبّع الحوار قد بدأ بالفعل، قد يلزم تنظيف صريح أيضاً في failure_route، ومسارات معالجة CANCEL، ومسارات رفض الـbackend، وفروع dispatcher المُستنفَدة. خفض autoexpire من 3,600 إلى 1,800 كحزام أمان. رفع SHM من 64 ميغا إلى 1 جيغا — لكنّ التنظيف هو ما أوقف التسرّب؛ زيادة SHM فقط منحتنا وقتاً للتشخيص.

التعميم: أيّ htable أو dialog AVP أو $xavp تُنشئه لكلّ مكالمة يحتاج مساراً للتنظيف على كلّ نهاية، بما في ذلك مسارات الفشل. حساب SHM ليس مع جامع نفايات.

2. حساسية فحص dispatcher

القيمة الافتراضية ds_probing_threshold=1 تعني أنّ فقدان OPTIONS واحد يكفي ليُعتبر backend ميتاً. تحت الحمل، قد يُفوِّت backend واحدةً فعلاً. شاهدنا خمسة backends سليمة تنقلب إلى Inactive خلال 60 ثانية عند 300 CPS، فلم يبقَ هدف dispatch.

الإصلاح: ds_probing_threshold 1→5، ds_inactive_threshold 1→5، فاصل ping 60→30. الفاصل الأقصر زاد من تواتر العيّنات، والعتبات الأعلى منعت فشل OPTIONS واحدة من قلب الحالة.

3. تجويع العاملين (workers)

64 عاملاً لـUDP كانت كافية لمعالجة INVITE لكنّها ليست كافية لمعالجة INVITE + ACK + إعادات الإرسال الناتجة عن تأخّر ACKs. إعادات الإرسال تُضخّم الحمل: طابور عالق يُولِّد طابوراً أكبر.

الإصلاح: العاملون 64→256 على الـSBC المُحدَّث. لاحظ تَبِعة ذلك على التبعيّات — انظر #7.

4. تراكم معاملات TM

طول عمر معاملات INVITE خطيرٌ عند CPS عالية. في إعدادنا، استطاعت معاملات INVITE غير المكتملة أن تبقى مقيمةً مدّةً كافية لتتراكم بسرعة تحت الحمل. عند 1,000 CPS، فإنّ عمراً فعّالاً بين 120 و180 ثانية يعني ما يزيد على 100 ألف معاملة تتنافس على SHM. رصدنا نحو 45 ألف معاملة عالقة قبل أن يصبح ضغط SHM ظاهراً.

الإصلاح: خفض نافذة مهلة INVITE الفعّالة إلى 30,000 ميلي-ثانية حيث ينسجم ذلك مع طبيعة حركتنا. طبقة TM نظّفت أسرع، وتوقّف SHM عن الانجراف للأعلى. كذلك شدّدنا مؤقّتات غير-INVITE، لكنّ عمر معاملة INVITE كان الرافعة الأبرز.

5. تسرّب جلسات RTPEngine

جلسات الميديا تُهدم عند BYE لكن ليس عند كلّ نهاية فشل — CANCEL، رفض backend، استنفاد dispatcher، انتهاء dialog. الجلسات الراكدة كانت تتراكم بالآلاف في الساعة وحمل النظام (load average) تجاوز 25 على خوادم ميديا 32-core.

الإصلاح: استدعاء صريح لـrtpengine_delete() في failure_route (CANCEL + استنفاد dispatcher)، وevent_route[dialog:failed]، ومسار BYE الفرعي. نفس مبدأ #1 — كلّ نهاية في routing.cfg تحتاج التنظيف.

6. حدود الجلسات في UAS

الإعدادات الافتراضية للـbackend متحفّظة. sessions-per-second=30 وmax-sessions=1000 يتبخّران عند 100 CPS. رفعها ضروري لكنّه غير كافٍ — يجب أن يتناسب مع سياسة CPS-لكلّ-IP-مصدر عبر السلسلة، وإلّا فإنّ مزوّداً واحداً يُجوِّع الباقين.

7. استنفاد اتصالات PostgreSQL

256 عاملاً × N اتصالاً لكلّ عامل = ما يكفي لتجويع قاعدة البيانات. اصطدمنا بـ"too many clients already" عند الإقلاع. حلّان: 1. رفع max_connections على الأسطول. 2. إزالة اتصال قاعدة بيانات غير مستخدم كان يُفتح لكلّ عامل لميزة لم تعد قائمة. ~256 اتصالاً لكلّ SBC، استُعيدت مجّاناً.

التعميم: أيّ وحدة معتمِدة على قاعدة البيانات أو أيّ DB handle مُهيَّأ يُهيَّأ لكلّ عامل قد يُضاعف استهلاك الاتصالات حين يزداد عدد العاملين. دقّق في DB handles قبل أن تُحجِّم العاملين.

8. حِمل الوحدات غير المستخدمة

ثماني وحدات مُحمَّلة لا تُستعمل، تأكل SHM + PKG لكلّ عامل. أربع عشرة عمليّة حساب MOS AVP لكلّ مكالمة — لا تُقرأ. تسعٌ وثلاثون استدعاءً لمؤقّت benchmark لكلّ INVITE — لا تُجمَّع. الإعدادات الميتة تتراكم على مرّ السنين؛ التحجيم فرصة ممتازة لتنظيفها.

9. Redis في المسار الساخن

محدود معدّل يومي (rate limiter) كان يأخذ 8–16 جولة شبكية لـRedis لكلّ مكالمة. عند بضع مئات CPS، هذه ضريبة ميلي-ثانية تتراكم في مجمع العاملين.

الإصلاح: نقل العدّاد المحلي إلى htable ($shtinc للـCPS، dialog profiles للمتزامنة). Redis بقي للحالة المُشتركة عنقوديّاً ولقناة الإشعارات الحيّة — اختفى دوره من المسار الساخن. التطبيق على مستوى الـSBC الواحد فقط؛ التنفيذ العنقودي شأن المرحلة الثانية.

10. كتابات dialog متزامنة

dialog db_mode=1 قد يُنفِّذ تحديثات قاعدة بيانات حاجبة عند انتقالات حالة الحوار. عند 1,000 CPS، حتى عددٌ قليل من كتابات الحوار لكلّ مكالمة قد يصبح آلاف الكتابات الحاجبة في الثانية. الحظر هو القاتل، لا المعدّل الخام وحده.

الإصلاح: db_mode 1→2 (تدفّق دوري متأخّر). الحالة لا تزال تُحفظ، فقط ليس بشكل متزامن في المسار الساخن. كذلك تفعيل auto_inv_100، تعطيل cdr_on_failed، ومضاعفة أحجام htable تقريباً عبر اللوحة.

11. SQL لكلّ مكالمة في مسار التوجيه

بعض قرارات routing كانت تُجري SELECT … WHERE prefix LIKE … || '%' على جدولٍ بمئات الآلاف من السطور، لكلّ مكالمة. حتى مع الفهرس الصحيح، هذه قراءة بدرجة ميلي-ثانية تعمل لكلّ INVITE.

الإصلاح: ترحيل مسار مطابقة البادئة إلى mtree — شجرة Kamailio لمطابقة أطول بادئة، تُحمَّل من view في قاعدة البيانات عند الإقلاع وقابلة لإعادة التحميل عبر JSON-RPC. بعد الترحيل: قراءات ما دون الميلي-ثانية داخل العمليّة؛ SQL بقي كاحتياط (أو أُزيل تماماً على المسارات التي صار التريَ مرجعها).

التعميم: إن كنت تُجري LIKE 'prefix%' على جدولٍ ساخن لكلّ مكالمة، فأنت على الأرجح تُريد mtree بدلاً منها. تكلفة 5 أسطر من modparam تستحقّ أسابيع إعادة الضبط.

12. منحدر البدء البارد لذواكر الإحماء

عند إعادة التشغيل، تكون ذواكر الإحماء (خريطة IP المزوّد، تخزين البادئة → الحامل) فارغة لمدّة ~10 دقائق ريثما يَملؤها rtimer. كلّ مكالمة في تلك النافذة تسقط على SQL لكلّ مكالمة — الذي ينهار تحت الحمل.

الإصلاح: إحماء متزامن لأصغر الذواكر وأشدّها أهميّة قبل أن يقبل الـlistener حركةً. الذواكر الأكبر تبقى على rtimer، لكنّ الـSBC لم يعد يقبل حركةً وهو في حالةٍ تجعل كلّ مكالمة تذهب إلى قاعدة البيانات.

13. مقبس UDP في النواة

على نشرنا لـKamailio 5.8، كان توازي استقبال UDP محدوداً فعلياً لكلّ مقبس listener. بعد ~600 CPS على IP واحد للـSBC، أصبح توصيل الحزم إلى ذلك الـlistener هو الاختناق.

الإصلاح: بنية listeners متعدّدة الـIP. كلّ ثنائي مزوّد/IP-مصدر مهمّ يحصل على IPs listener مخصّصة، فيتوزّع عمل استقبال UDP على عدّة مقابس. كان هذا أرخص وأكثر أماناً من إعادة هندسة نموذج العاملين أثناء مشروع التوسعة.


رقم الـ60,000 المتزامن

CPS محورٌ، والحوارات المتزامنة محور آخر. الرقم المتزامن يحدّده كم تبقى المكالمة حيّة أكثر مما تحدّده سرعة قبولك لها. ثلاثة عناصر يجب أن تكون صحيحة:

  • مؤقّتات جلسة RFC 4028 بـmin_se=90 وSession-Expires معقولة (استقرّينا على 1,800 ثانية). من دون مؤقّتات الجلسة، أعطال الشبكة العابرة تُيتّم حوارات تظلّ معلّقة لساعات.
  • dialog default_timeout=1,800. حدّ أعلى للحوار العالق بـ30 دقيقة بصرف النظر عن سلوك النقطتين الطرفيّتين.
  • max-sessions لوسيط الميديا بقدرة الأسطول. استقرّينا على 20 ألفاً لكل نسخة × 4 نسخ = 80 ألفاً — هامش مريح فوق ذروة الـ60 ألفاً مع إفساح لإعادات INVITE والتراكب القصير.

الـSBCs أنفسها تحمل الحالة المتزامنة بثمن قليل متى كان SHM مُحجَّماً صحيحاً. سعة المتزامن في الأساس حوار sst وdialog وأسطول الميديا، لا حوار عمليّة Kamailio.


ما تراجعنا عنه عمداً

اقتراح في مرحلة متأخّرة: راية لكلّ مزوّد لتجاوز وسيط الميديا للمزوّدين على peering مباشر، بكسر سقف ng-protocol للمقبس الواحد لتلك الفئة من الحركة. بنينا الميزة، اختبرناها، ثمّ تراجعنا عنها بالكامل.

لماذا: - أدخلت قراراً وقت التشغيل في routing يُؤثّر على مسار ميديا المكالمة — صنفٌ من القرارات يصعب تصحيحه بعد الحادثة. - نفس الإنتاجيّة قابلة للوصول بإضافة نسخ وسيط ميديا أفقياً — نمط تحجيم معروف الكميّة. - تشغيلياً، «كلّ مكالمة تمرّ بوسيط الميديا» ثابتٌ يُسهَل الدفاع عنه، أمّا «كلّ مكالمة تمرّ بوسيط الميديا إلّا إذا كان لهذا المزوّد الراية» فقاعدة أصعب. الثوابت التي تستطيع أن تذكرها في جملة واحدة هي الثوابت التي تستطيع أن تُراقبها.

التراجع كان نظيفاً: عمود الـschema مُسقَط، وتغييرات routing لم تصل إلى الإنتاج، ونصّ الـspec مُرتجَع. بعد عشرة أسابيع لم نندم. الدرس ليس أنّ الميديا المباشرة سيّئة — بل أنّ إضافة سلوك قابل للإعداد على مسار المكالمة قرارٌ حامل، يستحقّ spec مستقلّ، لا ملحقاً لميزة سعة.


النمط، في فقرة واحدة

إن أردتَ خلاصةً واحدة: تحجيم Kamailio انضباطٌ في إيجاد التكلفة لكلّ مكالمة التي تنمو خطّياً مع الـCPS، وإزالتها. SHM ينمو لأنّ تنظيف event_route[dialog:failed] ناقص. اتصال Redis في المسار الساخن. LIKE على جدول ساخن. تجويع عاملين يُولِّد إعادات إرسال تُولِّد تجويعاً أكبر. الـSBC يصبح سريعاً ليس لأنّ Kamailio صار أسرع، بل لأنّ الحلقة حول كلّ INVITE صارت أنظف.

ابنِ الحلقة الفارغة أوّلاً. ثمّ أَضِفْ ما يجب. وتحقّق أنّ ما يجب إضافته هو فعلاً O(1) لكلّ مكالمة.


ما القادم

في تدوينات لاحقة: - ترحيل mtree بتفصيل أعمق — view الـschema، إعداد الوحدة، نمط إعادة التحميل عبر JSON-RPC، والأخطاء التي منعتنا من نسخ النمط ميكانيكياً على مسارٍ ساخنٍ ثانٍ. - نظرة طويلة على dialog db_mode وأيّ حالة تنجو من إعادة تشغيل SBC وأيّها لا. - قصّة مؤقّتات الجلسة: لماذا min_se=90 لا 1,800 الكلاسيكية، وما أثر ذلك على إعادات INVITE في وسط الحوار من نقاط طرفيّة سيّئة السلوك.

إن أردتَ موضوعاً تالياً، راسلني.