Họ đang đo gì
Bạn có từng chạy migration trên hệ thống thật chưa, và có nghĩ tới trạng thái “code cũ và code mới cùng chạy” không.
Trả lời ngắn~30 giây
Không đổi tên. Làm expand–contract: thêm cột mới, ghi cả hai cột ở phiên bản code tiếp theo, backfill theo lô, chuyển đọc sang cột mới, rồi ở một lần deploy sau nữa mới bỏ cột cũ. Lý do bắt buộc phải nhiều bước là trong lúc rolling deploy luôn có khoảng thời gian code cũ và code mới cùng gọi vào một database — nên schema phải tương thích với CẢ HAI.
Giải thích sâu
Bước hay bị làm sai là backfill. UPDATE users SET new_col = old_col trên 200 triệu hàng là một transaction khổng lồ: nó giữ khoá, thổi phồng WAL, và ở Postgres còn tạo ra 200 triệu tuple chết khiến autovacuum vật lộn nhiều giờ sau đó. Cách đúng là chia lô theo khoá chính, mỗi lô vài nghìn hàng, commit từng lô, có nghỉ giữa các lô, và theo dõi replica lag để dừng khi bản sao tụt lại.
Chi tiết engine rất đáng nói: ALTER TABLE … ADD COLUMN có DEFAULT là thao tác tức thời trên Postgres 11 trở lên vì default được lưu ở metadata chứ không viết lại bảng. Trước đó thì nó viết lại toàn bộ bảng dưới khoá ACCESS EXCLUSIVE — tức là downtime. Biết ranh giới phiên bản này là khác biệt giữa “migration chạy trong 5ms” và “site sập 40 phút”.
Điều cần nói thêm ở mức senior là hàng đợi khoá. Ngay cả một DDL nhanh cũng phải XIN được ACCESS EXCLUSIVE, và nếu có một truy vấn dài đang chạy, DDL của bạn xếp hàng chờ — rồi mọi truy vấn tới sau lại xếp hàng sau DDL. Một ALTER 5ms có thể chặn cả bảng trong hai phút theo cách đó. Cách phòng là đặt lock_timeout ngắn và thử lại, thay vì để nó chờ vô hạn.
Bước contract cũng cần kỷ luật: xoá cột cũ chỉ nên làm sau khi bạn CHẮC không còn phiên bản nào đang chạy đọc nó — nghĩa là sau khi rollback window đã đóng. Xoá quá sớm biến một lần rollback bình thường thành sự cố, vì bản cũ vừa được khôi phục sẽ query một cột không còn tồn tại.
Backfill theo lô, có phanh
-- Lặp cho tới khi hết hàng, mỗi vòng là một transaction riêng
WITH batch AS (
SELECT id FROM users
WHERE new_email IS NULL AND email IS NOT NULL
ORDER BY id
LIMIT 5000
FOR UPDATE SKIP LOCKED
)
UPDATE users u SET new_email = u.email
FROM batch b WHERE u.id = b.id;
-- Giữa các lô: nghỉ, và dừng nếu bản sao tụt quá xa
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;Câu hỏi tiếp theo họ sẽ hỏi
?Thêm NOT NULL cho cột mới thì sao?
Đừng thêm trực tiếp — nó quét toàn bảng để kiểm tra dưới khoá. Cách rẻ hơn: thêm CHECK (col IS NOT NULL) NOT VALID (tức thời), chạy VALIDATE CONSTRAINT (chỉ khoá nhẹ), rồi mới đặt NOT NULL, lúc này Postgres tin ràng buộc đã kiểm và không quét lại.
?Tạo index trên bảng đang chạy?
CREATE INDEX CONCURRENTLY. Nó không chặn ghi, đổi lại chạy lâu hơn, không nằm trong transaction được, và có thể để lại index hỏng nếu thất bại — nên phải kiểm tra pg_index.indisvalid sau đó và xoá đi nếu hỏng.
?Làm sao biết không còn code nào đọc cột cũ?
Không đoán. Đổi tên cột cũ thành old_email_deprecated trước một nhịp deploy, hoặc bật pg_stat_statements và tìm truy vấn còn nhắc tới nó. Im lặng vài ngày rồi mới xoá.
Trả lời thế này là mất điểm
- Trả lời “chạy
ALTER TABLE … RENAME COLUMN, nó nhanh mà”. Nhanh thật, và nó làm hỏng mọi instance code cũ đang chạy ngay giây đó. - Không nhắc tới rollback. Migration chỉ đi được một chiều là migration bạn không dám deploy chiều tối thứ Sáu.
- Backfill bằng một câu UPDATE duy nhất. Trên bảng 200 triệu hàng đó là sự cố chứ không phải migration.
Trả lời thế này là ghi điểm
- Chủ động tách “deploy code” khỏi “chạy migration” và nói rõ thứ tự an toàn của từng loại thay đổi.