8 phút

Thay đổi schema không gián đoạn với mẫu expand/contract

Lập kế hoạch và triển khai thay đổi schema không gián đoạn với mẫu expand/contract, backfill an toàn, bản phát hành tương thích, xác minh và rollback.

Thay đổi schema không gián đoạn với mẫu expand/contract

Vì sao thay đổi schema gây gián đoạn

Thay đổi schema gây gián đoạn khi các phiên bản ứng dụng, worker nền và cơ sở dữ liệu không còn thống nhất về cấu trúc và giá trị hợp lệ. Sự cố có thể lộ rõ, chẳng hạn mọi request đều trả lỗi, hoặc diễn ra dần dần như độ trễ truy vấn tăng, ghi thất bại, replica bị chậm và hàng đợi job cần chạy lại.

Một lần triển khai production hiếm khi thay đổi mọi tiến trình cùng lúc. Triển khai cuốn chiếu khiến các instance ứng dụng cũ và mới chạy song song. Worker chạy lâu có thể giữ bản build cũ hàng giờ, client di động có thể vẫn hoạt động trong nhiều tháng, còn job báo cáo hoặc tích hợp có thể dùng bảng mà không đi qua ứng dụng chính. Tất cả đều dùng chung một cơ sở dữ liệu.

Các lỗi thường gặp gồm:

  • Mã mới ghi vào một cột trước khi migration tạo cột đó hoàn tất.
  • Mã cũ đọc bảng hoặc cột mà bản phát hành sau đã đổi tên hoặc xóa.
  • Việc viết lại bảng, backfill hoặc tạo index tiêu tốn đủ I/O và CPU để làm chậm lưu lượng thông thường.
  • Một lệnh schema chờ khóa trong khi request xếp hàng phía sau.
  • Một ràng buộc mới từ chối dữ liệu ghi từ tiến trình chưa được nâng cấp.

Điều nguy hiểm thường nằm ở lúc lấy khóa, không phải thời gian thực thi trên lý thuyết. Một lệnh ALTER TABLE nhanh có thể phải chờ sau giao dịch dài. Trong lúc chờ, các truy vấn sau đó có thể xếp hàng sau khóa schema đang chờ, biến một migration nhỏ thành tình trạng đình trệ toàn ứng dụng.

Không gián đoạn đòi hỏi mọi trạng thái trung gian của cơ sở dữ liệu vẫn dùng được với mọi phiên bản ứng dụng còn có thể chạy. Hãy thêm cấu trúc tương thích trước, chuyển lưu lượng và dữ liệu theo các bước có kiểm soát, rồi chỉ bỏ đường cũ khi thành phần tiêu thụ cuối cùng đã biến mất.

Cách làm này phù hợp với hệ thống có lưu lượng trực tiếp, triển khai cuốn chiếu, mục tiêu sẵn sàng cao hoặc quy trình khôi phục tốn kém. Một công cụ nội bộ nhỏ với cơ sở dữ liệu ít hoạt động có thể phù hợp hơn với cửa sổ bảo trì đã được kiểm thử. Quyết định nên phản ánh chi phí của sự cố và độ phức tạp vận hành của migration.

Expand/contract theo cách dễ hiểu

Mẫu expand/contract biến một thay đổi không tương thích thành chuỗi bản phát hành tương thích. Cơ sở dữ liệu tạm thời hỗ trợ hai cách biểu diễn trong khi mã và dữ liệu chuyển từ cách cũ sang cách mới.

Chuỗi này có ba phần:

  • Mở rộng bằng cách thêm cột, bảng, index hoặc ràng buộc mà không xóa bất cứ thứ gì mã hiện tại cần.
  • Chuyển đổi bằng cách triển khai mã tương thích, chuyển dữ liệu lịch sử và hướng thao tác đọc, ghi sang cách biểu diễn mới.
  • Thu hẹp bằng cách xóa mã cũ và đối tượng cơ sở dữ liệu sau khi việc xác minh chứng minh chúng không còn được dùng.

Giả sử một bảng PostgreSQL lưu tên đầy đủ của một người trong full_name, còn ứng dụng cần các trường first_namelast_name riêng. Giai đoạn mở rộng thêm các cột cho phép NULL và vẫn giữ full_name. Một bản phát hành tương thích ghi các cách biểu diễn cần thiết trong giai đoạn chuyển đổi. Backfill tách các giá trị hiện có, với chính sách rõ ràng cho những tên không thể tách đáng tin cậy. Chỉ chuyển thao tác đọc khi các trường mới đã đủ đầy.

Thứ tự này phù hợp với triển khai cuốn chiếu vì bản build cũ vẫn tìm thấy full_name, còn bản build mới tìm thấy cả ba cột. Nó cũng giữ được đường rollback ứng dụng. Nếu bản phát hành mới hoạt động sai, bản build trước vẫn chạy được vì các phụ thuộc schema của nó chưa bị xóa.

Rollback cơ sở dữ liệu khác rollback ứng dụng. Đảo ngược migration sau khi dữ liệu đã biến đổi có thể làm mất thông tin hoặc khôi phục giá trị lỗi thời. Trong giai đoạn chuyển đổi, nên đưa lưu lượng ứng dụng về cách biểu diễn đã biết, còn các đối tượng cơ sở dữ liệu được thêm vào vẫn giữ nguyên. Hãy sửa migration theo hướng đi tới sau khi sự cố ổn định.

Mẫu này không có nghĩa mọi thay đổi đều cần mã ghi kép. Thêm một cột tùy chọn mà chỉ mã mới dùng có thể chỉ cần một migration bổ sung và một lần triển khai. Đổi tên, thay đổi cách biểu diễn, tách bảng và thay đổi trường bắt buộc thường cần nhiều giai đoạn hơn vì hai phiên bản ứng dụng không thể dùng chung schema một cách an toàn nếu không làm vậy.

Phân loại thay đổi trước khi chọn các bước

Kế hoạch migration cần phù hợp với rủi ro thực tế về khóa, viết lại bảng, khả năng tương thích và chuyển đổi dữ liệu của thao tác. Coi mọi ALTER TABLE là giống nhau sẽ dẫn đến quy trình rườm rà không cần thiết hoặc bản phát hành không an toàn.

Thay đổi bổ sung thường dễ nhất. Một cột cho phép NULL, bảng riêng hoặc index được tạo bằng phương thức trực tuyến thường có thể được đưa vào trước khi mã ứng dụng dùng đến. Lệnh vẫn cần khóa, vì vậy hãy kiểm thử hành vi của nó trên bảng và khối lượng giao dịch tương tự production.

Thay đổi phá hủy gồm xóa hoặc đổi tên cột, thu hẹp kiểu dữ liệu, thay bảng và thêm ràng buộc chặt hơn. Những thay đổi này làm mất hiệu lực giả định của mã hiện có. Hãy để chúng ở giai đoạn thu hẹp, sau khi đã xóa tham chiếu mã và các thành phần tiêu thụ bên ngoài.

Các thao tác thay đổi dữ liệu cần được đánh giá riêng. Chuyển đổi timestamp, chuẩn hóa số điện thoại, gộp bản ghi hoặc tách văn bản tự do có thể làm mất thông tin. Hãy xác định cách xử lý giá trị không hợp lệ và mơ hồ trước khi bắt đầu backfill. Nếu biến đổi không thể đảo ngược, hãy giữ dữ liệu nguồn đến khi kết quả qua kiểm tra ở cấp độ nghiệp vụ.

Một lần rà soát trước khi chạy nên trả lời năm câu hỏi:

  • Mỗi câu lệnh yêu cầu loại khóa nào, có thể chờ hoặc giữ khóa đó bao lâu?
  • Thao tác có viết lại bảng, tạo nhiều WAL hoặc làm replica chậm không?
  • Ứng dụng, job, báo cáo và thành phần change-data-capture nào dùng các đối tượng bị ảnh hưởng?
  • Cả bản phát hành hiện tại lẫn bản đề xuất có thể chạy với mọi trạng thái chuyển tiếp không?
  • Tín hiệu nào sẽ dừng thao tác, và trạng thái chính xác nào còn lại sau khi dừng?

Hãy chạy đúng migration đó trên dữ liệu có quy mô và phân bố thực tế. Một bảng kiểm thử với một nghìn hàng gọn gàng nói lên rất ít về bảng production có hàng trăm triệu hàng, tuple rộng, hàng chết, giá trị lệch phân bố và giao dịch chạy lâu.

Mở rộng an toàn trong PostgreSQL

Một đợt mở rộng PostgreSQL an toàn dùng thay đổi metadata ngắn, thời gian chờ khóa có giới hạn và các thao tác trực tuyến riêng khi cơ sở dữ liệu yêu cầu. Thêm cấu trúc mới trước khi triển khai mã phụ thuộc vào nó.

Thêm cột cho phép NULL không có giá trị mặc định thường là thao tác metadata ngắn:

BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

Timeout giúp bản phát hành không chờ vô hạn phía sau một giao dịch đang mở. Nếu không lấy được khóa kịp thời, hãy để migration thất bại, kiểm tra tác nhân chặn và thử lại vào thời điểm an toàn hơn. Đừng tự động thử lại trong vòng lặp dày đặc vì các yêu cầu khóa lặp lại có thể liên tục ảnh hưởng đến lưu lượng production.

Các bản PostgreSQL hiện đại có thể thêm cột với giá trị mặc định hằng số mà không cần ghi ngay giá trị đó vào mọi hàng hiện có. Tối ưu này không khiến mọi giá trị mặc định đều vô hại. Biểu thức biến động có thể đòi hỏi viết lại bảng, và ALTER TABLE vẫn cần khóa ACCESS EXCLUSIVE trong thời gian ngắn. Hãy xác nhận hành vi theo phiên bản PostgreSQL đang triển khai và biểu thức cụ thể, thay vì dựa vào quy tắc chung.

CREATE INDEX thông thường có thể chặn ghi. Dùng tạo đồng thời khi bảng phải tiếp tục ghi được:

CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);

CREATE INDEX CONCURRENTLY không thể chạy trong transaction block. Nó mất nhiều thời gian hơn, thực hiện thêm công việc và có thể chờ các giao dịch cũ, nhưng insert, update và delete thông thường vẫn tiếp tục được. Nó vẫn dùng CPU, I/O và WAL, nên hãy theo dõi độ trễ cơ sở dữ liệu và replica khi chạy.

Một lần tạo đồng thời thất bại có thể để lại index không hợp lệ. Hãy kiểm tra trạng thái index trước khi thử lại, rồi chủ động xóa hoặc tạo lại đối tượng không hợp lệ. Công cụ migration bọc mọi tệp trong transaction cần có chế độ không dùng transaction được hỗ trợ cho thao tác index đồng thời.

Bảng mới thường dễ đưa vào hơn biến đổi tại chỗ. Với quan hệ một-nhiều hoặc nhiều-nhiều, hãy thêm bảng đích và các index của nó trong khi vẫn giữ cột nguồn. Trì hoãn xóa nguồn đến khi dữ liệu ghi mới, dữ liệu lịch sử, thao tác đọc và các thành phần tiêu thụ phía dưới đã chuyển xong.

Thay đổi kiểu cần cẩn trọng hơn. Một số chỉ thay đổi metadata, trong khi số khác viết lại mọi hàng hoặc giữ khóa hạn chế quá lâu. Với chuyển đổi rủi ro, hãy thêm cột có kiểu đích, điền theo lô, chuyển truy cập ứng dụng, rồi xóa cột gốc sau. Cách này cũng cho đội ngũ nơi ghi lại lỗi chuyển đổi thay vì buộc một lệnh ALTER COLUMN TYPE lớn phải thành công hoặc thất bại như một khối.

Triển khai mã vẫn tương thích

Mã ứng dụng tương thích chấp nhận các giá trị chuyển tiếp còn thiếu và không bao giờ đòi hỏi migration phá hủy trong cùng đợt rollout. Mở rộng cơ sở dữ liệu nên hoàn tất trước khi instance ứng dụng đầu tiên bắt đầu dùng đối tượng mới.

Ghi kép hữu ích khi cả hai cách biểu diễn phải luôn được cập nhật. Hãy thực hiện cả hai lượt ghi trong cùng giao dịch cơ sở dữ liệu nếu có thể. Lượt ghi thứ hai bất đồng bộ có thể thất bại sau khi lượt đầu thành công, tạo ra sai lệch mà các lần đọc sau có thể lộ ra.

Logic ghi kép cũng cần một nguồn quyết định. Nếu phone_e164 được suy ra từ phone, hãy xác định đầu vào nào được ưu tiên khi cả hai cùng được cung cấp và áp dụng cùng cách chuẩn hóa trong API handler, worker, import và công cụ quản trị. Nếu không, hai đường mã đều có vẻ đúng có thể lưu kết quả khác nhau.

Nên chuyển thao tác đọc muộn hơn thao tác ghi. Hãy tiếp tục đọc trường đã ổn định trong khi dữ liệu ghi mới điền cả hai dạng và backfill xử lý hàng lịch sử. Sau khi xác minh, triển khai đường đọc ưu tiên trường mới và chỉ dùng giá trị cũ theo quy tắc fallback đã xác định. Hãy đo mức sử dụng fallback. Fallback im lặng có thể che giấu dữ liệu chưa hoàn chỉnh mãi mãi.

Một chuỗi phát hành điển hình:

  • Bản phát hành 1 thêm các đối tượng cơ sở dữ liệu mới mà không đổi hành vi ứng dụng.
  • Bản phát hành 2 ghi các cách biểu diễn chuyển tiếp nhưng tiếp tục đọc theo cách đã ổn định.
  • Bản phát hành 3 chuyển thao tác đọc sau khi backfill và kiểm tra nhất quán đạt yêu cầu.
  • Bản phát hành 4 ngừng duy trì cách biểu diễn cũ khi tiêu chí rollback hết hiệu lực.
  • Bản phát hành 5 xóa tham chiếu mã cũ, sau đó mới dọn dẹp cơ sở dữ liệu.

Hãy tách hợp đồng API công khai khỏi thay đổi schema vật lý. Đổi tên cột cơ sở dữ liệu không buộc phải đổi tên trường ngay trong phản hồi web, di động hoặc tích hợp. Thay đổi các hợp đồng đó theo chính sách tương thích riêng, đặc biệt khi client không thể nâng cấp cùng server.

Hãy lập danh sách mọi tác nhân ghi. HTTP handler chỉ là một nguồn thay đổi. Consumer hàng đợi, job theo lịch, script import, công cụ sửa dữ liệu, trigger cơ sở dữ liệu và thao tác quản trị trực tiếp có thể tiếp tục tạo các hàng theo hình dạng cũ. Khi phù hợp, hãy gắn tên ứng dụng cho kết nối cơ sở dữ liệu và ghi log việc dùng đường chuyển tiếp để tiến trình bị bỏ sót trở nên rõ ràng.

Tiến trình chạy lâu có thể giữ các giả định cũ qua prepared statement, metadata cache hoặc lớp object-relational mapping. Hãy kiểm thử restart cuốn chiếu và hành vi connection pool trước giai đoạn thu hẹp. Một tiến trình chưa nhận lưu lượng gần đây vẫn có thể lỗi ở lần đầu một job hiếm chạy.

Backfill dữ liệu mà không làm quá tải cơ sở dữ liệu

Chụp snapshot trước khi sửa schema
Chụp snapshot trước các thay đổi rủi ro để có thể hoàn nguyên nhanh khi cần.

Backfill an toàn cập nhật các lô nhỏ, có thể tiếp tục và giảm tốc khi sức khỏe production xấu đi. Chỉ bắt đầu sau khi các tác nhân ghi trực tiếp có thể duy trì cách biểu diễn mới.

Chọn lô theo thời gian chạy và tác động lên cơ sở dữ liệu, không theo số hàng cố định. Một nghìn hàng hẹp có thể xong trong vài mili giây, trong khi một nghìn hàng chứa giá trị lớn hoặc biến đổi tốn kém có thể tạo ra I/O đáng kể. Hãy bắt đầu thận trọng và hướng tới giao dịch hoàn tất trong vài giây. Commit giữa các lô để khóa và phiên bản hàng cũ không tích tụ trong một giao dịch.

PostgreSQL không hỗ trợ trực tiếp ORDER BYLIMIT trên UPDATE thông thường. Hãy chọn một lô trong common table expression, rồi cập nhật các hàng đó:

WITH batch AS (
    SELECT id
    FROM my_table
    WHERE id > $1
      AND new_col IS NULL
    ORDER BY id
    LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
  AND target.new_col IS NULL
RETURNING target.id;

Ứng dụng lưu id hoàn tất lớn nhất làm cursor. Cập nhật có điều kiện giúp chạy lại vẫn idempotent, nên sự cố sau commit không làm hỏng các hàng đã xử lý. Hãy lưu tiến độ đủ cẩn thận để cursor không vượt qua lô chưa commit.

Cursor id tăng dần tránh quét lặp lại phần đầu bảng, nhưng không bắt được các chỉnh sửa đến muộn hoặc hàng được chèn dưới cursor. Hãy kết thúc bằng lượt xử lý bù trên mọi giá trị NULL còn lại. Nếu định danh không có thứ tự hoặc hàng có thể chuyển giữa các trạng thái đủ điều kiện, hãy dùng work table hoặc checkpoint rõ ràng khác thay vì cho rằng một lần quét tiến về phía trước là hoàn chỉnh.

Nhiều worker có thể nhận hàng với FOR UPDATE SKIP LOCKED, nhưng chạy song song tăng áp lực ghi và làm phức tạp việc theo dõi tiến độ. Đừng kết hợp các hàng bị bỏ qua với cursor tiến lên vĩnh viễn vượt qua chúng. Hàng đợi định danh đã nhận hoặc quét lại điều kiện phù hợp an toàn hơn cho worker song song.

Hãy điều tiết theo các chỉ số production như độ trễ truy vấn, số kết nối hoạt động, thời gian chờ khóa, lượng WAL tạo ra, độ trễ replay replica và số hàng chết tăng lên. Dừng khi vượt ngưỡng, rồi tiếp tục từ checkpoint. Sleep cố định đơn giản, nhưng phản hồi từ cơ sở dữ liệu đáp ứng tốt hơn với thay đổi lưu lượng.

Tránh thay đổi mọi hàng khi chỉ một số cần xử lý. Lọc theo trường mới, trạng thái nguồn hoặc marker migration. Nếu biến đổi tốn kém, hãy tính bên ngoài transaction update khi tính nhất quán cho phép, sau đó thực hiện ghi có điều kiện ngắn. Hãy giữ số lượng và mẫu các giá trị bị từ chối thay vì âm thầm bịa dữ liệu.

Autovacuum và replica phải hấp thụ công việc sau mỗi update. Backfill có thể hoàn tất thành công trên primary trong khi replica chậm rất xa hoặc table bloat làm giảm hiệu năng các truy vấn sau này. Giới hạn tốc độ nên tính cả chi phí chậm trễ đó, không chỉ thời gian chạy tức thì của lô.

Xác minh dữ liệu và lưu lượng production

Migration chỉ sẵn sàng để thu hẹp khi kiểm tra dữ liệu, telemetry ứng dụng và bằng chứng về phụ thuộc cùng xác nhận đường mới là nguồn chính. Chỉ số job hoàn tất không đủ để chứng minh tính đúng đắn.

Hãy bắt đầu bằng độ đầy đủ và tính nhất quán. IS DISTINCT FROM của PostgreSQL so sánh giá trị đồng thời xử lý rõ NULL, khác với <>, vốn cho kết quả không xác định khi một trong hai phía là NULL:

SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;

Đừng chạy lặp lại phép đếm toàn bảng không có index trên bảng rất lớn, đang bận. Hãy dùng xác minh có kiểm soát một lần, phạm vi định danh có giới hạn, mẫu hoặc quy trình xác minh tạm thời tiến dần qua bảng. Phương pháp phù hợp phụ thuộc vào chi phí của việc sai và năng lực còn trống của cơ sở dữ liệu.

Xác minh nên bao gồm:

  • Không còn giá trị thiếu ngoài dự kiến trong những hàng cần trường mới.
  • Giá trị mới khớp với biến đổi đã thống nhất, kể cả đầu vào sai định dạng và rỗng.
  • Hàng mới và update vẫn nhất quán sau khi hoàn tất lượt xử lý lịch sử.
  • Mức dùng fallback khi đọc đã đạt ngưỡng kế hoạch, thường là bằng không đối với lưu lượng do server kiểm soát.
  • Tỷ lệ lỗi, độ trễ truy vấn, khóa và độ trễ replica vẫn nằm trong giới hạn phát hành.

Hãy so sánh cả kết quả nghiệp vụ lẫn các cột. Nếu migration thay đổi giá, quyền, trạng thái tài khoản hoặc định danh, hãy xác minh tổng số và invariant mà người dùng phụ thuộc. Hai cột có thể khớp về mặt cơ học nhưng cùng mã hóa sai quy tắc nghiệp vụ.

Quan sát một chu kỳ vận hành đầy đủ trước khi dọn dẹp. Khoảng thời gian đúng dựa trên hành vi thực tế của hệ thống, không phải quy tắc một tuần cố định. Có thể cần bao gồm xử lý cuối tháng, job thanh toán hiếm, retry hàng đợi chậm hoặc vòng đời dài nhất của một client di động cũ. Ghi lại bằng chứng cho thấy từng thành phần tiêu thụ đã chuyển đổi.

Nếu kiến trúc ứng dụng cho phép, hãy canary việc chuyển đường đọc. Gửi một phần nhỏ lưu lượng sang đường đọc mới, so sánh kết quả và mở rộng dần. Giữ hành động rollback đơn giản: chuyển thao tác đọc về cách biểu diễn đã ổn định mà không đảo ngược backfill.

Thêm ràng buộc sau khi dữ liệu sẵn sàng

Ràng buộc chỉ nên trở nên nghiêm ngặt sau khi mọi tác nhân ghi tuân thủ và dữ liệu hiện có đã được xác minh. Áp dụng NOT NULL, check hoặc khóa ngoại trong giai đoạn mở rộng có thể chặn lưu lượng hoặc từ chối dữ liệu ghi từ tiến trình cũ.

PostgreSQL có thể thêm check constraint dưới dạng NOT VALID, áp dụng quy tắc cho hàng mới hoặc đã thay đổi mà không cần quét ngay mọi hàng lịch sử. Hãy xác minh riêng sau backfill:

ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;

ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;

Sau khi xác minh thành công, các phiên bản PostgreSQL được hỗ trợ có thể dùng bằng chứng đó khi đặt cột thành NOT NULL, tránh phải quét toàn bảng lần nữa. Thay đổi cuối vẫn cần khóa bảng mạnh, vì vậy hãy dùng lock timeout có giới hạn và kế hoạch thử lại:

ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;

ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;

Check tạm thời có thể giữ lại nếu có giá trị, nhưng giữ các ràng buộc tương đương chỉ làm catalog rối hơn mà không thay đổi quy tắc.

Khóa ngoại có thể theo chuỗi tương tự với NOT VALIDVALIDATE CONSTRAINT. Dữ liệu ghi mới được kiểm tra sau khi tạo ràng buộc, còn xác minh lịch sử diễn ra sau. Hãy chủ động thêm index hỗ trợ khi hành vi delete hoặc update của quan hệ được tham chiếu có thể dẫn đến quét tốn kém.

Xác thực ở ứng dụng nên có trước khi ép buộc ở cơ sở dữ liệu, nhưng không thay thế nó. Mã tạo lỗi rõ ràng hơn cho người dùng, còn cơ sở dữ liệu bảo vệ dữ liệu được ghi từ mọi đường. Trong rollout, hãy theo dõi vi phạm ràng buộc để tìm tác nhân ghi mà kiểm tra phụ thuộc đã bỏ sót.

Thu hẹp đường cũ một cách an toàn

Tạo mẫu nhanh với PostgreSQL
Tạo nhanh ứng dụng có Postgres và lặp lại schema một cách an toàn khi phát hành.

Giai đoạn thu hẹp nên xóa phụ thuộc ứng dụng trước khi xóa đối tượng cơ sở dữ liệu. Khi telemetry và xác minh cho thấy đường mới là nguồn chính, việc dọn dẹp có thể đi qua các bản phát hành riêng.

Trước hết ngừng đọc trường cũ và bỏ logic fallback. Sau đó tắt ghi vào nó và quan sát production đủ lâu để bắt các đường hiếm. Xóa feature flag, trigger, compatibility view, script sửa dữ liệu và job theo lịch đề cập đến cách biểu diễn cũ. Hãy tìm trong mã nguồn đã xuất và mã migration, đồng thời kiểm tra báo cáo, truy vấn tích hợp và cấu hình change-data-capture bên ngoài repository chính.

Thứ tự dọn dẹp an toàn:

  • Bỏ các lượt đọc fallback và xác nhận chúng không còn xuất hiện trong telemetry.
  • Ngừng ghi cũ và xóa mã đồng bộ hóa.
  • Xóa tham chiếu ứng dụng khỏi mọi phiên bản có thể triển khai.
  • Xóa index và ràng buộc lỗi thời bằng phương thức trực tuyến phù hợp.
  • Xóa cột hoặc bảng cũ trong một bản phát hành cơ sở dữ liệu sau.

Xóa cột PostgreSQL chủ yếu là thay đổi catalog, nhưng vẫn cần khóa ACCESS EXCLUSIVE. Vì vậy, một câu lệnh ngắn có thể chờ sau giao dịch dài và chặn công việc diễn ra sau đó. Hãy đặt lock timeout, kiểm tra trước các giao dịch chạy lâu và lên lịch thử vào thời điểm ít rủi ro hơn.

Dùng DROP INDEX CONCURRENTLY cho index lỗi thời khi không thể chấp nhận chặn ghi. Tương tự tạo đồng thời, nó không thể chạy trong transaction block và có các hạn chế mà công cụ migration phải xử lý.

Đừng gộp dọn mã và xóa vật lý trong một bản phát hành. Việc tách ra cho phép ứng dụng đã dọn vẫn chạy với cơ sở dữ liệu còn đối tượng không dùng. Nếu ứng dụng gặp vấn đề, rollback vẫn khả thi mà không cần tạo lại schema hoặc khôi phục dữ liệu.

Trước khi xóa bảng, hãy kiểm tra quyền sở hữu của sequence, view, function, grant, trigger, replication publication và truy vấn bên ngoài. Tránh dùng CASCADE như đường tắt trong migration production vì nó có thể xóa các phụ thuộc không thuộc thay đổi dự kiến.

Xử lý rollback và bước thất bại

Kế hoạch rollback nên xác định hành động an toàn cho từng giai đoạn thay vì dựa vào một migration xuống chung chung. Đối tượng bổ sung, di chuyển dữ liệu, chuyển đường đọc và xóa có đặc tính khôi phục khác nhau.

Nếu mở rộng không lấy được khóa, hãy giữ nguyên ứng dụng và thử lại sau khi xử lý giao dịch đang chặn. Nếu tạo index đồng thời thất bại, hãy kiểm tra xem có để lại index không hợp lệ không và dọn đúng đối tượng đó trước lần thử tiếp theo.

Nếu backfill tạo tải, hãy dừng nó. Các lô idempotent đã commit có thể giữ nguyên. Giảm kích thước lô hoặc tốc độ, xử lý biến đổi tốn kém và tiếp tục từ checkpoint. Hoàn tác hàng triệu update đúng thường chỉ tăng rủi ro mà không giúp production phục hồi.

Nếu đường đọc mới trả kết quả sai, hãy chuyển thao tác đọc về cách biểu diễn cũ trong khi giữ dữ liệu mới để chẩn đoán. Chỉ tiếp tục ghi kép khi chắc chắn chúng đúng. Khi chính tác nhân ghi bị lỗi, hãy tắt nó hoặc rollback ứng dụng trước khi sửa các hàng bị ảnh hưởng.

Sau giai đoạn thu hẹp, khôi phục có thể cần phục hồi dữ liệu chứ không chỉ triển khai bản build cũ. Hãy xác định rõ điểm không thể quay lại. Tạo bản sao lưu hoặc snapshot theo chính sách khôi phục của hệ thống, kiểm thử việc khôi phục trước phát hành và giữ đối tượng cũ trong khoảng lưu trữ đã thống nhất khi chi phí cho phép.

Lệnh schema có thể mang tính giao dịch, nhưng hiệu ứng bên ngoài không phải lúc nào cũng được bao phủ. Thao tác index đồng thời, thông điệp hàng đợi, thay đổi cache và triển khai ứng dụng không dùng chung một giao dịch nguyên tử. Runbook cần mô tả trạng thái có thể quan sát sau từng lỗi một phần và lệnh có thể tiếp tục an toàn từ trạng thái đó.

Tránh các bẫy migration thường gặp

Phần lớn migration không gián đoạn thất bại vì ép buộc trạng thái mới quá sớm hoặc quên một thành phần tiêu thụ trạng thái cũ. Các bẫy sau cần được rà soát rõ ràng trước khi phê duyệt.

  • Thêm NOT NULL khi instance ứng dụng cũ vẫn có thể bỏ qua trường.
  • Chạy backfill lớn trong một giao dịch, giữ khóa và phiên bản hàng quá lâu.
  • Đổi tên cột như thể đó là thay đổi bổ sung, dù mã cũ vẫn dùng tên gốc.
  • Chuyển thao tác đọc trước khi mọi đường ghi và hàng lịch sử điền cách biểu diễn mới.
  • Coi triển khai thành công là bằng chứng báo cáo, worker, replica và tích hợp tương thích.

Một lỗi tinh vi khác đến từ đồng bộ hai chiều. Trigger sao chép old_col sang new_col, trong khi mã ứng dụng sao chép new_col ngược về old_col. Khác biệt về chuẩn hóa hoặc thứ tự trigger có thể tạo vòng lặp, ghi đè giá trị có chủ đích hoặc làm quyền quyết định không rõ ràng. Hãy ưu tiên một chiều duy nhất và ghi rõ cách biểu diễn nào là nguồn chính trong từng bản phát hành.

Giá trị mặc định có thể che giấu việc tác nhân ghi chưa cập nhật. Nếu cột bắt buộc mới nhận giá trị mặc định rỗng hoặc chung chung, mã cũ có vẻ tương thích trong khi lưu dữ liệu sai về nghĩa. Hãy dùng giai đoạn cho phép NULL khi sự vắng mặt mang thông tin chẩn đoán hữu ích, rồi áp dụng quy tắc thực sau khi mọi tác nhân ghi cung cấp giá trị có ý nghĩa.

Feature flag tự nó không làm lệnh schema không tương thích trở nên an toàn. Đường mã bị tắt vẫn có thể được nạp, chuẩn bị hoặc chạy bởi tiến trình cũ. Đối tượng cơ sở dữ liệu phải được giữ lại đến khi không còn phiên bản nào đang hoạt động hoặc có thể triển khai tham chiếu nó.

Quyền sở hữu migration cũng quan trọng. Hãy giao cho một người hoặc đội ngũ theo dõi việc chuyển đổi đến hết giai đoạn thu hẹp, gồm cả ngày xác minh và xóa. Nếu không, cột tạm, flag và job đồng bộ có thể tồn tại nhiều tháng, làm tăng chi phí cho mọi thay đổi về sau.

Thay thế cột điện thoại mà không gián đoạn

Dùng Planning Mode cho migration
Lập bản đồ các bản phát hành, backfill và truy vấn xác minh trong Planning Mode của Koder.ai.

Thay customers.phone bằng customers.phone_e164 đã chuẩn hóa cần một cột bổ sung, chính sách chuyển đổi rõ ràng, mã tương thích, backfill có giới hạn, chuyển đường đọc và dọn dẹp trễ. Chính sách chuyển đổi phải có trước SQL vì không phải mọi giá trị đã lưu đều có thể chuẩn hóa tự động.

Hãy bắt đầu bằng cách phân loại giá trị hiện có. Số hợp lệ có thể được chuyển đổi khi biết ngữ cảnh quốc gia cần thiết. Giá trị trống có thể thành NULL. Số mơ hồ hoặc sai định dạng nên đi vào báo cáo ngoại lệ thay vì bị đoán. Hãy quyết định sản phẩm có bắt buộc mọi khách hàng phải có số điện thoại không, vì điều đó quyết định sau này NOT NULL có phù hợp hay không.

Thêm cột với lock timeout ngắn:

BEGIN;
SET LOCAL lock_timeout = '2s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

Triển khai mã chuẩn hóa dữ liệu đầu vào mới và ghi phone cùng phone_e164 trong một giao dịch. Ban đầu vẫn đọc phone. Cập nhật mọi tác nhân ghi, gồm import tài khoản, công cụ hỗ trợ, worker job và test tạo fixture khách hàng.

Backfill các hàng đủ điều kiện trong transaction ngắn. Ghi lại định danh đã xử lý cuối cùng, số đã chuyển đổi, số bị bỏ qua và lý do theo từng nhóm lỗi. Giới hạn tốc độ job theo độ trễ production và độ trễ replica. Khi lượt tiến hoàn tất, hãy quét lại các giá trị NULL đủ điều kiện để bắt các insert đồng thời hoặc hàng bị bỏ lỡ sau restart.

Chạy kiểm tra nhất quán với cùng quy tắc chuẩn hóa như ứng dụng, rồi lấy mẫu thủ công tiền tố quốc tế, số máy nhánh, giá trị trống, bản ghi liên hệ trùng và dữ liệu import cũ. Số lượng hàng chỉ chứng minh độ bao phủ, không chứng minh số điện thoại đúng.

Triển khai đường đọc trả về phone_e164 khi có và chỉ dùng phone cho ngoại lệ được ghi log. Theo dõi mức dùng fallback và lỗi chuẩn hóa. Hãy xử lý các ngoại lệ còn lại thay vì để fallback thành hành vi vĩnh viễn.

Khi trường mới trở thành nguồn chính, bỏ fallback và ngừng ghi phone. Quan sát các job hiếm và lưu lượng tích hợp qua một chu kỳ vận hành phù hợp. Chỉ thêm ràng buộc đã xác minh nếu quy tắc sản phẩm yêu cầu.

Cuối cùng, xóa tham chiếu mã đến phone. Xóa index hoặc ràng buộc của nó riêng, rồi xóa cột trong migration sau với thời gian chờ khóa có giới hạn. Nếu chuyển đường đọc thất bại trước khi xóa cột, hãy rollback hành vi ứng dụng trong khi cả hai cột vẫn khả dụng.

Ví dụ này cũng cho thấy một vấn đề thuộc miền nghiệp vụ mà cơ chế schema không giải quyết được: tách hoặc chuẩn hóa dữ liệu do con người nhập không phải lúc nào cũng không mất mát. Kế hoạch migration phải giữ lại các ngoại lệ và cho người phụ trách cách giải quyết chúng.

Kiểm tra từng bản phát hành trước khi phát hành

Checklist phát hành nên chứng minh tính tương thích, giới hạn tác động production và nêu hành động khôi phục cho giai đoạn hiện tại. Hãy lưu bằng chứng cùng thay đổi để người vận hành không phải dựng lại ý định trong lúc xảy ra sự cố.

Trước khi triển khai, hãy xác nhận:

  • Phiên bản ứng dụng hoạt động với trạng thái cơ sở dữ liệu trước và sau bản phát hành này.
  • Lock timeout và statement timeout được đặt cho các lệnh schema có thể chờ phía sau lưu lượng.
  • Job backfill hoặc xác minh có kiểm soát tiến độ, dừng, tiếp tục và giới hạn tốc độ.
  • Dashboard bao gồm lỗi, độ trễ, khóa, tải cơ sở dữ liệu, WAL và độ trễ replica.
  • Hành động rollback đã được kiểm thử mà không phụ thuộc vào đối tượng đã bị xóa.

Ghi lại điều kiện hoàn tất rõ ràng. Ví dụ gồm không có lỗi nhất quán mới trong một chu kỳ job đầy đủ, không có lượt đọc fallback từ lưu lượng do server kiểm soát, mọi thành phần tiêu thụ đã biết đã nâng cấp và truy vấn xác minh có kiểm soát thành công. Phần trăm hoàn thành hữu ích khi backfill, nhưng xử lý 100 phần trăm không đồng nghĩa chính xác 100 phần trăm.

Hãy rà soát thứ tự migration độc lập với code review. Một tập hợp SQL và thay đổi ứng dụng đúng vẫn có thể thất bại nếu triển khai thực hiện sai thứ tự. Nêu rõ bước nào chỉ được bắt đầu sau khi bước khác hoàn tất.

Điều kiện dừng nên là số liệu khi có thể. Xác định độ trễ truy vấn, thời gian chờ khóa, độ trễ replica, tỷ lệ lỗi và thời lượng lô chấp nhận được. Khi vượt ngưỡng, người vận hành cần biết nên dừng job, hủy câu lệnh đang chờ hay chuyển hướng thao tác đọc mà không phải xin phê duyệt mới trong lúc sự cố.

Migration chỉ hoàn tất khi cách biểu diễn mới xử lý đọc và ghi, dữ liệu lịch sử đã qua xác minh, đối tượng cũ đã bị xóa và cơ chế vận hành tạm thời đã biến mất.

Biến quy trình thành thứ có thể lặp lại

Runbook migration dùng lại được biến expand/contract thành công việc phát hành thông thường, với chủ sở hữu rõ ràng và các cổng đo lường được. Nó cần đủ ngắn để làm theo trong lúc triển khai trực tiếp và đủ cụ thể để mô tả trạng thái lỗi một phần.

Dùng năm phần trong runbook:

  • Mở rộng: thao tác schema chính xác, khóa dự kiến, timeout và yêu cầu giao dịch.
  • Tương thích: mã, tác nhân ghi, tác nhân đọc, flag, client bị ảnh hưởng và thứ tự triển khai.
  • Backfill: chính sách biến đổi, chia lô, checkpoint, điều tiết và xử lý ngoại lệ.
  • Xác minh: kiểm tra SQL, invariant nghiệp vụ, telemetry và ngưỡng hoàn tất.
  • Thu hẹp: xóa phụ thuộc, thời gian quan sát, dọn dẹp vật lý và giới hạn khôi phục.

Gán chủ sở hữu và ngày hoàn tất dự kiến cho mọi đối tượng chuyển tiếp. Theo dõi cột, index, flag, trigger và job ở cùng một nơi. Dọn dẹp là một phần của migration, không phải bảo trì tùy chọn.

Với các đội xây dựng bằng Koder.ai, Planning Mode có thể giúp nêu rõ các giai đoạn và checkpoint này trước khi bắt đầu thay đổi production. Xuất mã nguồn cũng cho phép SQL migration và logic tương thích được review như các phần mã ứng dụng khác. Koder.ai hỗ trợ triển khai, hosting, snapshot và rollback, nhưng không nên cho rằng rollback ứng dụng sẽ đảo ngược một biến đổi dữ liệu đã commit. Hãy giữ khả năng tương thích schema đến khi kế hoạch khôi phục cơ sở dữ liệu không còn phụ thuộc vào cách biểu diễn cũ.

Nếu có thể, hãy lên lịch công việc ghi nhiều vào thời điểm lưu lượng thấp, nhưng đừng coi thời điểm là biện pháp an toàn duy nhất. Giao dịch có giới hạn, điều tiết dựa trên phản hồi, tiến độ quan sát được và hành động dừng đã kiểm thử mới giúp migration trực tuyến dễ quản lý khi lưu lượng hoặc dữ liệu có diễn biến khác dự kiến.

Câu hỏi thường gặp

Vì sao thay đổi schema có thể gây gián đoạn?

Thay đổi schema làm production lỗi khi phiên bản ứng dụng cũ và mới mong đợi các cấu trúc cơ sở dữ liệu khác nhau. Trong lúc triển khai cuốn chiếu, cả hai phiên bản có thể chạy đồng thời, nên việc xóa hoặc đổi tên cột quá sớm có thể khiến thao tác đọc hoặc ghi thất bại.

Mẫu migration expand/contract là gì?

Expand/contract chia một thay đổi không tương thích thành các giai đoạn an toàn. Trước hết thêm cấu trúc mới, sau đó chuyển mã và dữ liệu sang đó, rồi mới xóa cấu trúc cũ khi mọi thành phần tiêu thụ đã ngừng dùng nó.

Làm sao đổi tên hoặc thay thế cột cơ sở dữ liệu mà không gián đoạn?

Hãy thêm cột mới trước và giữ nguyên cột cũ. Triển khai mã có thể làm việc với cả hai trường, backfill các hàng hiện có theo lô nhỏ, chuyển thao tác đọc sau khi xác minh, rồi xóa cột cũ trong một bản phát hành sau.

Tôi có thể thêm cột PostgreSQL mà không chặn lưu lượng không?

Thông thường là có. Cột có thể nhận NULL và không có giá trị mặc định thường chỉ là thay đổi metadata ngắn trong PostgreSQL, nhưng vẫn cần khóa bảng. Hãy đặt thời gian chờ khóa ngắn để migration thất bại thay vì chờ sau một giao dịch dài.

Làm sao tạo index mà không chặn ghi?

Dùng CREATE INDEX CONCURRENTLY khi bảng cần tiếp tục ghi được. Lệnh này mất nhiều thời gian hơn, tăng tải cơ sở dữ liệu và không thể chạy trong transaction block, vì vậy hãy theo dõi độ trễ, WAL và độ trễ replica khi chạy.

Khi nào ứng dụng nên ghi kép trường cũ và mới?

Ghi cả hai giá trị trong cùng một giao dịch cơ sở dữ liệu khi cả hai cách biểu diễn cần luôn được cập nhật. Xác định trường nào được ưu tiên nếu chúng không khớp, và áp dụng cùng quy tắc chuẩn hóa trong API, worker, import và công cụ hỗ trợ.

Nên backfill bảng PostgreSQL lớn an toàn như thế nào?

Xử lý các lô ngắn, có thể tiếp tục và commit sau từng lô. Lưu checkpoint, chỉ cập nhật những hàng vẫn cần xử lý, đồng thời giảm tốc hoặc dừng tác vụ khi độ trễ truy vấn, thời gian chờ khóa, lượng WAL hoặc độ trễ replica tăng lên.

Làm sao biết backfill đã hoàn tất và chính xác?

Đừng chuyển thao tác đọc chỉ vì backfill đã xong. Hãy kiểm tra các giá trị bắt buộc có tồn tại, so sánh cách biểu diễn cũ và mới, theo dõi các lượt đọc fallback, và xác nhận dữ liệu ghi mới vẫn nhất quán sau khi xử lý dữ liệu lịch sử.

Khi nào nên thêm NOT NULL, check constraint hoặc khóa ngoại?

Thêm các ràng buộc nghiêm ngặt sau khi dữ liệu hiện có đã qua xác minh và mọi tác nhân ghi đang hoạt động đều cung cấp giá trị hợp lệ. PostgreSQL cho phép thêm một số ràng buộc dưới dạng NOT VALID, áp dụng chúng với hàng mới, rồi xác minh dữ liệu lịch sử riêng.

Khi nào có thể xóa đường schema cũ một cách an toàn?

Trước hết bỏ các lượt đọc fallback, sau đó ngừng ghi theo đường cũ và quan sát hệ thống qua một chu kỳ vận hành đầy đủ. Khi mọi ứng dụng, job, báo cáo, tích hợp và client không còn tham chiếu đối tượng cũ, hãy xóa mã liên quan rồi xóa cột hoặc bảng cơ sở dữ liệu trong một bản phát hành sau.

Related posts