UECジャーナル

開眼☆シェルスクリプト 【10】Dropboxもどきを作る(1) ― データの同期と排他制御の実装

はじめに

複数のマシンのデータを自動で同期する仕組みを作る。作るのは数台のローカルPCの所定のディレクトリを、リモートのサーバ経由で同期をとるアプリケーションSYNCBOXとする。

この同期アプリケーションでは、ファイルは消せず、集積していくようにする。削除がからむと途端にコードが面倒になるので扱わない。その結果として、スクリプトはとても短くなる。

動作環境

ここでサーバはsync.example.jpとする。サーバとクライアント間は公開鍵認証でssh接続できるものとする。

サーバとクライアントには次のディレクトリを用意する。~/SYNCBOX/ は同期するディレクトリ、~/.syncbox/ はプログラム等のファイル置き場とする。

リスト1: ディレクトリ

/home/ueda/
├── .syncbox
└── SYNCBOX

rsync(1)よるデータの同期

rsync(1)はそれぞれのクライアントから起動する。ここでは次のコマンドで同期を実行する。

リスト2: rsyncコマンドの使い方

#A. リモートからローカルマシンへ同期
$ rsync -auz --timeout=30 sync.example.jp:/home/ueda/SYNCBOX/ /home/ueda/SYNCBOX/
#B. ローカルマシンからリモートへ同期
$ rsync -auz --timeout=30 /home/ueda/SYNCBOX/ sync.example.jp:/home/ueda/SYNCBOX/

rsync(1)は第1引数に同期元、第2引数に同期先を指定する。rsync(1)は同期元のディレクトリの最後に/が指定されているかどうかで違う動作をする。/が指定されていれば、そのディレクトリ以下を同期の対象とし、/が指定されていないとそのディレクトリも含めて同期の対象とする。rsync(1)では、どちらかといえば/を指定して利用するシーンが多いと思う。

rsync(1)で使用する代表的なオプションは次のとおり。

-a
ファイルの属性をなるべく残す
-u
同期先に新しいファイルがあればそちらを残す
-z
データを圧縮して送受信
--timeout=30
30秒通信が途絶えると処理を終了
--delete
送信元にないファイルやディレクトリは送信先でも削除

--deleteオプションには注意しておきたい。rsync(1)に--deleteを指定して同期を実行すると、何かの操作ミスで削除してはいけないファイルを削除してしまうことがある。--deleteの使用には注意が必要だ。

今回のケースでは、rsync(1)は同期をとるマシンのどちらか一方で起動されると、もう一方のマシンでも起動されることになる。どっちのマシンでもrsync(1)が動作し、連携して同期を取る。rsync(1)は、通信が途絶えてもしばらく立ち上がりっぱなしでリトライを繰り返す。今回はこの挙動は不要なので、クライアント側で--timeout=30を指定している。こうすると、通信が指定した秒数以上に途絶えた場合、クライアント側、サーバ側のrsync(1)共にすぐに終了する。

リスト3: rsync(1)の使用例

# ローカルからリモートへコピー (ローカル側)
$ touch file1 file2
$ rsync -auz ./ sync.example.jp:~/

# リモートにローカルのファイルが転送されている (リモート側)
$ ls
file1  file2
$ 
# deleteオプション付きでもう一度ローカルからリモートへコピー (ローカル側)
$ rm file1
$ rsync -auz --delete ./ sync.example.jp:~/

# ローカルにないfile2が消える (リモート側)
$ ls
file1
$ 
# リモートでファイルを更新 (リモート側)
$ echo 'ファイルを更新' > file1

# ローカルからリモートへ同期 (ローカル側)
$ rsync -auz ./ sync.example.jp:~/

# リモートのfile1の方が新しいので同期されない (リモート側)
$ cat file1
ファイルを更新
$ 

ssh(1)よるコマンドのリモート実行

ssh(1)を使うことでリモート側でコマンドを実行させることができる。

リスト4: タイムアウトを指定してのコマンドリモート実行

$ ssh -o ConnectTimeout=5 sync.example.jp "mkdir -p /home/ueda/.syncbox/LOCK"

これは、リモート側でmkdir -p /home/ueda/.syncbox/LOCKを実行せよと、クライアント側から依頼を出すコマンドとなる。

排他区間を用意

アトミックに処理されるコマンドを利用して排他処理を実現する。ここではmkdir(1)を使って排他制御とする※1

リスト5: 排他の実験スクリプト

$ cat locktest.sh 
#!/bin/sh

exec 2> /dev/null

for n in $(seq 1000) ; do
        mkdir ./LOCK && touch ./LOCK/$n &
done
$ 

このスクリプトは、「./LOCKディレクトリを作ってうまくいったら./LOCKディレクトリの下に番号を名前にしてファイルを作る」というプロセスを1000個、バックグラウンド処理で立ち上げるというものだ。

実行すると、./LOCKディレクトリの下には必ず1つだけファイルがあり、たまに、一番最初に立ち上がるプロセスよりも後のmkdir(1)が成功して、1以外のファイルができているはずだ。リスト6に実行例を示す。

リスト6: 排他処理の実験

$ ./locktest.sh 
$ ls ./LOCK && rm -Rf ./LOCK 
1
$ 
$ ./locktest.sh 
$ ls ./LOCK && rm -Rf ./LOCK 
8
$ 

同期アプリケーションではリモートのサーバで、かつ複数のクライアントがいる状況でこの排他区間を作る必要がある。ssh(1)コマンドを使ってリモート側にディレクトリを作ればよい。

リスト7: 排他区間の作り方

$ cat SYNCBOX.SYNC 
#!/bin/sh -xv
# SYNCBOX.SYNC
exec 2> /tmp/$(basename $0)

server=sync.example.jp
dir=/home/ueda

#ロックを取る
ssh $server "mkdir $dir/.syncbox/LOCK" || exit 0

#!!!!排他区間!!!!

#ロックを手放す
ssh $server "rmdir $dir/.syncbox/LOCK"
$ 

このケースでは、ssh(1)コマンドはロックが取得できない場合や、通信がうまくうかない場合には0以外の値を返すことになる。

同期処理を実装

リスト8: SYNCBOX.SYNC

#!/bin/sh -xv
# SYNCBOX.SYNC
# written by R. Ueda (ユニバーサル・シェル・プログラミング研究所) Jul. 21, 2012

exec 2> /tmp/$(basename $0)

server=sync.example.jp
dir=/home/ueda

MESSAGE() {
        DISPLAY=:0 notify-send "SYNCBOX: $1"
}

ERROR_CHECK(){
        [ "$?" = "0" ] && return
        DISPLAY=:0 notify-send "SYNCBOX: $1"
        exit 1
}

#ロックがとれなかったらすぐ終了
ssh -o ConnectTimeout=5 $server "mkdir $dir/.syncbox/LOCK" || exit 0

#pull############################
MESSAGE "受信開始"
rsync -auz --timeout=30 $server:$dir/SYNCBOX/ $dir/SYNCBOX/
ERROR_CHECK "受信中断"
MESSAGE "受信完了"

#push############################
MESSAGE "送信開始"
rsync -auz --timeout=30 $dir/SYNCBOX/ $server:$dir/SYNCBOX/
ERROR_CHECK "送信中断"
MESSAGE "送信完了"

ssh -o ConnectTimeout=5 $server "rmdir $dir/.syncbox/LOCK"

exit 0

Dropdox風ということで、同期処理のメッセージをデスクトップへ通知させたい。このスクリプトではDesktop Notifications Framework (Galago desktop presence framework)で提供されているnotify-send(1)というコマンドを使って、デスクトップ上にメッセージを出している。

ロックは通信が途絶えたりその他エラーが起こったりすると外れない。これに関してはサーバ側でモニタリング用のスクリプトを走らせておいて、ロックが残った場合にはそちらでロックを外す処理を実行する。

※1 使用するファイルシステムやカーネル内部での実装にも依存するのだが、より厳密に処理する必要がある場合には、シンボリックリンクの作成を使用してアトミックな処理とした方がよい。シンボリックリンクの作成はアトミックに処理される。

Software Design 2012年10月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【10】Dropboxもどきを作る(1) ― データの同期と排他制御の実装」より加筆修正後転載

Last modified: 2014-01-13 00:00:00