rsync で pdumpfs みたいなことをする

いままで履歴つきのバックアップは pdumpfs*1 でとっていたのですが,rsync のオプション(--link-dest)を使うと同等のことをできるらしいと知りました。

サンプル

バックアップ元のファイル群をサンプルとして作成します。

$ mkdir -p work/src/foo
$ echo "baz" > work/src/foo/bar

これで,

- work/
- work/src/
- work/src/foo/
- work/src/foo/bar

のような構造ができました。これのバックアップをとっていきます。

まずは普通に rsync

履歴つき(差分)バックアップをとろうにも,まずはフルバックアップがないとお話にならないので,初回はふつうに rsync でとります((じっさいは後述するように --link-dest は対象ディレクトリの存在有無は確認しないので,この段階から --link-dest を指定してもいいのですが。))。

$ mkdir -p work/dst

$ rsync -avv --delete work/src/ work/dst/1/

building file list ... 
done
created directory work/dst/1
deleting in .
delta-transmission disabled for local transfer or --whole-file
./
foo/
foo/bar
total: matches=0  hash_hits=0  false_alarms=0 data=4

sent 163 bytes  received 54 bytes  434.00 bytes/sec
total size is 4  speedup is 0.02

ソースディレクトリとなる work/src/ の末尾にスラッシュ(/)をつけ忘れないよう注意。

  • SRCの末尾に/をつける。たいてい必要。
    • SRCスラッシュの有無は、mv SRC DESTmv SRC/* DEST の違いと一緒。スラッシュの後ろに*が省略されているものと考える。
    • DESTのスラッシュの有無は関係なし。
地雷だらけのrsyncを理解する。 - こせきの技術日記

この説明がとてもしっくりきました。

上記引用のように宛先ディレクトリ(DEST)の末尾スラッシュは必要ありません。が,対称性を重視していちおうこちらにもつけてあります。ちなみに,上記のように work/dst/1/ がターゲットディレクトリとなる場合,work/dst/ までのディレクトリは存在している必要があります(なので事前に作成している)。ターゲット自身となる 1/ ディレクトリについては,存在しない場合に rsync が作成してくれます。mkdir -p 的にはやってくれないってことですね。

いよいよ --link-dest つきで rsync

ベースとなるフルバックアップがとれたので,いよいよ差分バックアップをおこないます。

めんどうなのでファイルの追加・編集はとりあえずなしで。

$ rsync -avv --delete --link-dest=../1 work/src/ work/dst/2/

building file list ... 
done
created directory work/dst/2
deleting in .
delta-transmission disabled for local transfer or --whole-file
./
foo/
foo/bar is uptodate
total: matches=0  hash_hits=0  false_alarms=0 data=0

sent 120 bytes  received 39 bytes  318.00 bytes/sec
total size is 4  speedup is 0.03

ログをみるとわかりますが,

foo/bar is uptodate

のように,bar というファイルはすでに最新版であると判断されています(work/dst/2/ にはもともと存在していなかったのにね)。

実際にハードリンクされているかどうか,i-node 番号をみてみましょう。

$ find work/dst -ls

3605251  4 drwxr-xr-x  4 dayflower users  4096 May 13 11:08 work/dst
3605252  4 drwxr-xr-x  3 dayflower users  4096 May 13 11:07 work/dst/1
3605254  4 drwxr-xr-x  2 dayflower users  4096 May 13 11:07 work/dst/1/foo
3605256  4 -rw-r--r--  2 dayflower users     4 May 13 11:07 work/dst/1/foo/bar
3605257  4 drwxr-xr-x  3 dayflower users  4096 May 13 11:07 work/dst/2
3605258  4 drwxr-xr-x  2 dayflower users  4096 May 13 11:07 work/dst/2/foo
3605256  4 -rw-r--r--  2 dayflower users     4 May 13 11:07 work/dst/2/foo/bar

foo/bar の i-node 番号は 1/ 下も 2/ 下も 3605256 となっており,同一です。またリンクカウントも 2 になっています。

ディレクトリエントリ(foo/)の i-node 番号は異なりますが,これは Unixファイルシステムではディレクトリにハードリンクは貼れないためです。

なお,ソースディレクトリ側の i-node 番号をみてみますと,

$ find work/src -ls

3605248  4 drwxr-xr-x  3 dayflower users  4096 May 13 11:07 work/src
3605249  4 drwxr-xr-x  2 dayflower users  4096 May 13 11:07 work/src/foo
3605250  4 -rw-r--r--  1 dayflower users     4 May 13 11:07 work/src/foo/bar

3605250 となっており,異なります。これはローカルであろうと実体コピーをするという rsync の仕様です。そもそもバックアップ用途ですしね。

--link-dest相対パスで指定するときの注意

さきほど --link-dest=../1 のように指定しました。普通の感覚だと --link-dest=work/dst/1 のように,カレントディレクトリからの相対パスで書きそうなものですが,

--link-destの利用で特に注意が必要なのは、--link-destで指定するディレクトリは、コピー先ディレクトリからの相対パスで指定する必要があるということです。

rsyncで差分バックアップを行うための「--link-dest」オプション - ITmedia エンタープライズ

If DIR is a relative path, it is relative to the destination directory.

rsync (1)

のように,相対パスで指定する場合は,宛先ディレクトリからの相対パスで記述する必要があるのです*2--link-dest=1/ でもないことに注意。

(なお ITmedia の記述では相対パスで書かなきゃいけないように感じますが,絶対パスで指定することもできます; インタラクティブに実行する場合などは,絶対パスで指定しておくと間違いがないかと思います)

カレントディレクトリからの相対パスで実際にやってみましょう。

$ rm -rf work/dst/2

$ rsync -avv --delete --link-dest=work/dst/1 work/src/ work/dst/2/

building file list ... 
done
created directory work/dst/2
deleting in .
delta-transmission disabled for local transfer or --whole-file
./
foo/
foo/bar
total: matches=0  hash_hits=0  false_alarms=0 data=4

sent 163 bytes  received 54 bytes  434.00 bytes/sec
total size is 4  speedup is 0.02

先ほどと異なり,

foo/bar is uptodate

とはなりませんでした。これは,--link-dest オプションが指定されなかった場合の挙動と同じです。つまり work/dst/2/ が空なので,まるごとコピーしようとしているということです。

i-node 番号を確認してみます。

$ find work/dst -ls

3605251  4 drwxr-xr-x  4 dayflower users  4096 May 13 11:08 work/dst
3605252  4 drwxr-xr-x  3 dayflower users  4096 May 13 11:07 work/dst/1
3605254  4 drwxr-xr-x  2 dayflower users  4096 May 13 11:07 work/dst/1/foo
3605256  4 -rw-r--r--  1 dayflower users     4 May 13 11:07 work/dst/1/foo/bar
3605257  4 drwxr-xr-x  3 dayflower users  4096 May 13 11:07 work/dst/2
3605258  4 drwxr-xr-x  2 dayflower users  4096 May 13 11:07 work/dst/2/foo
3605259  4 -rw-r--r--  1 dayflower users     4 May 13 11:07 work/dst/2/foo/bar

見事に i-node 番号が異なって(36052563605259)いますね。リンクカウントも双方 1 となっていますし。


以上のように,--link-dest で指定したディレクトリが存在しなかった場合でも,rsync はなんの文句もいわずに実行してしまいます。気をつけましょう。

その代わり,フルバックアップの際と差分バックアップの際とで同一のコマンドラインを使うことができます(具体例は後述します)。


なお rsync 2.6.4 以降では,--link-dest には複数のディレクトリを指定することができます。そのようにすると,指定した順に同一のファイルを探し,みつかったところにハードリンクを貼ることになります。適用可能な局面がちょっと思いつきませんが。

注意しておくこと

ハードリンクを利用しているので履歴を保存していってもあまり容量を食わないというメリットがある半面,デメリットももちろん存在します*3

i-node が枯渇する

さきにも触れましたが,ディレクトリエントリ自体にハードリンクは貼れません。このため,たとえ変更がなくても履歴をどんどんとっていくと,ディレクトリエントリの数だけ i-node が消費されていきます。

もちろんファイル実体と異なり,ディレクトリエントリに占めるディスク容量なんてたいしたことはありません。しかし,i-node の領域を静的に確保しているファイルシステムext2/3/4)では,ディスク容量が枯渇するよりさきに,i-node が枯渇してしまいます。

なお,XFS や ZFS など,よりモダンなファイルシステムでは i-node 用領域は動的に確保されるので,i-node が枯渇することはありません((といっても i-node 番号の数値がいつかは枯渇するかもしれません。これは OS 側だと  ino_t という型のサイズによります。手元の 32bit 環境(Linux 2.6.18 i686)では 32bit でした。ファイルシステム側はファイルシステムによるんだと思います。ext3 では 32bit ぽい。))。⇒ ファイルシステム諸元 - 詳解ファイルシステム

バックアップしたファイルを書き換えると……

バックアップされたファイルはハードリンクされているので,(変更がない限り)同じ実体をさしています。ですから,この実体を書き換えると,(同一のハードリンクとなる)履歴すべてに影響がでます。

ちょっと実験してみましょう。

$ echo "foo" > foo.txt
$ ls -i foo.txt

3605239 foo.txt


$ echo "bar" >> foo.txt
$ ls -i foo.txt

3605239 foo.txt


$ echo "baz" > foo.txt
$ ls -i foo.txt

3605239 foo.txt

シェル上からファイルの「追記」や「上書き」をしてみましたが,i-node 番号は変わりませんでした(なお vi など一般的なエディタ等のアプリケーションで内容を書き換えると,コピーをとってから変更しているので i-node 番号が変わっていきます)。

もしファイルシステムが一部壊れた,とか,ウィルスに感染して一部ファイルがかわってしまった,とか,悪い人がバックアップファイルを直接いじった,とかいうことがあると「履歴」の部分はまったく強みをもちません。

その他利点と欠点について [http://slashdot.jp/~ruto/journal/362588:title] がとてもよくまとまっているので一読をおすすめします。

おまけ

4 世代の履歴つきバックアップをとるシェルスクリプトです。

#!/bin/sh

if [ $# -lt 2 ]; then
    echo "usage: $0 <SOURCE> <DESTINATION>"
    exit 1
fi  

force_trailing_slash() {
    case $1 in
        */) echo -n "$1"    ;;
        *)  echo -n "$1/"   ;;
    esac
}

SRC=`force_trailing_slash $1`
DST=`force_trailing_slash $2`

[ -d "${DST}3" ]      && rm -rf "${DST}3"
[ -d "${DST}2" ]      && mv "${DST}2" "${DST}3"
[ -d "${DST}1" ]      && mv "${DST}1" "${DST}2"
[ -d "${DST}latest" ] && mv "${DST}latest" "${DST}1"

[ -d "${DST}latest" ] || mkdir -p "${DST}latest"

#[ -d "${DST}1" ] && \      # missing --link-dest dir will be ignored
LINK_DEST="--link-dest=../1"


echo "rsync -vvaHz --delete $LINK_DEST ${SRC} ${DST}latest/"
exec rsync -vvaHz --delete $LINK_DEST "${SRC}" "${DST}latest/"

rm -rf "${DST}3" の部分が,旧来の差分バックアップの観点からするとぎょっとしますが,ハードリンクの特性上,削除してしまっても構わないんです。


pdumpfs のように日付入りのフォルダ名にするには結局なんらかのスクリプトを書く必要がでてくると思います。が,単一の(C で書かれた)プログラムで更新状況を確認しつつガッとコピーしてくれるので,こちらのほうが pdumpfs より軽いのではないかなぁと思います。もちろんローカルなら pdumpfs どころか cp -al でもいいんでしょうけど,リモートのファイル群をバックアップするならどうせ rsync することになるので,一つのコマンドで完結しているのはうれしい。

*1:pdumpfs: a daily backup system similar to Plan9's dumpfs

*2:なんかひどい設計の気もしますが,ローカルフォルダをリモートホストにバックアップする際のことを考えてこのような仕様になっているんだと思います。

*3:もちろんこれは rsync だけではなく pdumpfs などについてもあてはまることですが。