Blue-Green على VM واحدة: نشر Node.js بدون downtime طويل
هتقلل توقف نشر Node.js من حوالي 45 ثانية إلى ثانية أو ثانيتين، بدون Kubernetes وبدون منصة deploy كبيرة. الفكرة: تشغل إصدارين على نفس الـ VM، وتخلي NGINX يحوّل الترافيك للإصدار السليم فقط.
مستوى القارئ: متوسط
المشكلة باختصار
الطريقة الشائعة على VM صغيرة هي: اسحب الكود، أوقف الخدمة، ابنِ النسخة الجديدة، شغّل الخدمة. الطريقة دي بتفشل لما عندك مستخدمين حقيقيين وقت النشر. أي طلب ييجي أثناء الإيقاف ياخد 502 أو timeout.
الافتراض هنا إن عندك تطبيق Node.js واحد، VM واحدة، NGINX قدامه، وحجم ترافيك متوسط مثل 20K إلى 80K زيارة يوميًا. مش محتاج cluster. محتاج فقط منفذين: واحد للإصدار الحالي وواحد للإصدار الجديد.
الفكرة بمثال بسيط
ركز في المثال ده. عندك مطعم بباب واحد للزبائن، ومطبخين خلف الباب. المطبخ الأزرق شغال الآن. أنت تجهز المطبخ الأخضر في الخلف، تختبر الأكل، ثم تقول للباب: وجّه الطلبات للمطبخ الأخضر. الزبون لم يرَ النقل.
ده بالظبط Blue-Green Deploy. الـ blue هو الإصدار الحالي على port 3001. الـ green هو الإصدار الجديد على port 3002. NGINX هو الباب. فحص /health هو اختبار إن المطبخ الجديد جاهز.
علميًا، Blue-Green Deploy هو نمط نشر يشغل بيئتين متشابهتين. واحدة تستقبل الترافيك، والثانية تتجهز وتُختبر. التحويل يتم بتغيير الـ routing ثم reload خفيف للـ proxy. في NGINX، reload يقرأ الإعداد الجديد بدون قتل الاتصالات الجارية غالبًا، بشرط إن الإعداد صحيح.
إعداد NGINX للتبديل بين الإصدارين
اعمل ملف upstream منفصل. ده يخلي سكربت النشر يغيّر سطر واحد بدل ما يلمس ملف NGINX كامل.
# /etc/nginx/conf.d/app-upstream.conf
upstream app_backend {
server 127.0.0.1:3001;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
ابدأ بـ blue على 3001. الإصدار التالي يشتغل على 3002. بعد فحص الصحة، السكربت يبدّل upstream إلى 3002 ثم يعمل nginx -s reload.
خدمتان systemd بدل خدمة واحدة
بدل خدمة واحدة اسمها app.service، اعمل template واحد يخدم blue وgreen. المكسب إنك تقدر تشغل الإصدار الجديد بدون لمس القديم.
# /etc/systemd/system/myapp@.service
[Unit]
Description=MyApp %i instance
After=network.target
[Service]
WorkingDirectory=/opt/myapp/%i/current
Environment=NODE_ENV=production
EnvironmentFile=/opt/myapp/%i/.env
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=3
User=myapp
Group=myapp
[Install]
WantedBy=multi-user.target
شغّل blue مرة أولى:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp@blue
curl -fsS http://127.0.0.1:3001/health
سكربت النشر العملي
السكربت التالي يفترض إن blue على 3001 وgreen على 3002. هو يعرف النشط من ملف صغير، يبني الإصدار الجديد في المجلد الآخر، يشغله، يفحص الصحة، ثم يبدّل NGINX.
#!/usr/bin/env bash
set -euo pipefail
ACTIVE_FILE=/opt/myapp/active-slot
ACTIVE=$(cat "$ACTIVE_FILE" 2>/dev/null || echo blue)
if [ "$ACTIVE" = "blue" ]; then
NEXT=green
NEXT_PORT=3002
else
NEXT=blue
NEXT_PORT=3001
fi
RELEASE_DIR="/opt/myapp/$NEXT/current"
REPO="git@github.com:company/myapp.git"
sudo -u myapp rm -rf "$RELEASE_DIR"
sudo -u myapp git clone --depth 1 "$REPO" "$RELEASE_DIR"
cd "$RELEASE_DIR"
sudo -u myapp npm ci --omit=dev
sudo systemctl restart "myapp@$NEXT"
for i in {1..20}; do
if curl -fsS "http://127.0.0.1:$NEXT_PORT/health" >/dev/null; then
break
fi
sleep 1
if [ "$i" = 20 ]; then
echo "health check failed for $NEXT"
sudo journalctl -u "myapp@$NEXT" -n 80 --no-pager
exit 1
fi
done
sudo sed -i "s/server 127.0.0.1:[0-9]*;/server 127.0.0.1:$NEXT_PORT;/" /etc/nginx/conf.d/app-upstream.conf
sudo nginx -t
sudo nginx -s reload
echo "$NEXT" | sudo tee "$ACTIVE_FILE" >/dev/null
sudo systemctl stop "myapp@$ACTIVE" || true
echo "deployed $NEXT on port $NEXT_PORT"
سيناريو واقعي: تطبيق SaaS صغير عنده 50K زيارة يوميًا. النشر التقليدي كان يسبب 35 إلى 60 ثانية من 502 أثناء npm ci وrestart. بعد الفصل بين build والتبديل، التوقف العملي بقى زمن reload فقط، غالبًا أقل من ثانيتين في قياس داخلي بسيط.
الـ trade-off هنا
المكسب واضح: نشر أسرع، rollback أبسط، وطلبات أقل تفشل وقت النشر. لو green فشل في /health، blue يفضل شغال.
التكلفة: استهلاك ذاكرة مضاعف وقت النشر. لو تطبيقك يستخدم 350MB RAM، هتحتاج تقريبًا 700MB أثناء فترة التبديل. كمان لازم تنتبه للـ migrations. أي migration تكسر التوافق بين الإصدارين ممكن تخلي blue يقع بعد ما green يغيّر شكل قاعدة البيانات.
أفضل طريقة مع قواعد البيانات: اجعل التغيير backward-compatible. أضف العمود أولًا، انشر الكود الذي يقرأ القديم والجديد، ثم احذف القديم في نشر لاحق. بدل ما تعمل rename مباشر في migration واحدة.
متى لا تستخدم هذه الطريقة
لا تستخدمها لو التطبيق stateful جدًا ويحفظ جلسات داخل الذاكرة فقط. انقل الجلسات إلى Redis أولًا. لا تستخدمها أيضًا لو عندك WebSocket طويل العمر وتحتاج draining محسوب؛ هنا تحتاج إعدادات graceful shutdown وتوقيت أطول قبل إيقاف الإصدار القديم.
ولو عندك أكثر من 3 أو 4 خدمات مترابطة، VM واحدة هتبقى عنق زجاجة. وقتها فكر في orchestrator أو منصة deploy تدعم health checks وrollbacks بشكل مركزي.
مصادر مهمة
الخطوة التالية
افتح إعداد NGINX الحالي عندك، وافصل الـ upstream في ملف مستقل. بعد كده شغّل نسخة ثانية من التطبيق على port مختلف، ولا تبدّل الترافيك إلا بعد ما curl /health يرجع نجاح 20 مرة متتالية.