2009 年 7 月 22 日 | カテゴリー: プログラム

20090722 rsync3 による世代管理バックアップスクリプト

勉強会のときにも使った python 製世代管理バックアップスクリプトです。

rsync3 のプロトコルによる高速化と、–rsync-path と sudo の組み合わせによる一般ユーザ権限ログインによる実行、そして –link-dest を使ったハードリンクによる差分バックアップを実現しています。
スクリプトでまかなっているところは、各オプションの指定方法が分かりにくかったり、空のディレクトリと同期してデータが全滅したりするのを防ぐところ。あと毎日のバックアップと毎週のスナップショットをとってローテーションをまわすところ。

スナップショットをとる曜日や、システムユーザ(一般ユーザ権限)等、直接スクリプトを編集して運用するつもりなので、下記に公開してるものをそのまま使われることは想定していません。
twitter 上でも少し話が出ましたが、元々 iSCSI 環境で問題が出たのと、GFS 等を利用できない環境のために作成したものです。

使い方

バックアップサーバ側で実行し、バックアップ対象のデータを pull します。

python pull_backup.py --hostname="apribase" --directory="/home/"

上記のように実行すると、hostname 用のディレクトリが /home/ 配下に生成され、日付付きでディレクトリが保存されます。
最新版にシンボリックリンクがはられ、次のバックアップ時には前回からの差分(正確には重複ファイルをハードリンクするのであって、差分という言葉は若干違う)を保存します。

home -> /home/apribase/home-200907-22
/home/apribase/home-200907-22

拡張オプションとして、exclude に対応してあります。
カンマ区切りで指定すると、rsync オプションの exclude を複数指定する記述に展開します。

python pull_backup.py --hostname="apribase" --directory="/home/" --exclude="db*,socket,server.pem,tls_sessions.db"

pull_backup.py

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#! /usr/bin/python
# -*- mode:python; coding:utf-8 -*-
# pull_backup.py
 
import calendar
import commands
import datetime
import os.path
import socket
from optparse import OptionParser
 
__author__ = "kei"
__date__ = "$2009/07/22 02:43:23$"
 
# ============================================================================
# setup option parser
# ============================================================================
 
parser = OptionParser(usage="python %prog --hostname=HOSTNAME --directory=DIRECTORY --exclude=FILE\n ex: python %prog --hostname=\"apribase\" --directory=\"/home/\" --exclude=\"db*,socket,server.pem,tls_sessions.db\"")
parser.add_option("--hostname", dest="hostname",
                  help="--hostname=\"apribase\"")
parser.add_option("--directory", dest="directory",
                  help="--directory=\"/home/\"")
parser.add_option("--exclude", dest="exclude",
                  help="--exclude=\"db*,socket,server.pem,tls_sessions.db\"")
options, args = parser.parse_args()
 
# ============================================================================
# target strings
# ============================================================================
 
def hostname():
    return options.hostname
 
def directory():
    return options.directory
 
def exclude():
    str = ""
    if not options.exclude == None:
        for e in options.exclude.split(","):
            str += "--exclude=" + "\"" + e + "\' "
    return str
 
def domain():
    return ".apribase.net"
 
def rsync_user():
    return "rsync"
 
def rsync_bin():
    return "/opt/rsync/bin/rsync"
 
# ============================================================================
# rsync
# ============================================================================
 
# rsync -azXA -e ssh --delete --rsync-path="sudo /opt/rsync/bin/rsync" --link-dest="/home/apribase/home/" rsync@apribase.apribase.net:/home/ /home/apribase/home-2009-07-22/
def rsync():
    return rsync_bin() + " -azXA -e ssh --delete " + rsync_path() + " " + link_dest() + " " + exclude() + " " + src() + " " + dst()
 
# rsync@apribase.apribase.net:/home/
def src():
    return rsync_user() + "@" + hostname() + domain() + ":" + directory() + "/"
 
# /home/apribase/home-2009-07-22/
def dst():
    return "/home/" + hostname() + directory() + "-" + datetime.date.today().isoformat() + "/"
 
# --rsync-path="sudo /opt/rsync/bin/rsync"
def rsync_path():
    return "--rsync-path=\"sudo /opt/rsync/bin/rsync\""
 
# --link-dest="/home/apribase/home/"
def link_dest():
    return "--link-dest=\"/home/" + hostname() + directory() + "/" + "\""
 
# prepare for rsync
def mkdir():
    return "mkdir -p " + dst()
 
# ============================================================================
# lotate backups
# ============================================================================
 
# lotate backup directory
# 7/20       7/21       7/22
#     06-30      06-30      07-07
#     07-07      07-07      07-14
#     07-14      07-14      07-21
#     -----      -----      -----
#     07-20      07-21      07-22
def lotate():
 
    def snapshot_day():
        return calendar.MONDAY
 
    # /home/apribase/home-2009-07-22
    def today():
        return "/home/" + hostname() + directory() + "-" + datetime.date.today().isoformat()
    # /home/apribase/home-2009-07-21
    def yesterday():
        return "/home/" + hostname() + directory() + "-" + (datetime.date.today() - datetime.timedelta(1)).isoformat()
    # /home/apribase/home-2009-06-30
    def oldest():
        return "/home/" + hostname() + directory() + "-" + (datetime.date.today() - datetime.timedelta(22)).isoformat()
    # /home/apribase/home
    def latest():
        return "/home/" + hostname() + directory()
 
    # rm -rf /home/apribase/home-2009-07-21; rm -f /home/apribase; ln -s home-2009-07-22 /home/apribase/home
    def daily():
        return "rm -rf " + yesterday() + "; rm -f " + latest() + "; ln -s " + os.path.basename(directory()) + "-" + datetime.date.today().isoformat() + " " + latest()
    # rm -rf /home/apribase/home-2009-06-30; rm -f /home/apribase; ln -s home-2009-07-22 /home/apribase/home
    def weekly():
        return "rm -rf " + oldest() + "; rm -f " + latest() + "; ln -s " + os.path.basename(directory()) + "-" + datetime.date.today().isoformat() + " " + latest()
 
    if((datetime.date.today() - datetime.timedelta(1)).weekday() == snapshot_day()):
        return weekly()
    else:
        return daily()
 
# ============================================================================
# check options
# ============================================================================
 
# script needs hostname and directory.
def check_none():
    if hostname() == None:
        parser.error("set hostname.")
    if directory() == None:
        parser.error("set directory.")
 
# valid hostname?
def check_hostname():
    try:
        socket.gethostbyname(hostname() + domain())
    except socket.gaierror:
        parser.error(hostname() + domain() + " is invalid hostname or ipaddress.")
 
# exist?
def check_directory():
    # python -c "import os; print os.path.exists(\"/\")"
    def command_python():
        return "python -c \"import os; print os.path.exists(\\\"" + directory() + "\\\")\""
    # ssh rsync@apribase.net 'python -c \"import os; print os.path.exists(\"/\")\"'
    def command_ssh():
        return "ssh " + rsync_user() + "@" + hostname() + domain() + " \'" + command_python() + "\'"
 
    # /home/apribase/ is TRUE, apribase is FALSE
    if not (directory().startswith("/")):
        parser.error(directory() + " is not full path.")
 
    # remote directory is not exist...
    print command_ssh()
    if (commands.getoutput(command_ssh()) == "False"):
        parser.error(hostname() + ":" + directory() + " is not exist.")
 
# /home/ to /home
def remove_end_slash(directory_):
    if (directory_.endswith("/")):
        return directory_.rstrip("/")
    return directory_
 
if __name__ == "__main__":
    check_none()
    check_hostname()
    check_directory()
    options.directory = remove_end_slash(options.directory) # override
    if not (os.path.exists(dst())):
        print mkdir()
        commands.getoutput(mkdir())
    print rsync()
    print commands.getoutput(rsync())
    print lotate()
    print commands.getoutput(lotate())

commands でシェルコマンドを叩く

commands.getoutput でシェルのコマンドを叩けます。
結果が return されるので print 文で出力してあげましょう。
これでシェルスクリプトが書けない僕でも python のおかげで結婚できました状態。

import commands
 
print commands.getoutput("echo HelloWorld!")

optparse でオプション指定

コマンド引数が args に。-h –help などが options にタプルで入ります。
add_options で追加していけます。
-d –directory みたいに2つセットしないといけないというわけでもなく、片方だけでも大丈夫でした。

生の argv を見るよりエラーチェックがやりやすいのが嬉しいかな。

from optparse import OptionParser
 
parser = OptionParser(usage="python %prog --hostname=HOSTNAME --directory=DIRECTORY --exclude=FILE\n ex: python %prog --hostname=\"apribase\" --directory=\"/home/\" --exclude=\"db*,socket,server.pem,tls_sessions.db\"")
parser.add_option("--hostname", dest="hostname",
                  help="--hostname=\"apribase\"")
parser.add_option("--directory", dest="directory",
                  help="--directory=\"/home/\"")
parser.add_option("--exclude", dest="exclude",
                  help="--exclude=\"db*,socket,server.pem,tls_sessions.db\"")
options, args = parser.parse_args()
 
print options
print args

os.path でディレクトリを操作する

os でディレクトリの操作などが行えます。ディレクトリの存在確認など。

import commands
import os.path
 
if not (os.path.exists("/home/apribase/workspace")):
    print commands.getoutput("mkdir /home/apribase/workspace")

ssh とワンライナーを使った離れ業

ssh の引数にコマンドを打つと、リモートでコマンドを実行してくれます。
これを使って python -c のワンライナーを飛ばすと、リモート側で実行できるので、それを commands で実行してリモートのディレクトリの存在を確認したり。
シングルクォーテーションとダブルクォーテーションの入れ子にものすごく悩みましたが。

ssh rsync@apribase.net 'python -c \"import os; print os.path.exists(\"/\")\"'

文字列操作

ここでは directory_ が文字列ということで。
末尾の文字をチェックしたり、末尾の指定文字を消した文字列を生成したり。

def remove_end_slash(directory_):
    if (directory_.endswith("/")):
        return directory_.rstrip("/")
    return directory_</code>

文字列と数字を変換する関数

str(1), int(“1″)。こんなかんじだったかな。
文字列結合のときに数字は結合できないので str() 関数で文字列を返す必要が出てきます。
オブジェクトに toString とかじゃないのですよ。

socket の使用も短く書ける

socket も便利なメソッドが揃っていてすぐに使えます。
例外等も公式ドキュメントにしっかり書いてあるので簡単でした。

import socket
 
try:
    socket.gethostbyname(hostname() + domain())
except socket.gaierror:
    parser.error(hostname() + domain() + " is invalid hostname or ipaddress.")

関数の中に関数?

そういうこともできるんです。
スコープ内でしか使わないからって意味でしか使わなかったけど、表現としてどういう美しさを表現するためのものかはまだ知らないのです。
あと、定数(変数)を返したかっただけなので、素直に代入演算子使ったほうがいいと思います。
関数スタックに積まれて遅くなるだけかと。

def lotate():
    def snapshot_day():
        return calendar.MANDAY

if not

曰く、「かっこいい」らしい。

if not (directory().startswith("/")):>

行コメントは?

# で。

# ============================================================================
# setup option parser
# ============================================================================

if __name__ == “__main__”: ?

python pull_backup.py のように実行されたとき、ここが実行されるのですよ。

datetime で日付操作

today() で今日が取得できて、timedelta で差分を足し引きすることで調整できたり。
isoformat() を使うと 2009-07-22 みたいに整形してくれます。

import datetime
 
# /home/apribase/home-2009-07-22
def today():
    return "/home/" + hostname() + directory() + "-" + datetime.date.today().isoformat()
# /home/apribase/home-2009-07-21
def yesterday():
    return "/home/" + hostname() + directory() + "-" + (datetime.date.today() - datetime.timedelta(1)).isoformat()

シェルスクリプトが書けない僕でも

python のおかげで身長が伸びて結婚できて幸せになれるよ!

2009 年 7 月 20 日 | カテゴリー: 日記

20090720 iPhone でオライリー

米O'Reilly、iPhone電子書籍「Learning Python」など17点公開 | パソコン | マイコミジャーナル
この記事を読んで勢いで買って試しましたよ!

分厚くて持ち歩きたくない書籍たちが iPhone で読めるという。
しかもただの pdf ではなくて、iPhone アプリとして。
さらにどれも5kクラスの書籍たちが、たったの600円。
さすがオライリー、わかってる。

Beautiful Code、合間に読むにしては持ち歩けないし置く場所もなかったのですごい嬉しいです。
あとは勢いに任せて Lerning Python も買いました。

Real World Haskell とか、ほかにも気になるけど読んでなかった本ばかりですよ。積みかけたけど自重。

ページ送りがすごくサクサクです。
目次からハイパーリンクをクリックして目的のページで飛ぶようなかんじで章も渡れるし、フォントやラインスペースも自由に変更可能と、細かいところがよくできてます。
# スクリーンショットは書籍物なので自重。

カメラと twitter と iPod くらいしか使ってなかったマイ iPhone が、ついに本気を出すとき。

2009 年 7 月 19 日 | カテゴリー: 日記

20090719 勉強会ナウ。 @sarian @umezo @Nananeko

@umezo と @Nananeko と突発的緩々勉強会。
資料とか用意しないよって断言してきたからライブコーディングしたけど、こっちのがいい気がします。

勉強会のために資料作ること考えてモチベーション下がるくらいなら、ライブコーディングして録画したほうがよっぽどいいと思う。

ねこっちと Scala トレース。
function21.scala とかカリーとかタプルに吹いたw

わたしは先日作ったバックアップスクリプトを使って「シェルスクリプトが分からない僕でも書ける python スクリプト」を話したけど、一通りのパッケージと「python があればなんでもできる」気にさせる意味でかなりいい題材だったと自負してる。

うめさんにも勢いで JavaScript と jQuery をライブコーディングしてもらったけど、すごかったw
悪い面も踏まえた上で「jQuery スゲー!」って言わせることができるのは、うめさんだからこそだと思うんだ。持ち上げてみた。

20090719 ここいちデビュー。

地味にココ壱初体験。
なかなか楽しかったから、またやろう。
map + lambda の熱さを語るの忘れてた。

2009 年 7 月 18 日 | カテゴリー: プログラム

20090718 rsync2 と rsync3 のベンチマークの比較とまとめ

rsync によるバックアップを考えるとき、CPU 負荷、転送効率、処理時間などが無視できなくなってきます。
下手をすると深夜中に終わらずに朝を迎え、負荷がユーザにまでかかってしまうことも。

今回は rsync2 を rsync3 にした場合、アルゴリズムの変更に伴って、具体的に何がどのように改善されるのかを実際に調査してみました。

rsync3 による性能向上のまとめ

先にまとめを書いてしまいます。劇的に性能は向上していました。
新規バックアップ時のパフォーマンス測定と、差分同期時のパフォーマンス測定を行いました。

クライアントを rsync3 にすると、ファイル転送が早々に行われるようになります。
サーバを rsync3 にすると、ディスク書き込みのための処理待ちが少なくなります。
両方を3にすると、同期処理を早々に終わらせてしまい、プロセス終了後にゆっくりとディスク書き込みを行ってくれます。

改善点 コメント
プロセス実行時間の改善 新規同期時に約10倍、差分同期時に2.86倍〜4.88倍、実行時間が改善される。プロセスが早々に終了するため、異常終了等の危険に巻き込まれる可能性も軽減される。これはクライアントとサーバをともに rsync3 にしたときに効果が現れる。
クライアント CPU 負荷の軽減 バックアップ元の負荷は、クライアントを rsync3 にすることで改善される。負荷が 100% になる時間が激減する。
サーバ CPU の処理効率の向上 クライアントとサーバがともに rsync3 のとき、序盤早々に計算を終わらせることができる。バックアップ先はバックアップがお仕事なのだから、CPU は遊ばせていないほうがいい。
書き込み開始時間の向上 転送開始時間と関係するが、データがすぐに送られてくるため、ディスク書き込みが早期に開始されるようになる。クライアントを rsync3 にしたときに効果が現れる。
書き込み効率の向上 ファイル書き込みを早期から行うこととなり、I/O wait は安定して 50% をキープして CPU の邪魔をすることがなくなる。サーバ側を rsync3 にすると効果が出るが、クライアントも 3 でないと足を引っ張られる。
転送開始時間の向上 転送が早々に行われるようになる。rsync2 では全インデックス作成が完了するまで転送が開始されなかったため、差分同期時に劇的な処理時間の差が生まれる。

基本的なパフォーマンス測定方法

ダミーデータを用意し、CPU 負荷などを vmstat で確認していく方針です。
ダミーデータをスクリプトで生成します。
クライアント側とサーバ側で同時に測定できるように、ssh でサーバ側の vmstat も起動するようにスクリプトをまとめます。
vmstat は時間出力をしてくれないので、時間出力を vmstat に混ぜるスクリプトも記述します。
vmstat の結果を OpenOffice Calc に貼り付け、グラフ生成で出力します。

新規バックアップのパフォーマンス測定

20090718 rsync ベンチ用ダミーデータ構成

2ホスト間で rsync2 と rsync3 の各組み合わせを試行します。
src には二分木のようなディレクトリツリーを用意し、dst は空のディレクトリとします。

詳細は以下のようなダミーデータツリーを生成するスクリプトを python にて記述しました。ファイルの中身は、一度 dd で /dev/random から出力したものを各ディレクトリにコピーしたものになります。
# /dev/zero から生成すると、圧縮オプションで測定ができない可能性もあったので。
# /dev/random をそれぞれ出力していたらダミーデータの生成が終わらないのでコピーしました。

項目
ディレクトリ構成 二分木のようなもの
ディレクトリ数 100
各ディレクトリのファイル数 1,000
ファイルサイズ 1KB
合計ファイル数 100,000
合計ファイルサイズ 100KB

20090718 プロセス実行時間 新規

rsync3 to rsync3 の組み合わせが圧倒的に早いです。
これは、クライアントがさっさとファイルを送信することに加えて、サーバがメモリ上にファイルを受け取った時点でレスポンスを返しているからかと。
プロセス終了後に、ディスク書き込みが行われていることが確認できます。
プロセスが早々に終わるということは、他のプロセスの負荷の影響を受けないということ。

20090718 src CPU 負荷 新規

クライアントを rsync3 にすることで、プロセスをすぐに終わらせようと最初から頑張ってくれます。
サーバ側が rsync2 の場合、終了レスポンスを返してくれず仕事が残るようで、他の組み合わせ同様終了まで時間がかかります。

20090718 dst CPU 負荷 新規

rsync3 はプロセスが早々に終了します。

20090718 dst IO wait 新規

クライアントが3だとすぐにファイルが転送されてくるため、書き込み開始が早くなります。

20090718 dst 残りメモリ 新規

クライアントが3だと転送が早いので、メモリに書き込まれていくことになります。

20090718 src CPU Idel 新規

rsync3 の組み合わせだと早々に処理を終わらせています。

20090718 dst CPU Idel 新規

中盤の 50% はディスク書き込み中。
プロセス終了後のディスク書き込みの完了までを考えても、rsync3 to rsync3 の組み合わせが最も早くなります。

差分同期のパフォーマンス測定

新規パフォーマンス測定と同様の構成で、リモート側は削除7%追加7%というファイルの差分をつくります。
rsync の組み合わせは 2 to 2 と 3 to 3 のみ確認します。

項目
削除ディレクトリ数 7
追加ディレクトリ数 7
合計削除ファイル数 7,000 (7%)
合計追加ファイル数 7,000 (7%)
合計ファイル数 100,000

20090718 プロセス実行時間 差分

プロセス実行時間は 2.86 倍から 4.88 倍と、少なくとも高速化されることは確かなようです。

20090718 転送量 差分

転送速度とグラフに書いてしまったけど、転送量の間違いですね。
処理時間の全体から考えて、先に転送を開始する分、1秒に送信する転送量は総合で結構な差が生まれます。

src CPU 負荷 差分

プロセス実行時間に伴って、負荷 100% の時間が半分以下に軽減されています。

20090718 dst CPU 負荷 差分

リモート側の CPU を効率よく使用しているようです。

20090718 dst IO wait 差分

IO wait が発生していてあたかも悪い数値のように見えそうになるけれども、転送が早々に行われているから、むしろよい数値。

20090718 dst 残りメモリ 差分

転送を先にすぐ終わらせるために、メモリが先に減り始めているのだと思われます。

20090718 src CPU Idel 差分

早々にプロセスを終了させているのは間違いないのだけど、何度やっても中盤で1秒 CPU を100%もっていかれてしまいます。
ディスク書き込み終了時とかに CPU を持って行かれているのでしょうか。

20090718 dst CPU Idel 差分

バックアップ側が仕事をしているほうが望ましいので、これはよい数値。

最後に

rsync3 は全体的に性能向上されていて、/opt/rsync に展開して使うとかすれば導入も簡単です。
–link-dest を使ったハードリンクや、–rsync-path を使った sudo 実行などと組み合わせるとかなり強力。

ただ、やっぱりメモリは一気に食いつぶしてしまうので、環境が許されるのであれば GFS 等と組み合わせたり、専用の製品を使ってどれほどの差が出るか確認してみたいものです。

2009 年 7 月 14 日 | カテゴリー: 買った本

4575836486.01.MZZZZZZZ.jpg4062836025.01.MZZZZZZZ.jpg4062836076.01.MZZZZZZZ.jpg408630421X.01.MZZZZZZZ.jpg4048678973.01.MZZZZZZZ.jpg4086013053.01.MZZZZZZZ.jpg4434133055.01.MZZZZZZZ.jpg

ひとひらおわりー。佳代ちゃん帰ってくると思わなかったよ!さちえがすごくかわいいなーもう。

化物語上下購入−。
もともと名前はよく聞いていたし、畳さんからクビキリサイクルを頂いてから読み始めた西尾維新さんの作品だけど、ようやくおもしろさがわかってきたかんじ。しかも戯言に比べると会話多めのラノベチックなかんじ?
wikipedia を見たらけっこう若い方だったということに驚き。

よくわかる現代魔法。
いままでスルーしてたけど、まりもさんと一緒にいた流れで購入。
出てくる単語にわずかににやっとできればいいかな程度で、そこまで期待しちゃいけないと思っていたけど、序盤から十分じゃないですか。
「そうねえ。素数を遅延評価で求めたりしてるわね」
あとプログラムと魔法を重ねることは、プログラマならわりと普段から実感あると思うんだ。

とある魔術の禁書目録。
間を空けると微妙で、16巻までは読んであるけど、17巻が途中のまま18巻でてしまいました。

マリみて。
・・・あれ?終わったと思ってた・・・。

フライ焙る心臓。
もとい、Flyable Heart。
原作既読者的に勢いで買ってしまったけど、早まったかもしれない。

Page 1 of 212
TOP