Migration Postgres Không Gián Đoạn Bằng NOT VALID

Cách thêm Foreign Key và ràng buộc CHECK trên bảng PostgreSQL lớn an toàn tuyệt đối, tránh nghẽn lock với NOT VALID và VALIDATE CONSTRAINT.

Migration Postgres Không Gián Đoạn Bằng NOT VALID
Trong bài viết này

Chạy một câu lệnh ALTER TABLE ADD CONSTRAINT thông thường trên bảng dữ liệu chứa hàng chục triệu bản ghi là một trong những nguyên nhân phổ biến nhất gây sập hệ thống cơ sở dữ liệu trong các đợt phát hành ứng dụng. Khi khai báo khóa ngoại (Foreign Key) hoặc ràng buộc kiểm tra (CHECK) theo cách mặc định, PostgreSQL sẽ quét toàn bộ dữ liệu lịch sử trong khi duy trì một khóa bảng chặt chẽ. Trong môi trường có lưu lượng truy cập cao, hành vi này tạo ra hiệu ứng dồn hàng đợi khóa (lock queue cascade), khiến connection pool của ứng dụng cạn kiệt chỉ trong vài giây và làm tê liệt các dịch vụ liên quan.

PostgreSQL cung cấp một giải pháp kỹ thuật chuẩn mực bằng cách tách quy trình tạo ràng buộc làm hai giai đoạn độc lập: đăng ký siêu dữ liệu tức thì với NOT VALID, sau đó xác thực dữ liệu nền với VALIDATE CONSTRAINT.

Bản Chất Của Bẫy Nghẽn Khóa Trong PostgreSQL

Khi bạn thêm một ràng buộc khóa ngoại vào bảng hiện có bằng cú pháp thông thường, PostgreSQL phải đảm bảo hai điều kiện khắt khe. Thứ nhất, mọi bản ghi mới được thêm hoặc sửa đổi trong tương lai phải tuân thủ quan hệ dữ liệu. Thứ hai, toàn bộ bản ghi lịch sử đã tồn tại từ trước trên đĩa cứng cũng phải hợp lệ. Để kiểm tra toàn bộ dữ liệu cũ, hệ quản trị bắt buộc phải thực hiện quét tuần tự toàn bộ bảng (sequential scan).

-- Cách làm trực tiếp dễ gây nghẽn hệ thống
ALTER TABLE orders 
  ADD CONSTRAINT fk_orders_user_id 
  FOREIGN KEY (user_id) REFERENCES users(id);

Trong suốt quá trình thực thi câu lệnh này, PostgreSQL áp đặt khóa SHARE ROW EXCLUSIVE lên cả bảng con lẫn bảng được tham chiếu. Đối với các ràng buộc CHECK, mức độ khóa thậm chí còn nghiêm ngặt hơn là ACCESS EXCLUSIVE.

Trong lúc database engine bận đọc hàng chục gigabyte dữ liệu từ ổ đĩa để kiểm tra, tất cả thao tác INSERT, UPDATE, hay DELETE từ phía người dùng gửi tới bảng đó đều bị chặn và đẩy vào hàng đợi chờ xử lý. Do cơ chế cấp phát khóa của PostgreSQL hoạt động theo nguyên tắc vào trước ra trước (FIFO), ngay cả các truy vấn đọc SELECT nhẹ nhàng đến sau cũng sẽ bị xếp hàng phía sau các transaction ghi đang chờ. Chỉ sau 5–10 giây, toàn bộ connection pool từ ứng dụng (như Node.js, Go, hay Django) sẽ chạm ngưỡng tối đa, máy chủ web bắt đầu trả về lỗi gateway timeout (504), và hệ thống rơi vào tình trạng ngưng trệ diện rộng.

Chiến Lược 2 Bước An Toàn Tuyệt Đối

Sơ đồ hàng đợi khóa PostgreSQL minh họa khóa bảng chặn các truy vấn ghi đồng thời

Giải pháp chuẩn xác cho kỹ thuật di chuyển schema không downtime là phân tách rõ ràng giữa việc bắt buộc áp dụng cho dữ liệu mới và việc quét kiểm tra dữ liệu cũ.

Bước 1: Gắn Ràng Buộc Với Tùy Chọn NOT VALID

Trong bước migration đầu tiên, hãy thêm từ khóa NOT VALID vào định nghĩa ràng buộc:

-- Bước 1: Khóa metadata trong vài phần nghìn giây
SET lock_timeout = '2s';

ALTER TABLE orders 
  ADD CONSTRAINT fk_orders_user_id 
  FOREIGN KEY (user_id) REFERENCES users(id) 
  NOT VALID;

Khi thực thi câu lệnh trên, PostgreSQL chỉ cập nhật các bảng danh mục hệ thống nội bộ (pg_constraint). Nó hoàn toàn bỏ qua việc quét kiểm tra toàn bộ bảng. Nhờ đó, khóa bảng chỉ được giữ trong vài mili-giây và giải phóng ngay lập tức.

Điểm then chốt là ngay sau khi transaction này commit, PostgreSQL sẽ lập tức áp dụng ràng buộc cho tất cả câu lệnh INSERT và UPDATE mới phát sinh. Không một bản ghi sai lệch nào có thể lọt vào hệ thống kể từ thời điểm này trở đi. Ứng dụng hoàn toàn yên tâm về tính nhất quán của dữ liệu mới.

Bước 2: Xác Thực Ràng Buộc Ở Tiến Trình Riêng

Sau khi ràng buộc đã được ghi nhận vào hệ thống, bạn thực hiện việc xác thực dữ liệu lịch sử trong một transaction hoặc bước triển khai hoàn toàn riêng biệt:

-- Bước 2: Quét dữ liệu nền mà không chặn ghi
ALTER TABLE orders 
  VALIDATE CONSTRAINT fk_orders_user_id;

Lệnh VALIDATE CONSTRAINT chỉ yêu cầu mức khóa nhẹ SHARE UPDATE EXCLUSIVE. Khóa này chỉ xung đột với các lệnh thay đổi cấu trúc bảng khác hoặc lệnh dọn dẹp hệ thống như VACUUM FULL; nó cho phép tất cả thao tác SELECT, INSERT, UPDATE, và DELETE tiếp tục diễn ra hoàn toàn bình thường trong suốt quá trình quét dữ liệu.

Nếu bảng có 50 triệu dòng, việc quét kiểm tra có thể mất vài phút xử lý I/O đĩa, nhưng người dùng và ứng dụng hoàn toàn không nhận thấy bất kỳ sự suy giảm hiệu năng hay gián đoạn nào. Khi hoàn tất, PostgreSQL đánh dấu cờ conisvalidated = true trong catalog hệ thống, cho phép bộ tối ưu hóa truy vấn (query planner) hoạt động với đầy đủ hiệu năng như một ràng buộc thông thường.

Xử Lý Ràng Buộc NOT NULL An Toàn

Thông thường, câu lệnh ALTER TABLE orders ALTER COLUMN user_id SET NOT NULL đòi hỏi khóa ACCESS EXCLUSIVE trong toàn bộ thời gian quét bảng. Trên các hệ thống lớn, giải pháp thay thế an toàn và tương thích rộng rãi được nhiều đội ngũ kỹ thuật áp dụng là sử dụng ràng buộc CHECK có điều kiện:

-- 1. Thêm ràng buộc kiểm tra không null mà không quét toàn bảng
ALTER TABLE orders 
  ADD CONSTRAINT chk_orders_user_id_not_null 
  CHECK (user_id IS NOT NULL) 
  NOT VALID;

-- 2. Xác thực dữ liệu cũ chạy ngầm song song
ALTER TABLE orders 
  VALIDATE CONSTRAINT chk_orders_user_id_not_null;

Ràng buộc CHECK (cột IS NOT NULL) sau khi xác thực hợp lệ cung cấp tính toàn vẹn dữ liệu hoàn toàn tương đương với thuộc tính NOT NULL ở cấp cột, trong khi loại trừ triệt để rủi ro nghẽn khóa ứng dụng.

Bốn Nguyên Tắc Khi Triển Khai Production

Để đảm bảo quá trình nâng cấp cơ sở dữ liệu diễn ra trơn tru trong pipeline CI/CD, hãy ghi nhớ bốn nguyên tắc thực chiến sau:

  1. Luôn đặt lock_timeout: Trước khi chạy bất kỳ câu lệnh DDL nào, hãy thiết lập SET lock_timeout = '2s'. Nếu có một truy vấn phân tích hoặc transaction dài đang chiếm khóa, lệnh migration sẽ tự hủy nhanh chóng thay vì xếp hàng chờ và chặn đứng mọi luồng xử lý khác trong hàng đợi FIFO.
  2. Tuyệt đối không gộp chung vào một transaction: Đặt cả hai câu lệnh ADD CONSTRAINT ... NOT VALID và VALIDATE CONSTRAINT trong cùng một khối BEGIN ... COMMIT sẽ làm mất đi toàn bộ lợi ích, vì PostgreSQL sẽ giữ mức khóa cao nhất cho đến khi toàn bộ khối transaction kết thúc.
  3. Làm sạch dữ liệu cũ trước khi xác thực: Nếu trong bảng lịch sử vẫn còn tồn tại bản ghi vi phạm ràng buộc, lệnh VALIDATE CONSTRAINT sẽ báo lỗi và hủy thao tác. Hãy chạy các script cập nhật dữ liệu theo từng lô nhỏ (batching) vào khung giờ thấp điểm trước khi kích hoạt lệnh xác thực.
  4. Đánh chỉ mục cho cột khóa ngoại: PostgreSQL không tự động tạo index cho các cột foreign key. Hãy luôn kết hợp tạo index bằng cú pháp CREATE INDEX CONCURRENTLY trên cột khóa ngoại để tránh quét toàn bảng khi có thao tác xóa hoặc cập nhật ở bảng cha.

Chủ động làm chủ cơ chế NOT VALID giúp biến những đợt thay đổi schema rủi ro cao thành các thao tác tự động, an toàn và êm ái trên hệ thống production.

Nguồn tham khảo

  1. postgresql.org
  2. postgresql.org

Có hỗ trợ AI · PKN duyệt lại

0

Phản hồi

Đang tải bình luận…