Docker & Application

Zero-Downtime Deployment với Docker: Blue/Green và Rolling Update

Hướng dẫn triển khai phát hành không gián đoạn (Zero-Downtime) cho ứng dụng Docker: cơ chế Rolling Update cụm Multi-Server, Blue/Green nội bộ và kiểm tra healthcheck tự động.

Triển khai Zero-Downtime Deployment cho cụm Docker Blue Green và Rolling Update đơn giản
MỤC LỤC

Khi vận hành website trên nền tảng Docker trong môi trường production, tình trạng người dùng gặp lỗi 502 Bad Gateway mỗi khi cập nhật phiên bản mới sẽ gây gián đoạn trực tiếp đến trải nghiệm và tính liên tục của dịch vụ.

Nếu bạn vẫn đang áp dụng quy trình phát hành truyền thống theo kiểu triển khai Docker Compose cơ bản: chạy lệnh docker compose pull && docker compose down && docker compose up -d, hệ thống sẽ xuất hiện khoảng gián đoạn dịch vụ (Downtime Window) kéo dài từ 5 đến 30 giây. Đó là thời điểm container cũ đã dừng nhưng container mới vẫn đang trong giai đoạn nạp framework, kết nối cơ sở dữ liệu hoặc khởi tạo bộ đệm. Trường hợp bản phát hành mới phát sinh lỗi cấu hình hoặc thiếu biến môi trường khiến container không thể khởi động, dịch vụ sẽ gián đoạn cho đến khi người vận hành thực hiện quy trình rollback khẩn cấp.

Trong kiến trúc cụm máy chủ Multi-Server với bộ cân bằng tải Nginx và kho lưu trữ tệp tin phi trạng thái, giải pháp tiêu chuẩn là triển khai Zero-Downtime Deployment (phát hành không gián đoạn). Mục tiêu là hạn chế tối đa mã lỗi HTTP 502/503 trong suốt quá trình cập nhật, giữ cho các phiên làm việc của người dùng diễn ra liền mạch, đồng thời hỗ trợ khôi phục phiên bản cũ an toàn nếu bản cập nhật mới gặp sự cố.

Bài viết này hướng dẫn hai chiến lược phát hành phù hợp với hệ thống Docker: Rolling Update trên cụm Multi-Server và Blue/Green Deployment trên từng máy chủ, kết hợp kỹ thuật thay đổi cấu trúc cơ sở dữ liệu hai pha (Expand and Contract) và cơ chế dừng tiến trình an toàn (Graceful Shutdown).

Hai chiến lược phát hành không gián đoạn Zero-Downtime cho Docker

Chiến lược 1: Rolling Update trên cụm Multi-Server

Khi hệ thống vận hành từ 2 Web Node trở lên phía sau Nginx Load Balancer, giải pháp trực diện và hiệu quả nhất là Rolling Update (cập nhật cuốn chiếu từng máy chủ).

Nguyên tắc cơ bản: không cập nhật đồng loạt mọi máy chủ cùng lúc. Thay vào đó, bạn tạm ngắt lưu lượng đến từng máy chủ, cập nhật phiên bản mới, kiểm tra trạng thái hoạt động rồi mới đưa trở lại cụm để phục vụ người dùng.

Tiến trình Rolling Update:
Thời điểm T0: LB ──> [ Web-01 (v1) ] + [ Web-02 (v1) ]
Thời điểm T1: LB ──> [ Web-02 (v1) ] | Web-01 tạm rút để nâng cấp lên v2
Thời điểm T2: LB ──> [ Web-01 (v2) ] | Web-02 tạm rút để nâng cấp lên v2
Thời điểm T3: LB ──> [ Web-01 (v2) ] + [ Web-02 (v2) ] (Hoàn tất)

Quy trình 4 bước triển khai

Bước 1: Rút Web-01 khỏi khối upstream (drain connection)

Trên máy chủ Nginx Load Balancer, mở tệp cấu hình upstream và thêm cờ down cho Web-01:

nginx
upstream web_cluster {
    least_conn;
    server 10.0.0.11:80 down; # Tạm thời ngừng nhận request mới
    server 10.0.0.12:80;
    keepalive 64;
}

Nạp lại cấu hình Nginx bằng lệnh:

bash
sudo nginx -s reload

Bước 2: Cập nhật container trên Web-01

Đăng nhập SSH vào máy chủ Web-01, kéo bản dựng (image) mới và tái tạo container:

bash
docker compose pull app
docker compose up -d --no-deps app

(Tham số --no-deps giúp Docker tái tạo riêng dịch vụ app mà không khởi động lại các container phụ trợ khác như Redis hay dịch vụ thu thập log).

Bước 3: Kiểm tra healthcheck nội bộ

Trước khi định tuyến lưu lượng ngoài trở lại Web-01, bạn cần kiểm tra xem ứng dụng đã sẵn sàng xử lý yêu cầu hay chưa qua endpoint kiểm tra sức khỏe:

bash
curl -sf http://127.0.0.1:80/healthz > /dev/null

Nếu lệnh trả về mã trạng thái 200 OK, ứng dụng đã khởi động thành công, thiết lập kết nối cơ sở dữ liệu ổn định và sẵn sàng tiếp nhận yêu cầu.

Điều kiện an toàn: Nếu lệnh curl không thành công hoặc trả về mã lỗi HTTP 5xx, bạn cần tạm dừng quá trình triển khai, giữ nguyên Web-02 đang phục vụ lưu lượng và tiến hành rollback Web-01 về phiên bản trước đó để kiểm tra nguyên nhân. Người dùng bên ngoài vẫn tiếp tục truy cập bình thường qua Web-02.

Bước 4: Đưa Web-01 trở lại cụm và lặp lại với Web-02

Sau khi Web-01 vượt qua bước kiểm tra, bạn quay lại Nginx Load Balancer, bỏ cờ down của Web-01, nạp lại cấu hình Nginx (sudo nginx -s reload), sau đó lặp lại quy trình tương tự cho máy chủ Web-02.

Chiến lược 2: Blue/Green Deployment trên từng máy chủ

Khi cần xây dựng quy trình phát hành tự động khép kín trên từng máy chủ mà không muốn can thiệp vào cụm Nginx Load Balancer trung tâm, bạn có thể triển khai mô hình Blue/Green cục bộ ngay trên từng node.

Kiến trúc môi trường song song

Trên từng máy chủ Web Node, bạn duy trì hai nhóm container có cấu hình tương đương:

  • Môi trường Blue: Lắng nghe trên cổng nội bộ 8001 (phiên bản v1 đang phục vụ lưu lượng thực tế).
  • Môi trường Green: Lắng nghe trên cổng nội bộ 8002 (phiên bản v2 chuẩn bị phát hành).
  • Một dịch vụ Nginx cục bộ (Local Reverse Proxy) lắng nghe cổng 80/443 và chuyển tiếp lưu lượng vào một trong hai cổng trên qua tệp cấu hình upstream.

Cấu hình Docker Compose

Tệp docker-compose.yml:

yaml
version: '3.8'

services:
  # Phiên bản Blue
  app-blue:
    image: ${APP_IMAGE_BLUE}
    container_name: web_app_blue
    restart: always
    environment:
      - PORT=8001
      - DB_HOST=10.0.0.20
    ports:
      - "127.0.0.1:8001:8001"
    stop_signal: SIGQUIT
    stop_grace_period: 30s

  # Phiên bản Green
  app-green:
    image: ${APP_IMAGE_GREEN}
    container_name: web_app_green
    restart: always
    environment:
      - PORT=8002
      - DB_HOST=10.0.0.20
    ports:
      - "127.0.0.1:8002:8002"
    stop_signal: SIGQUIT
    stop_grace_period: 30s

Tệp định tuyến Nginx cục bộ /etc/nginx/conf.d/app-upstream.conf:

nginx
upstream current_backend {
    server 127.0.0.1:8001; # Trỏ vào Blue hoặc Green
    keepalive 32;
}

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://current_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Kịch bản tự động hóa chuyển đổi

Kịch bản deploy.sh dưới đây hỗ trợ tự động hóa các bước khởi chạy, kiểm tra sức khỏe và đảo cổng upstream trên Nginx:

bash
#!/usr/bin/env bash
set -euo pipefail

# 1. Xác định môi trường đang chạy và môi trường mục tiêu
CURRENT_PORT=$(grep -oP '(?<=server 127.0.0.1:)[0-9]+' /etc/nginx/conf.d/app-upstream.conf)

if [ "$CURRENT_PORT" = "8001" ]; then
    TARGET_COLOR="green"
    TARGET_PORT="8002"
    OLD_COLOR="blue"
else
    TARGET_COLOR="blue"
    TARGET_PORT="8001"
    OLD_COLOR="green"
fi

echo "Dang phat hanh phien ban moi len moi truong: $TARGET_COLOR (Port $TARGET_PORT)..."

# 2. Khoi chay container moi tren moi truong muc tieu
docker compose pull "app-$TARGET_COLOR"
docker compose up -d "app-$TARGET_COLOR"

# 3. Kiem tra suc khoe (Pre-flight Healthcheck) toi da 5 lan
echo "Dang kiem tra tinh trang hoat dong cua container moi..."
HEALTHY=false
for i in {1..5}; do
    if curl -sf "http://127.0.0.1:$TARGET_PORT/healthz" > /dev/null; then
        HEALTHY=true
        break
    fi
    echo "Thu lai lan $i sau 2 giay..."
    sleep 2
done

# 4. Kiem tra ket qua truoc khi chuyen luu luong
if [ "$HEALTHY" = false ]; then
    echo "Loi nghiem trong: Container $TARGET_COLOR khong vuot qua kiem tra suc khoe!"
    echo "Huy bo phat hanh. Dung container loi..."
    docker compose stop "app-$TARGET_COLOR"
    exit 1
fi

# 5. Dao cong tren Nginx (Zero-Downtime Switch)
echo "Container moi hoat dong hoan hao. Tien hanh dao luu luong sang $TARGET_COLOR..."
sudo sed -i "s/server 127.0.0.1:$CURRENT_PORT;/server 127.0.0.1:$TARGET_PORT;/" /etc/nginx/conf.d/app-upstream.conf
sudo nginx -s reload

echo "Chuyen doi thanh cong! Luu luong hien tai dang do vao: $TARGET_COLOR."

# 6. Cho doi xu ly not request cu va dung container cu
sleep 15
echo "Dung container cu: app-$OLD_COLOR..."
docker compose stop "app-$OLD_COLOR"

echo "Phat hanh hoan tat khong gián doan!"

Di chuyển cơ sở dữ liệu hai pha

Một nguyên nhân phổ biến khiến việc phát hành gặp sự cố gián đoạn là sự bất tương thích giữa mã nguồn và cấu trúc cơ sở dữ liệu (Database Schema Migration).

Trong quá trình thực hiện Rolling Update hoặc Blue/Green Deployment, cả hai phiên bản ứng dụng (v1 và v2) đều chạy song song trong vài phút và cùng đọc ghi vào một cơ sở dữ liệu chung.

Nếu câu lệnh migration làm thay đổi cấu trúc bảng theo hướng không tương thích ngược (chẳng hạn đổi tên cột user_name thành username, hoặc xóa một cột mà phiên bản v1 vẫn đang dùng), các container v1 chưa kịp nâng cấp sẽ báo lỗi ngay lập tức (thường gặp là lỗi Column not found).

Để duy trì tính toàn vẹn và không làm gián đoạn dịch vụ, bạn nên áp dụng mẫu hình Mở rộng và Thu hẹp (Expand and Contract):

Pha 1: Expand (Mở rộng)
       - Thêm cột mới `username` cho phép NULL.
       - Cả code v1 và code v2 đều hoạt động bình thường.

Pha 2: Deploy Code mới
       - Code v2 đọc/ghi đồng thời cả 2 cột hoặc ưu tiên cột mới.
       - Quá trình Rolling Update diễn ra an toàn.

Pha 3: Contract (Thu hẹp - thực hiện sau khi hệ thống ổn định)
       - Chạy script đồng bộ dữ liệu cũ sang cột mới.
       - Chạy migration xóa bỏ cột cũ `user_name`.

Cơ chế tắt tiến trình an toàn trong Docker

Khi container phiên bản cũ nhận lệnh dừng, nếu tiến trình bị ngắt cưỡng bức bằng tín hiệu SIGKILL, các kết nối của người dùng đang truyền dữ liệu dở dang (như phiên thanh toán hoặc tiến trình tải tệp) sẽ bị ngắt đột ngột, dễ dẫn đến sai lệch dữ liệu.

Để xử lý vấn đề này, bạn nên cấu hình cơ chế tắt tiến trình an toàn (Graceful Shutdown) bằng hai tham số trong docker-compose.yml:

yaml
services:
  app:
    # 1. Chi dinh tin hieu tat nhe nhang phu hop voi framework
    stop_signal: SIGQUIT
    
    # 2. Cho phep tien trinh xu ly not request toi da 30 giay truoc khi kill
    stop_grace_period: 30s
  • Với Nginx và PHP-FPM: Sử dụng tín hiệu SIGQUIT. Khi nhận tín hiệu này, tiến trình cha ngừng tiếp nhận request mới, chờ các worker xử lý nốt các phiên hiện có rồi mới giải phóng tài nguyên.
  • Với Node.js / Go / Python: Đón bắt tín hiệu SIGTERM trực tiếp trong mã nguồn ứng dụng:
javascript
// Vi du xu ly Graceful Shutdown trong Node.js (Express)
process.on('SIGTERM', () => {
  console.log('Nhan tin hieu SIGTERM. Dang dong HTTP server an toan...');
  server.close(() => {
    console.log('HTTP server da dong. Ngat ket noi Database...');
    db.disconnect();
    process.exit(0);
  });
});

Kiểm tra và xác nhận quá trình phát hành

Để xác nhận quy trình triển khai không làm gián đoạn dịch vụ thực tế, bạn có thể thực hiện bài kiểm tra tải:

Từ một máy bên ngoài, sử dụng Apache Benchmark (ab) để gửi liên tục 1,000 yêu cầu tới website trong khi chạy kịch bản cập nhật:

bash
ab -n 1000 -c 10 https://example.com/

Trong lúc lệnh ab đang chạy, bạn thực thi script phát hành trên máy chủ:

bash
./deploy.sh

Kết quả kiểm tra đạt yêu cầu khi:

text
Complete requests:      1000
Failed requests:        0
Non-2xx responses:      0
Requests per second:    142.50 [#/sec] (mean)
Time per request:       70.174 [ms] (mean)

Chỉ số Failed requests duy trì ở mức 0 và toàn bộ phản hồi trả về mã thành công cho thấy hệ thống đã chuyển giao phiên bản mới mà không làm rớt kết nối nào của người dùng.

Sau khi đã hoàn thiện chu trình triển khai ứng dụng trên cụm Multi-Server, bước tiếp theo để đưa hệ thống vào vận hành là rà soát lại các ranh giới bảo mật, kế hoạch cắt chuyển DNS và phương án dự phòng khẩn cấp tại bài viết checklist kiểm tra an toàn trước khi chuyển website sang Multi-Server.

Nội dung hướng dẫn này có hữu ích với hệ thống của bạn không?

Chia sẻ bài viết hữu ích

Gửi bài viết này cho đồng nghiệp

Đọc tiếp

Bài nên đọc tiếp

Xem tất cả bài về Docker & Application