勉強会のときにも使った 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 のおかげで身長が伸びて結婚できて幸せになれるよ!
米O'Reilly、iPhone電子書籍「Learning Python」など17点公開 | パソコン | マイコミジャーナル
この記事を読んで勢いで買って試しましたよ!
分厚くて持ち歩きたくない書籍たちが iPhone で読めるという。
しかもただの pdf ではなくて、iPhone アプリとして。
さらにどれも5kクラスの書籍たちが、たったの600円。
さすがオライリー、わかってる。
Beautiful Code、合間に読むにしては持ち歩けないし置く場所もなかったのですごい嬉しいです。
あとは勢いに任せて Lerning Python も買いました。
Real World Haskell とか、ほかにも気になるけど読んでなかった本ばかりですよ。積みかけたけど自重。
ページ送りがすごくサクサクです。
目次からハイパーリンクをクリックして目的のページで飛ぶようなかんじで章も渡れるし、フォントやラインスペースも自由に変更可能と、細かいところがよくできてます。
# スクリーンショットは書籍物なので自重。
カメラと twitter と iPod くらいしか使ってなかったマイ iPhone が、ついに本気を出すとき。
@umezo と @Nananeko と突発的緩々勉強会。
資料とか用意しないよって断言してきたからライブコーディングしたけど、こっちのがいい気がします。
勉強会のために資料作ること考えてモチベーション下がるくらいなら、ライブコーディングして録画したほうがよっぽどいいと思う。
ねこっちと Scala トレース。
function21.scala とかカリーとかタプルに吹いたw
わたしは先日作ったバックアップスクリプトを使って「シェルスクリプトが分からない僕でも書ける python スクリプト」を話したけど、一通りのパッケージと「python があればなんでもできる」気にさせる意味でかなりいい題材だったと自負してる。
うめさんにも勢いで JavaScript と jQuery をライブコーディングしてもらったけど、すごかったw
悪い面も踏まえた上で「jQuery スゲー!」って言わせることができるのは、うめさんだからこそだと思うんだ。持ち上げてみた。
地味にココ壱初体験。
なかなか楽しかったから、またやろう。
map + lambda の熱さを語るの忘れてた。
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 に貼り付け、グラフ生成で出力します。
新規バックアップのパフォーマンス測定
2ホスト間で rsync2 と rsync3 の各組み合わせを試行します。
src には二分木のようなディレクトリツリーを用意し、dst は空のディレクトリとします。
詳細は以下のようなダミーデータツリーを生成するスクリプトを python にて記述しました。ファイルの中身は、一度 dd で /dev/random から出力したものを各ディレクトリにコピーしたものになります。
# /dev/zero から生成すると、圧縮オプションで測定ができない可能性もあったので。
# /dev/random をそれぞれ出力していたらダミーデータの生成が終わらないのでコピーしました。
| 項目 | 値 |
|---|---|
| ディレクトリ構成 | 二分木のようなもの |
| ディレクトリ数 | 100 |
| 各ディレクトリのファイル数 | 1,000 |
| ファイルサイズ | 1KB |
| 合計ファイル数 | 100,000 |
| 合計ファイルサイズ | 100KB |
rsync3 to rsync3 の組み合わせが圧倒的に早いです。
これは、クライアントがさっさとファイルを送信することに加えて、サーバがメモリ上にファイルを受け取った時点でレスポンスを返しているからかと。
プロセス終了後に、ディスク書き込みが行われていることが確認できます。
プロセスが早々に終わるということは、他のプロセスの負荷の影響を受けないということ。
クライアントを rsync3 にすることで、プロセスをすぐに終わらせようと最初から頑張ってくれます。
サーバ側が rsync2 の場合、終了レスポンスを返してくれず仕事が残るようで、他の組み合わせ同様終了まで時間がかかります。
rsync3 はプロセスが早々に終了します。
クライアントが3だとすぐにファイルが転送されてくるため、書き込み開始が早くなります。
クライアントが3だと転送が早いので、メモリに書き込まれていくことになります。
rsync3 の組み合わせだと早々に処理を終わらせています。
中盤の 50% はディスク書き込み中。
プロセス終了後のディスク書き込みの完了までを考えても、rsync3 to rsync3 の組み合わせが最も早くなります。
差分同期のパフォーマンス測定
新規パフォーマンス測定と同様の構成で、リモート側は削除7%追加7%というファイルの差分をつくります。
rsync の組み合わせは 2 to 2 と 3 to 3 のみ確認します。
| 項目 | 値 |
|---|---|
| 削除ディレクトリ数 | 7 |
| 追加ディレクトリ数 | 7 |
| 合計削除ファイル数 | 7,000 (7%) |
| 合計追加ファイル数 | 7,000 (7%) |
| 合計ファイル数 | 100,000 |
プロセス実行時間は 2.86 倍から 4.88 倍と、少なくとも高速化されることは確かなようです。
転送速度とグラフに書いてしまったけど、転送量の間違いですね。
処理時間の全体から考えて、先に転送を開始する分、1秒に送信する転送量は総合で結構な差が生まれます。
プロセス実行時間に伴って、負荷 100% の時間が半分以下に軽減されています。
リモート側の CPU を効率よく使用しているようです。
IO wait が発生していてあたかも悪い数値のように見えそうになるけれども、転送が早々に行われているから、むしろよい数値。
転送を先にすぐ終わらせるために、メモリが先に減り始めているのだと思われます。
早々にプロセスを終了させているのは間違いないのだけど、何度やっても中盤で1秒 CPU を100%もっていかれてしまいます。
ディスク書き込み終了時とかに CPU を持って行かれているのでしょうか。
バックアップ側が仕事をしているほうが望ましいので、これはよい数値。
最後に
rsync3 は全体的に性能向上されていて、/opt/rsync に展開して使うとかすれば導入も簡単です。
–link-dest を使ったハードリンクや、–rsync-path を使った sudo 実行などと組み合わせるとかなり強力。
ただ、やっぱりメモリは一気に食いつぶしてしまうので、環境が許されるのであれば GFS 等と組み合わせたり、専用の製品を使ってどれほどの差が出るか確認してみたいものです。
ひとひらおわりー。佳代ちゃん帰ってくると思わなかったよ!さちえがすごくかわいいなーもう。
化物語上下購入−。
もともと名前はよく聞いていたし、畳さんからクビキリサイクルを頂いてから読み始めた西尾維新さんの作品だけど、ようやくおもしろさがわかってきたかんじ。しかも戯言に比べると会話多めのラノベチックなかんじ?
wikipedia を見たらけっこう若い方だったということに驚き。
よくわかる現代魔法。
いままでスルーしてたけど、まりもさんと一緒にいた流れで購入。
出てくる単語にわずかににやっとできればいいかな程度で、そこまで期待しちゃいけないと思っていたけど、序盤から十分じゃないですか。
「そうねえ。素数を遅延評価で求めたりしてるわね」
あとプログラムと魔法を重ねることは、プログラマならわりと普段から実感あると思うんだ。
とある魔術の禁書目録。
間を空けると微妙で、16巻までは読んであるけど、17巻が途中のまま18巻でてしまいました。
マリみて。
・・・あれ?終わったと思ってた・・・。
フライ焙る心臓。
もとい、Flyable Heart。
原作既読者的に勢いで買ってしまったけど、早まったかもしれない。





























