Nâng cấp phiên bản lớn là việc mỗi năm làm một lần, nên không ai thuộc. Phần này chạy pg_upgrade thật từ PostgreSQL 16 lên 17, đo từng bước, và tìm ra một thứ nó cố tình bỏ lại.

Thời gian từng bước, giới hạn của --link, và thống kê bị mất

Bố trí

Cơ sở dữ liệu 603 MB: 3.000.000 dòng, 6 bảng, 3 chỉ mục, 1 view, 1 vai trò có quyền SELECT.

Cần một môi trường có cả hai bộ nhị phân. Trên Alpine:

FROM postgres:17-alpine
RUN apk add --no-cache postgresql16 postgresql16-client

Bản 17 nằm ở /usr/local/bin, bản 16 ở /usr/libexec/postgresql16.

Bốn bước, bốn con số

Bước Thời gian
initdb cụm mới 0,7 s
pg_upgrade --check 0,9 s
pg_upgrade (sao chép, mặc định) 2,4 s
pg_upgrade --link 1,8 s

--check kết thúc bằng:

*Clusters are compatible*

Bước này chạy được khi máy chủ cũ đang chạykhông đổi gì cả. Nó kiểm mọi thứ mà bản nâng cấp thật sẽ kiểm: kiểu dữ liệu không còn hỗ trợ, thư viện thiếu, giao dịch chuẩn bị sẵn còn treo, tablespace.

Chạy nó vài ngày trước ngày nâng cấp là cách rẻ nhất để biết mình sẽ gặp gì.

Sau nâng cấp, kiểm lại: PostgreSQL 17.11, 3.000.000 dòng, 6 bảng, 3 chỉ mục, 1 view, và vai trò lẫn quyền đều còn nguyên. Khác với pg_dump ở phần 49 — pg_upgrade làm việc ở mức cụm nên nó mang theo mọi thứ.

Lần thử đầu tiên của tôi thất bại:

could not create hard link between old and new data directories: Cross-device link
In link mode the old and new data directories must be on the same file system.

Tôi để hai cụm ở hai volume Docker khác nhau — hai hệ tệp. Liên kết cứng không bắc qua được.

Đặt cả hai vào cùng một volume thì chạy: 1,8 s so với 2,4 s.

Chênh lệch nhỏ ở đây vì cơ sở dữ liệu chỉ 603 MB. Điểm quan trọng là cách hai chế độ tăng theo kích thước:

Cơ chế Tăng theo dữ liệu
Sao chép Chép từng tệp sang thư mục mới
--link Tạo liên kết cứng Gần như không

Trên 500 GB, chế độ sao chép là hàng chục phút; --link vẫn là vài phút. Đó là lý do nó tồn tại.

Cái giá của --link: sau khi nâng cấp, cụm cũ không dùng lại được. Hai cụm chia sẻ cùng các tệp dữ liệu, nên chạy cụm cũ sẽ làm hỏng cụm mới. Không có đường lùi ngoài bản sao lưu.

Với chế độ sao chép thì cụm cũ vẫn nguyên vẹn và khởi động lại được — đó là lưới an toàn đáng giá nếu bạn có thời gian và dung lượng.

Cái bẫy lớn nhất: thống kê không được mang sang

Ngay sau khi pg_upgrade báo thành công:

select count(*) from pg_stats where tablename = 'd';
--  0

select reltuples from pg_class where relname = 'd';
--  -1

Không có một dòng thống kê nào. reltuples = -1 là dấu "chưa biết gì" — không phải "0 dòng" mà là "chưa từng đo".

Hậu quả trên một truy vấn tra cứu:

Ước lượng Thực tế Lệch
Ngay sau pg_upgrade 19.715 300 66×
Sau ANALYZE 300 300 chính xác

Sai 66 lần. Ở truy vấn nhỏ này thời gian gần như không đổi, nhưng phần 29 đã đo một trường hợp mà đúng tình trạng này — bộ lập lịch không có thống kê nên dùng độ chọn lọc mặc định — làm truy vấn chậm 800 lần.

Đây là lý do máy chủ vừa nâng cấp xong thường "chậm bất thường trong vài giờ đầu", rồi tự khỏi khi autovacuum kịp chạy ANALYZE.

Cách chữa nằm ngay trong thông báo cuối của pg_upgrade:

vacuumdb --all --analyze-in-stages

Mất 0,8 giây trên cơ sở dữ liệu này. Trên cơ sở dữ liệu lớn thì lâu hơn nhiều, và đó chính là lý do có cờ --analyze-in-stages: nó chạy ba lượt với default_statistics_target tăng dần — lượt đầu rất nhanh và cho thống kê thô để hệ thống dùng được ngay, hai lượt sau tinh dần.

Chạy nó ngay sau khi khởi động cụm mới, trước khi mở cho người dùng nếu có thể.

Thứ tự chạy và thời gian ngừng dịch vụ

# 1. kiểm trước — máy chủ cũ vẫn đang chạy, không đổi gì
pg_upgrade --old-datadir=/old --new-datadir=/new \
           --old-bindir=/usr/libexec/postgresql16 --new-bindir=/usr/local/bin --check

# 2. dừng máy chủ cũ  <- bắt đầu tính thời gian ngừng
pg_ctl -D /old stop

# 3. nâng cấp
pg_upgrade --old-datadir=/old --new-datadir=/new \
           --old-bindir=/usr/libexec/postgresql16 --new-bindir=/usr/local/bin --link

# 4. khởi động máy chủ mới  <- kết thúc thời gian ngừng
pg_ctl -D /new start

# 5. thống kê
vacuumdb --all --analyze-in-stages

# 6. chỉ khi đã chắc
./delete_old_cluster.sh

Thời gian ngừng dịch vụ là từ bước 2 tới bước 4 — 1,8 giây trong phép đo này. Bước 5 chạy được khi đã mở lại cho người dùng, dù hệ thống sẽ chậm cho tới khi nó xong.

Bước 6 là bước tôi khuyên đợi ít nhất một ngày. Kịch bản delete_old_cluster.sh xoá vĩnh viễn cụm cũ, và với chế độ sao chép thì đó là đường lùi duy nhất của bạn.

Ba việc kiểm trước ngày nâng cấp

Extension. pg_upgrade không nâng cấp extension. Nếu bạn dùng postgis hay timescaledb, phiên bản tương thích phải được cài trên cụm mới trước khi chạy pg_upgrade. Đây là nguyên nhân thất bại phổ biến nhất, và --check bắt được nó — thông báo "Checking for presence of required libraries".

Sao chép. Máy dự phòng không được nâng cấp theo. Sau pg_upgrade, mọi replica phải dựng lại từ đầu bằng pg_basebackup — phần 53 đã đo con số đó: 78,9 giây cho 38 MB, và nó tăng theo kích thước.

Nếu bạn không chấp nhận được khoảng thời gian không có máy dự phòng, cách khác là dùng sao chép logic (phần 52): dựng cụm mới ở phiên bản mới, sao chép logic sang, rồi chuyển đổi. Thời gian ngừng dịch vụ tính bằng giây, nhưng bạn phải tự lo chuỗi, quyền, và DDL.

default_statistics_target và các tham số khác. pg_upgrade không mang postgresql.conf sang. Cụm mới dùng cấu hình mặc định của initdb, nên mọi thứ đã đo ở phần 44 tới 48 — shared_buffers, work_mem, max_wal_size, checkpoint_completion_target — đều trở về mặc định.

Đây là chỗ dễ mất nhiều nhất mà không ai để ý: cụm mới chạy đúng, dữ liệu đủ, và chậm hơn hẳn vì shared_buffers về 128 MB.

Thử ba mươi giây

Chạy --check trên hệ thống hiện tại của bạn ngay hôm nay:

pg_upgrade --old-datadir=... --new-datadir=... \
           --old-bindir=... --new-bindir=... --check

Nó không đổi gì, chạy được khi máy chủ đang phục vụ, và mất chưa tới một giây trên cơ sở dữ liệu vừa. Nếu nó báo lỗi extension, bạn vừa tiết kiệm được một buổi tối.

Và so cấu hình hai bên:

select name, setting from pg_settings
where source = 'configuration file' order by name;

Danh sách đó là danh sách bạn phải chép sang tay.

Phần sau đo bảo mật: vai trò, quyền, và bảo mật ở mức dòng — từng lớp chặn được cái gì.