
2026/04/18 22:29
DigitalOcean から Hetzner に移行すること
RSS: https://news.ycombinator.com/rss
要約▶
日本語翻訳:
トルコ発のソフトウェア企業が、DigitalOcean から専用 Hetzner サーバーへのシームレスなゼロダウンタイム移行を成功させ、巨額の経費削減と大幅なハードウェアアップグレードを実現しました。特に印象的なのは、月次費用を 1,432 ドルからわずか 233 ドルへ削減しつつ、旧型の vCPU/SSD スペックを、NVMe ストレージを搭載した高性能 AMD EPYC プロセッサへとインフラストラクチャをアップグレードしたことです。この移行は、GitLab や Neo4j などのクリティカルなアプリケーションを多数利用者に提供するという複雑な生産環境において、大きな中断なしに実行されました。
本プロジェクトは、248 GB のデータをマルチスレッドバックアップと rsync を用いて高速に移動させる堅固な 6 段階移行戦略に基づいていました。主要な技術的なハイライトとしては、データベースシステムを MySQL 5.7 からバージョン 8.0 へ、オペレーティングシステムを CentOS 7 から AlmaLinux 9.7 へとアップグレードしたことです。チームは、
mysql.user テーブルのスキーマ不一致の解消、再пликаケーション時の重複キーエラーを冪等な実行モードで対処し、SUPER プライバリジの取り消しによるセキュリティ脆弱性の是正など、複数の重要な技術的障壁を克服することに成功しました。DNS プロパゲーション期間がわずか 5 分であったにも関わらず、サービスは安定していました。同チームは、この費用対効果の高いアプローチを他者に複製できるよう、Python 製の移行スクリプトを GitHub でオープンソース化しています。本文
デジタルオシアンからヘッツナー・デディケイテッドサーバーへの本番環境移行:248GB の MySQL データ(30 のデータベース)、34 の Nginx サイト、GitLab EE、Neo4j、および多数ユーザーにサービスを提供しているモバイルアプリのトラフィックを完全に中断することなく成功裏に実行しました。
移転した理由
トルコでソフトウェア企業を経営するのは、近年ますます高価になっています。暴騰するインフレ率と米国ドルに対するトルコリラの著しい価値低下により、ドル建てでのインフラコストが深刻な負担となりました。2 年前には妥当に見えた請求額も、為替相場が数倍に跳ね上がった今では全く異なる衝撃をもたらします。
私たちは毎月の DigitalOcean(ドロープル)利用料として、192GB の RAM、32vCPU、600GB SSD、および 2 つのブロックボリューム(それぞれ 1TB)、バックアップ機能付きで $1,432 を支払っていました。サーバー自体は問題なかったのですが、パフォーマンスに対する単価が意味をなさなくなってしまいました。
その時、ヘッツナー AX162-R というサーバーを発見しました。
| 特徴 | DigitalOcean | Hetzner AX162-R |
|---|---|---|
| CPU | 32 vCPU | AMD EPYC 9454P (48 コア / 96 スレッド) |
| RAM | 192 GB | 256 GB DDR5 |
| ストレージ | 600 GB SSD + 2x1 TB ボリューム | 1.92 TB NVMe Gen4 RAID1 |
| 月次コスト | $1,432 | $233 |
| 節減額 | — | 月間 $1,199 |
これは年間 $14,388 を節約できる計算です。かつ、すべての面で客観的に高性能なサーバーです。決断は容易でした。
私は約 8 年間 DigitalOcean の顧客でしたが、製品自体は大変良く、信頼性や開発者体験については不満はありませんでした。しかし、今これらの数字を見る限り、これまでの年間で失っていた資金に少し淋しさを感じざるを得ません。もし定常的なワークロードを処理し、DigitalOcean のエコシステム機能は能動的に利用していないのであれば、次の更新前にデディケイテッドサーバーの価格を確認することを強くお勧めします。
稼働していた環境
これは玩具のようなプロジェクトではありませんでした。スタックには以下が含まれていました:
- MySQL データベース 30 つ(データ量 248GB)
- 複数のドメインで運用されている Nginx 仮想的ホスト 34 カ所
- GitLab EE(バックアップ容量 42GB)
- Neo4J グラフデータベース(グラフ DB コンテンツ 30GB)
- 多数の背景ワーカーを管理する Supervisor
- Gearman ジョブキュー
- 数十万のユーザーにサービスを提供している複数種類のライブモバイルアプリ
旧サーバー: CentOS 7 —— エンド オブ ライフ已过ですが、まだ運用環境で稼働中。
新サーバー: AlmaLinux 9.7 —— RHEL 9 と互換性があり、CentOS の自然な後継者です。この移行は、数年間セキュリティ更新を受けなかった OS からついに解放される機会でもありました。
戦略:ゼロ ダウンタイム
DNS を変更して全サービスを再起動し、ベストを祈るような素朴なアプローチは許されませんでした。代わりに、6 つのフェーズからなる適切な移行計画を策定しました:
フェーズ 1 —— 新しいサーバーへの全スタックインストール
Nginx(ソースコードからのコンパイルでフラグを同一にしたもの)、PHP(Remi リポジトリ経由で旧サーバーと同じ .ini 設定ファイルを使用)、MySQL 8.0、Neo4J グラフ DB、GitLab EE、Node.js、Supervisor、Gearman の全サービスが、DNS レコードに触れる前の段階ですべて旧サーバーの挙動と一致するように設定されました。
SSL 証明書は、旧サーバーから
/etc/letsencrypt/ ディレクトリ全体を rsync で転送する方式で処理しました。移行完了後、すべてのトラフィックが新しいサーバーを経由し始めたタイミングで、一度にすべての証明書の強制更新を行いました:
certbot renew --force-renewal
フェーズ 2 —— Web ファイルのクローン化(rsync)
/var/www/html ディレクトリ全体(約 65GB、150 万個のファイル)を SSH を介した rsync --checksum フラグを用いて新しいサーバーにクローンしました。スイッチオーバー直前には、初期クローン後に更新されたファイルを捕捉する最終的なインクリメンタル同期を実行しました。
フェーズ 3 —— MySQL マスターからスレーブへのレプリケーション設定
データベースをオフラインにしてダンプ・レストアを行うのではなく、ライブ レプリケーションを設定しました。旧サーバーをマスター、新サーバーをリーダースレーブとしました。初期バッチ読み込みには
mydumper を使用し、ダンプメタデータに記録された正確な binlog ポジションからレプリケーションを開始しました。これにより、スイッチオーバーの瞬間まで両方のデータベースがリアルタイム同期を維持できました。
フェーズ 4 —— DNS TTL の削減
DigitalOcean DNS API をスクリプト化して、すべての A レコードと AAAA レコードの TTL を 3600 秒から 300 秒に低下させました(MX または TXT レコードには触れず、メールレコードの TTL 変更は配信性に影響を与える可能性があるため)。1 時間経過して古い TTL がグローバルに破棄されるのを待ち、その時点で DNS クイックスウィッチオーバー(転換)を行い、所要時間は 5 分以内に収めました。
フェーズ 5 —— 旧サーバーの Nginx をリバースプロキシへの変換
Python スクリプトを作成し、34 の Nginx サイト設定のうちすべての
server {} ブロックを解析し、オリジナルのバックアップを取りながら、新しいサーバーへのプロキシ構成に置換しました。これにより、DNS プロパゲーション期間中に古い IP にアクセスされたリクエストも静かに転送され、ユーザーにはどの障害も認識されませんでした。
フェーズ 6 —— DNS クイックスウィッチオーバーと旧サーバーの廃止
単一の Python スクリプトが DigitalOcean API にアクセスし、A レコードをすべて新しいサーバー IP に切り替える作業を行い、わずか数秒で完了しました。旧サーバーは冷 standby(待機)として 1 週間維持した後、シャットダウンされました。
重要な知見: サービスが使えなくなる時間帯は一切存在しませんでした。トラフィックは常に直接アクセスされるか、あるいはプロキシ経由で提供されていました。
MySQL データ移行
これはこの全操作の中で最も複雑な部分でした。
データのダンプ化
標準的な
mysqldump の代わりに mydumper を使用し、驚くべき性能向上を得ました。新しいサーバーの 48 コア CPU を並列エクスポートおよびインポートに活用した結果、従来シングルスレッドの mysqldump で数日かかっていた作業が数時間で完了しました。大規模な MySQL データベースを移行する際に mydumper/myloader を使用しない場合は、困難なやり方を行っていることになります。
mydumper \ --threads 32 \ --compress \ --trx-consistency-only \ --skip-definer \ --chunk-filesize 256 \ -v 3 \ --outputdir /root/mydumper_backup/
主なダンプのメタデータファイルには、スナップショット時点の binlog ポジションが記録されました:
- ファイル: mysql-bin.000004
- ポジション: 21834307
これがレプリケーションの開始ポイントとなります。
ダンプデータの転送(新サーバーへ)
ダンプ完了後、SSH を介した
rsync でデータを新サーバーに転送しました。圧縮されたチャンクが 248GB に達していたため、他の転送方法よりもはるかに高速でした:
rsync -avz --progress /root/mydumper_backup/ root@NEW_SERVER:/root/mydumper_backup/
ここで
mydumper の --compress フラグの効果が顕著に表れました。圧縮されたチャンクがネットワーク回路上で素早く転送されました。
データの読み込み
myloader \ --threads 32 \ --overwrite-tables \ --ignore-errors 1062 \ --skip-definer \ -v 3 \ --directory /root/mydumper_backup/
MySQL 5.7 から 8.0 への移行問題
CentOS 7 で固定されていたため、MySQL 5.7(数年間運用されてきた陳腐化バージョン)でも固定されていました。移行前には
mysqlcheck --check-upgrade を実行し、データが MySQL 8.0 と互換性があることを確認しました。結果はクリアだったので、新しいサーバーに最新の MySQL 8.0 コミュニティエディションをインストールしました。すべてのプロジェクトで即座にパフォーマンス改善が見られました——MySQL 8.0 の改良されたオプタイマイザーと InnoDB 拡張機能によりクエリ実行時間が大幅に減少しました。
ただし、バージョンアップは一つの微妙な問題を招きました。 インポート後、
mysql.user テーブルの列構造が誤っており、期待される 51 列に対して 45 列となっていました。その結果 mysql.infoschema が欠落し、ユーザー認証が失敗しました。
修正方法:
systemctl stop mysqld mysqld --upgrade=FORCE --user=mysql &
しかし、最初の実行では以下のエラーが発生しました:
ERROR: 'sys.innodb_buffer_stats_by_schema' is not VIEW
sys スキーマがビューとしてではなく普通テーブルとしてインポートされていました。解決策:再度アップグレードを実行。成功。
MySQL レプリケーションの設定
両方のダンプをインポートした後、新しいサーバーを旧サーバーのレプリカとして設定しました:
CHANGE MASTER TO MASTER_HOST='OLD_SERVER_IP', MASTER_USER='replicator', MASTER_PASSWORD='...', MASTER_PORT=3306, MASTER_LOG_FILE='mysql-bin.000004', MASTER_LOG_POS=21834307; START SLAVE;
ほぼ即座にレプリケーションがエラー 1062(重複キー)で停止しました。これはダンプを 2 パスで行ったため、その間のギャップで特定テーブルに行が書き込まれ、インポートされたダンプと binlog の再生の両方で同じ行を挿入しようとしたことが原因です。
修正方法:
SET GLOBAL slave_exec_mode = 'IDEMPOTENT'; START SLAVE;
IDEMPOTENT モードでは、重複キーエラーや行欠落エラーを静かにスキップします。すべての重要データベースが 1 つのエラーもなく同期されました。数分後には
Seconds_Behind_Master が 0 に落ちました。
クイックスウィッチオーバー前のテスト
DNS レコードを触る前に、新サーバー上の全サービスが正しく動作しているかを確認する必要がありました。トリック:ローカルマシンの
/etc/hosts ファイルを一時的に編集し、ドメイン名を新サーバーの IP に指し示すようにしました。
# /etc/hosts(ローカルマシン) NEW_SERVER_IP yourdomain1.com NEW_SERVER_IP yourdomain2.com # ... それぞれすべてのドメインに対して同様
これにより、ブラウザと Postman は新しいサーバーにアクセスしましたが、世界の他の場所は依然として古いサーバーへ行きました。API エンドポイントをテストし、管理パネルを確認し、各サービスが正しく応答しているか検証しました。この確認後にのみクイックスウィッチオーバーに進みました。
隠された SUPER権限の問題
マスターとスレーブ間のレプリケーションが完全に同期した後、新しいサーバーでは本来あるべきではないときに INSERT ステートメントが成功していたことに気づきました。
read_only = 1 が設定されていましたが、書き込みが行われていました。
理由:すべての PHP アプリケーションユーザーに SUPER 権限が付与されていました。MySQL において、SUPER は
read_only を回避します。
SHOW GRANTS FOR 'some_db_user'@'localhost'; -- 結果: GRANT SELECT, INSERT, UPDATE, DELETE, ..., SUPER, ... ON *.*
24 のアプリケーションユーザーからすべて撤回しました:
REVOKE SUPER ON *.* FROM 'some_db_user'@'localhost'; -- すべてのユーザーに対して繰り返し実行 FLUSH PRIVILEGES;
その後、
read_only = 1 がアプリケーションユーザーからのすべての書き込みを正しくブロックしながら、レプリケーションの継続を許容しました。
DNS の準備
すべてのドメインは DigitalOcean DNS を通じて管理されており(ネームサーバーは GoDaddy から指し示されていました)、DigitalOcean API に対して TTL 削減スクリプトを作成しました。MX または TXT レコードには触れず、メールレコードの TTL 変更は Google Workspace との配信性に影響を与える可能性があるためです。
# A と AAAA レコードのみを対象 if record["type"] in ("A", "AAAA"): update_record_ttl(domain, record["id"], 300)
古い TTL が破棄されるのを待ち、1 時間経過後、準備完了でした。
旧サーバー Nginx をリバースプロキシへの変換
34 の設定ファイルを manualmente 編集するのではなく、すべての設定ファイル内の
server {} ブロックを解析し、主要なコンテンツブロックを特定してプロキシ構成に置換し、オリジナルを .backup ファイルとしてバックアップする Python スクリプトを書きました。
server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; location / { proxy_pass https://NEW_SERVER_IP; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_ssl_verify off; proxy_read_timeout 150; } }
重要点:
proxy_ssl_verify off — 新しいサーバーの SSL 証明書はドメインに対して有効であり、IP アドレスに対しては有効ではありません。両方を経営しているため、ここでは検証を無効化しても問題ありません。
クイックスウィッチオーバー
レプリケーションが
Seconds_Behind_Master: 0 で、リバースプロキシも準備できた時点で、以下の順序でクイックスウィッチオーバーを実行しました:
- 新サーバー:
STOP SLAVE; - 新サーバー:
SET GLOBAL read_only = 0; - 新サーバー:
RESET SLAVE ALL; - 新サーバー:
supervisorctl start all - 旧サーバー:
(プロキシが稼働)nginx -t && systemctl reload nginx - 旧サーバー:
supervisorctl stop all - Mac:
(DNS: すべての A レコードを新サーバー IP に切り替え)python3 do_cutover.py - 待機:伝播完了まで約 5 分
- 旧サーバー:全 crontab エントリーをコメントアウト
DNS クイックスウィッチオーバースクリプトは DigitalOcean API にアクセスし、すべての A レコードを新サーバー IP に変更しました——所要時間は約 10 秒です。
クイックスウィッチオーバー後の最後の作業
移行後、多数の GitLab プロジェクトウェブフックが依然として旧サーバー IP を指していたことに気づきました。GitLab API を通じてすべてのプロジェクトをスキャンし、バッチで更新するスクリプトを作成しました。
最終的な数値
$1,432/月から $233/月へと減少し、年間 $14,388 を節約しました。かつ、より高性能なマシンを手に入れました:
- CPU: 32vCPU から 96 ロジカル CPU(AMD EPYC 9454P、48 コア x 2 スレッド)へ
- RAM: 192GB から 256GB DDR5 へ
- ストレージ:混合された約 2.6TB から 2TB NVMe RAID1 へ
- ダウンタイム:0 分
全体的な移行は約 24 時間で行われました。ユーザーへの影響はありませんでした。
主な教訓
- MySQL レプリケーションはゼロダウンタイム移行のための最良の友です。早期に設定し、追いつかせることで、確信を持ってクイックスウィッチオーバーを実行できます。
- 移行前に MySQL ユーザー権限を確認してください。SUPER 権限は
を回避するため、アプリユーザーが持っていればスレーブ環境は実際にはリードオンリーではありません。read_only - 全てをスクリプト化してください。DNS 更新、Nginx 構成書き換え、ウェブフック更新などを 34 カ所以上のサイトで行うと時間がかかるうえにエラーを起こす可能性があります。
+mydumper
は大規模データセットに対して大幅にmyloader
よりも優れています。32 スレッドによる並列ダンプ/レストアによって、元数日かかっていた作業を数時間に短縮できました。mysqldump- クラウドプロバイダーは定常ワークロードには高価です。オートスケーリングまたは一時的インフラストラクチャを使用していない場合、デディケイテッドサーバーはわずかなコストでより高いパフォーマンスを提供することが多いです。
全てのスクリプト GitHub に公開
この移行で使用したすべての Python スクリプトはオープンソース化されており、GitHub で入手可能です:
— DigitalOcean のドメインおよび A レコード、IP、TTL をリストdo_list_domains_ttl.py
— A/AAAA レコードの TTL をバッチで 300 秒に削減do_ttl_update.py
— DNS ゾーンを DigitalOcean から Hetzner DNS へ移行do_to_hetzner_bulk_dns_records_import.py
— すべての A レコードを旧サーバー IP から新サーバー IP に切り替えdo_cutover_to_new_ip.py
— すべての Nginx サイト設定をリバースプロキシ構成に変換nginx_reverse_proxy_update.py
— 2 つの MySQL サーバー間でのテーブルごとの行数を比較mysql_compare.py
— すべての GitLab プロジェクトウェブフックを新サーバー IP に更新final_gitlab_webhook_update.py
— mydumper ライブラリmydumper
すべてのスクリプトは安全に変更内容を適用前にプレビューできる
DRY_RUN = True モードをサポートしています。