こんにちは。石橋を叩き過ぎて割るタイプの福田です。
春の足音を聴きながら、雲方面への移住を進めています。
さて、寒さもピークの昼下がり、ソイラテを片手にラップトップ整理。
2年物・書き捨てのPythonスクリプトに、ふと足を止めました。
すれっからしですが、意外に役立つので取り上げておきます。
Mongo Oscillator
今から2年前の2015年3月、MongoDBのバージョン3.0がリリースされ、新しいストレージエンジンWiredTigerが選択できるようになりました。その後、同3.2で従来のMMAPv1に代わりデフォルトのエンジンとなります。
当時のホワイトペーパーには、大幅なパフォーマンス向上、透過的圧縮、フラグメンテーションからの解放とあり、食指が動きます。
しかし、直ぐにアップグレードするのは気が引けるので、諸々のテストを実施しました。
はじめに、実運用のケースに即したテストとして、一定量のデータに対して継続的な更新操作を行った際にデータの破損や肥大化、レプリケーションの破綻が起こらないかを確認するための簡単なコードを書きました。
当初、バックアップのダンプデータを入力としてシミュレーションを行っていましたが、以下の点で不満が出てきます。
- I/Oの面での不利
- BSONのデコードやドメイン特有の処理のオーバーヘッド
- 複数環境でのテストツール一式の可搬性の欠如
- 更新のシミュレーションの煩雑さと再現性の欠如
これらの問題点を解消すべく書いたのが、こちらのPythonスクリプトになります。
唐突すぎて何をやっているのか分からないと思いますので、簡単に解説します。
- generate_object関数
seq_noで与えられた番号にkey_shift値を加えた値をシードとして生成した乱数を元に、所与の平均値と標準偏正規分布に従ったサイズのデータフィールドを持つオブジェクトを生成します。
オブジェクトの_idはseq_noのハッシュ値とし、幾つかの属性値を含む辞書を返します。
- shoot関数
split_tasks関数で分割されたタスクを受け取り、担当分のデータの挿入/更新を行います。
並列処理の単位となります。
- split_tasks関数
n件のデータ生成タスクを、parallel数のタスクに分割し、開始と終了番号のタプルを返します。
使い方
テストドライバのホストで、パラメータを指定して実行するだけです。
$ python mos.py -h usage: mos.py [-h] [-n N] [-w W] [-p P] [-s S] [-z Z] [-c C] [-d D] --host HOST [--db DB] [--collection COLLECTION] Mongo Oscillator optional arguments: -h, --help show this help message and exit -n N number of objects to generate -w W mongodb replicaset write concern -p P number of processes to run in parallel (split n into p tasks) -s S key shift offset -z Z average object size -c C cheat ratio -d D stddev --host HOST comma separated mongodb replicaset hosts --db DB mongodb database name --collection COLLECTION
実行例
単発の実行
$ python mos.py -p 4 --host yin,yang -n 10000 -z 4096 -d 1024 Namespace(c=0.9, collection='star', d=1024, db='shooting', host='yin,yang', n=10000, p=4, s=1, w=2, z=4096) 30659 n=0 (2500) 10.1889410019 sec 245.36406674 tps 30657 n=0 (2500) 10.6970849037 sec 233.708531109 tps 30656 n=0 (2500) 11.1619198322 sec 223.975806812 tps 30658 n=0 (2500) 11.3631420135 sec 220.009571034 tps 30651 n=10000 11.3728001118 sec 879.290931145 tps
実際には、以下のように1ずつキーシフトしながら複数回のデータ上書き更新をシミュレートします。
$ for x in `seq 1 4`; do python mos.py -p 4 --host yin,yang -n 10000 -z 4096 -d 1024 -s $x; done
10000件、4分割(4プロセス)で、キーを1つずつシフトして4回書込み(初回の挿入と3回の更新)
パラメータの説明
- -n 生成するオブジェクト数 (データの総件数)
- -p タスク分割数(1つのタスクにつき1プロセス割り当て)、デフォルト値:2
- -s キーシフトのオフセット(後述します)、デフォルト値:1 ※1
- -z 平均のオブジェクトサイズ(byte)、デフォルト値:50 ※2
- -d 標準偏差、デフォルト値:-1 (-zで指定した平均のオブジェクトサイズの10%)
- -c ランダム生成文字列使い回し比率(生成の負荷を調整)、デフォルト値:0.9 ※3
- –host MongoDB ホスト名(レプリカセットの場合はカンマ区切りで複数指定可)、必須
- –db MongoDB データベース名、デフォルト値:shooting
- –collection MongoDB コレクション名、デフォルト値:star
※1 シーケンスで採番したキーを指定数分ずらすことで(オブジェクトからキーを切り離してシフト)、サイズの変動を伴うオブジェクトの更新をシミュレートしています。
更新時に全体の総サイズを変えずにオブジェクトサイズの変動をシミュレートすることで、フラグメンテーションとそれに伴うデータの肥大化傾向の有無や度合いを観察できることを期待しました。
以下、小さなデータを生成し、挙動を簡単に説明します。
データ件数3、平均データサイズ40、標準偏差10でデータ生成します。
$ python mos.py -p 1 --host yin,yang -n 3 -z 40 -d 10 -s 1
mongo shellでの確認
r0:PRIMARY> use shooting r0:PRIMARY> db.star.find() { "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 52, "data" : "NnzxHPXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYi", "s eq" : 0, "last_modified" : 1486707174.010212 } { "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 57, "data" : "ceRMIqFXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYiNLxB ", "seq" : 1, "last_modified" : 1486707174.016221 } { "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 2, "la st_modified" : 1486707174.01845 }
何度やっても生成結果が同じになります。
r0:PRIMARY> db.star.find() { "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 52, "data" : "NnzxHPXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYi", "s eq" : 0, "last_modified" : 1486707200.27103 } { "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 57, "data" : "ceRMIqFXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYiNLxB ", "seq" : 1, "last_modified" : 1486707200.276858 } { "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 2, "la st_modified" : 1486707200.279132 }
1回目のシフト
$ python mos.py -p --host yin,yang -n 3 -z 40 -d 10 -s 2
シフト後の状態
r0:PRIMARY> db.star.find() { "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 57, "data" : "ceRMIqFXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYiNLxB ", "seq" : 0, "last_modified" : 1486707251.232851 } { "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 1, "la st_modified" : 1486707251.232893 } { "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 40, "data" : "uiduXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 2, "la st_modified" : 1486707251.234418 }
_idの集合は据え置かれて、dataおよびlengthがシフトしているのが分かると思います。
2回目のシフト
$ python mos.py -p --host yin,yang -n 3 -z 40 -d 10 -s 3
シフト後の状態
r0:PRIMARY> db.star.find() { "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 0, "la st_modified" : 1486707260.945545 } { "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 40, "data" : "uiduXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 1, "la st_modified" : 1486707260.942702 } { "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 28, "data" : "PXMXXceRMIqFFEiwuLZXCxnbbyqt", "seq" : 2, "last_modified" : 1486707260.943574 }
※2 dataフィールドのサイズ(バイト)を指定。data以外のフィールドがあるため、db.stats()のavgObjSizeとの差異が生じ、差異の影響はこの値が大きいほど小さくなります。
※3 生成のオーバーヘッドを減らすため、予めランダム文字列のテンプレートを用意しておいて使いまわし、パラメータで指定した一定の比率部分のみオブジェクト毎にランダム生成を行うようにしました(0.9という設定は各オブジェクト毎に決定されたdataフィールドのサイズのうち9割をテンプレートから使いまわすことを意味します)。
折しも
バージョン3.4を検証中のGCP環境での実行結果を上げておきます。
データディレクトリのある/dev/sdb1の使用量は、既存のデータがあるため0.64Tがベースになります。
無事更新操作が完了し、ストレージサイズの肥大化がないことが確認できました。
- MongoDBのバージョン: 3.4.1
- 構成: 2ノード(PRI, SEC)+ ARBのレプリカセット
- インスタンスタイプ
- PRI,SEC: n1-highmem-4 (CPU:4, Mem:26G, SSD)
- ARB: g1-small (CPU:1, Mem:1.7G)
- ドライバホスト:n1-highmem-4(CPU:4, Mem:26G)
テストパラメータ
- 件数: 10,000,000
- 平均オブジェクトサイズ: 40960
- σ: 10240
- キーシフト: 1-8
- 上書き回数: 7
- write concern: 2
- cheat_ratio: 0.95
まとめ
MongoDBのデータ更新をシミュレートする、Pythonスクリプトを取り上げました。
実際、バージョン3.0の初めの頃にこのテスト実行した際に良好な結果が得られず、移行を見合わせる判断材料となりました。
その後、バージョン3.2にて無事アップグレードと同時にWiredTigerへの移行を完了し、今日も正常稼働を続けています。
世の中にはY
おや、誰かきたようなのでこの辺で。