الأدلة التقنية

موازنة الحمل Load Balancing أمام خوادم Asterisk مع Kamailio

نضع Kamailio وسيطاً أمام خادمي Asterisk، ونوزع المكالمات بينهما بوحدة dispatcher، ونرى الإخفاق Failover عند توقف أحدهما، ونصد فيضاناً بوحدة pike.

موازنة الحمل Load Balancing أمام خوادم Asterisk مع Kamailio

في مقالات هذه المرحلة بنينا المقسم على خادم Asterisk واحد، وهذا يكفي مكتباً أو شركة متوسطة، ولكن ماذا لو كبر العدد إلى آلاف المكالمات في الوقت نفسه، أو أردت أن تحدث الخادم دون أن تقطع المكالمات، أو أن تصمد الخدمة لو تعطل خادم؟ الخادم الواحد هنا نقطة فشل واحدة Single Point of Failure، فإذا توقف توقف كل شيء، وإذا امتلأ لم يبق مكان. والحل أن تضع أمام الخوادم وسيطاً ذكياً يوزع المكالمات عليها ويتجاوز المتعطل منها، وهذا بالضبط عمل Kamailio.

وهذا المقال من مرحلة الاتصالات الهاتفية VoIP، وهو آخرها، وسوف نبني فيه Kamailio وسيطاً أمام خادمي Asterisk، ونوزع المكالمات بينهما، ثم نوقف أحدهما عمداً ونرى كيف يتابع النظام دون أن يشعر المتصل. وكل ذلك داخل شبكة Docker داخلية معزولة على جهازك. وفيما يلي ترتيب مقالات المرحلة:

وسوف نتناول فيه ما يلي:

  • ما دور وسيط SIP Proxy، وكيف يختلف عن المقسم Asterisk.
  • بناء المختبر: Kamailio أمام خادمي Asterisk في Containers.
  • توزيع الحمل Load Balancing بوحدة dispatcher، مع إثبات التوزيع بالأرقام.
  • الإخفاق Failover: توقف خادم وكيف يتجاوزه الوسيط تلقائياً، ثم عودته.
  • صد فيضان الطلبات بوحدة pike، والإشارة إلى معالج الصوت rtpengine.

ما دور وسيط SIP Proxy؟

لتفهم دور Kamailio، قارنه بما يفعله Asterisk. فالمقسم Asterisk يسمى وكيل مستخدم خلفي Back-to-Back User Agent، واختصاراً B2BUA، أي أنه ينهي المكالمة عنده ثم ينشئ مكالمة جديدة نحو الطرف الآخر، فهو طرف كامل في كل مكالمة، يعالج الصوت، ويشغل البريد الصوتي، وينفذ خطة الاتصال. أما Kamailio فهو وسيط SIP Proxy، أي أنه يوجه رسائل SIP بين الأطراف دون أن ينهي المكالمة عنده، فلا يعالج الصوت ولا يشغل خطة اتصال، وإنما يقرر إلى أين تذهب الرسالة ويمررها، ثم يبتعد عن مسار الصوت.

وهذا الفرق هو سر قدرة Kamailio على التحمل: لأنه لا يحمل عبء الصوت ولا منطق المكالمة، يستطيع خادم واحد منه أن يوجه عشرات الآلاف من المكالمات المتزامنة، بينما يقف خلفه عدد من خوادم Asterisk تتقاسم العمل الثقيل. والصورة التالية تبين التوزيع:

هاتف يرسل مكالمته إلى وسيط Kamailio الذي يوزع المكالمات على خادمي Asterisk خلفه، as1 وas2، بالتناوب، فتذهب المكالمة الأولى إلى as1 والثانية إلى as2، بينما يبقى Kamailio خارج مسار الصوت الذي يمر مباشرة
Kamailio يوجه الإشارة ويوزعها، وخوادم Asterisk تحمل عبء المكالمات

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

خريطة المختبر

نبني في المختبر Kamailio وسيطاً على العنوان 10.55.70.10، وخلفه خادما Asterisk على 10.55.70.21 و10.55.70.22، وهاتفاً برمجياً يتصل عبر الوسيط. وكل ذلك في شبكة Docker داخلية معزولة أنشأناها بالخيار --internal. والصورة التالية تبين الخريطة:

شبكة Docker داخلية باسم are-kam-net على النطاق 10.55.70.0/24، فيها وسيط Kamailio باسم are-kam-proxy على 10.55.70.10، وخادما Asterisk باسم are-kam-as1 على 10.55.70.21 وare-kam-as2 على 10.55.70.22، وهاتف برمجي caller على 10.55.70.50 يتصل عبر الوسيط، والشبكة معزولة بلا طريق إلى الإنترنت
وسيط واحد أمام خادمين، وكلها Containers على شبكة معزولة

نستخدم لخادمي Asterisk الـ Image نفسها التي بنيناها في مقال تأمين المقسم، وهي are-pbxsec-tools:1، ونبني لـ Kamailio Image صغيرة من Ubuntu 24.04 فيها الوحدات التي نحتاجها، ونثبت إصدار كل حزمة:

FROM ubuntu:24.04
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      kamailio=5.7.4-1build7 \
      kamailio-extra-modules=5.7.4-1build7 \
      tcpdump=4.99.4-3ubuntu4.24.04.1 \
      sngrep=1.8.0-1build2 \
      iproute2 iputils-ping procps \
 && rm -rf /var/lib/apt/lists/*

وخادما Asterisk إعدادهما واحد: كل منهما يثق بالوسيط Kamailio ويقبل مكالماته دون مصادقة بينهما، لأن المصادقة مهمة الوسيط. وهذا هو as/pjsip.conf، وفيه قسم identify يتعرف على الوسيط بعنوانه:

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060

[kamailio]
type=endpoint
context=from-proxy
disallow=all
allow=ulaw,alaw
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes

[kamailio]
type=identify
endpoint=kamailio
match=10.55.70.10

وخطة الاتصال في as/extensions.conf قصيرة: يرد الخادم على أي مكالمة وينتظر ثانيتين ثم يغلق، وهذا يكفي لنرى أي خادم رد على كل مكالمة من سجل المكالمات CDR:

[from-proxy]
exten => _X.,1,NoOp(Backend handling call to ${EXTEN})
 same => n,Answer()
 same => n,Wait(2)
 same => n,Hangup()

الآن ننشئ الشبكة الداخلية ونشغل الخادمين:

docker network create --internal --subnet 10.55.70.0/24 are-kam-net

for pair in as1:10.55.70.21 as2:10.55.70.22; do
  n=${pair%%:*}; ip=${pair#*:}
  docker run -d --name are-kam-$n --network are-kam-net --ip $ip \
    --cap-add NET_ADMIN --cap-add NET_RAW \
    -v "$PWD/as/pjsip.conf:/etc/asterisk/pjsip.conf" \
    -v "$PWD/as/extensions.conf:/etc/asterisk/extensions.conf" \
    -v "$PWD/as/rtp.conf:/etc/asterisk/rtp.conf" \
    -v "$PWD/as/logger.conf:/etc/asterisk/logger.conf" \
    are-pbxsec-tools:1 asterisk -f
done

إعداد Kamailio: وحدة dispatcher

الوحدة التي توزع المكالمات في Kamailio اسمها dispatcher، وهي تقرأ قائمة الخوادم الخلفية من ملف، وتختار لكل مكالمة خادماً بحسب خوارزمية تحددها أنت، وتفحص الخوادم دورياً لتعرف المتاح منها. نكتب أولاً قائمة الخوادم في dispatcher.list، كلاهما في مجموعة واحدة رقمها 1:

# group 1: the two Asterisk backends
1 sip:10.55.70.21:5060 0 0 weight=50;description=as1
1 sip:10.55.70.22:5060 0 0 weight=50;description=as2

ثم نكتب إعداد Kamailio في kamailio.cfg، وأهم ما فيه تحميل الوحدات وضبط dispatcher ليفحص الخوادم كل أربع ثوان برسالة OPTIONS، وهو ما نحتاجه لكشف الخادم المتعطل:

listen=udp:10.55.70.10:5060

loadmodule "dispatcher.so"
loadmodule "pike.so"
loadmodule "htable.so"
# ... tm, sl, rr, pv, ctl ...

modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list")
modparam("dispatcher", "ds_ping_interval", 4)
modparam("dispatcher", "ds_ping_method", "OPTIONS")
modparam("dispatcher", "ds_probing_mode", 1)

وأما منطق التوجيه فقلبه سطر واحد: لكل مكالمة جديدة نختار خادماً بالخوارزمية رقم 4، وهي التوزيع بالتناوب Round-Robin، ثم نمرر الرسالة إليه، ونسجل في حال فشله مساراً بديلاً نعود إليه:

request_route {
    # ... فحص الفيضان ومعالجة الترويسات ...
    if (is_method("INVITE|REGISTER|MESSAGE")) {
        record_route();
        if (!ds_select_dst("1", "4")) {
            send_reply("503", "No backend available");
            exit;
        }
        t_on_failure("MANAGE_FAILURE");
        route(RELAY);
        exit;
    }
    route(RELAY);
}

بعد تشغيل Kamailio نسأله عن حالة الخوادم بالأمر kamcmd dispatcher.list:

docker exec are-kam-proxy kamcmd dispatcher.list

والمخرج المهم منه حالة كل خادم:

URI: sip:10.55.70.22:5060
FLAGS: AP
    BODY: weight=50;description=as2
URI: sip:10.55.70.21:5060
FLAGS: AP
    BODY: weight=50;description=as1

الرمز FLAGS: AP يعني أن الخادم فعال Active وأن الوسيط يفحصه دورياً Probing، أي أن الخادمين حيان وجاهزان. وهذا الفحص الدوري هو ما يجعل Kamailio يعرف المتعطل قبل أن ترسل إليه مكالمة.

توزيع الحمل Load Balancing

لنثبت أن التوزيع يعمل فعلاً، نجري ثماني مكالمات متتابعة عبر الوسيط من هاتف برمجي، ثم نعد كم مكالمة رد عليها كل خادم من سجل المكالمات CDR. نفرغ أولاً السجل في الخادمين، ثم نطلق المكالمات:

for n in as1 as2; do
  docker exec are-kam-$n sh -c ': > /var/log/asterisk/cdr-csv/Master.csv'
done

{ for i in 1 2 3 4 5 6 7 8; do echo "/dial [email protected]"; sleep 4; echo "/hangup"; sleep 1; done; echo "q"; } | \
  docker run -i --rm --name are-kam-caller --network are-kam-net --ip 10.55.70.50 \
  -v "$PWD/caller:/conf" -v "$PWD/sounds:/sounds:ro" \
  are-pbxsec-tools:1 baresip -f /conf

for n in as1 as2; do
  echo -n "$n: "; docker exec are-kam-$n sh -c 'wc -l < /var/log/asterisk/cdr-csv/Master.csv'
done

والمخرج سوف يكون كما يلي:

as1: 4
as2: 4

وزع Kamailio الثماني مكالمات بالتساوي: أربع لكل خادم، وهذا هو التناوب Round-Robin بالضبط. ونرى القرار نفسه في سجل الوسيط، حيث يطبع لكل مكالمة الخادم الذي أرسلها إليه:

dispatcher: INVITE to sip:[email protected] via sip:10.55.70.22:5060
dispatcher: INVITE to sip:[email protected] via sip:10.55.70.21:5060
dispatcher: INVITE to sip:[email protected] via sip:10.55.70.22:5060
dispatcher: INVITE to sip:[email protected] via sip:10.55.70.21:5060

المكالمة تذهب إلى 10.55.70.22 ثم 10.55.70.21 ثم تعود، أي أنها تتناوب بين الخادمين. ولو أردت توزيعاً يراعي حمل كل خادم بدل التناوب الأعمى، فهناك خوارزميات أخرى في وثائق dispatcher، منها التوزيع بحسب عدد المكالمات القائمة، وهو الأنسب عندما تختلف مدد المكالمات كثيراً.

الإخفاق Failover: توقف خادم

الآن نأتي إلى أهم ميزة، وهي أن يصمد النظام عند تعطل خادم. نوقف الخادم الثاني as2 عمداً، كأنه تعطل أو أعيد تشغيله للتحديث، وننتظر حتى يكشف Kamailio ذلك بفحصه الدوري:

docker stop are-kam-as2
sleep 14
docker exec are-kam-proxy kamcmd dispatcher.list

والمخرج سوف يكون كما يلي:

URI: sip:10.55.70.22:5060
FLAGS: IP
URI: sip:10.55.70.21:5060
FLAGS: AP

تغير رمز الخادم as2 من AP إلى IP، والحرف I يعني Inactive، أي أن الوسيط لم يعد يرسل إليه لأن فحصه بـ OPTIONS لم يرجع، بينما بقي as1 فعالاً AP. الآن نجري ست مكالمات أخرى ونرى أين تذهب كلها:

{ for i in 1 2 3 4 5 6; do echo "/dial [email protected]"; sleep 4; echo "/hangup"; sleep 1; done; echo "q"; } | \
  docker run -i --rm --name are-kam-caller --network are-kam-net --ip 10.55.70.50 \
  -v "$PWD/caller:/conf" -v "$PWD/sounds:/sounds:ro" are-pbxsec-tools:1 baresip -f /conf

docker exec are-kam-as1 sh -c 'wc -l < /var/log/asterisk/cdr-csv/Master.csv'

والمخرج سوف يكون كما يلي، وكل المكالمات ذهبت إلى as1 وحده:

6

الست مكالمات كلها ردها as1، ولم تفشل واحدة منها رغم أن نصف الطاقة توقف، وهذا هو الإخفاق Failover: المتصل لا يشعر بشيء، والخدمة مستمرة. وعندما نعيد تشغيل as2، يكشفه الفحص الدوري خلال ثوان ويعيده إلى الخدمة:

docker start are-kam-as2; sleep 12
docker exec are-kam-proxy kamcmd dispatcher.list
URI: sip:10.55.70.22:5060
FLAGS: AP
URI: sip:10.55.70.21:5060
FLAGS: AP

عاد as2 إلى AP، فيستأنف الوسيط توزيع المكالمات عليه. وهذا يعني عملياً أنك تستطيع أن تحدث خوادمك واحداً واحداً دون أن تقطع الخدمة: توقف خادماً، تحدثه، تعيده، ثم تنتقل إلى التالي، والوسيط يوزع على الباقي طوال الوقت.

صد فيضان الطلبات مع وحدة pike

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

modparam("pike", "sampling_time_unit", 2)
modparam("pike", "reqs_density_per_unit", 30)
modparam("htable", "htable", "ipban=>size=8;autoexpire=120;")

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

request_route {
    if (src_ip!=myself && $sht(ipban=>$si)!=$null) {
        xlog("L_WARN", "pike: dropping request from blocked $si\n");
        exit;
    }
    if (!pike_check_req()) {
        xlog("L_ALERT", "pike: BLOCKING address $si\n");
        $sht(ipban=>$si) = 1;
        exit;
    }
    # ... بقية التوجيه ...
}

لنجربها نرسل مئتي طلب بأسرع ما يمكن من عنوان واحد نحو الوسيط، ثم نقرأ سجله:

docker run --rm --network are-kam-net --ip 10.55.70.64 \
  -v "$PWD/flood.py:/flood.py:ro" are-pbxsec-tools:1 python3 -I /flood.py 10.55.70.64 200
docker logs are-kam-proxy 2>&1 | grep -E 'BLOCKING|dropping' | head

والمخرج سوف يكون كما يلي:

ALERT: <script>: pike: BLOCKING address 10.55.70.64
WARNING: <script>: pike: dropping request from blocked 10.55.70.64
WARNING: <script>: pike: dropping request from blocked 10.55.70.64

بعد نحو ثلاثين طلباً أعلنت pike العنوان 10.55.70.64 مصدر فيضان، فأضفناه إلى قائمة الحظر، وأسقط الوسيط بقية طلباته، وكان عددها 160 طلباً من أصل المئتين. ونتأكد من وجوده في جدول الحظر:

docker exec are-kam-proxy kamcmd htable.dump ipban
name: 10.55.70.64
value: 1

وبهذا يحمي الوسيط خوادم Asterisk خلفه من أن يغرقها فيضان، فيتلقى الضربة في مقدمة الشبكة ويبقي الخدمة قائمة لبقية المستخدمين.

الصوت عبر الوسيط: rtpengine

قلنا في أول المقال إن Kamailio يوجه الإشارة ويبتعد عن مسار الصوت، فالصوت يمر مباشرة بين الهاتف وخادم Asterisk. وهذا يكفي في الشبكة الداخلية، ولكن في حالات كثيرة تحتاج أن يمر الصوت أيضاً عبر الوسيط: عندما تكون الهواتف خلف NAT كما شرحنا في تتبع مكالمة SIP، أو حين تريد تسجيل المكالمات في نقطة واحدة، أو تحويل الترميز Transcoding بين ترميزات مختلفة. وعندها تضيف معالج وسائط منفصلاً اسمه rtpengine، يتحكم فيه Kamailio عبر وحدة خاصة، فيعدل عناوين الصوت في SDP ويمرره عبر نفسه.

وفكرة الربط أن Kamailio يستدعي rtpengine عند كل مكالمة ليعيد كتابة وصف الوسائط:

loadmodule "rtpengine.so"
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")

# داخل التوجيه، عند الطلب والرد:
rtpengine_manage();

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

قبل أن تنقل هذا إلى سيرفر حقيقي

  • في المختبر وثق خادما Asterisk بالوسيط بعنوانه فقط، وفي الإنتاج اجعل بينهما مصادقة أو شبكة خاصة، حتى لا ينتحل أحد عنوان الوسيط ويرسل مكالمات مباشرة إلى الخوادم.
  • الوسيط نفسه نقطة فشل، فلا معنى لأن تحمي الخوادم وتترك الوسيط وحيداً، والحل المعتاد وسيطان يعملان معاً بعنوان افتراضي ينتقل بينهما Virtual IP عبر أداة مثل keepalived.
  • فعل تسجيل الهواتف على الوسيط نفسه بوحدة registrar وusrloc حتى يعرف أين يجد كل مستخدم، فنحن في المختبر وجهنا المكالمات الصادرة فقط ولم نعالج التسجيل.
  • راقب حالة الخوادم وزمن استجابتها من kamcmd dispatcher.list، فالوسيط يقيس زمن كل فحص، ويكشف الخادم البطيء قبل أن يتحول إلى متعطل.
  • أمن الوسيط كما أمنا المقسم في مقال تأمين المقسم، فـ pike طبقة ضد الفيضان، وليست بديلاً عن كلمات المرور القوية وTLS وجدار الحماية.

إزالة المختبر

نزيل ما أنشأناه بالاسم فقط، دون أوامر التنظيف العامة التي قد تحذف Containers أو Volumes تخص مشاريع أخرى على جهازك:

docker rm -f are-kam-proxy are-kam-as1 are-kam-as2
docker network rm are-kam-net
docker rmi are-kam-tools:1 are-pbxsec-tools:1

تدريب عملي

  • غير الخوارزمية في ds_select_dst من 4 إلى 10 (التوزيع بحسب عدد المكالمات القائمة)، ثم أجر مكالمات مختلفة المدة، وراقب هل تغير التوزيع عن التناوب المتساوي.
  • أوقف الخادمين معاً، ثم أجر مكالمة، واقرأ الرد الذي يرجعه الوسيط، وفسر لماذا رد 503 بدل أن تنتظر المكالمة.

الخلاصة

  • Kamailio وسيط SIP Proxy يوجه الإشارة ولا ينهي المكالمة، فيتحمل عشرات الآلاف من المكالمات أمام خوادم Asterisk التي تحمل عبء الصوت.
  • وحدة dispatcher توزع المكالمات على الخوادم الخلفية، وأثبتنا بالتناوب Round-Robin توزيعاً متساوياً: أربع مكالمات لكل خادم من ثمان.
  • الفحص الدوري بـ OPTIONS يكشف الخادم المتعطل فيصبح IP، فتذهب كل المكالمات إلى الباقي دون أن يشعر المتصل، ثم يعود الخادم تلقائياً عند تشغيله.
  • وحدة pike تكشف فيضان الطلبات من عنوان واحد وتحظره، فيحمي الوسيط خوادمه من الإغراق في مقدمة الشبكة.
  • الصوت يمر مباشرة افتراضياً، فإذا لزم أن يمر عبر الوسيط لأجل NAT أو التسجيل أو تحويل الترميز، أضفت معالج الوسائط rtpengine.
  • في الإنتاج اجعل للوسيط نفسه نسخة ثانية بعنوان افتراضي، وأمنه كما تؤمن المقسم، فهو أيضاً نقطة فشل وهدف للفيضان.
دليلتتبع مكالمة SIP عبر الشبكة: لماذا يصل الصوت في اتجاه واحد؟نبني مقسماً وهاتفين خلف NAT في تجربة عملية، ونتتبع المكالمة حزمة بحزمة حتى نعرف لماذا يضيع الصوت وكيف نصلحه.دليلتأمين المقسم من الاحتيال على المكالمات Toll Fraudنهاجم مقسم Asterisk في مختبر معزول بأدوات فحص SIP حقيقية، ثم نسد الثغرات بكلمات مرور قوية وfail2ban وTLS وSRTP ونرى الفرق على الشبكة.دليلمصطلحات الاتصالات الهاتفية VoIP للمبتدئينشرح مبسط لمصطلحات الهاتف عبر الإنترنت، وما يمر في الشبكة بين ضغط زر الاتصال ووصول الصوت إلى الطرف الآخر.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وبناء المختبر على Kamailio 5.7.4 وخادمي Asterisk 20.6.0 من حزم Ubuntu 24.04 في شبكة Docker داخلية معزولة، مع توزيع حمل بوحدة dispatcher، وإخفاق عند توقف خادم، وصد فيضان بوحدة pike.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.