Chiến lược Deployment — tradeoff giữa các cách

Tài liệu này so sánh các chiến lược deploy (triển khai) phổ biến, để chọn đúng cách cho từng bài toán và để trả lời phỏng vấn có chiều sâu thay vì chỉ nêu tên. Bối cảnh áp dụng: ứng dụng Laravel chạy trong Docker, deploy qua GitLab CI/CD, đứng sau Nginx (theo đúng tech stack trong CV).

Bảng so sánh nhanh

Chiến lược Downtime (thời gian gián đoạn) Chi phí hạ tầng Tốc độ rollback (quay lại bản cũ) Độ phức tạp
Recreate Có (vài giây–vài phút) Thấp (1 bản) Chậm (deploy lại bản cũ) Thấp
Rolling Gần như 0 Thấp–trung bình Trung bình Trung bình
Blue-Green 0 Cao (2 bản chạy song song) Tức thì (đổi lại traffic) Trung bình
Canary 0 Trung bình–cao Nhanh (cắt traffic canary) Cao

1. Recreate (dừng bản cũ, chạy bản mới)

Cách làm: dừng hẳn container/process cũ, deploy bản mới, khởi động lại.

2. Rolling deployment (cập nhật dần từng instance)

Cách làm: có nhiều instance (nhiều bản container chạy song song, gọi là replica) chạy cùng version. Cập nhật lần lượt từng instance — dừng 1, deploy bản mới, đợi healthcheck (kiểm tra sức khoẻ — endpoint xác nhận app đã sẵn sàng nhận traffic) pass, rồi mới sang instance kế tiếp. Traffic luôn có ít nhất 1 instance phục vụ.

3. Blue-Green deployment

Cách làm: giữ 2 môi trường độc lập, giống hệt nhau về hạ tầng — Blue (đang chạy, nhận traffic thật) và Green (bản mới, chưa nhận traffic). Deploy và test kỹ trên Green, sau đó đổi traffic (đổi upstream Nginx, hoặc đổi DNS/load balancer) từ Blue sang Green trong 1 lần.

4. Canary deployment

Cách làm: đưa bản mới ra cho một phần nhỏ traffic thật (vd 5%), theo dõi metrics/error rate (đây là lúc Prometheus + Grafana phát huy tác dụng), nếu ổn thì tăng dần % cho tới 100%. Nếu lỗi, cắt canary về 0% ngay.

Chọn cách nào?

flowchart TD
  A{"Chấp nhận downtime<br/>vài giây–vài phút?"}
  A -->|"Có — nội bộ, ít traffic"| R["Recreate<br/>đơn giản nhất, 1 bản"]
  A -->|"Không"| B{"Có orchestrator?<br/>(K8s / Swarm, ≥2 instance)"}
  B -->|"Không — 1 VPS + Docker Compose"| BG["Blue-Green<br/>rollback tức thì, tốn x2 RAM"]
  B -->|"Có"| C{"Có observability đủ tốt?<br/>(metric tách theo pool, alert)"}
  C -->|"Chưa"| RO["Rolling<br/>mặc định hợp lý"]
  C -->|"Có, và release rủi ro cao"| CA["Canary<br/>rủi ro thấp nhất, vận hành nặng nhất"]

Điểm chung của cả 4 cách khi có database dùng chung: migration phải backward-compatible trong lúc có 2 version code cùng tồn tại. Đây là điều hay bị bỏ qua và là nguồn lỗi production phổ biến nhất khi làm zero-downtime deploy.

5. Khó khăn khi vận hành thật (cả 4 cách đều dính)

Chọn được chiến lược mới xong một nửa. Phần còn lại là mấy chỗ hay làm hỏng đêm deploy:

6. Case thực tế có thể gặp và cách xử lý

Case 1 — Rolling deploy đang chạy dở, instance bản cũ đồng loạt 500. Migration release này RENAME một cột. Instance chưa cập nhật vẫn SELECT tên cũ. Xử lý ngay: đẩy nốt rolling cho xong thật nhanh (đi tới nhanh hơn quay lại, vì rollback lúc này còn phải rollback cả schema). Nếu lỗi lan sang tính năng tính tiền → bật maintenance mode, hoàn tất migrate, tắt maintenance. Ngăn lần sau: CI chặn merge nếu migration có RENAME/DROP — bắt tách thành 2 release theo expand-contract.

Case 2 — Blue-green switch xong, user kêu mất giỏ hàng / bị đăng xuất. SESSION_DRIVER=file, mỗi màu có filesystem riêng. Xử lý ngay: switch ngược về màu cũ (session cũ vẫn còn ở đó), thông báo, rồi sửa cấu hình. Ngăn lần sau: session + cache bắt buộc lên Redis dùng chung trước khi bật blue-green. Đây là điều kiện, không phải tối ưu.

Case 3 — Canary 5% chạy 20 phút, error rate hai bên bằng nhau, không kết luận được gì. 5% của traffic thấp = vài chục request, phần lớn là bot và healthcheck. Xử lý ngay: lọc bot/healthcheck khỏi metrics, hoặc nâng lên 20–25% để có đủ mẫu, hoặc chấp nhận đây là bài toán không hợp canary. Ngăn lần sau: trước khi làm canary, tính thử: cần bao nhiêu request/phút để 5% cho ra con số có nghĩa. Không đủ → dùng blue-green + feature flag.

Case 4 — Rollback container xong nhưng queue vẫn fail hàng loạt. Bản mới đã đẩy job với payload kiểu mới (hoặc job class mới) vào Redis; worker bản cũ không hiểu. Xử lý ngay: dừng worker, xem failed_jobs, sửa payload hoặc giữ worker ở bản mới trong khi web về bản cũ (worker và web không bắt buộc cùng version). Ngăn lần sau: job class mới phải có mặt ở worker từ release trước khi có code đẩy job đó. Payload chỉ thêm field, không đổi ý nghĩa field cũ.

Case 5 — Chạy 2 pool xong DB báo too many connections. Mỗi container mở pool riêng; blue-green/canary nhân đôi số connection trong lúc chồng lấn. Xử lý ngay: giảm số worker PHP-FPM của màu idle, hoặc tắt sớm màu cũ. Ngăn lần sau: tính max_connections theo đỉnh lúc chồng lấn, không theo lúc chạy bình thường; hoặc đặt PgBouncer (connection pooler — gom nhiều connection app thành ít connection DB) ở giữa.

Case 6 — Deploy "thành công" nhưng trang trắng, console báo 404 file JS. Vite build ra file có hash mới; user đang mở tab cũ xin file hash cũ đã bị xoá. Xử lý ngay: khôi phục thư mục asset bản cũ (hoặc rollback), báo user tải lại trang. Ngăn lần sau: asset các bản để chung một volume/CDN, chỉ dọn sau vài ngày — đừng xoá theo mỗi lần deploy.

7. Câu hỏi phụ interviewer hay hỏi tiếp

"Zero-downtime nghĩa là user không bao giờ thấy lỗi?" Không. Nó chỉ nói dịch vụ luôn có instance phục vụ. User vẫn có thể thấy lỗi do bug bản mới, do session bay, do asset 404. Zero-downtime là bài toán hạ tầng; "không lỗi" là bài toán migration + state + test.

"Blue-green và canary khác nhau chỗ nào? Nghe cũng giống nhau." Blue-green chuyển 100% traffic một phát — dùng để rollback nhanh. Canary chuyển từng phần — dùng để phát hiện lỗi sớm với ít user bị ảnh hưởng. Blue-green trả lời "quay lại có nhanh không", canary trả lời "có nên đi tiếp không".

"Trên 1 VPS chạy Docker Compose thì rolling được không?" Không thật sự. Compose đọc deploy.replicas nhưng bỏ qua deploy.update_configup -d dừng hết rồi mới dựng lại. Muốn không gián đoạn trên 1 VPS thì làm blue-green bằng 2 service + đổi upstream Nginx.

"Rollback mất bao lâu, và có phải lúc nào cũng rollback được không?" Đổi upstream + nginx -s reload là dưới 1 giây. Nhưng chỉ rollback được khi schema DB vẫn tương thích với code cũ, và khi bản lỗi chưa ghi dữ liệu hỏng. Đó là lý do migration phải backward-compatible — nó chính là thứ giữ cho nút rollback còn hoạt động.

"Đo gì để biết bản mới ổn?" Bốn nhóm: error rate 5xx, latency p95/p99, throughput (request/giây — tụt đột ngột nghĩa là traffic không tới được), và một chỉ số nghiệp vụ (số đơn hàng, số đăng nhập thành công). Ba cái đầu xanh mà đơn hàng về 0 thì vẫn là hỏng.

"Team 3 người, 1 VPS, bạn chọn gì?" Blue-green. Recreate cho test/staging vì đơn giản và rẻ. Canary chỉ khi traffic đủ lớn và đã có Prometheus + Grafana + người trực — nếu không, nó chỉ thêm việc mà không thêm an toàn.

"Feature flag thay được canary không?" Thay được phần lớn nhu cầu, và rẻ hơn nhiều: bật tính năng cho 5% user bằng Laravel Pennant, không cần chia traffic hạ tầng, tắt bằng 1 dòng config. Canary chỉ hơn khi rủi ro nằm ở chính bản build (nâng PHP, đổi thư viện, đổi config), chứ không nằm ở một tính năng cụ thể.

Tóm tắt để trả lời phỏng vấn

"Bốn cách khác nhau ở hai trục: bao nhiêu traffic thấy bản mới cùng lúc, và quay lại nhanh cỡ nào. Recreate rẻ và đơn giản nhưng có downtime. Rolling là mặc định khi đã có orchestrator. Blue-green đổi chi phí gấp đôi tạm thời lấy nút rollback tức thì — đây là cái tôi chọn cho 1 VPS chạy Docker Compose, vì Compose không rolling thật được. Canary an toàn nhất nhưng chỉ đáng khi traffic đủ lớn và có người đọc dashboard. Điểm chung quan trọng nhất: cả bốn cách đều có lúc 2 version code dùng chung 1 database, nên migration phải backward-compatible — không có nó thì nút rollback chỉ là trang trí."

Xem thực hành cụ thể (blue-green cho Laravel + GitLab CI/CD trên 1 VPS) ở laravel-gitlab-cicd.md, và cách dùng branch/tag để đưa code qua các môi trường ở git-workflow.md.