Kubernetes سطح: پیشرفته

Rolling، Blue/Green یا Canary؟ استراتژی‌های استقرار بدون Downtime در Kubernetes

مقایسه‌ی عملی Rolling Update، Blue/Green و Canary در کوبرنتیز با YAML آماده، Graceful Shutdown، Argo Rollouts و Gateway API.

۵ دقیقه مطالعه

«استقرار بدون 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 این مشکل را حل می‌کند:

  1. Expand: ستون جدید را اضافه کنید، بدون حذف ستون قدیمی. نسخه‌ی جدید در هر دو می‌نویسد.
  2. Migrate: داده‌های قدیمی را به ستون جدید منتقل کنید.
  3. Contract: در انتشار بعدی، وقتی نسخه‌ی قدیمی دیگر اجرا نمی‌شود، ستون قدیمی را حذف کنید.

کدام را انتخاب کنم؟

  • برای اکثر سرویس‌ها: Rolling Update با Probe، preStop و PDB کافی است.
  • برای انتشارهای پرریسک یا سرویس‌هایی که Rollback آنی می‌خواهند: Blue/Green.
  • برای سرویس‌های پرترافیک که می‌خواهید تأثیر نسخه‌ی جدید را روی کاربران واقعی اندازه بگیرید: Canary با Argo Rollouts.

جمع‌بندی

Downtime صفر یک تنظیم نیست؛ ترکیبی از Probe درست، خاموش شدن تمیز برنامه، استراتژی مناسب و Migrationهای سازگار است. از Rolling Update درست شروع کنید و فقط وقتی نیاز واقعی دارید، سراغ Blue/Green و Canary بروید.