2026-04-22
Blue/green ile zero-downtime deploy
Deploy sırasında aktif request'leri kaybetmemek için iki backend container'ı paralel tutup nginx'in önünden port swap yapıyorum. Eskiden deploy'u gece yapıyorduk, "downtime penceresi" açılıyordu. Şimdi gün içi de gönderebiliyorum.
Temel fikir
İki slot var: blue ve green. Her ikisi de aynı imajın farklı tag'ini farklı portta çalıştırıyor. Aktif slot bir dosyada tutuluyor:
/var/lib/pricoda/<service>-active-slot → "blue" veya "green"
Nginx, aktif slot'un port'una proxy_pass ile bağlı. Deploy sırasında:
- Pasif slot'ta yeni container'ı başlat (eski aktif çalışmaya devam ediyor).
- Yeni container health check geçsin diye 120s bekle.
- Nginx conf'unda
proxy_passport'unu sed ile swap et,nginx -s reload. - 60s grace period, eski container hâlâ uçuşta olan request'leri bitirsin.
- Eski container'ı
docker stop --time 90ile durdur.
Reload graceful. Mevcut bağlantılar yeni conf'a değmiyor, kapanınca düşüyorlar. Yeni bağlantılar yeni upstream'e gidiyor.
Spring Boot tarafı: graceful shutdown
SIGTERM gelince Tomcat'in hemen kapanmaması lazım. Spring'in graceful mod'u:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 90s
graceful moduyla yeni request almıyor ama açık olanları bitiriyor. 90s timeout yeterli, Decodo + parse kombinasyonu en kötü ~1 dk sürüyor.
Ama bir tuzak var: HTTP request'ler drain oluyor, async executor'daki task'lar drain olmuyor. Uzun scraping job'u SIGTERM alınca yarıda kesiliyor. Bunu henüz çözmedim, planda:
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(120);
timeout-per-shutdown-phase da 120s'ye çıkıyor, docker stop --time 150 oluyor. ~%90+ async job korunur.
Nginx hot-swap
Nginx config'de upstream port'u placeholder değil, literal. Her deploy'da sed ile değiştiriyorum:
sed -i "s/proxy_pass http:\\/\\/127.0.0.1:8082/proxy_pass http:\\/\\/127.0.0.1:8083/" \
/etc/nginx/conf.d/pricoda-global-backend.conf
sudo nginx -t && sudo nginx -s reload
İki detay:
nginx -tçok önemli. Test etmeden reload atarsan ve config bozuksa downtime alırsın. Test geçmiyorsa deploy'u durdur.nginx -s reloadSIGHUP gönderir, master yeni worker başlatır, eski worker'lar mevcut bağlantıları bitirip kapanır. Graceful.
Jenkins agent'ının sed çalıştırıp sonra sudo'suz nginx -s reload demesi için iki şey gerekti:
/etc/nginx/conf.d/dizininedevopsgrubu yazma izni./etc/sudoers.d/devops-nginx→ NOPASSWDnginx -tvenginx -s reload.
İkisini de tek seferlik yaptım, bir daha dokunmadım.
Health check ve auto-rollback
Yeni container ayağa kalktığında /actuator/health 200 dönene kadar bekliyorum. 120s limitim var:
for i in {1..60}; do
if curl -fs http://127.0.0.1:$NEW_PORT/actuator/health > /dev/null; then
echo "Healthy at attempt $i"
break
fi
sleep 2
if [ $i -eq 60 ]; then
echo "Health check failed, rolling back"
docker stop $NEW_CONTAINER && docker rm $NEW_CONTAINER
exit 1
fi
done
Başarısızsa yeni container'ı öldürüyorum, nginx'e dokunmuyorum (hâlâ eski slot'a pointing). Deploy başarısız ama prod etkilenmedi.
Slot tracking
Aktif slot'un hangisi olduğunu bilmek lazım ki yeni deploy pasif olana gitsin. Basit bir file:
CURRENT=$(cat /var/lib/pricoda/pricoda-global-backend-active-slot)
if [ "$CURRENT" = "blue" ]; then
NEW_SLOT=green
NEW_PORT=8083
else
NEW_SLOT=blue
NEW_PORT=8082
fi
Deploy sonunda slot dosyasını güncelle. Bir dahaki deploy diğerine gidecek.
Yakalanan hatalar
- Scheduled task'lar iki yerde tetiklenebilir: blue-green grace period'da (60s) iki container paralel.
@Scheduledcron'u iki kez çalışır. Şu an scheduled search kapalı olduğu için acil değil ama ShedLock eklemek gerekecek. - Database migration: yeni kod eski şemayla çalışmalı (backward-compatible) veya migration önden. Kırıcı migration'ları deploy'dan bağımsız ayrı fazda yapıyorum.
- Session state: stateless olmak zorunda. Session sticky olursa grace period'da user yanlış slot'ta bulunur. Bizde JWT olduğu için sorun yok.
Sonuç
Deploy artık iş saatlerinde de gönderilebiliyor. En kötü senaryoda (yeni container açılamadıysa) auto-rollback devreye giriyor ve kimse fark etmiyor. 5 dakikalık deploy penceresi 0 dakikaya düştü.
Tek eksik: async executor graceful drain. Onu da ekleyince deploy sırasında en uzun scraping job'u bile korunmuş olacak.