今年の初め、社内のメディアストレージクラスタが過負荷の上限に達しました。従業員から毎日約12万件の画像やPDFファイル(新規データ量として300GB以上に相当)がアップロードされていました。そこで浮上したのが、ホットバックアップの確保とNginxによる読み取り負荷分散のため、プライマリノードからセカンダリノードへ全ファイルを即座に複製しなければならないという厳しい課題でした。
この規模でCephやGlusterFSを構築するのは、いわば「牛刀をもって鶏を割く」ようなものです。システムはわずか2ノード構成であり、分散クラスタを組むのは構成が肥大化するうえ、ハートビートの維持だけで貴重なRAMを消費してしまいます。検討を重ねた結果、私たちのチームはCentOS Stream 9上の定番コンビであるLsyncd + Rsyncを採用することにしました。本番環境で6か月以上運用した結果、消費メモリは40MB未満に収まり、ほぼメンテナンスフリーで稼働し続けています。
クイックスタート:5分で構築完了
構築モデルは、同一の10Gbpsプライベートネットワーク内にある2台のサーバーで構成されます:
- 送信元サーバー(Source Node):
192.168.1.10 - 送信先サーバー(Target Node):
192.168.1.20 - 同期対象ディレクトリ:
/var/www/uploads/
ステップ1:パスワードなしのSSH鍵認証を設定する
Lsyncdはroot権限のバックグラウンドプロセスとして動作し、SSH経由でRsyncを呼び出します。そのため、送信元ノードから送信先ノードへパスワードなしで直接SSH接続できる権限が必要です。
送信元サーバー上でEd25519鍵ペアを生成し、送信先サーバーへ転送します:
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
ssh [email protected]コマンドで接続テストを行います。パスワードを求められずに送信先のシェルへ直接ログインできれば、第1ステップは完了です。
ステップ2:RsyncとLsyncdのインストール
Rsyncは両方のノードに必要です。一方、Lsyncdは送信元サーバーにのみインストールします。CentOS Stream 9では、このパッケージはEPELリポジトリに用意されています。
以下のコマンドを両方のサーバーで実行します:
sudo dnf install -y epel-release
sudo dnf install -y rsync
続けて、送信元サーバーにLsyncdをインストールします:
sudo dnf install -y lsyncd
ステップ3:Lsyncd設定ファイルの作成
Lsyncdの設定はLua構文で記述します。バックアップ用に元の設定ファイルを退避させ、/etc/lsyncd.confに新規ファイルを作成します:
sudo cp /etc/lsyncd.conf /etc/lsyncd.conf.bak
sudo nano /etc/lsyncd.conf
LAN環境向けの標準的な設定内容は以下の通りです:
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
statusInterval = 10,
maxProcesses = 2
}
sync {
default.rsyncssh,
source = "/var/www/uploads",
host = "[email protected]",
targetdir = "/var/www/uploads",
delay = 3,
rsync = {
archive = true,
compress = false,
_extra = { "--bwlimit=60000" }
}
}
ステップ4:起動と動作確認
まだ存在しない場合は、両方のサーバーで/var/www/uploadsディレクトリを事前に作成しておきます。送信元サーバーでサービスを起動・有効化します:
sudo systemctl enable --now lsyncd
sudo systemctl status lsyncd
テストファイルを素早く作成してみましょう:touch /var/www/uploads/demo-sync.txt。送信先ノードのuploadsディレクトリを確認すると、約3〜5秒後にファイルが同期されているはずです。
仕組みの深掘り:Lsyncdはどのように動作するのか?
「なぜ毎分cronジョブでrsyncを実行する構成にしないのか?」と疑問に思う方も多いでしょう。
問題はディスクI/Oにあります。ディレクトリ内のファイル数が80万件規模に膨らむと、rsyncが実行されるたびにディレクトリツリー全体をスキャンし、タイムスタンプやファイルサイズを比較しなければなりません。その結果、実際には2件しか新規ファイルが作成されていないにもかかわらず、ディスクI/Oが常時ボトルネックとなり、CPU使用率が急上昇してしまいます。
Lsyncdは、以下の2層の処理を組み合わせることでこの課題を根本から解決します:
- Linux inotify: LsyncdはLinuxカーネルからのイベント(作成、書き込み、削除、移動)を直接監視します。カーネルが変更されたファイルをピンポイントで通知するため、ディスク全体をスキャンする必要がありません。
- バッチ集約機構(Aggregator): Lsyncdはイベントが発生しても即座にrsyncを呼び出しません。遅延ウィンドウ内(上記設定では
delay = 3秒)に変更をすべて蓄積します。3秒が経過した時点で変動のあったファイル一覧を集約し、その対象ファイル群のみを1回のrsync実行で同期します。
主要パラメータの意味と役割
- default.rsyncssh: LsyncdがSSH経由でファイルをプッシュします。安全性が高く、外部へ追加のサービスポートを公開する必要がありません。
- delay = 3: イベント集約の待機時間(秒)です。この値を0に設定してはいけません。ユーザーが500MBの動画ファイルをアップロードしている最中にdelay = 0となっていると、書き込みが完了していない段階でrsyncが細切れにトリガーされ続け、リソースの浪費やファイル破損の原因になります。
- compress = false: 1Gbps以上の社内LAN環境では、gzip圧縮を無効化してください。CPUによる圧縮・解凍のオーバーヘッドがなくなり、実転送速度が大幅に向上します。圧縮を有効にすべきなのは、50Mbps未満の細い回線を経由してインターネット越しに同期する場合のみです。
- –bwlimit=60000: 帯域幅を約60MB/sに制限します(rsyncの単位はKiB/s)。このフラグにより、rsyncがLAN帯域を占有し、同一ネットワーク上のデータベースなどの他サービスを圧迫するのを防止します。
大規模ファイルシステム向けの本番環境チューニング
Linuxのデフォルト設定は開発環境程度を想定したものです。ファイル数が数十万件を超えると、カーネルパラメータを適切にチューニングしない限りシステム障害につながります。
1. inotifyの監視上限数を引き上げる
CentOS Stream 9のデフォルトでは、inotifyに割り当てられる監視ディスクリプタ数はわずか8,192個です。管理対象ディレクトリの総数がこの値を超えると、ディスクに数百GBの空き容量があっても、Lsyncdは有名なNo space left on deviceエラーを出して即座にクラッシュします。
現在の制限値を確認します:
cat /proc/sys/fs/inotify/max_user_watches
sysctl設定ファイルを用いて、上限を100万ディレクトリまで引き上げます:
cat << 'EOF' | sudo tee /etc/sysctl.d/99-lsyncd.conf
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 1024
fs.inotify.max_queued_events = 32768
EOF
sudo sysctl --system
64ビットシステムにおいて、監視ディスクリプタ1個あたり約1KBのカーネルメモリを消費します。上限いっぱいの100万フォルダを監視したとしても最大約1GBのRAM消費にとどまり、システムの絶対的な安定性が手に入ります。
2. 高性能が求められる場合はRsyncデーモン方式へ移行する
SSH経由の同期はセキュアですが、SSHの暗号化処理に伴うCPU負荷が発生します。信頼できるプライベートサブネット内に2台のノードが存在し、1時間あたり数万件の小さなファイルを同期する必要がある場合は、ポート873でrsyncデーモンを直接稼働させることで、CPU負荷を約35〜40%削減できます。
送信先サーバーで/etc/rsyncd.confを編集します:
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsync.lock
log file = /var/log/rsync.log
[media_mirror]
path = /var/www/uploads
hosts allow = 192.168.1.10
read only = false
uid = nginx
gid = nginx
firewalldでポートを開放し、rsyncデーモンを起動します:
sudo firewall-cmd --permanent --add-service=rsyncd
sudo firewall-cmd --reload
sudo systemctl enable --now rsyncd
送信元サーバーで、/etc/lsyncd.conf内のsyncブロックを調整します:
sync {
default.rsync,
source = "/var/www/uploads",
target = "192.168.1.20::media_mirror",
delay = 3,
rsync = {
archive = true,
compress = false
}
}
3. 一時ファイルや不要なログを除外する
エディタの一時ファイルやログファイルに無駄なI/Oを費やすべきではありません。設定にexclude除外フィルタを追加します:
sync {
default.rsyncssh,
source = "/var/www/uploads",
host = "[email protected]",
targetdir = "/var/www/uploads",
exclude = {
'*.tmp',
'*.swp',
'.git*',
'cache/**'
}
}
運用現場で得た教訓と注意点
検証環境でスムーズに動作したからといって、本番環境でも何事もなく動くとは限りません。以下は、私たちのチームがダウンタイムを経験しながら身をもって学んだ3つの実践的教訓です:
deleteパラメータの取り扱いには細心の注意を払う
デフォルト設定では、default.rsyncsshは削除ミラーリングが有効になっています。つまり、送信元でファイルが削除されると、送信先ノードのファイルも自動的に削除されます。
これはホットバックアップにおいて諸刃の剣となります。送信元で誤ってrm -rfを実行してしまった場合、わずか3秒で送信先のデータまで全消去されてしまいます。安全なバックアップを主目的に据えるなら、自動削除機能は無効にしておくのが確実です:
delete = false
ディスク容量の節約のために不要ファイルを削除したい場合は、delete = 'running'に設定するのがより安全なアプローチです。このパラメータを指定すると、Lsyncdは新規ファイルの同期プロセスがすべて完了した後にのみ送信先の削除を実行するため、ネットワークが不安定な際の不意なデータ損失を防ぐことができます。
Lsyncdは単方向(Master-Slave)専用ソリューションである
双方向の相互同期(AからBへプッシュしつつ、同時にBからAへもプッシュする構成)は絶対に設定しないでください。Lsyncdには競合解決(コンフリクトリゾリューション)の仕組みが備わっていません。双方向で同時に同一ファイルが更新されると、両ノード間でイベントが無限に連鎖し、激しい競合状態(レースコンディション)のループが発生します。その結果、CPU使用率は100%に達し、ログファイルは数十GBに肥大化してサービス停止に追い込まれます。両方のノードから書き込みを行う要件がある場合は、GlusterFS、MooseFS、またはMinIOなどのオブジェクトストレージの採用を検討してください。
カーネルのOOM Killerによる強制終了から自動復旧させる
サーバー上のPHP-FPMやElasticsearchなどの高負荷アプリケーションがRAMを食いつぶすと、LinuxのOOM Killerがメモリ解放のためにLsyncdを対象に選んでkillしてしまうことがあります。サービスがサイレント停止するのを防ぐため、systemdのオーバーライド設定を作成しておきます:
sudo systemctl edit lsyncd
以下の内容を記述し、プロセス停止から5秒後にsystemdが自動再起動するように設定します:
[Service]
Restart=always
RestartSec=5s
同期の健全性を監視するには、ステータスファイルを確認するだけです:
cat /var/log/lsyncd/lsyncd.status
Inotify watchesが実際のフォルダ数と一致しており、Delay queuesが定常的に0を維持していれば、システムは極めて安定して稼働しています。
