開眼シェルスクリプト2012年10月号

Pocket
LINEで送る

出典: 技術評論社SoftwareDesign

10. 開眼シェルスクリプト 第10回 オンラインストレージもどきを作る(1)

10.1. はじめに

 前回、前々回は並列処理のテクニックを扱いましたが、
今回からは具体的なアプリケーションの作成に戻り、
VPS経由で、複数のマシンのデータを自動で同期する仕組みを作ります。
要はDropboxのようなものですが、さすがにあの完成度で作り切るのは大変ですので、
簡易的なものをシェルスクリプトで作ってみましょう。

 作るのは数台のローカルPCの所定のディレクトリを、
リモートのサーバ経由で同期をとるというアプリケーションです。

 名前は、Dropboxのアイコンを眺めていたら豆腐のように見えたので、
豆腐ボックスとします。敢えてふにゃふにゃな名前にしましたが、
だからといってかっこいい名前は思いついていません。

 豆腐ボックスは、ファイルを消さずにとにかく集積していくように作ります。
削除がからむと途端にコードが面倒になるので、少なくとも今回は扱いません。
その代わり、今回もソースは全部掲載できるほど短くなっています。

「政事は豆腐の箱のごとし、箱ゆがめば豆腐ゆがむなり。」 —- 二宮尊徳

10.2. 環境

 今回は、筆者の手持ちのマシンを総動員します。
クライアントPCには、次の二台を準備しました。

  • Ubuntu 12.04 x64 on ThinkPad SL510
  • Ubuntu 12.04 x64 on ThinkPad X201

私の自宅のThinkPadと仕事用のThinkPadです。
この連載にも何回か登場していますが、
現在は64ビット版のUbuntuが搭載されています。

 今回はPCのサスペンド機能やデスクトップへの通知機能など、
きわどい機能が登場しますので、一応、Ubuntu Linux上での動作を前提としておきます。
しかし、UNIX系OSならちょっと修正すれば動くはずです。

 今回はサーバ側もUbuntuですが、こちらはsshとrsyncが動けば何でもいいでしょう。

  • Ubuntu 12.04 x64 on さくらのVPS (ホスト名:tofu.usptomonokai.jp)

 サーバとクライアント間は、鍵認証でssh接続できることとします。
rsyncのポート指定をいちいち書いていると面倒なので、
tofu.usptomonokai.jpのsshのポート番号は22番とします。

10.3. 作業開始

 では、作っていきましょう。まず、クライアントマシンとサーバに、
リスト1のようにディレクトリを作ります。
クライアント側は、一台でプログラミングして他のマシンにscpすればよいでしょう。

・リスト1: ディレクトリ

1
2
3
/hoge/ueda
├── .tofubox
└── TOFUBOX

~/TOFUBOX/ は同期するディレクトリで、
~/.tofubox/ はプログラム等のファイル置き場です。

10.3.1. 豆腐ボックスのコアテクノロジー(単なるrsyncとmkdir)

 rsyncは、各クライアントから起動します。
豆腐ボックスでは、リスト2のコマンドと共に使います。

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

1
2
3
4
#A. リモートからローカルマシンへ同期
ueda@X201:~$ rsync -auz --timeout=30 tofu.usptomonokai.jp:/home/ueda/TOFUBOX/ /home/ueda/TOFUBOX/
#B. ローカルマシンからリモートへ同期
ueda@X201:~$ rsync -auz --timeout=30 /home/ueda/TOFUBOX/ tofu.usptomonokai.jp:/home/ueda/TOFUBOX/

rsyncは(特に --delete オプションをつけると)
失敗すると怖いコマンドの一つですが、
基本的にcpやscpと同じで、左側に同期元、右側に同期先を書きます。
ディレクトリの後ろにスラッシュを入れる癖をつけておけば、
あとは直感的に動くはずです。

 今回使うオプション -auz --timeout=30 には、次の意味があります。
度々悲劇を起こす --delete も書いておきます。

オプション 意味
-a ファイルの属性をなるべく残す。
-u 同期先に新しいファイルがあればそちらを残す。
-z データを圧縮して送受信
--timeout=30 30秒通信が途絶えるとあきらめて終了
--delete 送信元にないファイルやディレクトリを送信先で消去

 ちなみに、rsyncは同期をとるマシンのどちらか一方で起動されると、
もう一方のマシンでも起動されます。
どっちのマシンでもrsyncが動いて、連携して同期を取ります。
rsyncは、通信が途絶えてもしばらく立ち上がりっぱなしでリトライを繰り返します。
今回はこの挙動は邪魔なのでクライアント側でtimeoutを指定しておきます。
こうすると、通信が指定した秒数以上に途絶えた場合、
クライアント側、サーバ側のrsync共にすぐ止まります。

 ちょっと u と delete の実験をしてみましょう。

・リスト3: rsyncの実験

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#ローカルマシンにfile1を作る
ueda@X201:~/hoge$ echo これはファイル1 > file1
#リモートマシンにfile2を作る
ueda@tofu:~/hoge$ echo これはファイル2 > file2
#ローカルからリモートへコピー
ueda@X201:~/hoge$ rsync -auz ./ tofu.usptomonokai.jp:~/hoge/
#リモートにローカルのファイルが転送される
ueda@tofu:~/hoge$ ls
file1  file2
#今度はdeleteオプション付きでもう一度ローカルからリモートへ
ueda@X201:~/hoge$ rsync -auz --delete ./ tofu.usptomonokai.jp:~/hoge/
#ローカルにないfile2が消える
ueda@tofu:~/hoge$ ls
file1
#リモートでファイルを更新
ueda@tofu:~/hoge$ echo リモートでfile1を作ったよ > file1
#ローカルからリモートへ同期
ueda@X201:~/hoge$ rsync -auz ./ tofu.usptomonokai.jp:~/hoge/
#リモートのfile1の方が新しいので同期しない
ueda@tofu:~/hoge$ cat file1
リモートでfile1を作ったよ

 また、sshコマンドを使うとリモート側でコマンドが実行できます。
例えば、リスト4のように書きます。

・リスト4: sshのタイムアウト設定

1
ssh -o ConnectTimeout=5 tofu.usptomonokai.jp "mkdir $dir/.tofubox/LOCK"

これは、リモート側で mkdir $dir/.tofubox/LOCK
をやってくれとクライアント側から依頼を出すコマンドです。
sshでもrsync同様、タイムアウトを設定します。
ここでは5秒としました。

10.3.2. 排他区間を作る

 ここから本番コードを書いていきます。最初に、
サーバと通信するクライアントが同時に一つになるように、
排他制御を実現しましょう。
rsyncはいくつ同時に行っても多少のことではおかしなことにはならないので、
もしかしたら不要かもしれませんが、今回は排他処理を行います。
次回11月号の内容で、排他制御が生きてきます。

 シェルスクリプトで排他を行うときには、
「OS側が同時に二つ以上実行できないコマンドの終了ステータスを使う」
という定石があります。
例えば、 mkdir コマンドでディレクトリを作ることを考えてみましょう。
あるディレクトリは、一つのマシンに一つしか存在しません。
もし二つのプログラムが mkdir を使って同じディレクトリを作ろうとしても、
うまくいくのはどちらか一方で、
もう一方の mkdir は失敗してゼロでない終了ステータスを返します。
mkdir が同時に二つ成功したら、同じディレクトリが二つできてしまいます。
当然、OS側はそういうことは認めない作りになっています。

 リスト5のスクリプトで試してみましょう。

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

1
2
3
4
5
6
7
8
ueda@SL510:~$ cat locktest.sh
#!/bin/bash

exec 2> /dev/null

for n in {1..1000} ; do
        mkdir ./LOCK && touch ./LOCK/$n &
done

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

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

・リスト6: 排他の実験

1
2
3
4
5
6
ueda@SL510:~$ ./locktest.sh
ueda@SL510:~$ ls ./LOCK && rm -Rf ./LOCK
1
ueda@SL510:~$ ./locktest.sh
ueda@SL510:~$ ls ./LOCK && rm -Rf ./LOCK
8

 豆腐ボックスではリモートのサーバで、
かつ複数のクライアントがいる状況でこのような排他区間を作らなくてはなりませんが、
sshコマンドを使ってリモート側にディレクトリを作るようにすればよいということになります。
ということで、豆腐ボックスのための排他区間を作り出すコードをリスト7のように書きます。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
ueda@SL510:~/.tofubox$ cat TOFUBOX.SYNC
#!/bin/bash -xv
# TOFUBOX.SYNC
exec 2> /tmp/$(basename $0)

server=tofu.usptomonokai.jp
dir=/home/ueda

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

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

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

ちなみにsshコマンドは、ロックが取れなくても通信できなくても1を返します。

 このロックには一つ課題があります。
通信をするのがすべて堅牢なサーバ機ならともかく、
今回は個人用PCがクライアントにいますので、
通信がブチッと切れて LOCK
ディレクトリが残ってしまう可能性があります。
この課題については、後から対応します。

10.3.3. 同期処理を実装

 では作った排他区間内に同期処理を実装しましょう。
サーバからデータを引っ張って反映し、
その後クライアントの変更をサーバに反映します。
この一連の処理を排他区間内に書くと、リスト8のようになります。

 この例では、 notify-send というコマンドを使って、
デスクトップ上にアラートを出すようにしています。
この処理はディストリビューションに依存しますが、
Ubuntu 12.04 の場合、 notify-send が実行されると、
下の図のような箱が画面の右上に表示されます。
DISPLAY=:0 というのは、 notify-send
に、自分のデスクトップを教えるために書いています。

・リスト8: TOFUBOX.SYNC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/bin/bash -xv
# TOFUBOX.SYNC
# written by R. Ueda (USP研究所) Jul. 21, 2012

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

server=tofu.usptomonokai.jp
dir=/home/ueda

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

ERROR_CHECK(){
        [ "$(echo ${PIPESTATUS[@]} | tr -d ' 0')" = "" ] && return
        DISPLAY=:0 notify-send "豆腐: $1"
        exit 1
}

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

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

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

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

exit 0
_images/notify.png

図:notify-sendで表示されるダイアログ

 ロックは、通信が途絶えたりその他エラーが起こったりすると外れないのですが、
このスクリプトではそれを前提としています。残ったロックは、サーバ側で外します。

 このスクリプトは排他区間でサスペンドがかかると、
ssh や rsyncの途中であればゼロ以外の終了ステータスを返して終わります。
しかし、
他のコマンドを実行している間やコマンドとコマンドの間でサスペンドがかかると、
そのままrsyncが走ってしまいます。
残念ながらOSのサスペンドは trap コマンドで検知できないようですので、
date コマンドを使って、リスト9のようなスクリプトを作ります。

・リスト9: TOFUBOX.SUSSTOP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
ueda@SL510:~/.tofubox$ cat TOFUBOX.SUSSTOP
#!/bin/bash
# TOFUBOX.SUSSTOP
# written by R. Ueda (USP研究所) Jul. 21, 2012

FROM=$(date +%s)

while sleep 1 ; do
        TO=$(date +%s)
        DIFF=$1) TO - FROM 
        if [ "$DIFF" -gt 2 ] ; then
                killall TOFUBOX.SYNC
                FROM=$(date +%s)
        fi
        FROM=$TO
done

 このスクリプト( TOFUBOX.SUSSTOP )は、
1秒ごとにdateコマンドを呼んで、3秒以上間があいたら
TOFUBOX.SYNC をコロすものです。
TOFUBOX.SUSSTOP を実行しておけば、
サスペンドすると数秒で TOFUBOX.SYNC が止まります。
数秒間なら rsync が走っても事故にはならないでしょう。

 実験してみましょう。リスト10が実験の例です。

・リスト10: サスペンドからの復帰時にTOFUBOX.SYNCを止める

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#あるターミナルで SUSSTOP を実行
ueda@SL510:~/.tofubox$ ./TOFUBOX.SUSSTOP 2> hoge
#別のターミナルで SYNC を実行
ueda@SL510:~/TOFUBOX$ ~/.tofubox/TOFUBOX.SYNC
######################################
# TOFUBOX.SYNCが終わる前にサスペンド -> 復帰
######################################

#hogeファイルを見ると TOFUBOX.SYNC が止まっている。
ueda@SL510:~/.tofubox$ less hoge
...
+ TO=1342317384
+ DIFF=10
+ '[' 10 -gt 2 ']'
+ killall TOFUBOX.SYNC
date +%s)
date +%s
...

10.3.4. サーバ側でロックをはずす処理

 ロックを強制的に外すという処理は、
排他制御に完備性があればやってはいけません。
しかし、今回はそうも言ってられないので実装します。
ロックを外した瞬間に何が起こるかということを考え、
慎重に実装しなければなりません。

 ここで効いてくるのは rsync と ssh のタイムアウトです。
もし、サーバ側でロックができてからrsyncが始まらなかったり、
rsyncが終わってからしばらくロックが外れなかったりした場合は、
クライアント側では rsync も ssh も終わって通信していない状態になっています。

 sshでは5秒、rsyncでは30秒でタイムアウトするので、
サーバ側では、LOCKがあるのに60秒以上rsyncが走っていないときには、
クライアント側はすでにスクリプトが終わっているか、
サスペンドしていて後でkillされると判断できます。
厳密にはクライアント側でsshとrsync以外の処理で25秒くらい
かかってしまうとこの判断は間違いになってしまいますが、
このようなことはよほどPCが不安定にならない限り起こりません。
万が一そうなってしまったら降参ということにしましょう。

 リスト11のシェルスクリプトをサーバ側で実行します。

・リスト11: TOFUBOX.RMLOCK

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
ueda@tofu:~$ cat .tofubox/TOFUBOX.RMLOCK
#!/bin/bash
# TOFUBOX.RMLOCK
# written by R. Ueda (USP研究所) Jul. 21, 2012

dir=/home/ueda/.tofubox

exec 2> $dir/LOG

n=0
while sleep 3 ; do
        n=$2) $n + 1 

        ls -d $dir/LOCK &> /dev/null || n=0
        ps cax | grep -q rsync && n=0

        if [ "$n" -eq 20 ] ; then
                rmdir $dir/LOCK
                n=0
        fi
done

 このスクリプトは、とりあえずリスト12のようにバックグラウンドで走らせておきましょう。
(脚注:余談ですが、この終わらないスクリプトを間違えてcronにしかけたら、
ロードアベレージが500を越えました。)

・リスト12: TOFUBOX.RMLOCKの実行

ueda@tofu:~$ ~/.tofubox/TOFUBOX.RMLOCK &

  ps cax のオプションcは、実行中のプロセスをコマンドで表示するときに使います。
よくpsしたらgrepのプロセスも引っかかるということがありますが、
それを避けることができます。リスト13のようにgrepと組み合わせると便利です。

・リスト13: TOFUBOX.RMLOCKの実行

1
2
3
4
5
6
7
8
9
ueda@SL510:~/.tofubox$ ps cax | grep "vi$"
 6032 pts/0    S+     0:00 vi
 6273 pts/2    S+     0:00 vi
#viのプロセスがあると、grepが0を返し、$?に入る。
ueda@SL510:~$ ps cax | grep -q vi$ ; echo $?
0
#ないプロセスをgrepすると、grepが1を返す。
ueda@SL510:~$ ps cax | grep -q hoge$ ; echo $?
1

リスト12の13行目は、grepの終了ステータスを見て、
0ならnを0にしています。

10.3.5. 実行

 今回は、 TOFUBOX.SUSSTOP をバックグラウンド実行、
TOFUBOX.SYNC をcrontabを使って定期的に起動させることにします。
次回、もう少し気の利いたタイミングで TOFUBOX.SYNC を起動させることを試みます。

・リスト14: crontabへの記述

1
2
3
4
5
6
7
ueda@SL510:~$ ~/.tofubox/TOFUBOX.SUSSTOP &
ueda@SL510:~$ crontab -e
#これを加筆
*/3 * * * * /home/ueda/.tofubox/TOFUBOX.SYNC
ueda@X201:~$ crontab -e
#もう一方のマシンではこれを加筆
*/4 * * * * /home/ueda/.tofubox/TOFUBOX.SYNC

  */3 というのは、3分おきという意味です。
これで、SL510側では3分おき、X201側では4分おきに同期処理が起動します。
3分と4分でずらしたのは、両方3分にすると必ずロックの取り合いになるからです。
これでも12分に一回、ロックの取り合いになりますが、
ロックのテストにはちょうどよいでしょう。
また、 TOFUBOX.SUSSTOP は、マシンを再起動したら再度立ち上げなければなりません。
このあたりの改善は次号で扱います。

 実行した結果は特に載せませんが、
こっちのノートPCで作ったメモが、あっちのノートPCにひょっこり現れるという具合で、
なかなか便利です。
・・・まあ、Dropboxも使ってるんで、どっちを使おうかというところですが。

10.4. 終わりに

 今回は、オンラインストレージもどきを作ってみました。
サスペンドや通信エラーがからむのでややこしくなりました。
もしかしたらもう少し洗練した書き方もできたかもしれませんが、
それでも今回書いたスクリプトの行数:

  • TOFUBOX.SYNC: 37行
  • TOFUBOX.SUSSTOP: 15行
  • TOFUBOX.RMLOCK: 20行

は、驚異的に短いと言えます。

 ただ、今回作ったものは、

  • 3分あるいは4分ごとに意味なくrsyncが起動( notify-send がうるさい。)
  • 大きいファイルをTOFUBOXディレクトリにコピーしている間にrsyncが走ると中途半端なアップロードが発生
  • マシンを起動したときに手動で起動

など、いろいろ細かい不便さがあるので、
次回はもう少し凝ってみたいと考えています。

Pocket
LINEで送る

脚注   [ + ]

1. TO - FROM
2. $n + 1