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ần | Có thể chạy song song trong deploy? | Rủi ro cần xử lý |
|---|---|---|
| HTTP request release cũ/mới | Có, trong thời gian chuyển traffic | Schema phải tương thích cả hai phiên bản |
| Queue worker cũ/mới | Có thể tồn tại đồng thời | Payload và class phải tương thích khi retry |
| Migration additive | Thường an toàn hơn | Default, index, lock và dung lượng |
| Migration destructive | Không nên gộp vào bước đầu | Rollback không khôi phục dữ liệu |
| Backfill dữ liệu | Nên chạy riêng theo batch | Tả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.
2. Mô hình release directory và symlink
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 || trueKhô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ạn | Ví dụ | Mục tiêu |
|---|---|---|
| Expand | Thêm `display_name` nullable | Release cũ vẫn chạy với `name` |
| Dual read/write | Ghi cả name và display_name tạm thời | Giữ dữ liệu nhất quán khi chuyển code |
| Backfill | Copy name sang display_name theo batch | Không khóa table quá lâu |
| Switch | Release mới đọc display_name | Theo dõi error, null và queue |
| Contract | Xóa name trong deploy sau | Khô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=1Khô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/currentPayload 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ống | Hành động | Không nên làm |
|---|---|---|
| Lỗi PHP sau deploy | Đổi symlink về release trước, reload worker, giữ schema additive | Xóa ngay cột mới để “đồng bộ” |
| Migration fail trước khi switch | Dừng pipeline, xem lock/SQL, sửa migration rồi chạy lại | Tiếp tục đổi traffic khi schema chưa rõ |
| Dữ liệu đã backfill | Giữ schema mới, rollback code tương thích hoặc forward-fix | Chạy down() mù quáng trên production |
| Cột bị xóa nhầm | Dừng ghi, restore backup hoặc point-in-time recovery | Tạ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.
Đổi symlink là đủ để deploy
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.