«استقرار بدون Downtime» یکی از اولین خواستههای هر کارفرمای بانکی یا فینتک است. Kubernetes ابزارهای لازم را دارد، اما تنظیمات پیشفرض برای رسیدن به آن کافی نیست. در این مقاله سه استراتژی اصلی را با نمونهی واقعی مقایسه میکنم؛ از جمله Blue/Green که برای یک پلتفرم استریم ویدیو پیادهسازی کردم.
مقایسه در یک نگاه
| استراتژی | Downtime | Rollback | منابع اضافه | پیچیدگی | مناسب برای |
|---|---|---|---|---|---|
| Recreate | دارد | کند | ندارد | خیلی کم | محیط تست، سرویسهای Stateful خاص |
| Rolling Update | ندارد* | متوسط | کم | کم | اکثر سرویسها |
| Blue/Green | ندارد | آنی | ۱۰۰٪ در زمان استقرار | متوسط | سرویسهای حساس، انتشار بزرگ |
| Canary | ندارد | آنی | کم | زیاد | سرویسهای پرترافیک |
* به شرط تنظیم درست؛ که موضوع بخش بعد است.
Rolling Update: درست انجامش بدهید
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # one extra pod during the rollout
maxUnavailable: 0 # never drop below 4 ready pods
template:
spec:
terminationGracePeriodSeconds: 40
containers:
- name: api
readinessProbe:
httpGet: { path: /health/ready, port: 8080 }
periodSeconds: 5
failureThreshold: 2
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
دو جزئیات اینجا اهمیت زیادی دارند:
Readiness Probe: Kubernetes تا وقتی Pod جدید Ready نشده، ترافیک به آن نمیفرستد و Pod قدیمی را حذف نمیکند. بدون Probe، Pod جدید به محض اجرا ترافیک میگیرد، حتی اگر برنامه هنوز در حال بالا آمدن باشد.
preStop و sleep: وقتی Pod در حال حذف است، حذف آن از Endpointهای Service و ارسال SIGTERM به کانتینر همزمان اتفاق میافتد. اگر برنامه بلافاصله بسته شود، Load Balancer ممکن است چند ثانیه هنوز درخواست به آن بفرستد و نتیجهاش خطای 502 است. چند ثانیه صبر در preStop این فاصله را پوشش میدهد.
یک PodDisruptionBudget هم اضافه کنید تا عملیات نگهداری Nodeها (kubectl drain) همهی Podها را همزمان حذف نکند:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: api }
spec:
minAvailable: 3
selector:
matchLabels: { app: api }
Blue/Green: دو نسخه، یک کلید
در Blue/Green نسخهی جدید (Green) کامل کنار نسخهی فعلی (Blue) بالا میآید، تست میشود و بعد Service در یک لحظه به آن سوئیچ میکند:
# Two deployments: api-blue (version: blue) and api-green (version: green)
apiVersion: v1
kind: Service
metadata: { name: api }
spec:
selector:
app: api
version: blue # the only line that changes on switch
ports: [{ port: 80, targetPort: 8080 }]
# 1. deploy green next to blue and wait until it is fully ready
kubectl rollout status deployment/api-green -n production
# 2. smoke-test green through its own internal service
curl -sf http://api-green.production.svc/health/ready
# 3. switch traffic
kubectl patch service api -n production -p '{"spec":{"selector":{"version":"green"}}}'
# rollback = switch back
kubectl patch service api -n production -p '{"spec":{"selector":{"version":"blue"}}}'
نسخهی Blue را تا مطمئن شدن از سلامت Green (مثلاً یک ساعت) نگه دارید و بعد Scale آن را به صفر برسانید.
Canary: کمکم و با اندازهگیری
در Canary، اول مثلاً ۱۰٪ ترافیک به نسخهی جدید میرود. اگر متریکها سالم بود، بهتدریج بیشتر میشود. دو روش استاندارد امروز اینهاست:
روش اول: Gateway API
Gateway API جانشین رسمی Ingress در Kubernetes است و تقسیم وزنی ترافیک را بهصورت استاندارد پشتیبانی میکند:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: api, namespace: production }
spec:
parentRefs: [{ name: public-gateway, namespace: gateway }]
hostnames: [api.example.ir]
rules:
- backendRefs:
- name: api-stable
port: 80
weight: 90
- name: api-canary
port: 80
weight: 10
روش دوم: Argo Rollouts
اگر از ArgoCD استفاده میکنید، Argo Rollouts مراحل Canary را خودکار و حتی بر اساس متریکهای Prometheus، Rollback خودکار انجام میدهد:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: { name: api }
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 10m }
- analysis:
templates: [{ templateName: error-rate }]
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
# selector and template: same as a normal Deployment
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata: { name: error-rate }
spec:
metrics:
- name: error-rate
interval: 1m
count: 5
failureLimit: 1
successCondition: result[0] < 0.01
provider:
prometheus:
address: http://monitoring-kube-prometheus-prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{app="api",status=~"5.."}[2m]))
/ sum(rate(http_requests_total{app="api"}[2m]))
اگر درصد خطای نسخهی جدید از ۱٪ بیشتر شود، Rollout خودکار متوقف و به نسخهی پایدار برگردانده میشود؛ بدون اینکه کسی نیمهشب بیدار شود.
پاشنهی آشیل: دیتابیس
در همهی این استراتژیها، مدتی هر دو نسخه همزمان با یک دیتابیس کار میکنند. پس Migration باید با هر دو نسخه سازگار باشد. الگوی Expand / Contract این مشکل را حل میکند:
- Expand: ستون جدید را اضافه کنید، بدون حذف ستون قدیمی. نسخهی جدید در هر دو مینویسد.
- Migrate: دادههای قدیمی را به ستون جدید منتقل کنید.
- Contract: در انتشار بعدی، وقتی نسخهی قدیمی دیگر اجرا نمیشود، ستون قدیمی را حذف کنید.
کدام را انتخاب کنم؟
- برای اکثر سرویسها: Rolling Update با Probe،
preStopو PDB کافی است. - برای انتشارهای پرریسک یا سرویسهایی که Rollback آنی میخواهند: Blue/Green.
- برای سرویسهای پرترافیک که میخواهید تأثیر نسخهی جدید را روی کاربران واقعی اندازه بگیرید: Canary با Argo Rollouts.
جمعبندی
Downtime صفر یک تنظیم نیست؛ ترکیبی از Probe درست، خاموش شدن تمیز برنامه، استراتژی مناسب و Migrationهای سازگار است. از Rolling Update درست شروع کنید و فقط وقتی نیاز واقعی دارید، سراغ Blue/Green و Canary بروید.