> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# عنوان IP عائم بين سيرفرين مع Keepalived وVRRP
- URL: https://arabroot.io/articles/عنوان-ip-عائم-بين-سيرفرين-مع-keepalived-و-vrrp/
- Published: 2026-10-09T13:15:00.000Z
- Updated: 2026-10-09T13:00:45.000Z
- Description: كيف ينقل Keepalived عنواناً واحداً بين سيرفرين تلقائياً عندما يتوقف أحدهما، مع تجربة حقيقية تقيس زمن الانتقال بالثواني.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, الشبكات والوصول, DevOps وCI/CD, الاستضافة الذاتية

في مقال [مفاهيم التوفر العالي](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D8%AA%D9%88%D9%81%D8%B1-%D8%A7%D9%84%D8%B9%D8%A7%D9%84%D9%8A-high-availability-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) ذكرنا أن السيرفر الثاني وحده لا يجعل الخدمة متوفرة، فالمستخدم يتصل بعنوان واحد، وإذا كان هذا العنوان هو عنوان السيرفر الأول فلن يصل إلى الثاني مهما كان جاهزاً. والحل البسيط أن تغير سجل DNS إلى عنوان السيرفر الثاني عند العطل، ولكن هذا الحل بطيء لأن المتصفحات والمزودين يحفظون العنوان القديم حتى تنتهي مدة الصلاحية TTL، ويحتاج شخصاً مستيقظاً يغيره. والحل الأنسب على شبكة واحدة هو عنوان IP عائم Floating IP، أي عنوان ثالث لا يملكه سيرفر بعينه، يحمله السيرفر الأساسي ما دام سليماً، وينتقل تلقائياً إلى الاحتياطي خلال ثوان إذا توقف، والتطبيق والمستخدم لا يعرفان أي سيرفر يرد عليهما.

والأداة الأشهر لذلك على Linux هي [Keepalived](https://www.keepalived.org/?ref=arabroot.io)، وهي مفتوحة المصدر بترخيص GPL الإصدار 2 أو أحدث، وتستخدم بروتوكول VRRP لتقرر أي سيرفر يحمل العنوان. وسوف نناقش في هذا المقال ما يلي:

- كيف يعمل بروتوكول VRRP، وما هي الأولوية Priority والإعلانات Adverts، وكيف يحسب السيرفر الاحتياطي متى يأخذ العنوان.
- الاستعادة التلقائية للدور Preemption ومتى توقفها بالخيار `nopreempt`.
- سكربت الفحص Track Script الذي يراقب nginx، وسكربت الإشعار Notify Script الذي يعمل عند كل تغيير.
- الإرسال المباشر Unicast بدلاً من Multicast، ولماذا تحتاجه في السحابة، وما هي مخاطره الأمنية.
- تجربة عملية حقيقية بسيرفرين في Docker، نوقف فيها nginx ثم نوقف السيرفر الأساسي فجأة بالأمر `docker kill` ونقيس زمن الانتقال من التوقيتات الفعلية، ثم نرى الرجوع مع `nopreempt` ودونه، ثم نصنع انقسام الدماغ Split Brain بأيدينا.
- حدود هذه الطريقة في السحابة، وكيف تنقل العنوان عبر واجهة المزود API بدلاً من ARP.

## كيف يعمل العنوان العائم مع VRRP؟

بروتوكول VRRP (Virtual Router Redundancy Protocol) صمم في الأصل لجعل بوابة الشبكة Gateway متكررة، فبدلاً من أن تشير كل الأجهزة إلى موجه واحد، تشير إلى عنوان افتراضي يحمله أحد الموجهات، وKeepalived تستخدم الفكرة نفسها لأي خدمة. والإصدار الثاني موصوف في [RFC 3768](https://www.rfc-editor.org/rfc/rfc3768.html?ref=arabroot.io)، والإصدار الثالث الذي يدعم IPv4 وIPv6 معاً كان في [RFC 5798](https://www.rfc-editor.org/rfc/rfc5798.html?ref=arabroot.io)، ثم حلت محله في أبريل 2024 الوثيقة [RFC 9568](https://www.rfc-editor.org/rfc/rfc9568.html?ref=arabroot.io) التي أصلحت أخطاء في وصف حالات البروتوكول، وغيرت تسمية الدور Master إلى Active Router، أما Keepalived فما زالت تستخدم كلمة MASTER في إعداداتها وسجلاتها، لذلك سوف نستخدمها في هذا المقال. والصورة التالية تبين الفكرة كما سوف نبنيها في التجربة:

![مخطط يبين كلاينت يتصل بالعنوان http://10.231.88.100 فيصل إلى السيرفر node1 بالعنوان 10.231.88.11 الذي يحمل العنوان العائم VIP 10.231.88.100 وحالته MASTER بأولوية 150، والسيرفر node2 بالعنوان 10.231.88.12 حالته BACKUP بأولوية 100، وبينهما سهم أزرق متقطع VRRP advert every 1 s بأولوية 150 عبر unicast وبروتوكول IP رقم 112، وعلى كل سيرفر nginx وkeepalived وسهم check every 1 s، وفي الأسفل أربع خطوات عندما يتوقف nginx على node1: يفشل الفحص مرتين فيدخل node1 حالة FAULT، ثم يحذف العنوان ويرسل أولوية 0، ثم يصبح node2 هو MASTER ويضيف العنوان، ثم يرسل gratuitous ARP لتعرف الشبكة عنوان MAC الجديد](https://arabroot.io/content/images/2026/10/keepalived-01-vip-vrrp.webp)

التطبيق يتصل دائماً بالعنوان العائم، وKeepalived تقرر أي سيرفر يحمله

والآلية كما في الوثيقة تتلخص في النقاط التالية:

- **الموجه الافتراضي Virtual Router:** مجموعة سيرفرات تشترك في رقم تعريف واحد VRID من 1 إلى 255، ويسمى في Keepalived `virtual_router_id`، ولكل مجموعة عنوان أو أكثر.
- **الإعلانات Advertisements:** السيرفر الذي يحمل العنوان يرسل كل ثانية افتراضياً رسالة تقول «أنا هنا وأولويتي كذا»، وتذهب الرسالة إلى عنوان Multicast هو `224.0.0.18` ببروتوكول IP رقم 112، ومع الخيار `unicast_peer` تذهب مباشرة إلى عناوين الزملاء كما سوف نفعل في التجربة.
- **الأولوية Priority:** رقم من 1 إلى 254 للسيرفرات الاحتياطية، والقيمة الافتراضية 100، والقيمة 255 محجوزة للسيرفر الذي يملك العنوان فعلاً، والقيمة 0 لها معنى خاص: السيرفر الأساسي يرسلها عندما يتنحى بإرادته، حتى يأخذ الاحتياطي الدور فوراً دون أن ينتظر انتهاء المهلة.
- **إعلان العنوان Gratuitous ARP:** عندما يأخذ سيرفر العنوان يرسل إلى الشبكة رسالة ARP دون أن يسأله أحد، تقول إن هذا العنوان أصبح عند عنوان MAC الخاص به، فتحدث الأجهزة والسويتشات جداولها. وتذكر الوثيقة أيضاً عنوان MAC افتراضياً من الشكل `00-00-5E-00-01-{VRID}`، ولكن Keepalived تستخدم عنوان MAC الحقيقي للسيرفر مع Gratuitous ARP افتراضياً، إلا إذا فعلت الخيار `use_vmac`.

ولاحظ أن Keepalived تستخدم الإصدار الثاني من VRRP افتراضياً لعناوين IPv4، والإصدار الثالث دائماً لعناوين IPv6، كما في [توثيق ملف الإعداد](https://www.keepalived.org/documentation/keepalived-conf/?ref=arabroot.io)، ويمكنك تغيير ذلك بالخيار `vrrp_version`.

### الأولوية ومتى يقرر الاحتياطي أن الأساسي توقف

السيرفر الاحتياطي لا يفحص الأساسي بنفسه، وإنما ينتظر الإعلانات، فإذا لم يصله إعلان خلال مدة تسمى في الوثيقة Master\_Down\_Interval أصبح هو الأساسي، وهذه المدة تحسب كما يلي:

```text
Skew_Time            = ((256 - Priority) * Advertisement_Interval) / 256
Master_Down_Interval = (3 * Advertisement_Interval) + Skew_Time
```

فمع إعلان كل ثانية تكون المدة لسيرفر أولويته 100 هي 3 ثوان مضافاً إليها 0.61 ثانية، أي 3.61 ثانية، ولسيرفر أولويته 150 هي 3.41 ثانية. والسبب في إضافة Skew\_Time أن السيرفر ذا الأولوية الأعلى ينتظر وقتاً أقل، فإذا كان هناك أكثر من سيرفر احتياطي فاز صاحب الأولوية الأعلى دون أن يتنافسوا. وسوف ترى هذه الأرقام نفسها في سجلات التجربة.

### الاستعادة التلقائية للدور Preemption

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

- `nopreempt`: السيرفر العائد يبقى احتياطياً، والعنوان يبقى حيث هو حتى يحدث عطل جديد، وشرط هذا الخيار أن تكون الحالة الأولى في الإعداد `state BACKUP` وليس `MASTER`.
- `preempt_delay`: السيرفر العائد يأخذ الدور ولكن بعد مهلة إضافية بالثواني، فيكون لديك وقت لتتأكد أنه مستقر.

### سكربت الفحص Track Script

VRRP وحده يعرف فقط هل السيرفر حي ويرسل الإعلانات، ولا يعرف هل nginx أو HAProxy يعمل عليه، فقد يتوقف nginx ويبقى السيرفر يحمل العنوان ويستقبل الطلبات ويرفضها. والحل هو `vrrp_script`، وهو سكربت تشغله Keepalived كل فترة، ويقرر نتيجته كما في [التوثيق](https://www.keepalived.org/documentation/keepalived-conf/?ref=arabroot.io):

- `interval`: كل كم ثانية يعمل السكربت.
- `fall` و`rise`: كم فشلاً متتالياً قبل أن يعتبر الفحص فاشلاً، وكم نجاحاً متتالياً قبل أن يعود ناجحاً.
- `weight`: إذا لم تحدده (القيمة الافتراضية 0) فالفشل ينقل السيرفر إلى حالة FAULT، فيترك العنوان ويرسل أولوية 0\. وإذا حددت قيمة سالبة فالفشل يطرحها من أولوية السيرفر فقط، فيأخذ الاحتياطي الدور إذا أصبحت أولويته أعلى، وإذا حددت قيمة موجبة فالنجاح يضيفها إلى الأولوية.

ونحن سوف نستخدم الطريقة الأولى في التجربة لأنها أوضح: إذا توقف nginx فلا يجب أن يحمل هذا السيرفر العنوان أصلاً.

### سكربت الإشعار Notify Script

تشغل Keepalived سكربتاً عند كل تغيير في الحالة إذا حددته بالخيار `notify`، وتضيف إليه أربعة معاملات: النوع `INSTANCE` أو `GROUP`، ثم اسم الموجه الافتراضي، ثم الحالة الجديدة `MASTER` أو `BACKUP` أو `FAULT` أو `STOP`، ثم الأولوية. وهناك أيضاً `notify_master` و`notify_backup` و`notify_fault` لكل حالة على حدة. ونحن سوف نستخدمه في التجربة لتسجيل وقت كل تغيير بالملي ثانية، وفي الإنتاج تستخدمه لإرسال تنبيه، أو لنقل العنوان عبر واجهة المزود في السحابة كما سوف نرى في آخر المقال.

### الإرسال المباشر Unicast وأمانه

الإعلانات تذهب افتراضياً إلى عنوان Multicast، وهذا يعمل في شبكتك الخاصة، ولكن كثيراً من الشبكات لا تمرر Multicast، مثل أغلب الشبكات السحابية وبعض شبكات ال Containers، لذلك توفر Keepalived الخيار `unicast_peer` لترسل الإعلانات مباشرة إلى عناوين الزملاء، مع `unicast_src_ip` لتحديد العنوان الذي ترسل منه. ولاحظ أمرين عند استخدامه:

- **لا توجد مصادقة في VRRP:** الإصدار الثالث لا يتضمن أي نوع من المصادقة، ويشرح قسم الأمان في [RFC 5798](https://www.rfc-editor.org/rfc/rfc5798.html?ref=arabroot.io) أن الأنواع القديمة لم تكن تحمي فعلاً، وأن الحماية الوحيدة هي أن الإعلان يرسل بقيمة TTL تساوي 255 ويرفض إذا وصل بقيمة أقل، أي أنه لم يعبر أي موجه. أما الخيار القديم `authentication` مع `auth_type PASS` في Keepalived فهو كلمة مرور مكتوبة كما هي داخل الرسالة، ويذكر التوثيق أن المصادقة حذفت من الإصدار الثاني نفسه في RFC 3768 سنة 2004.
- **Unicast يضعف هذه الحماية:** عندما تعبر الإعلانات عدة موجهات تصل بقيمة TTL أقل، فيضطر المستقبل إلى قبولها، وهنا يستطيع من يرسل رسالة مزورة أن يأخذ العنوان أو يجبر السيرفرات على انتخاب جديد، ولهذا السبب أضافت Keepalived في الإصدار 2.4.0 الصادر في يونيو 2026 امتداداً اسمه `auth_hmac` يوقع كل إعلان بمفتاح مشترك، ويوصي [مقال المطورين](https://www.keepalived.org/articles/vrrp-hmac-authentication/?ref=arabroot.io) بتفعيله في كل استخدام لل Unicast. وهو امتداد مبني على مسودة في IETF وليس جزءاً من الوثيقة الرسمية، ويحتاج الإصدار 2.4.0 أو أحدث على كل السيرفرات، بينما أغلب التوزيعات ما زالت تقدم 2.3.x، ومنها Alpine 3.23 الذي استخدمناه في التجربة. لذلك إذا لم يتوفر لديك فاجعل الإعلانات داخل شبكة خاصة لا يصل إليها غير السيرفرين، واسمح بالبروتوكول 112 في جدار الحماية من عنوان الزميل فقط.

## تجربة عملية: سيرفران وعنوان عائم في Docker

في هذه التجربة سوف نبني على جهاز واحد شبكة Docker خاصة فيها ثلاثة Containers: `lab-ha-node1` و`lab-ha-node2`، على كل منهما nginx وKeepalived، و`lab-ha-client` يسأل العنوان العائم كل 200 ملي ثانية ويطبع الوقت ومن رد عليه. ونحتاج الصلاحية `NET_ADMIN` حتى تستطيع Keepalived أن تضيف العنوان إلى بطاقة الشبكة وتحذفه، كما في [صفحة capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html?ref=arabroot.io) التي تذكر أن هذه الصلاحية تشمل إعداد بطاقات الشبكة وعناوينها. وكل الأوامر والمخرجات التالية حقيقية من تشغيلنا، أما الأزمنة فهي للتوضيح فقط، والسبب أنها من تشغيل واحد على جهاز واحد، وسوف تختلف قليلاً عندك.

### ملفات التجربة

أنشئ مجلداً فارغاً، واحفظ فيه الملف التالي باسم `Dockerfile`، وهو يبني Image واحدة للسيرفرين والكلاينت من nginx الرسمي بإصدار محدد، ويضيف إليها Keepalived وأدوات القياس:

```dockerfile
FROM nginx:1.29.8-alpine
RUN apk add --no-cache keepalived coreutils tcpdump iptables
COPY --chmod=755 check_nginx.sh notify.sh /etc/keepalived/
COPY --chmod=755 start.sh client.sh /
CMD ["/start.sh"]
```

ثم سكربت الفحص `check_nginx.sh`، وهو ينجح فقط إذا رد nginx على هذا السيرفر خلال ثانية:

```bash
#!/bin/sh
# exit 0 only if nginx on this server answers within one second
exec curl -fsS -o /dev/null --max-time 1 http://127.0.0.1/
```

ثم سكربت الإشعار `notify.sh`، وهو يسجل كل تغيير في الحالة مع الوقت بالملي ثانية:

```bash
#!/bin/sh
# keepalived adds 4 arguments: INSTANCE|GROUP, name, new state, priority
echo "$(date +%H:%M:%S.%3N) $(hostname) $2 -> $3 (priority $4)" >> /var/log/keepalived-notify.log
```

ثم `start.sh`، وهو يكتب ملف `keepalived.conf` من متغيرات البيئة حتى نستخدم Image واحدة للسيرفرين، ثم يشغل nginx ثم Keepalived في المقدمة:

```bash
#!/bin/sh
# writes keepalived.conf from the environment, starts nginx, then keepalived in the foreground
set -e
echo "served by $(hostname)" > /usr/share/nginx/html/index.html
cat > /etc/keepalived/keepalived.conf <<CONF
global_defs {
    router_id $(hostname)
    script_user root
    enable_script_security
}

vrrp_script chk_nginx {
    script "/etc/keepalived/check_nginx.sh"
    interval 1
    fall 2
    rise 2
}

vrrp_instance VI_WEB {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority $PRIORITY
    advert_int 1
    #PREEMPT#
    unicast_src_ip $MY_IP
    unicast_peer {
        $PEER_IP
    }
    virtual_ipaddress {
        10.231.88.100/24 dev eth0
    }
    track_script {
        chk_nginx
    }
    notify /etc/keepalived/notify.sh
}
CONF
if [ "$NOPREEMPT" = "1" ]; then
    sed -i "s/#PREEMPT#/nopreempt/" /etc/keepalived/keepalived.conf
else
    sed -i "/#PREEMPT#/d" /etc/keepalived/keepalived.conf
fi
nginx
exec keepalived --dont-fork --log-console --log-detail
```

ثم سكربت الكلاينت `client.sh`:

```bash
#!/bin/sh
# asks the virtual IP every 200 ms and prints the time and who answered
while true; do
  r=$(curl -s --max-time 0.5 http://10.231.88.100/ || echo "NO ANSWER")
  echo "$(date +%H:%M:%S.%3N) $r"
  sleep 0.2
done
```

وأخيراً `up.sh`، وهو يشغل السيرفرين والكلاينت، ويقبل الكلمة `nopreempt` لنستخدمها لاحقاً:

```bash
# usage: up.sh [nopreempt]  starts node1 (priority 150), node2 (priority 100) and the client
NP=0; [ "$1" = "nopreempt" ] && NP=1
for n in 1 2; do
  if [ $n = 1 ]; then me=10.231.88.11; peer=10.231.88.12; prio=150; else me=10.231.88.12; peer=10.231.88.11; prio=100; fi
  docker run -d --name lab-ha-node$n --hostname node$n --network lab-ha-net --ip $me \
    --cap-add NET_ADMIN -e MY_IP=$me -e PEER_IP=$peer -e PRIORITY=$prio -e NOPREEMPT=$NP \
    lab-ha-keepalived:1
done
docker run -d --name lab-ha-client --hostname client --network lab-ha-net --ip 10.231.88.50 \
  lab-ha-keepalived:1 /client.sh
```

في الملفات أعلاه لاحظ التالي:

- الحالة الأولى في الإعداد `state BACKUP` على السيرفرين، والأولوية هي التي تقرر من يصبح الأساسي، وهذا شرط حتى يعمل `nopreempt` لاحقاً.
- `virtual_router_id 51` يجب أن يكون واحداً على السيرفرين، ومختلفاً عن أي مجموعة VRRP أخرى على الشبكة نفسها، وسوف نرى ماذا يحدث عندما يشترك فيه سيرفر غريب.
- `fall 2` مع `interval 1` تعني أن nginx يجب أن يفشل في فحصين متتاليين، أي حوالي ثانيتين، قبل أن يترك السيرفر العنوان.
- `enable_script_security` يمنع Keepalived من تشغيل سكربت يستطيع مستخدم عادي تعديله، و`script_user root` يحدد المستخدم، لأن المستخدم الافتراضي `keepalived_script` غير موجود في هذه Image، لذلك نسخنا السكربتات داخل ال Image بصلاحيات 755 بدلاً من ربطها من الجهاز.
- Keepalived وnginx يعملان في Container واحد هنا لتبسيط التجربة، أما على سيرفر حقيقي فتثبتها من حزمة التوزيعة وتشغلها كخدمة systemd بجانب nginx أو HAProxy.

### التشغيل: من يحمل العنوان؟

ننشئ الشبكة ونبني ال Image ونشغل كل شيء:

```bash
docker network create --subnet 10.231.88.0/24 lab-ha-net
docker build -t lab-ha-keepalived:1 .
sh up.sh
docker exec lab-ha-node1 keepalived --version
```

ثم ننظر إلى الإعداد الذي كتبه `start.sh` على السيرفر الأول:

```bash
docker exec lab-ha-node1 cat /etc/keepalived/keepalived.conf
```

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

```text
global_defs {
    router_id node1
    script_user root
    enable_script_security
}

vrrp_script chk_nginx {
    script "/etc/keepalived/check_nginx.sh"
    interval 1
    fall 2
    rise 2
}

vrrp_instance VI_WEB {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 150
    advert_int 1
    unicast_src_ip 10.231.88.11
    unicast_peer {
        10.231.88.12
    }
    virtual_ipaddress {
        10.231.88.100/24 dev eth0
    }
    track_script {
        chk_nginx
    }
    notify /etc/keepalived/notify.sh
}
```

ثم سجلات Keepalived على السيرفر الأول، دون سطور nginx:

```bash
docker logs lab-ha-node1 2>&1 | grep -v '"GET\|\[notice\]'
```

والمخرج سوف يكون كما يلي (مختصراً):

```text
Fri Oct  9 00:09:00 2026: Starting Keepalived v2.3.4 (06/10,2025)
Fri Oct  9 00:09:00 2026: WARNING - keepalived was built for newer Linux 6.16.12, running on Linux 5.15.167.4-microsoft-standard-WSL2 #1 SMP Tue Nov 5 00:21:55 UTC 2024
Fri Oct  9 00:09:00 2026: Command line: 'keepalived' '--dont-fork' '--log-console' '--log-detail'
Fri Oct  9 00:09:00 2026: Opening file '/etc/keepalived/keepalived.conf'.
...
Fri Oct  9 00:09:00 2026: VRRP sockpool: [ifindex(  2), family(IPv4), proto(112), fd(13,14) , unicast, address(10.231.88.11)]
Fri Oct  9 00:09:00 2026: VRRP_Script(chk_nginx) succeeded
Fri Oct  9 00:09:00 2026: (VI_WEB) Entering BACKUP STATE
Fri Oct  9 00:09:00 2026: VI_WEB: sending gratuitous ARP for 10.231.88.11
Fri Oct  9 00:09:00 2026: Sending gratuitous ARP on eth0 for 10.231.88.11
Fri Oct  9 00:09:04 2026: (VI_WEB) Receive advertisement timeout
Fri Oct  9 00:09:04 2026: (VI_WEB) Entering MASTER STATE
Fri Oct  9 00:09:04 2026: (VI_WEB) setting VIPs.
Fri Oct  9 00:09:04 2026: (VI_WEB) Sending/queueing gratuitous ARPs on eth0 for 10.231.88.100
Fri Oct  9 00:09:04 2026: Sending gratuitous ARP on eth0 for 10.231.88.100
...
Fri Oct  9 00:09:09 2026: (VI_WEB) Sending/queueing gratuitous ARPs on eth0 for 10.231.88.100
```

وسجل الإشعارات على السيرفرين:

```bash
docker exec lab-ha-node1 cat /var/log/keepalived-notify.log
docker exec lab-ha-node2 cat /var/log/keepalived-notify.log
```

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

```text
00:09:00.886 node1 VI_WEB -> BACKUP (priority 150)
00:09:04.293 node1 VI_WEB -> MASTER (priority 150)
00:09:06.666 node2 VI_WEB -> BACKUP (priority 100)
```

في المخرج أعلاه لاحظ التالي:

- الإصدار هو 2.3.4 من حزم Alpine 3.23، وآخر إصدار من المشروع وقت كتابة المقال هو 2.4.3 الصادر في يوليو 2026 كما في [ملاحظات الإصدار](https://www.keepalived.org/release-notes/Release-2.4.3/?ref=arabroot.io)، والتحذير عن نسخة Linux سببه أن الحزمة بنيت على نواة أحدث من نواة WSL2 التي يعمل عليها Docker Desktop، ولا يؤثر على التجربة.
- السيرفر الأول بدأ في حالة BACKUP، ولم يصله أي إعلان لأن الثاني لم يكن قد بدأ بعد، فأصبح MASTER بعد 3.41 ثانية تقريباً (من 00:09:00.886 إلى 00:09:04.293)، وهي بالضبط المدة التي حسبناها لأولوية 150.
- بعد أن أخذ العنوان أرسل خمس رسائل Gratuitous ARP، ثم خمساً أخرى بعد خمس ثوان، وهذه هي القيم الافتراضية للخيارين `vrrp_garp_master_repeat` و`vrrp_garp_master_delay`.
- السيرفر الثاني بدأ بعده ودخل حالة BACKUP لأنه وجد سيرفراً أولويته أعلى يرسل الإعلانات.

الآن نتأكد أين العنوان، ونرى الإعلانات نفسها على الشبكة بالأداة tcpdump من السيرفر الثاني:

```bash
docker exec lab-ha-node1 ip -4 addr show eth0
docker exec lab-ha-node2 ip -4 addr show eth0
docker exec lab-ha-node2 timeout 4 tcpdump -n -i eth0 -c 3 vrrp
```

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

```text
2: eth0@if2950: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    inet 10.231.88.11/24 brd 10.231.88.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 10.231.88.100/24 scope global secondary eth0
       valid_lft forever preferred_lft forever
2: eth0@if2951: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    inet 10.231.88.12/24 brd 10.231.88.255 scope global eth0
       valid_lft forever preferred_lft forever
00:10:18.572233 IP 10.231.88.11 > 10.231.88.12: VRRPv2, Advertisement, vrid 51, prio 150, authtype none, intvl 1s, length 20
00:10:19.572506 IP 10.231.88.11 > 10.231.88.12: VRRPv2, Advertisement, vrid 51, prio 150, authtype none, intvl 1s, length 20
00:10:20.572766 IP 10.231.88.11 > 10.231.88.12: VRRPv2, Advertisement, vrid 51, prio 150, authtype none, intvl 1s, length 20
```

والعنوان `10.231.88.100` موجود على السيرفر الأول فقط كعنوان ثانوي secondary، والإعلانات تذهب من الأول إلى الثاني مباشرة كل ثانية بالضبط، بالإصدار الثاني من VRRP ودون مصادقة كما ذكرنا. وأخيراً سجل الكلاينت:

```bash
docker logs lab-ha-client 2>&1 | tail -3
```

```text
00:10:29.683 served by node1
00:10:29.920 served by node1
00:10:30.171 served by node1
```

### نوقف nginx على السيرفر الأساسي

الآن نوقف nginx على السيرفر الأول دون أن نوقف السيرفر نفسه، وهو العطل الذي لا يراه VRRP وحده، ونطبع الوقت قبل الإيقاف مباشرة. ولاحظ أن الأزمنة من هنا حتى نهاية التجربة من تشغيلات سابقة لنفس الملفات، لذلك تختلف الساعات وعناوين MAC عن القسم السابق:

```bash
docker exec lab-ha-node1 sh -c 'echo "nginx stopped at $(date +%H:%M:%S.%3N)"; nginx -s stop'
docker logs lab-ha-client
```

والمخرج سوف يكون كما يلي (الجزء المهم من سجل الكلاينت):

```text
nginx stopped at 23:15:18.866
2026/10/08 23:15:18 [notice] 57#57: signal process started
...
23:15:18.462 served by node1
23:15:18.674 served by node1
23:15:19.015 NO ANSWER
23:15:19.228 NO ANSWER
23:15:19.442 NO ANSWER
23:15:19.653 NO ANSWER
23:15:19.875 NO ANSWER
23:15:20.087 NO ANSWER
23:15:20.297 NO ANSWER
23:15:20.513 NO ANSWER
23:15:21.231 NO ANSWER
23:15:21.450 served by node2
23:15:21.671 served by node2
```

ثم سجلات الإشعار وسجلات Keepalived على السيرفرين لنعرف ماذا حدث في كل لحظة:

```bash
docker exec lab-ha-node1 cat /var/log/keepalived-notify.log
docker exec lab-ha-node2 cat /var/log/keepalived-notify.log
docker logs lab-ha-node1 2>&1 | grep -v '"GET\|\[notice\]' | tail -5
docker logs lab-ha-node2 2>&1 | grep -E 'priority 0|timeout|MASTER STATE|setting VIPs'
```

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

```text
23:14:56.712 node1 VI_WEB -> BACKUP (priority 150)
23:15:00.119 node1 VI_WEB -> MASTER (priority 150)
23:15:20.723 node1 VI_WEB -> FAULT (priority 150)
23:14:58.181 node2 VI_WEB -> BACKUP (priority 100)
23:15:21.340 node2 VI_WEB -> MASTER (priority 100)
Thu Oct  8 23:15:19 2026: Script `chk_nginx` now returning 7
Thu Oct  8 23:15:20 2026: VRRP_Script(chk_nginx) failed (exited with status 7)
Thu Oct  8 23:15:20 2026: (VI_WEB) Entering FAULT STATE
Thu Oct  8 23:15:20 2026: (VI_WEB) sent 0 priority
Thu Oct  8 23:15:20 2026: (VI_WEB) removing VIPs.
Thu Oct  8 23:15:20 2026: (VI_WEB) Backup received priority 0 advertisement
Thu Oct  8 23:15:20 2026: (VI_WEB) Backup received priority 0 advertisement
Thu Oct  8 23:15:21 2026: (VI_WEB) Receive advertisement timeout
Thu Oct  8 23:15:21 2026: (VI_WEB) Entering MASTER STATE
Thu Oct  8 23:15:21 2026: (VI_WEB) setting VIPs.
```

والصورة التالية تبين هذا التسلسل على خط زمني، مع التجربتين التاليتين:

![ثلاثة خطوط زمنية من 0 إلى 5 ثوان: الأول NGINX STOPS ON NODE1 حيث توقف nginx عند 0 ثم دخل node1 حالة FAULT وأرسل أولوية 0 عند 1.86 ثانية ثم أصبح node2 هو MASTER عند 2.47 ثانية وجاء أول رد عند 2.58 ثانية ومنطقة حمراء فيها 9 طلبات دون رد، والثاني NODE1 KILLED حيث أوقف ال Container فجأة بالأمر docker kill عند 0 وأصبح node2 هو MASTER عند 2.55 ثانية أي 3 ضرب 1 ثانية زائد 0.61 ثانية بعد آخر إعلان وجاء أول رد عند 3.07 ثانية مع 5 طلبات دون رد، والثالث NODE1 COMES BACK حيث بدأ node1 عند 0 ودخل BACKUP عند 0.86 ثانية ثم أخذ العنوان عند 4.28 ثانية لأن preempt هو الافتراضي، ومع nopreempt يبقى node1 احتياطياً والحركة على node2](https://arabroot.io/content/images/2026/10/keepalived-02-failover-timeline.webp)

الأزمنة من تشغيل واحد على جهاز واحد في Docker، وهي للتوضيح فقط، والكلاينت يسأل كل 200 ملي ثانية

في المخرج أعلاه لاحظ التالي:

- توقف nginx عند 18.866، وفشل الفحص أول مرة عند الثانية 19 تقريباً بالرمز 7، وهو رمز curl عندما لا يستطيع الاتصال، ثم فشل مرة ثانية فدخل السيرفر الأول حالة FAULT عند 20.723، أي بعد 1.86 ثانية، وهذا هو أثر `fall 2` مع `interval 1`.
- عند دخوله FAULT أرسل أولوية 0 وحذف العنوان، فلم ينتظر السيرفر الثاني 3.61 ثانية، وإنما أصبح MASTER عند 21.340، أي بعد 0.62 ثانية تقريباً، وهي قيمة Skew\_Time لأولوية 100، لأن الوثيقة تجعل الاحتياطي ينتظر هذه المدة فقط بعد استلام أولوية 0.
- أول رد من السيرفر الثاني وصل عند 21.450، أي أن الخدمة توقفت 2.58 ثانية من جهة المستخدم، وفشلت تسعة طلبات، ولاحظ أن السطر 21.231 جاء بعد فجوة أطول لأن الطلب انتظر المهلة الكاملة 0.5 ثانية.
- الكلاينت لم يغير أي شيء، فقد حدث جدول ARP عنده تلقائياً من رسائل Gratuitous ARP، فقبل الانتقال كان العنوان العائم في جدوله عند `62:21:64:a1:ed:0e` وهو عنوان MAC للسيرفر الأول، وبعده أصبح عند `22:19:cb:af:2d:a6` وهو عنوان الثاني، وهذا ما يعطيه الأمر `ip neigh show 10.231.88.100` على الكلاينت.

وقد يتساءل البعض: هل نستطيع أن نقلل هذا الزمن؟ والإجابة نعم، بتقليل `interval` و`fall`، وبتقليل `advert_int` إلى أجزاء من الثانية، وهذا يحتاج الإصدار الثالث من البروتوكول لأن الإصدار الثاني في RFC 3768 يحسب الفترة بالثواني الكاملة، ولكن الثمن أن لحظة بطء واحدة في nginx أو في الشبكة تسبب انتقالاً كاملاً لا داعي له، والانتقال الخاطئ نفسه يقطع الاتصالات، لذلك ابدأ بالقيم المعقولة وقسها في بيئتك.

### الرجوع: السيرفر الأساسي يأخذ العنوان مرة أخرى

نعيد تشغيل nginx على السيرفر الأول، ونترك الإعداد كما هو، أي أن Preemption مفعل افتراضياً:

```bash
docker exec lab-ha-node1 sh -c 'echo "nginx started at $(date +%H:%M:%S.%3N)"; nginx'
docker exec lab-ha-node1 tail -2 /var/log/keepalived-notify.log
docker exec lab-ha-node2 tail -1 /var/log/keepalived-notify.log
docker logs lab-ha-node1 2>&1 | grep VI_WEB | grep -E 'discarding|timeout|MASTER STATE'
docker logs lab-ha-node2 2>&1 | grep VI_WEB | grep -E 'higher priority|BACKUP STATE' | tail -2
docker logs lab-ha-client
```

والمخرج سوف يكون كما يلي، مختصراً على السطور الخاصة بهذا الانتقال:

```text
nginx started at 23:16:24.385
23:16:25.734 node1 VI_WEB -> BACKUP (priority 150)
23:16:29.157 node1 VI_WEB -> MASTER (priority 150)
23:16:29.149 node2 VI_WEB -> BACKUP (priority 100)
Thu Oct  8 23:16:28 2026: (VI_WEB) received lower priority (100) advert from 10.231.88.12 - discarding
Thu Oct  8 23:16:29 2026: (VI_WEB) Receive advertisement timeout
Thu Oct  8 23:16:29 2026: (VI_WEB) Entering MASTER STATE
Thu Oct  8 23:16:29 2026: (VI_WEB) Master received advert from 10.231.88.11 with higher priority 150, ours 100
Thu Oct  8 23:16:29 2026: (VI_WEB) Entering BACKUP STATE
23:16:28.839 served by node2
23:16:29.049 served by node2
23:16:29.260 served by node1
23:16:29.489 served by node1
```

بعد فحصين ناجحين خرج السيرفر الأول من FAULT إلى BACKUP عند 25.734، ثم تجاهل إعلانات الثاني لأن أولويتها أقل من أولويته (`discarding`)، فانتهت المهلة بعد 3.42 ثانية وأخذ العنوان عند 29.157، والثاني رأى إعلاناً أولويته أعلى فتنحى. والكلاينت لم يفقد أي طلب هنا لأن الانتقال المنظم أسرع من 200 ملي ثانية، ولكن لاحظ أن الخدمة انتقلت مرتين لعطل واحد، وكل اتصال طويل كان مفتوحاً على السيرفر الثاني، مثل WebSocket أو رفع ملف كبير، انقطع في الانتقال الثاني دون أي سبب.

والأمر نفسه عندما يتوقف السيرفر بالكامل ثم يعود، فقد أوقفنا ال Container الأول فجأة بالأمر `docker kill`، وهو يشبه انقطاع الكهرباء عن السيرفر، ثم شغلناه مرة أخرى:

```bash
docker kill lab-ha-node1
docker inspect lab-ha-node1 --format '{{.State.FinishedAt}}'
docker logs lab-ha-client
```

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

```text
2026-10-08T23:17:11.236342209Z
23:17:10.748 served by node1
23:17:10.970 served by node1
23:17:11.237 NO ANSWER
23:17:11.949 NO ANSWER
23:17:12.662 NO ANSWER
23:17:13.375 NO ANSWER
23:17:14.095 NO ANSWER
23:17:14.307 served by node2
```

وسجل الإشعار على السيرفر الثاني يقول إنه أصبح MASTER عند `23:17:13.787`. والفرق هنا أن السيرفر الأول لم يستطع أن يرسل أولوية 0 لأنه توقف فجأة، فانتظر الثاني المهلة الكاملة 3.61 ثانية من آخر إعلان وصله، وآخر إعلان وصل قبل لحظة الإيقاف بحوالي ثانية لأن الإعلانات كل ثانية، لذلك أصبح MASTER بعد 2.55 ثانية من لحظة الإيقاف، ووصل أول رد بعد 3.07 ثانية. ثم شغلنا السيرفر الأول مرة أخرى بالأمر `docker start lab-ha-node1`، فدخل BACKUP بعد 0.86 ثانية من بدايته، ثم أخذ العنوان بعد 4.28 ثانية من بدايته، وهو الانتقال الثاني نفسه الذي رأيناه أعلاه.

### الرجوع مع nopreempt: العنوان يبقى حيث هو

نحذف ال Containers الثلاثة ونشغلها مرة أخرى مع `nopreempt`، فيضيف `start.sh` السطر إلى الإعداد:

```bash
docker rm -f lab-ha-node1 lab-ha-node2 lab-ha-client
sh up.sh nopreempt
docker exec lab-ha-node1 grep -n -A1 'advert_int' /etc/keepalived/keepalived.conf
```

```text
19:    advert_int 1
20-    nopreempt
```

ثم نوقف nginx على السيرفر الأول، وننتظر، ثم نشغله مرة أخرى:

```bash
docker exec lab-ha-node1 sh -c 'echo "nginx stopped at $(date +%H:%M:%S.%3N)"; nginx -s stop'
docker exec lab-ha-node1 sh -c 'echo "nginx started at $(date +%H:%M:%S.%3N)"; nginx'
docker exec lab-ha-node1 cat /var/log/keepalived-notify.log
docker exec lab-ha-node2 cat /var/log/keepalived-notify.log
```

والمخرج سوف يكون كما يلي، ومعه السطور التي تغير فيها من يرد في سجل الكلاينت:

```text
nginx stopped at 23:18:45.400
nginx started at 23:18:51.981
23:18:20.599 node1 VI_WEB -> BACKUP (priority 150)
23:18:24.014 node1 VI_WEB -> MASTER (priority 150)
23:18:46.613 node1 VI_WEB -> FAULT (priority 150)
23:18:53.608 node1 VI_WEB -> BACKUP (priority 150)
23:18:23.879 node2 VI_WEB -> BACKUP (priority 100)
23:18:47.217 node2 VI_WEB -> MASTER (priority 100)
23:18:45.602 NO ANSWER
...
23:18:47.170 NO ANSWER
23:18:47.383 served by node2
```

هذه المرة توقفت الخدمة 1.98 ثانية، والفرق عن المرة الأولى سببه موعد الفحص بالنسبة للحظة الإيقاف، والأهم أن السيرفر الأول عاد إلى BACKUP عند 53.608 وبقي احتياطياً، وسجله يقول `master set to 10.231.88.12`، والعنوان بقي على الثاني، فلا يوجد انتقال ثان. وهذا هو الإعداد الأنسب في أغلب الحالات، والسبب أن السيرفرين متساويان في القدرة عادة، فلا فائدة من إعادة العنوان إلى الأول، وإذا أردت إعادته فافعل ذلك يدوياً في وقت هادئ.

### انقسام الدماغ: نقطع الإعلانات بين السيرفرين

الآن نصنع العطل الذي شرحناه في [مفاهيم التوفر العالي](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D8%AA%D9%88%D9%81%D8%B1-%D8%A7%D9%84%D8%B9%D8%A7%D9%84%D9%8A-high-availability-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#split-brain): السيرفران يعملان، ولكن الإعلانات لا تصل بينهما، كما يحدث عندما يمنعها جدار حماية بعد تعديل خاطئ. وفي المحاولة الأولى كتبنا اسم البروتوكول:

```bash
docker exec lab-ha-node1 iptables -A INPUT -p vrrp -j DROP
```

```text
iptables v1.8.11 (nf_tables): unknown protocol "vrrp" specified
Try `iptables -h' or 'iptables --help' for more information.
```

والسبب أن هذه Image لا تحتوي على اسم vrrp في ملف أسماء البروتوكولات، لذلك نستخدم رقمه 112\. ونبدأ بقطع الإعلانات في اتجاه واحد، أي أن السيرفر الأول لا يستقبلها، وكان العنوان وقتها على السيرفر الثاني بسبب `nopreempt`:

```bash
docker exec lab-ha-node1 iptables -A INPUT -p 112 -j DROP
docker exec lab-ha-node1 tail -1 /var/log/keepalived-notify.log
docker exec lab-ha-node2 tail -1 /var/log/keepalived-notify.log
```

```text
23:19:51.687 node1 VI_WEB -> MASTER (priority 150)
23:19:51.687 node2 VI_WEB -> BACKUP (priority 100)
```

وهذه نتيجة مهمة: السيرفر الأول لم يعد يسمع الثاني فأصبح MASTER، وبدأ يرسل إعلانات بأولوية 150، والثاني ما زال يسمعه فتنحى له، أي أن قطع الاتجاه الواحد سبب انتقالاً لا داعي له ولكنه لم يسبب انقساماً. والآن نقطع الاتجاه الآخر أيضاً:

```bash
docker exec lab-ha-node2 iptables -A INPUT -p 112 -j DROP
docker exec lab-ha-node1 ip -4 addr show eth0 | grep inet
docker exec lab-ha-node2 ip -4 addr show eth0 | grep inet
docker exec lab-ha-node2 tail -1 /var/log/keepalived-notify.log
docker exec lab-ha-client sh -c 'echo "$(date +%H:%M:%S.%3N) $(ip neigh show 10.231.88.100 | cut -d" " -f5) $(curl -s --max-time 0.5 http://10.231.88.100/)"'
```

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

```text
    inet 10.231.88.11/24 brd 10.231.88.255 scope global eth0
    inet 10.231.88.100/24 scope global secondary eth0
    inet 10.231.88.12/24 brd 10.231.88.255 scope global eth0
    inet 10.231.88.100/24 scope global secondary eth0
23:20:30.327 node2 VI_WEB -> MASTER (priority 100)
23:20:37.139 c2:43:fe:6e:fe:19 served by node2
```

هذا هو انقسام الدماغ: السيرفران يحملان العنوان نفسه في الوقت نفسه، والكلاينت يذهب إلى السيرفر الذي أرسل آخر Gratuitous ARP، وهو الثاني هنا، وعلى شبكة فيها عدة أجهزة قد يذهب بعضها إلى الأول وبعضها إلى الثاني. وفي موزع حمل لا يحمل بيانات فالضرر محدود، أما إذا كان خلف العنوان قاعدة بيانات تقبل الكتابة على السيرفرين فالبيانات تنقسم. ثم نزيل القاعدتين ونرى كيف تحل Keepalived الموقف:

```bash
docker exec lab-ha-node1 iptables -D INPUT -p 112 -j DROP
docker exec lab-ha-node2 iptables -D INPUT -p 112 -j DROP
docker logs lab-ha-node2 2>&1 | grep VI_WEB | tail -3
docker logs lab-ha-node1 2>&1 | grep VI_WEB | tail -3
```

```text
Thu Oct  8 23:20:57 2026: (VI_WEB) Master received advert from 10.231.88.11 with higher priority 150, ours 100
Thu Oct  8 23:20:57 2026: (VI_WEB) Entering BACKUP STATE
Thu Oct  8 23:20:57 2026: (VI_WEB) removing VIPs.
Thu Oct  8 23:20:57 2026: (VI_WEB) Received advert from 10.231.88.12 with lower priority 100, ours 150, forcing new election
Thu Oct  8 23:20:57 2026: (VI_WEB) Sending/queueing gratuitous ARPs on eth0 for 10.231.88.100
Thu Oct  8 23:21:02 2026: (VI_WEB) Sending/queueing gratuitous ARPs on eth0 for 10.231.88.100
```

عندما عادت الإعلانات رأى كل سيرفر الآخر، فتنحى صاحب الأولوية الأقل، وأرسل صاحب الأولوية الأعلى Gratuitous ARP من جديد حتى تعرف الشبكة أين العنوان، وعاد الكلاينت إلى السيرفر الأول. ولاحظ أن Keepalived حلت الموقف بعد أن عادت الشبكة فقط، أما خلال الانقطاع فلا يوجد عندها أي طريقة لمنعه، لأن سيرفرين وحدهما لا يملكان نصاباً.

### خطأ شائع: سيرفر ثالث على نفس virtual\_router\_id

الآن نكسر الإعداد عن قصد بطريقة تحدث فعلاً عندما ينسخ أحد الفريق ملف الإعداد إلى سيرفر جديد للتجربة: نشغل Container ثالثاً بالإعدادات نفسها، أي نفس `virtual_router_id` ونفس العنوان العائم، ولكن دون الصلاحية `NET_ADMIN`، وزميله المحدد عنوان غير موجود:

```bash
docker run -d --name lab-ha-nocap --hostname nocap --network lab-ha-net --ip 10.231.88.13 \
  -e MY_IP=10.231.88.13 -e PEER_IP=10.231.88.99 -e PRIORITY=50 lab-ha-keepalived:1
docker logs lab-ha-nocap 2>&1 | grep -E 'Netlink|MASTER|setting VIPs'
docker exec lab-ha-client sh -c 'echo "$(date +%H:%M:%S.%3N) $(ip neigh show 10.231.88.100)"'
```

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

```text
Thu Oct  8 23:24:22 2026: Netlink: error: Operation not permitted(1), type=RTM_DELADDR(21), seq=1791501866, pid=0
Thu Oct  8 23:24:22 2026: Netlink: error: Operation not permitted(1), type=RTM_DELADDR(21), seq=1791501867, pid=0
Thu Oct  8 23:24:25 2026: (VI_WEB) Entering MASTER STATE
Thu Oct  8 23:24:25 2026: (VI_WEB) setting VIPs.
Thu Oct  8 23:24:25 2026: Netlink: error: Operation not permitted(1), type=RTM_NEWADDR(20), seq=1791501868, pid=0
23:24:45.586 10.231.88.100 dev eth0 lladdr 9e:2c:c8:fb:f9:fc ref 1 used 0/0/0 probes 4 REACHABLE
```

السيرفر الثالث لم يسمع أي إعلان لأنه يرسل إلى عنوان غير موجود والسيرفران لا يرسلان إليه، فأصبح MASTER، ولم يستطع أن يضيف العنوان إلى بطاقته بسبب غياب الصلاحية (`Operation not permitted`)، ولكنه أرسل Gratuitous ARP للعنوان العائم رغم ذلك، فأصبح العنوان في جدول الكلاينت عند `9e:2c:c8:fb:f9:fc`، وهو عنوان MAC للسيرفر الثالث الذي لا يملك العنوان أصلاً. وسجل الكلاينت بين أن الطلبات بدأت تفشل عند 23:24:31، وعادت ثم فشلت مرة أخرى، وبعد أن حذفنا السيرفر الثالث عند 23:24:46 بقيت الطلبات تفشل حتى 23:25:38، أي 52 ثانية تقريباً حتى انتهت صلاحية السطر القديم في جدول ARP عند الكلاينت وسأل الشبكة من جديد، وكان مجموع الطلبات التي لم ترد 88 طلباً. والدرس أن رقم `virtual_router_id` يجب أن يكون فريداً في كل شبكة، وأن تراقب سجلات Keepalived بحثاً عن أخطاء Netlink، وأن تنبه إذا وجد العنوان العائم على أكثر من سيرفر أو لم يوجد على أي سيرفر.

### حذف المختبر

نحذف ما أنشأناه بالأسماء فقط:

```bash
docker rm -f lab-ha-node1 lab-ha-node2 lab-ha-client
docker network rm lab-ha-net
docker rmi lab-ha-keepalived:1
```

```text
lab-ha-node1
lab-ha-node2
lab-ha-client
lab-ha-net
Untagged: lab-ha-keepalived:1
```

ولا تستخدم `docker system prune` أو `docker network prune` لتنظيف المختبر، والسبب أنها تحذف أشياء لمشاريع أخرى على الجهاز نفسه.

## العنوان العائم في السحابة

كل ما رأيته في التجربة يعتمد على شيء واحد: أن الشبكة تقبل Gratuitous ARP وتحدث جداولها، وهذا صحيح في شبكتك الخاصة وفي أغلب مراكز البيانات التقليدية، ولكنه غير صحيح في أغلب الشبكات السحابية، والصورة التالية تبين الفرق:

![مخطط من جزأين: في الجزء الأيسر YOUR OWN NETWORK LAYER 2 سويتش وسيرفران node1 وnode2 والسيرفر الثاني الذي أصبح MASTER يرسل gratuitous ARP إلى السويتش يقول إن العنوان عند عنوان MAC الخاص به، فيحدث السويتش جدوله فوراً ويتم الانتقال خلال ثوان، وفي الجزء الأيمن CLOUD NETWORK VPC شبكة المزود على شكل سحابة وسيرفران vm1 وvm2 والسيرفر الثاني يرسل ARP فتتجاهله الشبكة وعليه علامة X حمراء، فيشغل سكربت notify يطلب من provider API أن يعطيه العنوان العائم، فتنقله الشبكة إليه، والخطوات: VRRP عبر unicast يقرر من هو MASTER ثم سكربت الإشعار يطلب من المزود نقل العنوان](https://arabroot.io/content/images/2026/10/keepalived-03-cloud.webp)

كل مزود له اسم مختلف للعنوان: Floating IP في Hetzner وReserved IP في DigitalOcean وSecondary IP في AWS

- **Google Cloud:** يذكر دليل [أنماط العناوين العائمة](https://cloud.google.com/architecture/best-practices-floating-ip-addresses?ref=arabroot.io) أن شبكة VPC تعالج طلبات ARP داخل الشبكة البرمجية، وتتجاهل رسائل Gratuitous ARP، ويقترح بدائل منها موزع الحمل الداخلي والمسارات Routes، ومنها أنماط تستخدم Keepalived لاتخاذ القرار فقط.
- **Azure:** تجيب [الأسئلة الشائعة للشبكات الافتراضية](https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq?ref=arabroot.io) بأن Multicast وBroadcast غير مدعومين، لذلك لا تعمل Keepalived هناك بإعدادها الافتراضي، وتحتاج Unicast على الأقل مع طريقة أخرى لنقل العنوان.
- **AWS:** يذكر [توثيق العناوين المتعددة](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/MultipleIP.html?ref=arabroot.io) أن العنوان الخاص الثانوي Secondary Private IP يمكن نقله من سيرفر إلى آخر لتحويل الحركة إلى سيرفر احتياطي، وذلك عبر الواجهة [AssignPrivateIpAddresses](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API%5FAssignPrivateIpAddresses.html?ref=arabroot.io) مع الخيار `AllowReassignment`، وإذا كان معه عنوان عام Elastic IP فإنه ينتقل معه.
- **Hetzner Cloud:** العنوان العائم [Floating IP](https://docs.hetzner.com/cloud/floating-ips/overview/?ref=arabroot.io) تنقله بين السيرفرات في المنطقة الشبكية نفسها في أي وقت عبر الواجهة أو الأداة `hcloud`، ويجب أن تضيفه أيضاً إلى بطاقة الشبكة في السيرفر.
- **DigitalOcean:** العنوان المحجوز [Reserved IP](https://docs.digitalocean.com/products/networking/reserved-ips/?ref=arabroot.io) تنقله بين السيرفرات، ويذكر توثيقه أنه مخصص لبناء انتقال تلقائي.

والطريقة المعتادة في هذه البيئات أن تبقى Keepalived هي من يقرر من الأساسي عبر Unicast، ولكن سكربت `notify_master` هو الذي ينقل العنوان، فمثلاً في Hetzner يستدعي الأداة الرسمية بالأمر [hcloud floating-ip assign](https://github.com/hetznercloud/cli/blob/main/docs/reference/manual/hcloud%5Ffloating-ip%5Fassign.md?ref=arabroot.io) كما يلي، بعد أن تجهز التوكن مرة واحدة بالأمر `hcloud context create`:

```bash
#!/bin/sh
# notify_master on a Hetzner Cloud server: move the floating IP to this server
# FLOATING_IP and THIS_SERVER are the IP (or its name) and this server's name in Hetzner Cloud
exec hcloud floating-ip assign "$FLOATING_IP" "$THIS_SERVER"
```

ولاحظ أن الانتقال هنا يعتمد على واجهة المزود، فإذا كانت الواجهة نفسها متوقفة أو بطيئة لم ينتقل العنوان، وأن التوكن على السيرفر يستطيع نقل العناوين فاجعل صلاحياته في أضيق حد، وأن نقل العنوان قد يستغرق ثوان بحسب المزود. لذلك إذا كان مزودك يقدم موزع حمل مدار Managed Load Balancer بسعر مناسب فقد يكون أبسط من بناء كل هذا بنفسك.

## قبل أن تشغله في الإنتاج

- **ثبت من حزمة التوزيعة:** في Debian وUbuntu تجد الحزمة `keepalived` في المستودع الرسمي، وتعمل كخدمة systemd، وملف الإعداد في `/etc/keepalived/keepalived.conf`، وتأكد من الإصدار إذا أردت ميزات مثل `auth_hmac`.
- **اسمح بالبروتوكول 112 في جدار الحماية:** بين السيرفرين فقط وفي الاتجاهين، فقد رأيت في التجربة أن قطع اتجاه واحد سبب انتقالاً، وقطع الاتجاهين سبب انقسام الدماغ.
- **اجعل الخدمة تستمع على كل العناوين:** إذا كان nginx أو HAProxy يستمع على العنوان العائم بالتحديد، فلن يستطيع أن يبدأ على السيرفر الاحتياطي لأن العنوان ليس عنده، والحل أن يستمع على `0.0.0.0`، أو أن تفعل الإعداد `net.ipv4.ip_nonlocal_bind=1` الذي يسمح كما في [توثيق النواة](https://docs.kernel.org/networking/ip-sysctl.html?ref=arabroot.io) بربط الخدمة بعنوان غير موجود على السيرفر.
- **رقم `virtual_router_id` فريد:** لكل مجموعة على الشبكة نفسها، واكتبه في توثيق الشبكة حتى لا يكرره أحد.
- **إعداد متطابق على السيرفرين:** كل ما يخص الخدمة يجب أن يكون واحداً على السيرفرين، مثل إعداد nginx والشهادات، وإلا فالانتقال ينقل المستخدم إلى خدمة مختلفة، وأسهل طريقة أن تنشر الإعداد من مستودع Git على الاثنين معاً.
- **راقب العنوان نفسه:** نبه إذا لم يرد العنوان العائم، وإذا وجد على سيرفرين، وعلى كل انتقال من سجل الإشعارات، فالانتقال الذي لا يلاحظه أحد قد يخفي سيرفراً أساسياً معطلاً منذ أيام.
- **اختبر الانتقال بانتظام:** أوقف الخدمة على الأساسي في وقت هادئ وقس زمن التوقف من جهة المستخدم كما فعلنا في التجربة.

والخطوة الطبيعية بعد هذا المقال هي أن تضع HAProxy على السيرفرين خلف العنوان العائم، فيصبح موزع الحمل نفسه بلا نقطة فشل واحدة، وهو موضوع مخطط له بإذن الله في [مرحلة التوفر العالي](https://arabroot.io/start/#stage-ha)، وقد شرحنا تثبيت HAProxy نفسه في دليل [HAProxy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-haproxy-load-balancer/). أما إذا كان خلف العنوان قاعدة بيانات، فلا تجعل Keepalived وحدها تقرر الانتقال، والسبب أنها لا تملك نصاباً ولا تعرف أي نسخة متأخرة، وقد شرحنا البدائل في دليل [عناقيد قواعد البيانات](https://arabroot.io/articles/%D8%B9%D9%86%D8%A7%D9%82%D9%8A%D8%AF-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#client-routing).

## أخطاء شائعة

- **Keepalived دون سكربت فحص:** السيرفر حي ويرسل الإعلانات وnginx متوقف، فيبقى العنوان على سيرفر لا يخدم أحداً.
- **`nopreempt` مع `state MASTER`:** لا يعمل كما تتوقع، فالخيار يحتاج أن تكون الحالة الأولى BACKUP على كل السيرفرات.
- **رقم `virtual_router_id` مكرر:** سيرفر آخر بالرقم نفسه قد يأخذ العنوان أو يربك الشبكة، كما رأيت في التجربة مع السيرفر الثالث.
- **جدار حماية يمنع البروتوكول 112:** كل سيرفر يظن أن الآخر توقف، فيحملان العنوان معاً.
- **الاعتماد على Gratuitous ARP في السحابة:** العنوان يتغير على السيرفر ولا يتغير في الشبكة، فاستخدم واجهة المزود.
- **Unicast عبر شبكة يصل إليها غير السيرفرين دون `auth_hmac`:** أي جهاز يستطيع أن يرسل إعلاناً مزوراً ويأخذ العنوان.
- **Keepalived تقرر انتقال قاعدة البيانات:** سيرفران دون نصاب ينتهيان بانقسام الدماغ، واستخدم أداة تعرف حالة النسخ مثل Patroni.
- **قيم فحص صغيرة جداً:** كل لحظة بطء تسبب انتقالاً كاملاً، والانتقال نفسه يقطع الاتصالات.

## قائمة تحقق

- الحالة الأولى BACKUP على السيرفرين، والأولوية تحدد الأساسي، و`nopreempt` مفعل إلا إذا كان عندك سبب للرجوع التلقائي.
- سكربت فحص يفحص الخدمة نفسها بطلب حقيقي، مع `fall` و`rise` مناسبين.
- سكربت إشعار يسجل كل انتقال ويرسل تنبيهاً.
- رقم `virtual_router_id` فريد على الشبكة ومكتوب في التوثيق.
- البروتوكول 112 مسموح بين السيرفرين فقط وفي الاتجاهين.
- الخدمة تبدأ على السيرفر الاحتياطي رغم أن العنوان ليس عنده.
- في السحابة: سكربت `notify_master` ينقل العنوان عبر واجهة المزود، وتوكن بأضيق صلاحية.
- نفذت انتقالاً مخططاً وقست زمنه من جهة المستخدم، وتعرف ماذا يحدث عندما يعود السيرفر الأساسي.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- العنوان العائم يجعل سيرفرين يظهران للتطبيق كعنوان واحد، وKeepalived تقرر من يحمله ببروتوكول VRRP الموصوف اليوم في RFC 9568 بعد أن حلت محل RFC 5798.
- الاحتياطي ينتظر الإعلانات، فإذا غابت مدة ثلاثة إعلانات مضافاً إليها Skew\_Time أخذ العنوان، وإذا أرسل الأساسي أولوية 0 أخذه فوراً تقريباً.
- سكربت الفحص هو الذي يربط العنوان بصحة الخدمة، ورأيت في التجربة أن توقف nginx نقل العنوان خلال 2.58 ثانية، وأن إيقاف السيرفر فجأة نقله خلال 3.07 ثانية.
- Preemption الافتراضي يسبب انتقالاً ثانياً عند عودة السيرفر، و`nopreempt` يبقي العنوان حيث هو.
- سيرفران دون نصاب قد يحملان العنوان معاً عندما تنقطع الإعلانات، ورقم `virtual_router_id` المكرر قد يسرق العنوان من الشبكة.
- في السحابة لا تعمل Gratuitous ARP غالباً، فاستخدم Unicast وسكربت إشعار ينقل العنوان عبر واجهة المزود، أو موزع الحمل المدار من المزود.

[![](https://arabroot.io/content/images/2026/10/ha-concepts-cover-cover.webp)دليلمفاهيم التوفر العالي High Availability للمبتدئينماذا يعني أن تبقى خدمتك تعمل رغم تعطل جزء منها، وكيف تحسب التوفر وتجد نقاط الفشل الواحدة وتقرر متى يستحق ذلك تكلفته.](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D8%AA%D9%88%D9%81%D8%B1-%D8%A7%D9%84%D8%B9%D8%A7%D9%84%D9%8A-high-availability-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/haproxy-cover-cover-1.webp)دليلHAProxy: توزيع الحمل Load Balancing أمام خدماتكعندما يعمل تطبيقك على أكثر من سيرفر، يوزع HAProxy الطلبات بينها ويستبعد المعطل منها تلقائياً مع شهادات HTTPS.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-haproxy-load-balancer/)[![](https://arabroot.io/content/images/2026/10/db-clustering-cover-cover.webp)دليلعناقيد قواعد البيانات Database Clustering ومتى تحتاجهاماذا تعني كلمة Cluster في كل قاعدة بيانات، ومتى تحتاج نسخاً أو انتقالاً تلقائياً، ومتى يكفيك سيرفر واحد مع نسخ احتياطية جيدة.](https://arabroot.io/articles/%D8%B9%D9%86%D8%A7%D9%82%D9%8A%D8%AF-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/system-design-intro-cover-cover.webp)دليلمدخل إلى تصميم الأنظمة: من سيرفر واحد إلى ملايين المستخدمينكيف يكبر متجر إلكتروني من سيرفر واحد إلى نظام يخدم ملايين المستخدمين، وما الثمن الذي تدفعه في كل خطوة.](https://arabroot.io/articles/%D9%85%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%89-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D9%85%D9%86-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-%D9%88%D8%A7%D8%AD%D8%AF-%D8%A7%D9%84%D9%89-%D9%85%D9%84%D8%A7%D9%8A%D9%8A%D9%86/)

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

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على Keepalived 2.3.4 من حزم Alpine 3.23 فوق nginx:1.29.8-alpine في Docker Engine 29.8.0.