Zero-downtime deployment — Blue-Green và Canary thực chiến

Phong Hy

Đêm đó mình với hai đứa trong team ngồi canh cái lệnh docker compose up -d --build. App vừa bấm nút là gãy liền, user kêu ầm lên vì toàn 502 trong mấy phút. Cũng may lúc đó ít người xài, nhưng mình biết cái kiểu deploy "bấm nút rồi cầu nguyện" này không sống nổi với hệ thống có user thật.

Rồi mình mới để ý, chuyện deploy không downtime hoá ra không khó như mình tưởng. Chỉ cần nắm hai kỹ thuật căn bản: blue-greencanary. Hôm nay mình kể lại cho ai đang deploy kiểu cầu nguyện giống mình hồi đó.

DevOps Ảnh: Christina Morillo — Pexels

Vì sao deploy hay gây downtime?

Nguyên nhân chính là docker compose up -d sẽ stop container cũ rồi mới start container mới — giữa hai bước đó app chết, lâu hay mau tuỳ lúc build và khởi động. Kubernetes rolling update có đỡ hơn, nhưng nếu bản mới lỗi thì vẫn có thể rơi vô chuỗi 500 liên tiếp.

Vấn đề thật ra không phải "bản mới có lỗi hay không", mà là mình có đường lui không. Không có đường lui thì mỗi lần deploy là một lần đánh cược.

Blue-Green — hai môi trường, một cú switch

Ý tưởng đơn giản: chạy song song hai bản — blue là bản cũ, green là bản mới — và nginx chỉ trỏ traffic vào một trong hai. Deploy xong green, test xong, mới bấm nút switch.

upstream app {
    server 10.0.0.11:8080;  # blue — version cũ
    # server 10.0.0.12:8080;  # green — version mới
}

server {
    listen 80;
    location / {
        proxy_pass http://app;
    }
}

Switch chỉ là sửa lại comment rồi nginx -s reload. Điểm mấu chốt: reload nginx không drop những request đang xử lý — connection cũ chạy nốt, connection mới đi qua bản green. Rollback cũng chỉ là switch ngược lại, một câu lệnh là xong.

DevOps Ảnh: panumas nikhomkhai — Pexels

Nhược điểm là tốn gấp đôi tài nguyên vì hai môi trường chạy cùng lúc. Với dự án nhỏ thì không sao, nhưng hệ thống lớn phải tính kỹ. Một điểm chết nữa là database — bản green chạy migration đổi schema mà không tương thích ngược thì bản blue (đang dùng chung DB) gãy theo. Như bài expand & contract mình viết hồi trước, migrate phải backward compatible thì blue-green mới chạy được.

Canary — thả 5% user vô trước

Canary khác ở chỗ không switch toàn bộ, mà cho bản mới nhận một phần nhỏ traffic trước, quan sát, rồi tăng dần. Với nginx chỉ cần weight:

upstream app {
    server 10.0.0.11:8080 weight=95;
    server 10.0.0.12:8080 weight=5;
}

5% là vừa đủ để thấy lỗi mà chưa đủ gây thảm hoạ. Mình hay theo dõi ba thứ trong lúc canary: error rate, latency p95 và số lượng 5xx. Có lần canary 5% mà error rate vọt lên 12%, may là cứu kịp — chỉ vài chục user bị ảnh hưởng thay vì cả hệ thống.

DevOps Ảnh: panumas nikhomkhai — Pexels

Chọn cái nào?

  • Blue-green — khi cần rollback tức thì, hoặc app không tự scale linh hoạt.
  • Canary — khi có monitoring tốt, muốn test bản mới dưới tải thật trước khi phủ toàn bộ.
  • Nhiều hệ thống xài kết hợp: canary tăng dần, hễ thấy lỗi thì quay về bản cũ (blue-green đảo ngược).

Kinh nghiệm thực tế

  1. Healthcheck phải có. Không có healthcheck thì nginx vẫn gửi request vô container đang chết, user thấy 502 dù mình tưởng đã deploy xong.
  2. Migration DB là điểm chết. Migrate backward compatible trước, deploy app sau — đừng đảo ngược.
  3. Log + metric phải có trước khi canary. Không đo được thì đừng canary, vì không biết bản mới có lỗi hay không.
  4. Tự động hoá bước switch. Đừng để nửa đêm deploy còn sửa file nginx bằng tay — lệnh càng ít thì sai sót càng nhỏ.

Nói chung thì deploy không downtime không phải chuyện cao siêu gì. Bắt đầu từ blue-green với nginx là đủ cho phần lớn dự án nhỏ, rồi nâng dần lên canary khi hệ thống lớn hơn. Còn ai đang deploy kiểu cầu nguyện thì... ráng đổi sớm nha. Có ai từng gặp vụ deploy đêm gây sập app chưa? Kể mình nghe với.

📋 Phụ lục thuật ngữ

  • Blue-green deployment — kỹ thuật chạy song song hai môi trường (cũ/mới) rồi chuyển traffic qua lại
  • Canary release — cho bản mới nhận phần trăm nhỏ traffic, tăng dần sau khi quan sát
  • Rollback — quay về phiên bản cũ khi bản mới gặp lỗi
  • Healthcheck — cơ chế kiểm tra container còn sống và sẵn sàng nhận request
  • Backward compatible — migration tương thích ngược, code cũ vẫn chạy được với schema mới