Laravel deploy không downtime: chiến lược migration và rollback database

Quy trình deploy Laravel không downtime: tách release, migration backward-compatible, backfill, reload worker và rollback code không phá database.

NHT
· 9 phút đọc
Laravel zero downtime deployment với release pipeline, database migration và rollback

Laravel deploy không downtime không chỉ là đổi code mà không tắt web server. Phần khó hơn nằm ở database: release mới phải chạy được với schema cũ trong một khoảng thời gian, migration không giữ lock nguy hiểm, worker không dùng code cũ và rollback không phá dữ liệu đã được ghi bởi release mới.

Bài viết này trình bày chiến lược deploy theo release, migration backward-compatible, expand-and-contract, backfill theo batch, kiểm tra metadata lock và kế hoạch rollback. “Zero downtime” ở đây là mục tiêu giảm hoặc loại bỏ thời gian người dùng không truy cập được; không nên hứa rằng mọi ALTER TABLE đều hoàn toàn không chờ lock [1] [4].

Nguyên tắc an toàn: rollback code phải chạy được với schema mới, còn migration destructive nên được tách thành một bước cleanup sau khi release cũ không còn cần dữ liệu đó. Nếu xóa cột ngay trong deploy đầu tiên, bạn có thể không còn đường quay lại bằng cách chỉ đổi symlink.

1. Vì sao deploy code và migration cùng lúc dễ gây downtime?

Giả sử release mới đổi code từ `users.name` sang `users.display_name` và ngay lập tức xóa cột cũ. Trong lúc deploy, request đang chạy có thể đến release cũ, queue worker cũ hoặc một process chưa reload. Release cũ vẫn đọc `name`, nhưng database đã xóa cột, dẫn tới lỗi 500. Ngược lại, nếu code mới ghi dữ liệu theo định dạng mà release cũ không hiểu, rollback code cũng có thể thất bại.

Thành phầnCó thể chạy song song trong deploy?Rủi ro cần xử lý
HTTP request release cũ/mớiCó, trong thời gian chuyển trafficSchema phải tương thích cả hai phiên bản
Queue worker cũ/mớiCó thể tồn tại đồng thờiPayload và class phải tương thích khi retry
Migration additiveThường an toàn hơnDefault, index, lock và dung lượng
Migration destructiveKhông nên gộp vào bước đầuRollback không khôi phục dữ liệu
Backfill dữ liệuNên chạy riêng theo batchTải DB, lock và thời gian thực thi

Tài liệu Laravel khuyến nghị chạy các bước optimize/cache trong deployment và reload service chạy lâu sau khi có code mới [1]. Bạn có thể đối chiếu thêm quy trình chạy Laravel queue với Supervisor khi thiết kế bước reload worker. Điều đó vẫn chưa thay thế chiến lược schema: cache config hoặc restart worker không làm một migration destructive trở nên an toàn.

Một mô hình phổ biến là mỗi lần deploy tạo một thư mục release bất biến, chạy dependency install, build frontend, kiểm tra và trỏ symlink `current` sang release mới. Nginx/Apache luôn trỏ vào `current/public`. Khi cần rollback code, chỉ đổi symlink về release trước rồi reload worker; database vẫn phải tương thích với release đó.

set -euo pipefail
APP=/var/www/example.com
RELEASE="$APP/releases/$(date +%Y%m%d%H%M%S)"
mkdir -p "$RELEASE"
git clone --depth 1 "$REPO" "$RELEASE"
cd "$RELEASE"
composer install --no-dev --prefer-dist --optimize-autoloader
npm ci && npm run build
cp "$APP/shared/.env" .env
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate --force
ln -sfn "$RELEASE" "$APP/current"
php artisan reload || true

Không chạy đoạn script nguyên trạng nếu chưa có backup, lock deploy và kiểm tra quyền. Trên shared hosting, bạn có thể không có quyền tạo cấu trúc release hoặc chạy process monitor; hãy dùng cơ chế deploy mà nhà cung cấp hỗ trợ. Tài liệu Laravel cũng nhấn mạnh web server phải trỏ vào `public/index.php`, không trỏ vào root project để tránh lộ file nhạy cảm [1].

3. Expand-and-contract: chiến lược migration tương thích ngược

Expand-and-contract chia thay đổi lớn thành các bước nhỏ. Bước expand thêm cấu trúc mới nhưng chưa xóa cấu trúc cũ; code trung gian có thể đọc/ghi cả hai nếu cần. Sau khi mọi worker và request đã dùng code mới, backfill dữ liệu. Cuối cùng, sau một khoảng quan sát và khi chắc chắn không còn release cũ, bước contract mới xóa cột, index hoặc bảng không còn dùng.

Giai đoạnVí dụMục tiêu
ExpandThêm `display_name` nullableRelease cũ vẫn chạy với `name`
Dual read/writeGhi cả name và display_name tạm thờiGiữ dữ liệu nhất quán khi chuyển code
BackfillCopy name sang display_name theo batchKhông khóa table quá lâu
SwitchRelease mới đọc display_nameTheo dõi error, null và queue
ContractXóa name trong deploy sauKhông còn phụ thuộc release cũ
Schema::table('users', function (Blueprint $table) {
    $table->string('display_name')->nullable()->after('name');
});

// Release trung gian: tương thích với cả code cũ và mới
User::query()->whereNull('display_name')
    ->orderBy('id')
    ->limit($batchSize)
    ->eachById(function (User $user) {
        $user->forceFill(['display_name' => $user->name])->saveQuietly();
    });

Nếu dữ liệu lớn, không dùng một UPDATE khổng lồ trong transaction dài mà chưa đo. Chia batch theo primary key, theo dõi thời gian, rows affected, lock wait và replication lag nếu có. Backfill cũng cần idempotent để chạy lại sau khi process bị dừng.

4. Viết migration Laravel an toàn cho production

Migration nên nhỏ, có tên mô tả rõ và được review như code production. Dùng `php artisan migrate:status` để biết trạng thái, `migrate --pretend` để xem SQL trước khi chạy, và `--force` trong pipeline production khi quy trình đã có approval. Tùy chọn `--step` tạo batch riêng cho mỗi migration, giúp rollback theo bước trong phạm vi mà migration còn có thể đảo ngược [2].

php artisan migrate:status
php artisan migrate --pretend
php artisan migrate --force
php artisan migrate:rollback --pretend

# Chỉ rollback số bước đã biết, không dùng mù quáng
php artisan migrate:rollback --step=1

Không coi `down()` là bản backup. Nếu migration đã drop dữ liệu, rollback có thể tạo lại cột nhưng không tự phục hồi nội dung đã mất. Trước thay đổi destructive, cần backup có thể restore, thử restore ở môi trường riêng và ghi rõ thời gian khôi phục dự kiến.

5. MySQL Online DDL và metadata lock

Một số thao tác InnoDB như tạo secondary index có thể giữ table khả dụng cho đọc/ghi, nhưng MySQL vẫn cần chờ các transaction liên quan ở giai đoạn hoàn tất. Transaction dài hoặc idle transaction có thể khiến metadata lock kéo dài. Vì vậy, `ALGORITHM=INPLACE` hoặc `LOCK=NONE` không phải cam kết rằng migration sẽ không bao giờ chờ.

-- Xem transaction đang mở và metadata lock tùy cấu hình
SHOW FULL PROCESSLIST;
SELECT * FROM performance_schema.metadata_locks;

-- Ví dụ cần review kỹ trước khi chạy
ALTER TABLE orders
  ADD INDEX orders_status_created_at_idx (status, created_at),
  ALGORITHM=INPLACE, LOCK=NONE;

Trước migration lớn, kiểm tra kích thước bảng, lượng ghi, transaction đang mở, disk space và thời gian chạy dự kiến. Chọn maintenance window có giám sát dù mục tiêu là zero downtime. Nếu lock wait vượt ngưỡng, dừng migration có kiểm soát thay vì để request production xếp hàng.

6. Deploy code mới mà không làm worker chạy code cũ

HTTP request thường kết thúc nhanh, còn queue worker là process dài hạn nên giữ code đã nạp trong memory. Sau khi symlink sang release mới, cần reload hoặc terminate worker để process monitor khởi động worker với code mới. Laravel cung cấp `php artisan reload` cho các service chạy lâu trong tài liệu deployment; Horizon có `horizon:terminate` để worker thoát an toàn sau khi xử lý job hiện tại [1] [3].

# Ví dụ sau khi current đã trỏ release mới
php artisan reload

# Nếu dùng Horizon
php artisan horizon:terminate

# Kiểm tra worker không còn trỏ release cũ
ps -ef | grep -E '[q]ueue:work|[h]orizon'
readlink -f /var/www/example.com/current

Payload queue cũng cần backward-compatible. Job được dispatch từ release cũ có thể được xử lý sau deploy bởi release mới; nếu class, property hoặc format thay đổi không tương thích, retry sẽ thất bại. Đừng xóa class job cũ ngay trong release đầu tiên nếu queue còn job chưa xử lý.

7. Rollback code và rollback database: hai việc khác nhau

Tình huốngHành độngKhông nên làm
Lỗi PHP sau deployĐổi symlink về release trước, reload worker, giữ schema additiveXóa ngay cột mới để “đồng bộ”
Migration fail trước khi switchDừng pipeline, xem lock/SQL, sửa migration rồi chạy lạiTiếp tục đổi traffic khi schema chưa rõ
Dữ liệu đã backfillGiữ schema mới, rollback code tương thích hoặc forward-fixChạy down() mù quáng trên production
Cột bị xóa nhầmDừng ghi, restore backup hoặc point-in-time recoveryTạo lại cột rỗng và nghĩ dữ liệu đã trở lại

Rollback an toàn thường là rollback application trước, không rollback schema ngay. Nếu schema đã expand nhưng code cũ vẫn chạy được, bạn có thể ổn định dịch vụ rồi quyết định bước cleanup. Khi lỗi là dữ liệu, cần quy trình khôi phục database riêng; Git không thể phục hồi dữ liệu đã bị xóa khỏi MySQL.

8. Checklist deploy Laravel không downtime

  • Tạo backup và kiểm tra restore trước thay đổi schema có rủi ro.
  • Chạy test migration trên bản sao dữ liệu hoặc staging gần production.
  • Xác nhận release mới chạy được với schema hiện tại.
  • Tách expand, backfill, switch và contract thành các bước quan sát được.
  • Kiểm tra transaction dài, metadata lock, disk space và thời gian ALTER.
  • Build dependency/frontend trước khi đổi symlink; không build trực tiếp trong thư mục đang phục vụ.
  • Chạy migration với log, timeout và cơ chế lock deploy để tránh hai pipeline chạy đồng thời.
  • Reload queue worker/Horizon sau khi release mới active.
  • Kiểm tra health route, error rate, queue backlog và các endpoint quan trọng.
  • Giữ release cũ và schema additive đủ lâu để rollback code có ý nghĩa.

9. Những hiểu lầm thường gặp

Zero downtime nghĩa là không cần maintenance window

Không đúng. Một migration online vẫn có thể chờ metadata lock hoặc tiêu thụ CPU, I/O và disk. Maintenance window có giám sát vẫn hữu ích cho thay đổi lớn.

Có migrate:rollback là luôn khôi phục được database

Rollback chỉ chạy logic `down()` của migration theo batch/step. Nó không phục hồi dữ liệu đã bị drop, không hoàn tác side effect bên ngoài và có thể không phù hợp khi schema đã phục vụ release mới.

Còn config cache, asset build, worker, permission, health check và migration. Một release chỉ hoàn tất khi các lớp này cùng được kiểm tra.

10. Kết luận

Deploy Laravel không downtime là bài toán phối hợp giữa release management, schema compatibility, worker lifecycle và quan sát production. Hãy ưu tiên migration additive, backfill theo batch, kiểm tra metadata lock, reload worker và giữ rollback code độc lập với cleanup schema. Khi cần thay đổi destructive, hãy xem backup/restore và forward-fix là một phần bắt buộc của thiết kế, không phải việc xử lý sau sự cố.

Nguồn tham khảo

[1] Laravel Documentation — Deployment; [2] Laravel Documentation — Database Migrations; [3] Laravel Documentation — Horizon; [4] MySQL 8.4 Reference Manual — Online DDL Operations.