Mercurialリポジトリのバックアップを取る


2011年 07月 22日

Mercurialリポジトリのバックアップだって?

分散型なんでしょ。要らないじゃん。

OK、もちろんそういう観点はある。Mercurialは分散型なので、該当プロジェクトが熱いうちは、かなり障害に強い。中央リポジトリと言ったところで、それは単にプロジェクト内でそのリポジトリを中央と「見做して」いるだけで、基本的には他のリポジトリと変わらないからだ。せいぜい、コミットメールの設定等が行われているくらいだろう。だから、中央リポジトリサーバーがダウンしても気にせず作業を続けられるし、プロジェクトの誰かのリポジトリを改めて「中央と見做せ」ば良い。復旧も簡単だ。

だから、Subversionほどにはバックアップを気にする必要はない。

それでも、Mercurialで管理している情報の重要さを鑑みれば、バックアップはあるに越したことはない。ことバックアップが重要になってくるのは、前述「該当プロジェクトが熱いうちは」という前提が崩れたときにある。開発を終え、少人数の保守要員が残り、保守案件も下火になり、メンバーもローカルPCを更新したりして、「まあサーバーにあるからいいでしょ。しばらく使わないし、必要になったらcloneしよ」といって皆のローカルディスクからはcloneされたリポジトリが消えていく。唯一残るリポジトリはサーバーのものだけ…。そういうときに限って、サーバーとは壊れるものなのだ。

だから、Mercurialのリポジトリを預かるサーバーは、やはりバックアップについて少なからず気にする必要がある。

push型バックアップ

ではバックアップを取ろう。ひとつ思いつくのは、リポジトリサーバーからバックアップサーバーへ、定期的にリポジトリを「押し付ける」ことだ。

やってみよう。サーバーのリポジトリのhgrcに下記記述をする。

    [paths]
    default = (バックアップ先リポジトリ)
    [hooks]
    changegroup = hg push

基本的には、これだけだ。つまり、自分が何かの変更を受け取ったら、そのままもう一段先にその変更を投げてしまうのである。これでほぼ完璧なバックアップが完成し、なおかつ維持される。

なーんだ。これだけでいいのか。と、最初は思ったのだが、良く考えるとこれは意外にめんどくさい。

上手く動かすためにはいくつか問題があるのだ。

  • 新たにリポジトリを作成するたびにバックアップ先リポジトリを用意する必要がある
  • 新たにリポジトリを作成するたびに[hooks]、[paths]の設定をする必要がある
  • push時に認証を要求されない設定になっている必要がある
  • push時にブランチが作成されたりする等のパターンでコケることがある(–new-branchとか–forceとか付けるか…)
  • ローカルからpushしたとき、「二段階目」のpush結果を受け取るため、メッセージが二重に見える(/dev/nullとかに結果捨てるか…)

四番目五番目については、解決策を模索すれば何かありそうな気はするのだが、致命的なのは一番目二番目だ。「大した手間じゃない」のは確かだが、リポジトリを作る度に何度も同じ設定をすることになるし、そんなことをしているとケアレスなミスを招きがちだ。リポジトリ作成を自動化するというのもひとつの手だが、「自動化されている」ことそのものが抜け落ちて手動で作成されてしまったり、あるいは特殊な要求に応えられなくなったりする。

それは避けたい。要するに「ランニングコスト」を上げたくないのだ。

もちろん、新規リポジトリを作成する頻度が低かったり、可能な限り常に二つのリポジトリを同期したいといったような要求であれば、この方法を掘り下げてみても良いだろう。ただ、今回考えているのは、多数のリポジトリをいくつものプロジェクトから預かるMercurialリポジトリサーバーのバックアップなのだ。そのため、この方法はこれ以上模索していない。

pull型バックアップ

そんなわけで、別の方法を考えてみる。押してダメなら引いてみれば良いじゃない。要するに、バックアップサーバーでリポジトリサーバーから定期的に「引っ張れ」ばいいのだ。

色々紆余曲折はあったが、troterとの共著にてこんなスクリプトが出来上がった。

#!/bin/sh
SERVER=192.168.xx.xx
PORT=22
DESTDIR=/var/backup/hg
SRCDIR=/var/lib/hg

PID=$$
SSH="/usr/bin/ssh -i /path/to/key"
DIRNAME=/usr/bin/dirname
HG=/usr/bin/hg
FIND=/usr/bin/find
WC=/usr/bin/wc
CUT=/bin/cut
LOGGER=/usr/bin/logger
LOGINFO="$LOGGER -p local0.info -t hgbackup[${PID}] INFO:"
LOGERR="$LOGGER -p local0.err -t hgbackup[${PID}] ERR:"

SRCPATHCNT=`echo $SRCDIR/ | $WC -c`

if $SSH $SERVER -p $PORT : > /dev/null 2>&1; then :; else
    $LOGERR SSH Connection failed: "$SSH $SERVER -p $PORT"
    exit 1
fi
for REPOPATH in `$SSH $SERVER -p $PORT $FIND $SRCDIR -type d -name .hg`; do
    REPOPATH=`$DIRNAME $REPOPATH`
    REPOS=`echo $REPOPATH | $CUT -b${SRCPATHCNT}-`
    DESTREPOS=$DESTDIR/$REPOS
    SRCREPOS=ssh://$SERVER:$PORT/$REPOPATH

    if [ ! -d $DESTREPOS/.hg ]; then
        INITCMD="$HG init $DESTREPOS"
        if eval $INITCMD > /dev/null 2>&1; then
            $LOGINFO Initialized: "$INITCMD"
        else
            $LOGERR Initialize failed: "$INITCMD"
        fi
    fi

    PULLCMD="$HG -R $DESTREPOS pull -e '$SSH' $SRCREPOS"
    if eval $PULLCMD > /dev/null 2>&1; then
        $LOGINFO Pulled: "$PULLCMD"
    else
        $LOGERR Pull failed: "$PULLCMD"
    fi
done

こいつを、バックアップサーバーのcronで定期的に回せばいい。自動運転のためにはバックアップサーバーからリポジトリサーバーへ認証なしでsshアクセスが通る必要があるので、passphraseなしの鍵を用意するなりssh-agentで鍵を登録するなりは必要だ。そこがクリアできさえすれば、あとはこのスクリプトが勝手にバックアップを取ってくれる。リポジトリサーバーで新規にリポジトリが作成されても、配置場所がバックアップ対象のディレクトリ以下であれば勝手に検知してバックアップする。階層が深くても、入れ子になっていても問題ない。

やったね。

cronでの起動間隔は、もちろん自由に決めて構わないが、せいぜい1日1回で十分だろう。何故? それは冒頭で述べた通り、「プロジェクトが熱いうちはバックアップはそれほど重要ではない」からだ。このバックアップが必要になるのは、プロジェクトが落ち着き、皆のローカルディスクからコピーが消えたときであり、そんなときにはそもそも頻繁に更新が行われるはずがない。あるいは、天災やら火事やらで普段使っているオフィスがまるごとどうにかなってしまったのなら、もはや1日分程度の遅れがどうとかこうとかってレベルは些細なことだ。いいから人命を優先したまえよ。

ところで、上記スクリプトには失敗検知の仕組みがない。基本的にpullはpushと違い失敗しづらいので、滅多に失敗することはないだろうが、それでも「同じ位置のリポジトリがごっそり別の物に変わっている」みたいなときには失敗する。そんなわけで、失敗したらメールを送信するなどといった仕組みはあってもいい。一応、loggerコマンドでsyslogに出してはいるので、syslog.confとかで失敗ログはメールでするような設定ができるならすると良いかもしれない。素のsyslogだとそういう設定は面倒らしいのだが…。

先に述べた通り、Mercurialであれば普段はバックアップを気にする必要はあまりない。それでも無視できないことなのは違いない。設置は若干面倒かもしれないが、なるべくランニングコストを下げることを意図してスクリプトを書いてみたので、機会があれば参考にしてほしい。

Special Thanks: troter