0

Migration database sang Cloud Server mới: dump, đồng bộ và cutover cho MySQL, PostgreSQL

Migration database thường là bước khiến dev ngại nhất khi chuyển sang Cloud Server mới. Copy code thì dễ, còn dữ liệu vẫn thay đổi từng giây trong lúc bạn copy. Nếu chỉ dump rồi restore một lần, mọi đơn hàng hay tài khoản tạo ra giữa hai thời điểm đó sẽ mất. Bài này đi từng bước để chuyển database MySQL hoặc PostgreSQL sang server mới, chọn cách làm theo mức downtime chấp nhận được, và kiểm tra dữ liệu trước khi trỏ ứng dụng sang.

Bài này giúp bạn

  • Chọn giữa "dump một lần" (có downtime ngắn) và "dump rồi đồng bộ bằng replication" (downtime gần như bằng 0) dựa trên kích thước dữ liệu.
  • Có bộ lệnh cho cả MySQL 8.x và PostgreSQL: dump, chuyển file, restore, bật đồng bộ.
  • Có quy trình cutover, cách so khớp dữ liệu và bảng lỗi thường gặp.

Môi trường mẫu: hai server Ubuntu 24.04, server cũ 10.0.0.10, server mới 10.0.0.20, database tên shopdb. IP, tên user và mật khẩu đều là ví dụ. Các lệnh viết theo tài liệu chính thức của MySQL và PostgreSQL, bạn nên chạy thử trên bản sao trước khi làm với dữ liệu thật.

Bước 0: Chọn cách migration theo downtime chấp nhận được

Trước khi gõ lệnh nào, hãy trả lời hai câu: database nặng bao nhiêu, và ứng dụng được phép ngừng ghi trong bao lâu.

# MySQL: dung lượng từng database (MB)
mysql -e "SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024) AS mb
          FROM information_schema.tables GROUP BY table_schema;"

# PostgreSQL: dung lượng database
sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size('shopdb'));"
Tình huống Cách làm Downtime
Database vài GB, chấp nhận dừng ghi 10–30 phút (thường vào ban đêm) Cách A: bật chế độ bảo trì, dump, restore, trỏ ứng dụng sang Bằng thời gian dump + chuyển + restore
Database lớn, hoặc không được dừng lâu Cách B: dump bản đầu tiên, bật replication để server mới đuổi kịp, rồi cutover Vài chục giây đến vài phút
Đổi engine (MySQL sang PostgreSQL) Ngoài phạm vi bài này, cần công cụ chuyển đổi schema riêng Tùy công cụ

Mẹo nhỏ: đo thời gian dump và restore trên một bản sao trước. Con số đó cho bạn biết Cách A có vừa với khung bảo trì hay không.

Bước 1: Chuẩn bị server mới

Cài đúng cùng major version với server cũ (hoặc mới hơn), không cài phiên bản cũ hơn. Restore từ bản mới sang bản cũ thường gặp lỗi cú pháp hoặc collation không tồn tại.

# Kiểm tra phiên bản trên server cũ
mysql -V
psql --version

Mở kết nối giữa hai server bằng IP nội bộ, chỉ cho phép đúng IP của server kia. Không mở port 3306 hoặc 5432 ra Internet.

# Trên server cũ: chỉ cho server mới kết nối vào MySQL/PostgreSQL
sudo ufw allow from 10.0.0.20 to any port 3306 proto tcp
sudo ufw allow from 10.0.0.20 to any port 5432 proto tcp

Kiểm tra dung lượng đĩa trên server mới: cần ít nhất gấp đôi kích thước database, vì sẽ có lúc vừa có file dump vừa có dữ liệu đã restore.

df -h /var/lib

Cách A với MySQL: dump một lần trong khung bảo trì

1. Dừng ghi. Bật trang bảo trì hoặc dừng worker, cron job và queue đang ghi vào database. Đây là bước hay bị quên nhất.

2. Dump trên server cũ.

mysqldump --single-transaction --routines --triggers --events \
  --databases shopdb | gzip > shopdb.sql.gz
  • --single-transaction mở một transaction trước khi dump, nên bảng InnoDB được dump ở trạng thái nhất quán mà không khóa bảng. Bảng MyISAM không được bảo vệ theo cách này, nên phải dừng ghi hoàn toàn.
  • --routines và --events không nằm trong mặc định của mysqldump. Thiếu hai cờ này, stored procedure và event scheduler sẽ không sang server mới. Kiểm tra lại bằng mysqldump --help trên phiên bản của bạn.

3. Chuyển file sang server mới.

rsync -avP shopdb.sql.gz deploy@10.0.0.20:/tmp/

rsync -P cho phép chạy lại tiếp nếu bị đứt kết nối giữa chừng, hữu ích với file vài chục GB.

4. Tạo user ứng dụng và restore trên server mới. User MySQL không nằm trong dump của một database, nên phải tạo lại.

mysql -e "CREATE USER 'shopapp'@'10.0.0.%' IDENTIFIED BY 'mat-khau-moi';"
gunzip < /tmp/shopdb.sql.gz | mysql
mysql -e "GRANT ALL ON shopdb.* TO 'shopapp'@'10.0.0.%';"

5. So khớp dữ liệu (xem Bước 2 bên dưới), rồi đổi chuỗi kết nối của ứng dụng sang 10.0.0.20 và tắt trang bảo trì.

Cách A với PostgreSQL: pg_dump và pg_restore

1. Dừng ghi giống như với MySQL.

2. Dump role và dữ liệu. pg_dump chỉ dump một database. Role (user) và tablespace là đối tượng toàn cụm, phải dump riêng bằng pg_dumpall.

# Trên server cũ
sudo -u postgres pg_dumpall --globals-only > globals.sql
sudo -u postgres pg_dump -Fd -j 4 -f shopdb_dump shopdb

-Fd -j 4 dump theo định dạng thư mục với 4 tiến trình song song. Định dạng thư mục là định dạng duy nhất hỗ trợ -j. pg_dump sẽ mở jobs + 1 kết nối, nên kiểm tra max_connections còn đủ chỗ.

3. Chuyển và restore.

rsync -avP globals.sql shopdb_dump deploy@10.0.0.20:/tmp/

# Trên server mới
sudo -u postgres psql -f /tmp/globals.sql
sudo -u postgres createdb -O shopapp shopdb
sudo -u postgres pg_restore -j 4 -d shopdb /tmp/shopdb_dump

4. Cập nhật thống kê để planner có số liệu đúng ngay từ truy vấn đầu tiên trên server mới:

sudo -u postgres vacuumdb --analyze-only -d shopdb

Cách B: đồng bộ bằng replication để cutover gần như không downtime

Ý tưởng chung: server mới nhận bản dump đầu tiên, sau đó nhận tiếp mọi thay đổi từ server cũ theo thời gian thực. Đến khi độ trễ về 0, bạn chỉ cần dừng ghi vài chục giây để chuyển.

MySQL: replication dựa trên binlog

Server cũ phải bật binary log (log_bin) và có server_id khác server mới. Trên MySQL 8.x, binlog bật sẵn theo mặc định, nhưng vẫn nên kiểm tra.

-- Trên server cũ
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'server_id';
CREATE USER 'repl'@'10.0.0.20' IDENTIFIED BY 'mat-khau-repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.20';

Dump kèm tọa độ binlog, nghĩa là ghi tên file binlog và vị trí vào đầu file dump:

mysqldump --single-transaction --source-data=2 --routines --triggers --events \
  --databases shopdb | gzip > shopdb.sql.gz
zcat shopdb.sql.gz | head -50 | grep "CHANGE REPLICATION SOURCE\|CHANGE MASTER"

Restore trên server mới (server_id khác, ví dụ 2), rồi trỏ replication về đúng tọa độ vừa lấy:

CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='10.0.0.10', SOURCE_USER='repl', SOURCE_PASSWORD='mat-khau-repl',
  SOURCE_LOG_FILE='binlog.000123', SOURCE_LOG_POS=4567,
  SOURCE_SSL=1;
START REPLICA;
SHOW REPLICA STATUS\G

Theo dõi Replica_IO_Running và Replica_SQL_Running (phải là Yes) và Seconds_Behind_Source (phải về 0). Cú pháp SOURCE/REPLICA áp dụng từ MySQL 8.0.23; bản cũ hơn dùng CHANGE MASTER TO và START SLAVE.

PostgreSQL: logical replication

Logical replication cho phép đồng bộ giữa hai major version khác nhau, nên đây cũng là cách nâng cấp PostgreSQL trong lúc migration. Server cũ cần wal_level = logical (đổi tham số này cần restart).

# Server mới: chỉ tạo schema, chưa có dữ liệu
sudo -u postgres pg_dump --schema-only -h 10.0.0.10 -U postgres shopdb \
  | sudo -u postgres psql -d shopdb
-- Server cũ
CREATE PUBLICATION mig_pub FOR ALL TABLES;

-- Server mới
CREATE SUBSCRIPTION mig_sub
  CONNECTION 'host=10.0.0.10 dbname=shopdb user=repl password=mat-khau-repl'
  PUBLICATION mig_pub;

Subscription sẽ tự copy dữ liệu ban đầu rồi nhận tiếp thay đổi. Ba điểm cần nhớ:

  • Bảng cần có primary key (hoặc replica identity) thì UPDATE và DELETE mới đồng bộ được.
  • Thay đổi schema (DDL) không được đồng bộ. Đóng băng migration schema trong suốt quá trình.
  • Giá trị sequence không được đồng bộ. Trước khi mở ghi trên server mới, phải đặt lại sequence (xem Bước 3).

Bước 2: So khớp dữ liệu trước khi cutover

Đừng tin vào "lệnh chạy xong không báo lỗi". Hãy so ít nhất ba thứ trên cả hai server:

-- 1. Số dòng các bảng quan trọng
SELECT COUNT(*) FROM orders;

-- 2. ID lớn nhất và bản ghi mới nhất
SELECT MAX(id), MAX(created_at) FROM orders;
# 3. MySQL: checksum từng bảng (chạy trên cả hai server và so kết quả)
mysql -e "CHECKSUM TABLE shopdb.orders, shopdb.customers;"

Với PostgreSQL không có CHECKSUM TABLE; bạn có thể so COUNT(*) và tổng một cột số, ví dụ SUM(total_amount), trên các bảng quan trọng.

Bước 3: Cutover

Thứ tự đúng cho Cách B:

  1. Hạ TTL bản ghi DNS của database (nếu ứng dụng kết nối bằng tên miền) xuống thấp từ 1 ngày trước.
  2. Dừng ghi trên server cũ: bật trang bảo trì, dừng worker.
  3. Chờ độ trễ replication về 0, so khớp lần cuối như Bước 2.
  4. MySQL: STOP REPLICA; RESET REPLICA ALL; trên server mới. PostgreSQL: đặt lại sequence rồi xóa subscription:
    -- Ví dụ cho một bảng; lặp lại cho mọi sequence
    SELECT setval('orders_id_seq', (SELECT MAX(id) FROM orders));
    DROP SUBSCRIPTION mig_sub;
    
  5. Đổi chuỗi kết nối của ứng dụng sang server mới, khởi động lại ứng dụng, mở ghi.
  6. Giữ server cũ ở chế độ chỉ đọc ít nhất vài ngày để có đường quay lại.
-- MySQL: khóa ghi server cũ sau cutover
SET GLOBAL super_read_only = ON;

Lỗi thường gặp

Triệu chứng Nguyên nhân Cách xử lý
Restore MySQL báo Unknown collation: 'utf8mb4_0900_ai_ci' Dump từ MySQL 8 nhưng restore vào bản cũ hơn hoặc MariaDB Cài cùng major version trên server mới, không hạ version
Ứng dụng báo Access denied sau khi chuyển User MySQL không có trong dump database, hoặc host trong user không khớp IP mới Tạo lại user với đúng host, hoặc dump role bằng pg_dumpall --globals-only với PostgreSQL
Stored procedure hoặc event biến mất Thiếu --routines --events khi dump Dump lại phần này hoặc chạy lại toàn bộ với đủ cờ
PostgreSQL báo duplicate key value violates unique constraint ngay sau cutover Sequence không được logical replication đồng bộ setval() cho từng sequence theo MAX(id)
UPDATE/DELETE không sang server mới Bảng thiếu primary key Thêm primary key, hoặc ALTER TABLE ... REPLICA IDENTITY FULL (tốn WAL hơn)
Seconds_Behind_Source tăng mãi Server mới yếu hơn hoặc đĩa chậm Kiểm tra IOPS và CPU trên server mới, tạm dừng job nặng trên server cũ
Hết đĩa khi restore Vừa giữ file dump vừa có dữ liệu restore Chuẩn bị gấp đôi dung lượng, hoặc pipe thẳng output của mysqldump qua SSH sang server mới thay vì lưu file

Chuẩn bị Cloud Server đích trên Bizfly Cloud

Nếu bạn chuyển database sang Bizfly Cloud Server, có hai điểm liên quan trực tiếp đến bài này. Thứ nhất, trang sản phẩm cho biết mỗi khách hàng có một mạng VPC riêng, nên bạn có thể cho hai server nói chuyện qua IP nội bộ như Bước 1 thay vì mở port database ra Internet. Thứ hai, trang sản phẩm ghi dữ liệu lưu trữ được nhân bản 3 lần. Dù vậy, nhân bản ở tầng lưu trữ không thay thế được backup logic, nên hãy giữ lịch dump định kỳ sau khi migration xong.

Trước khi chọn cấu hình, hãy dùng con số đo được ở Bước 0: dung lượng database, thời gian restore và mức CPU, RAM, IOPS hiện tại của server cũ.

Tóm tắt

  • Đo dung lượng và thời gian restore trước, rồi mới chọn dump một lần hay replication.
  • MySQL: --single-transaction --routines --triggers --events, thêm --source-data=2 nếu dùng replication.
  • PostgreSQL: pg_dumpall --globals-only cho role, pg_dump -Fd -j cho dữ liệu, nhớ đặt lại sequence khi dùng logical replication.
  • So khớp số dòng, ID lớn nhất và checksum trước khi trỏ ứng dụng sang.
  • Giữ server cũ ở chế độ chỉ đọc vài ngày để có đường quay lại.

Bạn đã từng gặp lỗi nào khi chuyển database sang server mới? Chia sẻ ở phần bình luận để mọi người cùng tránh.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí