- Di chuyển hạ tầng production trị giá $1.432/tháng sang máy chủ dedicated $233/tháng, đồng thời thay cả hệ điều hành mà vẫn duy trì liên tục dịch vụ không có downtime
- Tái dựng y hệt trên máy chủ mới với 30 cơ sở dữ liệu MySQL, 34 virtual host Nginx, GitLab EE, Neo4J, Supervisor, Gearman, rồi hoàn tất chuyển đổi bằng sao chép thời gian thực và đồng bộ gia tăng cuối cùng
- Trọng tâm của việc di chuyển cơ sở dữ liệu là kết hợp xử lý song song bằng mydumper·myloader với MySQL replication, đồng thời xử lý cả vấn đề về schema
sys và quyền phát sinh khi nâng từ MySQL 5.7 lên 8.0
- Cutover được thực hiện theo thứ tự giảm DNS TTL, chuyển Nginx trên máy chủ cũ sang reverse proxy, rồi thay đổi hàng loạt bản ghi A, nhờ đó các yêu cầu tới IP cũ trong lúc DNS đang truyền bá vẫn được chuyển sang máy chủ mới
- Kết quả là đạt được tiết kiệm $1.199/tháng, $14.388/năm, đồng thời nâng cấp CPU·bộ nhớ·lưu trữ và đạt 0 phút downtime
Bối cảnh di chuyển
- Trong bối cảnh vận hành một công ty phần mềm tại Thổ Nhĩ Kỳ, lạm phát tăng mạnh và đồng lira Thổ Nhĩ Kỳ suy yếu khiến gánh nặng chi phí hạ tầng tính theo đô la tăng lên đáng kể
- Chi phí máy chủ DigitalOcean hiện tại là $1.432 mỗi tháng, với cấu hình gồm 192GB RAM, 32 vCPU, 600GB SSD, 2 block volume 1TB, kèm backup
- Đích đến mới là máy chủ dedicated Hetzner AX162-R, với AMD EPYC 9454P 48 nhân 96 luồng, 256GB DDR5, cấu hình 1.92TB NVMe Gen4 RAID1
- Chi phí hàng tháng giảm xuống còn $233, tương đương tiết kiệm $1.199/tháng và $14.388/năm
- Dù không có bất mãn với độ tin cậy hay trải nghiệm dành cho nhà phát triển của máy chủ cũ, nhưng với workload steady-state thì tỷ lệ giá/hiệu năng không còn hợp lý nữa
Môi trường vận hành hiện tại
- Stack đang chạy không phải môi trường thử nghiệm đơn giản mà là môi trường production thực tế
- 30 cơ sở dữ liệu MySQL, tổng cộng 248GB dữ liệu
- Vận hành 34 virtual host Nginx trên nhiều domain
- Bao gồm cả backup GitLab EE dung lượng 42GB
- Vận hành Neo4J Graph DB dung lượng 30GB
- Quản lý hàng chục background worker bằng Supervisor
- Sử dụng hàng đợi tác vụ Gearman
- Vận hành ứng dụng mobile live phục vụ hàng trăm nghìn người dùng
- Hệ điều hành trên máy chủ cũ là CentOS 7, đã hết hỗ trợ
- Hệ điều hành trên máy chủ mới là AlmaLinux 9.7, bản phân phối tương thích RHEL 9 và là lựa chọn kế nhiệm tự nhiên cho CentOS
- Lần di chuyển này không chỉ để cắt giảm chi phí mà còn là cơ hội thoát khỏi hệ điều hành đã nhiều năm không nhận được bản vá bảo mật
Chiến lược không downtime
- Không chấp nhận cách chỉ đổi DNS và khởi động lại dịch vụ, mà tiến hành di chuyển không downtime bằng quy trình 6 bước
-
Bước 1: Cài đặt toàn bộ stack trên máy chủ mới
- Cài đặt Nginx bằng cách biên dịch từ source với cùng các flag như máy chủ cũ
- PHP được cài qua Remi repo và áp dụng các file cấu hình
.ini giống hệt máy chủ cũ
- Cài đặt MySQL 8.0, Neo4J Graph DB, GitLab EE, Node.js, Supervisor, Gearman và cấu hình để hành vi khớp với hệ thống hiện tại
- Trước khi đụng tới bản ghi DNS, mọi dịch vụ đã được căn chỉnh để hoạt động giống hệt máy chủ cũ
- Chứng chỉ SSL được xử lý bằng cách sao chép toàn bộ thư mục
/etc/letsencrypt/ từ máy chủ cũ qua rsync
- Sau khi toàn bộ traffic được chuyển sang máy chủ mới, thực hiện gia hạn cưỡng bức toàn bộ chứng chỉ bằng
certbot renew --force-renewal
-
Bước 2: Sao chép file web bằng rsync
- Sao chép toàn bộ thư mục
/var/www/html khoảng 65GB, 1,5 triệu file bằng rsync qua SSH
- Dùng tùy chọn
--checksum để kiểm tra tính toàn vẹn
- Ngay trước cutover, thực hiện thêm một lần đồng bộ gia tăng cuối cùng để phản ánh các file thay đổi
-
Bước 3: Thiết lập master-slave replication cho MySQL
- Thay vì dump rồi restore gây dừng cơ sở dữ liệu, cấu hình sao chép thời gian thực
- Máy chủ cũ được đặt làm master, máy chủ mới là slave chỉ đọc
- Phần nạp dữ liệu lớn ban đầu dùng
mydumper, sau đó bắt đầu replication từ đúng vị trí binlog được ghi trong metadata của bản dump
- Duy trì hai cơ sở dữ liệu ở trạng thái đồng bộ thời gian thực cho tới thời điểm cutover
-
Bước 4: Giảm DNS TTL
- Gọi DigitalOcean DNS API bằng script để giảm TTL của toàn bộ bản ghi A/AAAA từ 3600 giây xuống 300 giây
- MX, TXT record không thay đổi
- Việc đổi TTL cho bản ghi mail có thể gây vấn đề về khả năng gửi nhận nên được loại trừ
- Chờ 1 giờ để TTL cũ hết hạn trên toàn cầu, sau đó sẵn sàng cutover trong vòng 5 phút
-
Bước 5: Chuyển Nginx trên máy chủ cũ sang reverse proxy
- Một script Python phân tích các khối
server {} trong toàn bộ 34 cấu hình site Nginx
- Cấu hình cũ được backup rồi thay bằng cấu hình proxy trỏ sang máy chủ mới
- Nhờ vậy, trong lúc DNS đang lan truyền, các request đi vào IP cũ vẫn được chuyển lặng lẽ sang máy chủ mới
- Theo góc nhìn người dùng, không có gián đoạn nhìn thấy được
-
Bước 6: DNS cutover và tắt máy chủ cũ
- Gọi DigitalOcean API bằng script Python để đổi toàn bộ bản ghi A sang IP máy chủ mới chỉ trong vài giây
- Máy chủ cũ được giữ ở trạng thái cold standby trong 1 tuần rồi mới tắt
- Trong suốt quá trình, dịch vụ luôn được duy trì dưới dạng phản hồi trực tiếp hoặc phản hồi thông qua proxy, nên không có khoảng trống về tính sẵn sàng
Di chuyển MySQL
- Trong toàn bộ công việc, đây là giai đoạn phức tạp nhất
-
Dump dữ liệu
- Dùng mydumper thay cho
mysqldump chuẩn
- Tận dụng 48 CPU core trên máy chủ mới để export/import song song, rút ngắn công việc vốn mất vài ngày với
mysqldump đơn luồng xuống còn vài giờ
- Các tùy chọn chính được dùng gồm
--threads 32, --compress, --trx-consistency-only, --skip-definer, --chunk-filesize 256
- File
metadata của bản dump chính ghi lại vị trí binlog tại thời điểm snapshot
File: mysql-bin.000004
Position: 21834307
- Giá trị này được dùng làm điểm bắt đầu replication về sau
-
Truyền dump
- Sau khi hoàn tất dump, truyền sang máy chủ mới bằng rsync qua SSH
- Tổng cộng 248GB các chunk đã nén được truyền đi
- Tùy chọn
--compress của mydumper góp phần cải thiện tốc độ truyền qua mạng nhờ dữ liệu chunk đã được nén
-
Nạp dữ liệu
- Dùng
myloader
- Các tùy chọn chính là
--threads 32, --overwrite-tables, --ignore-errors 1062, --skip-definer
-
Vấn đề khi chuyển từ MySQL 5.7 lên 8.0
- Do môi trường CentOS 7, máy chủ cũ vẫn đang dừng ở MySQL 5.7
- Trước khi di chuyển, dùng
mysqlcheck --check-upgrade để kiểm tra dữ liệu có tương thích với MySQL 8.0 hay không, và kết quả là không có vấn đề
- Máy chủ mới cài MySQL 8.0 Community mới nhất
- Thời gian thực thi truy vấn trên toàn dự án giảm đáng kể, và trong bài gốc điều này được lý giải nhờ optimizer được cải thiện và các cải tiến của InnoDB trong MySQL 8.0
- Tuy nhiên vẫn phát sinh vấn đề do bước nhảy phiên bản
- Sau khi import, cấu trúc cột của bảng
mysql.user không phải 51 cột như mong đợi mà chỉ có 45 cột
- Kết quả là thiếu
mysql.infoschema và phát sinh lỗi xác thực người dùng
- Lần thử sửa đầu tiên dùng các lệnh sau
systemctl stop mysqld
mysqld --upgrade=FORCE --user=mysql &
- Lần thử đầu tiên thất bại với lỗi
ERROR: 'sys.innodb_buffer_stats_by_schema' is not VIEW
- Nguyên nhân là schema sys đã bị import thành bảng thường thay vì view
- Cách giải quyết là chạy
DROP DATABASE sys; rồi chạy lại nâng cấp
- Sau đó hoàn tất bình thường
Cấu hình MySQL replication
- Sau khi nạp xong dump trên cả hai máy chủ, máy chủ mới được cấu hình thành replica của máy chủ cũ
- Trong câu lệnh
CHANGE MASTER TO, chỉ định IP máy chủ cũ, người dùng replication, cổng 3306, MASTER_LOG_FILE='mysql-bin.000004', MASTER_LOG_POS=21834307
- Sau đó chạy
START SLAVE;
- Gần như ngay lập tức, replication dừng lại với error 1062 Duplicate Key
- Nguyên nhân là bản dump được thực hiện thành hai đợt, và giữa hai đợt có phát sinh ghi lên một số bảng, khiến dữ liệu trong dump đã import và dữ liệu phát lại từ binlog cố chèn trùng cùng một dòng
- Để xử lý, áp dụng các thiết lập sau
SET GLOBAL slave_exec_mode = 'IDEMPOTENT';
START SLAVE;
- Chế độ IDEMPOTENT sẽ âm thầm bỏ qua lỗi duplicate key và missing row
- Tất cả cơ sở dữ liệu quan trọng đều đồng bộ không lỗi, và chỉ trong vài phút giá trị
Seconds_Behind_Master đã giảm về 0
Kiểm tra trước cutover
- Trước khi đụng tới bản ghi DNS, cần xác nhận rằng mọi dịch vụ trên máy chủ mới hoạt động chính xác
- Cách kiểm tra là tạm thời chỉnh file
/etc/hosts trên máy local để ánh xạ domain sang IP máy chủ mới
- Trình duyệt và Postman sẽ gửi request tới máy chủ mới, trong khi người dùng bên ngoài vẫn tiếp tục truy cập máy chủ cũ
- Kiểm tra endpoint API, admin panel và trạng thái phản hồi của từng dịch vụ
- Sau khi xác nhận mọi hạng mục, mới tiến hành cutover thực tế
Vấn đề về quyền SUPER
- Sau khi master-slave replication đã đồng bộ hoàn toàn, phát hiện trên máy chủ mới rằng dù
read_only = 1, các câu lệnh INSERT vẫn thành công
- Nguyên nhân là mọi user ứng dụng PHP đều được cấp quyền SUPER
- Trong MySQL, quyền SUPER có thể bỏ qua
read_only
- Kết quả
SHOW GRANTS FOR 'some_db_user'@'localhost'; cho thấy có bao gồm quyền SUPER
- Lặp lại
REVOKE SUPER ON *.* FROM 'some_db_user'@'localhost'; cho tổng cộng 24 user ứng dụng
- Sau đó chạy
FLUSH PRIVILEGES;
- Từ thời điểm đó,
read_only = 1 mới chặn đúng các thao tác ghi của user ứng dụng, trong khi replication vẫn tiếp tục được phép
Chuẩn bị DNS
- Tất cả domain đều được quản lý bằng DigitalOcean DNS, còn nameserver được kết nối từ GoDaddy
- Công việc giảm TTL được script hóa để chạy với DigitalOcean API
- Phạm vi thay đổi chỉ giới hạn ở bản ghi A, AAAA
- MX, TXT record không bị đụng tới
- Do lo ngại khả năng phát sinh vấn đề với việc gửi nhận của Google Workspace, TTL của các bản ghi liên quan tới mail được giữ nguyên
- Sau khi chờ 1 giờ để TTL cũ hết hạn, hệ thống sẵn sàng cutover
Chuyển Nginx trên máy chủ cũ sang reverse proxy
- Thay vì chỉnh tay 34 file cấu hình, tác giả tự động chuyển đổi bằng script Python
- Script phân tích các khối
server {} trong mọi file cấu hình, xác định content block chính rồi thay bằng cấu hình proxy
- Cấu hình gốc được backup dưới dạng file
.backup
- Trong cấu hình ví dụ có áp dụng
proxy_pass https://NEW_SERVER_IP;, proxy_set_header Host $host;, proxy_set_header X-Real-IP $remote_addr;, proxy_read_timeout 150;
- Tùy chọn quan trọng là
proxy_ssl_verify off
- Vì chứng chỉ SSL trên máy chủ mới hợp lệ với domain nhưng không hợp lệ với địa chỉ IP
- Do môi trường này kiểm soát cả hai đầu nên có thể chấp nhận tắt xác minh trong trường hợp này
Quy trình cutover
- Điều kiện ngay trước cutover là độ trễ replication ở mức
Seconds_Behind_Master: 0 và reverse proxy đã sẵn sàng
- Trình tự thực hiện như sau
- Trên máy chủ mới chạy
STOP SLAVE;
- Trên máy chủ mới chạy
SET GLOBAL read_only = 0;
- Trên máy chủ mới chạy
RESET SLAVE ALL;
- Trên máy chủ mới chạy
supervisorctl start all
- Trên máy chủ cũ chạy
nginx -t && systemctl reload nginx để kích hoạt proxy
- Trên máy chủ cũ chạy
supervisorctl stop all
- Trên máy Mac local chạy
python3 do_cutover.py để đổi toàn bộ bản ghi A trong DNS sang IP máy chủ mới
- Chờ khoảng 5 phút để lan truyền
- Trên máy chủ cũ comment toàn bộ mục crontab
- Script DNS cutover gọi DigitalOcean API để thay toàn bộ bản ghi A trong khoảng 10 giây
Công việc bổ sung sau cutover
- Sau khi hoàn tất di chuyển, phát hiện nhiều webhook của các dự án GitLab vẫn đang trỏ tới IP máy chủ cũ
- Viết và áp dụng script quét toàn bộ project qua GitLab API rồi cập nhật hàng loạt webhook
Kết quả cuối cùng
- Chi phí hàng tháng giảm từ $1.432 xuống $233
- Số tiền tiết kiệm mỗi năm là $14.388
- Về hiệu năng cũng có được máy chủ mạnh hơn
- CPU tăng từ 32 vCPU lên 96 logical CPU
- RAM tăng từ 192GB lên 256GB DDR5
- Lưu trữ chuyển từ cấu hình hỗn hợp khoảng 2.6TB sang 2TB NVMe RAID1
- Downtime là 0 phút
- Toàn bộ quá trình di chuyển mất khoảng 24 giờ
- Không có ảnh hưởng tới người dùng
Bài học quan trọng
- MySQL replication là công cụ cốt lõi cho di chuyển không downtime
- Thiết lập sớm, cho nó bắt kịp đầy đủ rồi mới cutover
- Quyền của user MySQL cần được kiểm tra kỹ trước khi di chuyển
- Nếu có quyền SUPER,
read_only có thể bị bỏ qua và môi trường slave thực tế không còn là chỉ đọc
- Việc script hóa cập nhật DNS, chỉnh cấu hình Nginx, sửa webhook là rất quan trọng
- Nếu xử lý thủ công hơn 34 site thì sẽ tốn thời gian và tăng khả năng sai sót
- Tổ hợp mydumper + myloader nhanh hơn rất nhiều so với
mysqldump trên dataset lớn
- Với dump·restore song song 32 luồng, công việc từ vài ngày rút xuống vài giờ
- Với workload steady-state, nhà cung cấp cloud có thể khá đắt đỏ, còn máy chủ dedicated có thể mang lại hiệu năng cao hơn với chi phí thấp hơn
Script GitHub
- Toàn bộ script Python dùng cho lần di chuyển đã được công khai trên GitHub
- Danh sách script bao gồm
do_list_domains_ttl.py
- Truy vấn toàn bộ bản ghi A, IP và TTL của các domain trên DigitalOcean
do_ttl_update.py
- Giảm hàng loạt TTL của mọi bản ghi A/AAAA xuống 300 giây
do_to_hetzner_bulk_dns_records_import.py
- Di chuyển toàn bộ DNS zone từ DigitalOcean sang Hetzner DNS
do_cutover_to_new_ip.py
- Chuyển toàn bộ bản ghi A từ IP máy chủ cũ sang IP máy chủ mới
nginx_reverse_proxy_update.py
- Chuyển toàn bộ cấu hình site nginx sang cấu hình reverse proxy
mysql_compare.py
- So sánh row count của mọi bảng trên toàn bộ hai máy chủ MySQL
final_gitlab_webhook_update.py
- Cập nhật webhook của mọi dự án GitLab sang IP máy chủ mới
mydumper
- Mọi script đều hỗ trợ chế độ
DRY_RUN = True để xem trước an toàn trước khi áp dụng thực tế
1 bình luận
Ý kiến Hacker News
Vài tháng trước tôi đã chuyển hai máy chủ từ Linode và DO sang Hetzner, và cắt giảm chi phí rất đáng kể theo kiểu cùng mặt bằng. Điều ấn tượng hơn là đó là một mớ stack hỗn loạn gồm hàng chục trang web, nhiều ngôn ngữ khác nhau, thư viện cũ, cả MySQL lẫn Redis đan xen vào nhau. Thế mà Claude Code đã chuyển hết đống đó, thậm chí còn viết lại một phần mã để xử lý các thư viện không còn tồn tại. Giờ kiểu migration phức tạp thế này đã dễ hơn nhiều, nên tôi nghĩ về sau tính di động giữa các nhà cung cấp sẽ còn cao hơn nữa
Tôi đang lên kế hoạch chuyển từ AWS sang Hetzner. Amazon đôi khi tính giá đắt gấp 20 lần đối thủ, lại ép cam kết dài hạn nếu muốn có mức giá tạm ổn, và còn làm cho việc di chuyển dữ liệu ra ngoài trở nên cực kỳ đắt đỏ, nên tôi thấy rất thù địch với khách hàng. Họ có thể nghĩ phí egress sẽ giữ chân người dùng, nhưng thực tế nó lại tạo áp lực rằng chỉ cần chuyển một phần sang đối thủ là rồi sẽ phải chuyển cả hệ thống đi hết. Dù vậy, tôi chưa xây nền tảng của mình trên các dịch vụ độc quyền của Amazon, nên việc chuyển đi cũng dễ hơn phần nào
Mỗi khi đọc những bài như thế này, tôi lại thấy lạ vì mọi người ít khi nói về dự phòng hay load balancer. Nếu một máy chủ chết thì nhiều dịch vụ có thể cùng sập theo, nên tôi không rõ mọi người thật sự thấy điều đó là ổn hay sao. Có thể đã tiết kiệm được tiền, nhưng lại tốn thêm thời gian bảo trì và đau đầu về sau
Ở lithus.eu, chúng tôi đã nhiều lần chuyển khách hàng từ nhiều cloud khác nhau sang Hetzner. Thường thì chúng tôi dựng nhiều máy chủ, đôi khi là multi-AZ, rồi dùng Kubernetes để phân tán workload nhằm cung cấp HA. Với một node đơn thì Kubernetes có thể là quá tay, nhưng khi có nhiều node thì hợp lý hơn hẳn. Về backup, chúng tôi dùng cả Velero lẫn backup ở cấp ứng dụng; ví dụ với Postgres thì có cả backup WAL để hỗ trợ PITR. Dữ liệu trạng thái được đặt trên ít nhất hai node để đảm bảo HA. Về hiệu năng, bare metal nhìn chung cũng tốt hơn, và khá nhiều lần độ trễ phản hồi giảm một nửa so với AWS. Theo tôi, lý do không hẳn nằm ở bản thân ảo hóa mà ở những yếu tố xung quanh như NVMe, độ trễ mạng thấp hơn, và ít cache contention hơn. Tôi cũng đã viết thêm về chuyện này trong một bài HN trước đây
Bài này khá khó đọc. Cảm giác như Claude làm migration xong rồi lại để Claude viết luôn bản báo cáo. Nếu nhờ LLM mà tiết kiệm được như vậy thì rất tuyệt, nhưng đã công khai thành bài viết thì ít nhất cũng nên biên tập lại để bỏ bớt trùng lặp và giọng văn kiểu LLM
Tôi nghĩ nên cẩn thận với Hetzner. Trước đây tôi rất thích họ nhưng gần đây đã chuyển đi. Họ tắt toàn bộ khoảng 30 VM dùng trong pipeline CI/CD của chúng tôi chỉ vì một tranh chấp hóa đơn 36 đô la. Chúng tôi đã gửi bằng chứng thanh toán đầy đủ, kể cả sao kê ngân hàng, nhưng họ còn chẳng thèm xem, và trong lúc chúng tôi cố liên lạc khẩn cấp thì họ vẫn khóa sạch quyền truy cập. Giờ chúng tôi đã chuyển sang Scaleway
Vài tháng trước khi tìm phương án thay AWS cho một side project SaaS nhỏ, ban đầu tôi đã nghiêm túc cân nhắc Hetzner để tiết kiệm chi phí và ủng hộ cloud ở EU. Tôi sẵn sàng chấp nhận việc phải tự làm nhiều hơn, nhưng cuối cùng uy tín IP lại là thứ chặn đường. Một trong các rule của firewall AWS managed mà công ty tôi dùng đã chặn khá nhiều, thậm chí có thể là toàn bộ, dải IP Hetzner; ngay cả trên laptop công việc của tôi thì các website host trên IP Hetzner cũng không mở được vì chính sách IT. Dùng thứ như Cloudflare có thể đỡ hơn, nhưng tôi cũng từng thấy nói là bảo vệ DDoS của họ yếu. Cuối cùng tôi chọn DO App Platform ở vùng EU, và tùy chọn DB managed cũng là một lợi thế lớn
Việc chia sẻ trải nghiệm migration như thế này khá hữu ích, tôi rất cảm kích. Tôi nhìn sự so sánh giữa DO và Hetzner như trade-off giữa việc mở DoorDash/UberEats và tự nấu bữa tối. Tỷ lệ chênh lệch chi phí cũng na ná như vậy. Tôi làm việc với cả ba hyperscaler và cả on-prem, nhưng với các việc lặt vặt hay test PoC thì tôi vẫn quay lại console của DigitalOcean. Cái sự tiện lợi kiểu chỉ vài nút bấm là có server hay bucket, có sane default, backup bật bằng một checkbox, rõ ràng rất có giá trị nếu tính cả thời gian
Tôi khá tò mò họ backup DB kiểu gì. Có replica hay standby không, hay chỉ backup theo giờ thôi. Với kiểu cấu hình một máy chủ như thế này, nếu gặp lỗi phần cứng như SSD chết thì ứng dụng có thể dừng ngay, và nhất là nếu SSD hỏng thì có khi phải downtime vài giờ hoặc vài ngày trong lúc dựng lại
Ảnh meme trên phần header là do tôi làm. Tôi từng dùng nó trong bài này, nên thấy nó được dùng tới hai lần như vậy cũng vui