現場で使えるシェルスクリプト入門25:rsyncによる高速差分同期と安全なバックアップスクリプト

シェルスクリプト IT

前回の「入門24」では、tar コマンドと find コマンドを組み合わせて、設定ファイルや小規模データを圧縮・世代管理する自動バックアップスクリプトを作成しました。

tar による圧縮アーカイブは、設定ファイルやデータベースのバックアップなど、「特定時点の全データを1つのファイルとして独立保管・復元する」用途において非常に信頼性が高く、運用現場の基本となる手法です。

一方で、Webサイトの画像アセットや大容量ストレージのように、蓄積されるデータが数十GBを超え、日々の変更が一部のファイルにとどまるケースでは、差分検出アルゴリズムによって「変更があったファイルのみ」を高速に転送・同期できる rsync を組み合わせると、処理時間やストレージを抑えたより効率的な運用が可能になります。本記事では、rsync の基本構文から、注意しておきたい「末尾スラッシュ」の挙動、--dry-run や --delete を安全に使うためのポイント、そしてシェルスクリプトによる自動同期の実装までを分かりやすく解説します。

tarによるフルバックアップとrsyncによる差分同期の違い

バックアップを自動化するにあたり、まずは tar と rsync の特性の違いを把握し、用途に応じて使い分けることが重要です。

比較項目tar(アーカイブ・圧縮)rsync(差分同期)
主な方式指定した全ファイルを1つのファイル(.tar.gz等)にまとめる送信元と送信先の差分を検出し、変更点のみ同期する
適した用途設定ファイル、小〜中規模データベース、世代別バックアップ大容量ストレージ、Webコンテンツ、別サーバーへの複製
所要時間全データをまとめて処理・圧縮するため安定した時間を要する差分ファイルのみ転送するため2回目以降は短時間で完了する
データ管理特定日時の状態を単一アーカイブ(スナップショット)として独立保存最新状態をミラーリング同期(必要に応じてハードリンクで世代管理)

日々の運用現場では、「日常的な大容量ディレクトリの高速同期には rsync」、「月次や週次のスナップショット長期保管には tar」のように、それぞれの強みを組み合わせて併用する構成が広く採用されています。

rsyncの基本構文と主要オプション

rsync の基本書式は、標準的なコピーコマンド(cp)とよく似ています。

rsync [オプション] コピー元 コピー先

バックアップや同期を行う際、基本セットとして広く利用されているのが -avh オプションです。

  • -a(アーカイブモード):再帰的コピー(-r)、パーミッション保持(-p)、所有者・グループ保持(-o, -g)、タイムスタンプ保持(-t)、シンボリックリンク保持(-l)などを一括で有効化します。バックアップ用途では基本的に指定することが推奨されます。
  • -v(詳細表示 / verbose):処理中に転送されたファイル名などの詳細情報を画面に出力します。
  • -h(ヒューマンリーダブル):転送サイズをバイト単位ではなく「K」「M」「G」などの分かりやすい単位で表示します。

ローカル環境のバックアップを実行する最小限のコマンド例は次のようになります。

rsync -avh /var/www/html/ /backup/www/

ネットワーク越し(別サーバー等)に転送する場合は、-z(データ圧縮転送)を追加することで、転送帯域を節約して効率的に同期できます。

注意点:末尾スラッシュの有無による挙動の違い

rsync を扱う上で、最も意図しないミスが発生しやすいポイントが「コピー元の末尾にスラッシュ(/)を付けるかどうか」です。

スラッシュの有無によって、コピー先に作成されるディレクトリ構造が大きく変わります。

パターンA:末尾にスラッシュをつける場合(推奨されるケース多数)

rsync -avh /src/ /dest/

末尾にスラッシュを付けた場合、/src/ ディレクトリの「中身だけ」が /dest/ 直下にコピーされます。バックアップ先で同じ階層構成をそのまま再現したい場合は、この書き方が一般的です。

パターンB:末尾にスラッシュをつけない場合

rsync -avh /src /dest/

末尾にスラッシュがない場合、/dest/ の配下に src ディレクトリ「そのもの」が作成され、結果として /dest/src/... という入れ子構造になります。

「バックアップ先を開いたら、意図せずフォルダが二重になっていた」というトラブルの多くは、この末尾スラッシュの付け忘れが原因です。シェルスクリプトで変数展開する際は、末尾スラッシュの有無を設計段階で統一しておきましょう。

–deleteオプションによるミラーリングと安全対策

通常の rsync は「追加・更新されたファイル」をコピー先に反映しますが、「送信元ですでに削除されたファイル」は送信先にそのまま残り続けます。

送信元と送信先を同一の状態に保つ(ミラーリングする)ためには、--delete オプションを使用します。

rsync -avh --delete /src/ /dest/

ただし、この --delete は強力である反面、指定ミスによるデータ消失のリスクを伴います。安全運用のために、以下の2つの対策を講じる習慣をつけましょう。

対策1:–dry-run(-n)による事前シミュレーション

--dry-run(または -n)オプションを付与すると、「実際のファイル移動や削除を行わず、実行された場合に何が起こるか」を画面に出力して確認できます。

rsync -avh --delete --dry-run /src/ /dest/

予期せぬファイルが削除対象になっていないかを事前にチェックすることで、取り返しのつかないミスを未然に防ぐことができます。

対策2:スクリプト内でのディレクトリ存在チェック

外付けHDDやマウントされたネットワークドライブを送信元としている場合、万一マウントが外れて送信元が「空ディレクトリ」になっていると、--delete によってバックアップ先のファイルが全削除されてしまう危険があります。

そのため、スクリプト内では送信元および送信先が存在するかを事前に検証するガード処理を組み込むのが確実です。

削除・消失ファイルへの対策:–ignore-missing-argsと終了コード24の制御

日々の同期処理やバックアップを自動化していると、「同期しようとしたファイルが直前に削除された」「同期の最中に一時ファイルやログが消えた」といった状況に遭遇することがあります。

デフォルトの rsync ではこれらをエラーや警告として扱いますが、シェルスクリプトで自動運用する際は、スクリプトが不意に停止しないよう適切にハンドリングしておくことが望ましいです。

指定ファイルが存在しなくてもエラーにしない:–ignore-missing-args

特定のファイルリストや複数の引数を指定して同期を行う場合、送信元に対象ファイルが存在しないと rsync は No such file or directory エラー(終了コード 3 等)を出力して異常終了します。

このようなケースで、「送信元にファイルが存在しなくてもエラーとせず、単にスキップして処理を続けたい」場合に役立つのが --ignore-missing-args オプションです。

# 送信元ファイルが事前に削除されていてもエラーにならずスキップする
rsync -avh --ignore-missing-args /path/to/file1.txt /path/to/file2.txt /backup/

バッチ処理やリストからのバックアップなど、一時ファイルが消えている可能性がある場面で有効なオプションです。

同期中にファイルが消えた場合の警告:終了コード24のハンドリング

稼働中のWebサーバーやアプリケーションのログ、一時ディレクトリを同期していると、「rsyncがファイル一覧を走査したあと、転送を開始するまでのわずかな間にファイルが削除された」という事象が起こり得ます。

この場合、rsyncは以下のようなメッセージを出力し、終了コード「24」で終了します。

file has vanished on sender: /var/www/html/tmp/session_98234.tmp
rsync warning: some files vanished before they could be transferred (code 24)

これは「転送前にファイルが消失した」という警告(部分転送)であり、重大な障害ではないケースが多く見られます。しかし、スクリプト内で単純な成功(終了コード 0)のみを期待していると、スクリプト全体が異常終了とみなされてしまいます。

そのため、スクリプト内では case 文を活用し、終了コード「0(成功)」だけでなく「24(消失ファイルのスキップ)」も許容範囲として処理を継続させる設計が広く採用されています。

実践:安全な自動同期バックアップスクリプトの作成

ここまでの知識を統合し、サンプルバックアップスクリプトを作成してみます。

#!/bin/bash
# ==============================================================================
# スクリプト名: backup_rsync.sh
# 概要: rsyncを用いたディレクトリの高速差分同期バックアップ
# ==============================================================================

set -uo pipefail

# --- 設定項目 ---
readonly SRC_DIR="/var/www/html"          # バックアップ対象(末尾スラッシュなしで定義)
readonly DEST_DIR="/backup/www_mirror"    # 同期先ディレクトリ
readonly LOG_DIR="/var/log/backup"        # ログ保存先
readonly LOG_FILE="${LOG_DIR}/rsync_$(date +%Y%m%d).log"

# --- ログディレクトリの準備 ---
mkdir -p "$LOG_DIR"

log_message() {
    local level="$1"
    local message="$2"
    echo "$(date '+%Y-%m-%d %H:%M:%S') [${level}] ${message}" | tee -a "$LOG_FILE"
}

log_message "INFO" "バックアップ処理を開始します。"

# --- 安全性チェック: 送信元ディレクトリの存在確認 ---
if [[ ! -d "$SRC_DIR" ]]; then
    log_message "ERROR" "送信元ディレクトリが存在しません: ${SRC_DIR}"
    exit 1
fi

# 送信元が空ディレクトリでないか簡易確認(意図しない全削除防止)
if [[ -z "$(ls -A "$SRC_DIR" 2>/dev/null)" ]]; then
    log_message "ERROR" "送信元ディレクトリが空です。処理を中断します: ${SRC_DIR}"
    exit 1
fi

# --- 送信先ディレクトリの準備 ---
if [[ ! -d "$DEST_DIR" ]]; then
    log_message "INFO" "送信先ディレクトリを作成します: ${DEST_DIR}"
    mkdir -p "$DEST_DIR"
fi

# --- rsync 同期実行 ---
# 注意: 末尾スラッシュを明示的に付与してディレクトリの中身を同期
log_message "INFO" "同期を実行中: ${SRC_DIR}/ ➔ ${DEST_DIR}/"

rsync -avh --delete --ignore-missing-args \
    --exclude="*.tmp" \
    --exclude="cache/" \
    --exclude=".git/" \
    "${SRC_DIR}/" "${DEST_DIR}/" >> "$LOG_FILE" 2>&1
RSYNC_STATUS=$?

# 終了コードの判定(コード24: 送信元の一時ファイル消失も許容)
case "$RSYNC_STATUS" in
    0)
        log_message "INFO" "バックアップ同期が正常に完了しました。"
        exit 0
        ;;
    24)
        log_message "WARN" "同期中に一部のファイルが消失(削除)されましたが、処理は継続されました(終了コード: 24)。"
        exit 0
        ;;
    *)
        log_message "ERROR" "rsyncの実行中にエラーが発生しました(終了コード: ${RSYNC_STATUS})。ログを確認してください: ${LOG_FILE}"
        exit "$RSYNC_STATUS"
        ;;
esac

スクリプトの工夫と安全ポイント

  • 空ディレクトリの判定:ls -A で送信元にファイルが存在するかを判定し、マウント外れ等による不用意なミラーリング全削除を防止しています。
  • 不要ファイルの除外(--exclude):キャッシュフォルダや一時ファイル(*.tmp)、Gitリポジトリ(.git/)など、バックアップ不要なデータを除外して容量と転送時間を節約しています。
  • 終了コード24の適切なハンドリング:case 文で終了コードを分岐させ、転送中のファイル消失(コード24)を警告扱いとして安全に終了(exit 0)させています。
  • ファイル消失に備えたオプション:--ignore-missing-args を付与し、指定した一時ファイルが事前に削除されていた場合のエラー停止を防止しています。

発展:–link-destを活用した省容量な世代管理

rsync は単なるミラーリングだけでなく、「ディスク容量は変更分しか使わないのに、毎回フルバックアップとして使える世代管理」を実現できます。その鍵となるのが --link-dest オプションです。

ハードリンクとは?(名札のイメージ)

Linuxでは、「ファイルの中身(データ実体)」と「ファイル名(名札)」が分かれて管理されています。

  • 通常のコピー:データ実体そのものを新しく複製するため、同じ容量をもう一度消費します。
  • ハードリンク:データ実体は複製せず、「同じデータ実体に新しい名札(別名)をもう1つ貼る」仕組みです。そのため、ディスク容量を追加で消費しません。

一般的なショートカット(シンボリックリンク)とは異なり、ハードリンク同士には主従関係がありません。そのため、参照元の古い世代のフォルダを後から削除しても、新しい世代のファイルが壊れて消える心配はありません(データ実体を参照する名札がすべて消えるまでデータは残ります)。

–link-dest の具体的な動作例

例えば、全体で 100GB あるデータのうち、今日変更されたファイルが 1MB だけだったケースを考えてみます。

# 前日(yesterday)のバックアップを参照し、本日(today)のバックアップを作成
rsync -avh --delete --link-dest=/backup/yesterday /src/ /backup/today/
  • 変わっていないファイル(約100GB分):昨日のデータ実体へハードリンク(新しい名札)を貼るだけなので、追加のディスク容量はほぼ 0バイト で済みます。
  • 変更されたファイル(1MB分):新しいデータ実体として書き込まれます。

この結果、新しく消費したディスク容量はわずか1MBなのに、/backup/today/ フォルダを開くと100GBすべてのファイルが揃った状態になります。復元の際も過去の差分を辿って結合する手間がなく、その日のフォルダをそのまま取り出すだけで復旧できるのが大きな魅力です。

💡 ハードリンク運用時の重要な注意点

  • 同一ファイルシステム内でのみ有効:ハードリンクはファイル管理番号(i-node)を共有する仕組みのため、別ディスクや異なるパーティションを跨いで作成することはできません。--link-dest を使う際は、前回と今回のバックアップ先が同じファイルシステム上(同一ディスク・パーティション)にあることを確認してください。
  • ディレクトリ自体の挙動:Linuxの仕様上、循環参照(無限ループ)を防ぐためディレクトリそのものへのハードリンクは禁止されています。rsync はディレクトリ階層を通常通り新規作成し、その中にある「個々のファイル」に対してハードリンクを生成します。

まとめ

今回は、大容量データの効率的な同期を支える rsync コマンドの基本と、安全な自動バックアップスクリプトの実装方法を解説しました。

  • 基本オプションは -avh:パーミッションやタイムスタンプを維持したまま、安全かつ詳細にコピー可能。
  • 末尾スラッシュの有無を常に意識:ディレクトリの中身(src/)を同期するのか、ディレクトリ自体(src)を複製するのかで挙動が激変する。
  • --delete 使用時は入念な事前検証を:意図しない全削除を防ぐため、事前の --dry-run 検証とスクリプト内での送信元ディレクトリ存在チェックを徹底する。
  • ファイル消失に備えたエラーハンドリング:--ignore-missing-args の指定や終了コード「24」の許容分岐で、一時ファイル消失による不意な異常終了を防止する。
  • 大容量の世代管理には --link-dest:ハードリンクを活用することで、容量を抑えながら毎回フルバックアップとして使える世代管理体制を構築できる(同一ファイルシステム内で有効)。

前回の tar + find による圧縮アーカイブと、今回の rsync による差分同期を組み合わせることで、データの規模や重要度に応じた柔軟なバックアップ体制を構築できます。用途に合わせてぜひ活用してみてください。

現場で使えるシェルスクリプト入門 連載ナビゲーション

コメント

タイトルとURLをコピーしました