普段MongoDBのReplicaSetsを使ってるのですが、 今後件数が増えてきたらShardingとかしなきゃだよなー、と。 で、その時に困るのが、"預言者じゃねーしデータの偏りなんて予測できにゃい"的な。 ちょっとみてくとMongoDBはチャンクというある程度のまとまった単位でデータを保持して、 それを手動でもサクっと動かせますよ、と。そんな背景でチョロっとやってみます。 ■ MongoDBのバージョンは2.2が出たよんって事なので。
$ /usr/local/mongodb/bin/mongod --version db version v2.2.0, pdfile version 4.5
ちなみに32ビットのVirtualBox上のUbuntu1インスタンスで検証しやす。。 ■ データ格納用のディレクトリを作ります。 ほんとはもっといっぱい立ち上げたかったんだけどmongodは2インスタンスで。 Configサーバー用のヤツもディレクトリ掘っときやす。
$ mkdir -p /data/shard01 $ mkdir -p /data/shard02 $ mkdir -p /data/config
■ Configサーバー(--configsvr)を立ちあげてみます。
$ /usr/local/mongodb/bin/mongod --configsvr --dbpath /data/config ~略~ Fri Oct 27 15:32:39 [initandlisten] waiting for connections on port 27019 Fri Oct 27 15:32:39 [websvr] admin web console waiting for connections on port 28019
何も指定しないと27019ポートになるのですねん。 ■ Shardサーバー(--shardsvr) 上記で掘ったディレクトリ(/data/shard01)を指定して立ち上げます。
$ /usr/local/mongodb/bin/mongod --shardsvr --dbpath /data/shard01 ~略~ Fri Oct 27 15:38:22 [initandlisten] waiting for connections on port 27018 Fri Oct 27 15:38:22 [websvr] admin web console waiting for connections on port 28018
こっちは何も指定しないと27018ポートになりました。 で、続いて/data/shard02を指定して立ち上げます。
$ /usr/local/mongodb/bin/mongod --shardsvr --dbpath /data/shard02 ~略~ Fri Oct 27 15:38:37 [initandlisten] ERROR: listen(): bind() failed errno:98 Address already in use for socket: 0.0.0.0:27018 Fri Oct 27 15:38:37 [initandlisten] ERROR: addr already in use
あ、別にポート番号シクヨロでやってくれるわけじゃないのよねん、とw ってことで--portで↓27017にしてみました。
$ /usr/local/mongodb/bin/mongod --shardsvr --port 27017 --dbpath /data/shard02
こういう2インスタンス目のポート番号が1インスタンス目より若くなっちゃうけど、 単なる検証だからいっかって先に進んでしまう自分の人間性はもうどうにもならんのでしょうなw ■ ルーティングサーバー(mongos)を立ちあげてみます あとからチャンクの移動とかやってみたいんだけど、チラっとググってみたら、 最初は64メガである程度のサイズになると200メガになるとかって話で、 そんなに大量データ突っ込んだりするのもアレだなぁって思ってたら、 chunkSize 1ってやってあげるとチャンクのサイズが1MBになるらしいのでソレで。
$ /usr/local/mongodb/bin/mongos --configdb localhost:27019 --chunkSize 1 ~略~ Fri Oct 27 15:51:02 [mongosMain] ERROR: listen(): bind() failed errno:98 Address already in use for socket: 0.0.0.0:27017 Fri Oct 27 15:51:02 [mongosMain] ERROR: addr already in use
また怒られちゃったので(懲りてないw)、--port 27020で指定します。
$ /usr/local/mongodb/bin/mongos --configdb localhost:27019 --port 27020 --chunkSize 1
ってか、mongosは各アプリケショーンサーバーに立てられるらしいので、 どっか一個mongosが死んでもOK的な感じに出来る、と。 その辺の検証も時間見つけてやりたいな。。 とりあえずここまででソレっぽいプロセスは全部起動しましたかねー、と。 ■ Shardの設定をしてみる。 mongosのシェルでホゲホゲするらしいので。
$ /usr/local/mongodb/bin/mongo localhost:27020/admin MongoDB shell version: 2.2.0 connecting to: localhost:27020/admin ~略~ mongos> db.runCommand( { addshard : "localhost:27018" } ); { "shardAdded" : "shard0000", "ok" : 1 } mongos> db.runCommand( { addshard : "localhost:27017" } ); { "shardAdded" : "shard0001", "ok" : 1 }
追加出来たみたいです。 ■ テストデータの準備 ShardのKeyは下記の@doryokujinさんのスライドで良い例になっている月とユーザーにしてみます。 [slideshare id=7568009&w=427&h=356&fb=0&mw=0&mh=0&style=border:1px solid #CCC;border-width:1px 1px 0;margin-bottom:5px&sc=no]
って事でmongosに向かってデータを突っ込んでみます。 これ系のデータを作る方法はイロイロあるのでしょうけれども自分は、、 ・Excelに1行を貼り付けて ・レコード毎に別な値になってて欲しいところをカラムを別にして ・ビヨーってやってインクリメントさせていって固定部分はそのままコピペして ・最後にExcelのデータをエディタにコピペして不要なタグを取り除きます そんなこんなで↓のような100万件くらいのテストデータを作ってシェルから実行します。 #昔はExcelで6万件くらいしか扱えなかったですが、今はこういうのヨユーですねぇ.. #こういうの自分オリジナルなテストデータ作る君的なスクリプトをストックしておきたいものです。。#!/bin/sh /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymmd":"201101", "user_id":10000000, "comment":"hogehoge1"})" /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymmd":"201101", "user_id":10000001, "comment":"hogehoge2"})" /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymmd":"201101", "user_id":10000002, "comment":"hogehoge3"})" ~略~ /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymmd":"201212", "user_id":10055307, "comment":"hogehoge1018028"})" /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymmd":"201212", "user_id":10055308, "comment":"hogehoge1018029"})" /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymmd":"201212", "user_id":10055309, "comment":"hogehoge1018030"})"
■ テストデータの投入 Ubuntuに持ってって叩いて暫く待ちますよ、と。 ファイルのサイズが結構な感じになりますた。
$ ls -lh testdata.sh -rw-r--r-- 1 hoge hoge 156M Oct 26 17:50 testdata.sh
ズゴって叩いたら、標準出力出まくりなので、 ・Ctrl+zしてから ・bgでバックグランド実行にしてその窓閉じましたw
$ chmod 755 testdata.sh $ ./testdata.sh MongoDB shell version: 2.2.0 connecting to: localhost:27020/survey MongoDB shell version: 2.2.0 connecting to: localhost:27020/survey
スゲー時間かかるので、70万件手前くらいでやめときました。 こんだけありゃいいんじゃねーの?っていう。
mongos> db.comments.find().count(); 691718
■ ってか、どのデータをどうShardするかやってないじゃんねw surveyというデータベースをSharding対象にしやす。
mongos> db.runCommand({enablesharding: "survey"}); { "ok" : 0, "errmsg" : "access denied - use admin db" }
admin使えやって事ですので…
mongos> use admin switched to db admin mongos> db.runCommand({enablesharding: "survey"}); { "ok" : 1 }
Shard対象のコレクションをシャーディングのキーと一緒に指定しやす。
mongos> db.runCommand({shardcollection: "survey.comments", key: {"yyyymm": 1, "user_id": 1}}); { "proposedKey" : { "yyyymmdd" : 1, "user_id" : 1 }, "curIndexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "survey.comments", "name" : "_id_" } ], "ok" : 0, "errmsg" : "please create an index that starts with the shard key before sharding." }
そんなインデックスありませんがな、と言われたよw ってことで、作りやす。インデックス。
db.comments.ensureIndex({"yyyymm": 1, "user_id": 1}); mongos> db.comments.getIndexes(); [ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "survey.comments", "name" : "_id_" }, { "v" : 1, "key" : { "yyyymm" : 1, "user_id" : 1 }, "ns" : "survey.comments", "name" : "yyyymm_1_user_id_1" } ]
インデックスも作ったし、シャードの設定してやるZEと。
mongos> db.runCommand({shardcollection: "survey.comments", key: {"yyyymm": 1, "user_id": 1}}); { "ok" : 0, "errmsg" : "found null value in key { yyyymm: null, user_id: 10000000.0 } for doc: { _id: ObjectId('508a481a0e0873f2f309d9cc'), yyyymmd: "201101", user_id: 10000000.0, comment: "hoge" }" }
ちっくしょー、そんなキーありゃしませんがなってなんか怒られたぞよ、と。 確かに上記のシェルで突っ込んだデータのキー名がyyyymm"d"ってなっちゃってる...orz って事でキー名変更~。こういう事の積み重ねが慣れに繋がるんでしょね、とポジティブに捉える事にしやす。。 んま、MongoDBはユルいアレですが、この辺はちゃんとみてくれるんで良かったなーって気がします。
mongos> db.comments.update({}, { $rename : { "yyyymmd" : "yyyymm" } }, true, true);
で、いよいよ、、、。
mongos> use admin switched to db admin mongos> db.runCommand({shardcollection: "survey.comments", key: {"yyyymm": 1, "user_id": 1}}); { "collectionsharded" : "survey.comments", "ok" : 1 }
あー、やっとシャーディング出来たっぽい。。 確認してみます。
mongos> db.printShardingStatus(); --- Sharding Status --- sharding version: { "_id" : 1, "version" : 3 } shards: { "_id" : "shard0000", "host" : "localhost:27018" } { "_id" : "shard0001", "host" : "localhost:27017" } databases: { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "survey", "partitioned" : true, "primary" : "shard0001" } survey.comments chunks: shard0000 10 shard0001 111 too many chunks to print, use verbose if you want to force print
チャンクあり過ぎで全部表示できねーよって言われたよw んま、そうこうしてる間にも絶賛Sharding中。
$ ./mongotop ns total read write 2012-10-29T05:54:57 survey.comments 159ms 1ms 158ms survey.sysytem.indexes 0ms 0ms 0ms survey.system.namespaces 0ms 0ms 0ms survey.system.indexes 0ms 0ms 0ms local.system.replset 0ms 0ms 0ms local.system.indexes 0ms 0ms 0ms admin.system.indexes 0ms 0ms 0ms
$ ./mongostat connected to: 127.0.0.1 insert query update delete getmore command flushes mapped vsize res faults locked db idx miss % qr|qw ar|aw netIn netOut conn time 0 0 0 0 0 17 0 256m 379m 185m 0 survey:16.3% 0 0|0 0|0 1k 2k 6 14:55:02
シャーディング先のmongodで確認。ズンズン増えてってます~。
$ ./mongo localhost:27018/survey MongoDB shell version: 2.2.0 connecting to: localhost:27018/survey > db.comments.count(); 184383 > db.comments.count(); 214954 > db.comments.count(); 224716 > db.comments.count(); 345718
最終的にmongosから見たところの合計のレコード数が691717で、
mongos> db.comments.count(); 691717
ポート番号27017のインスタンスが、、
connecting to: localhost:27017/survey > db.comments.count(); 345999
ポート番号27018のインスタンスが、、
connecting to: localhost:27018/survey > db.comments.count(); 345718
ちゃんと足し算した結果が691717になってて、 ほぼ均等に振れててナイスだぜー、と。 ■ データを偏らせてみる 今の感じだと同じ月に同じユーザーが投稿しまくったら偏る感じになります。 って事で、先程のシェルをいじって年月が201101でユーザーIDが10000000のデータを ガツっと突っ込んでみることにします。
#!/bin/sh /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymm":"201101", "user_id":10000000, "comment":"same_month_user"})" /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymm":"201101", "user_id":10000000, "comment":"same_month_user"})" /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymm":"201101", "user_id":10000000, "comment":"same_month_user"})" /usr/local/mongodb/bin/mongo localhost:27020/survey --eval "db.comments.save({"yyyymm":"201101", "user_id":10000000, "comment":"same_month_user"})"
6万弱くらい用意して叩きやしたよ、と。
$ wc -l same_month_user.sh 59054 same_month_user.sh $ ./same_month_user.sh
経過をみていくと、ポート番号が27018のインスタンスは レコード数がズンズン増えていきますが、
$ ./mongo localhost:27018/survey MongoDB shell version: 2.2.0 connecting to: localhost:27018/survey > db.comments.count(); 347803 > db.comments.count(); 367593 > db.comments.count(); 367643 > db.comments.count(); 370911
ポート番号が27017はピクリともしません。
$ ./mongo localhost:27017/survey MongoDB shell version: 2.2.0 connecting to: localhost:27017/survey > db.comments.count(); 345999 > db.comments.count(); 345999 > db.comments.count(); 345999
■ チャンクの状態を確認してみやす。 どのデータがどのシャードIDなのよっていうのは、 printShardingStatusにtrue付けて叩いてあげるとズダーっと出てきます。
mongos> db.printShardingStatus(true); --- Sharding Status --- sharding version: { "_id" : 1, "version" : 3 } shards: { "_id" : "shard0000", "host" : "localhost:27018" } { "_id" : "shard0001", "host" : "localhost:27017" } databases: { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "survey", "partitioned" : true, "primary" : "shard0001" } survey.comments chunks: shard0000 60 shard0001 61 { "yyyymm" : { $minKey : 1 }, "user_id" : { $minKey : 1 } } -->> { "yyyymm" : "201101", "user_id" : 10013326 } on : shard0000 Timestamp(2000, 0) { "yyyymm" : "201101", "user_id" : 10013326 } -->> { "yyyymm" : "201101", "user_id" : 10024513 } on : shard0000 Timestamp(3000, 0) { "yyyymm" : "201101", "user_id" : 10024513 } -->> { "yyyymm" : "201101", "user_id" : 10036702 } on : shard0000 Timestamp(4000, 0) { "yyyymm" : "201101", "user_id" : 10036702 } -->> { "yyyymm" : "201101", "user_id" : 10059075 } on : shard0000 Timestamp(5000, 0)
今回のデータは↓に該当しますので、shard0000の27018の方が偏ってったのがわかりました。
{ "yyyymm" : { $minKey : 1 }, "user_id" : { $minKey : 1 } } -->> { "yyyymm" : "201101", "user_id" : 10013326 } on : shard0000 Timestamp(2000, 0)
■ チャンクを手動で移動してみる ポート番号28018(shard0000)にあるチャンクデータを、 ポート番号28017(shard0001)のインスタンスの方に持ってってみます。
mongos> db.runCommand({moveChunk : "survey.comments", find : { user_id : "10000000" }, to : "shard0001"}); { "ok" : 0, "errmsg" : "access denied - use admin db" }
adminじゃなきゃダメよー、と。こういうの多いなぁw んでadminで叩いたら、、、
mongos> use admin switched to db admin mongos> db.runCommand({moveChunk : "survey.comments", find : { user_id : "10000000" }, to : "shard0001"}); { "errmsg" : "exception: right object ({ user_id: "10000000" }) doesn't have full shard key ({ yyyymm: 1.0, user_id: 1.0 })", "code" : 10199, "ok" : 0 }
チャンクのキーは年月+ユーザーIDで、上記はユーザーIDしか指定してないので、 シャードのキーをfullでちゃんと指定しなさいと。そりゃそうすねw
mongos> db.runCommand({moveChunk : "survey.comments", find : { "yyyymm" : "201101", user_id : "10000000" }, to : "shard0001"}); { "millis" : 1211, "ok" : 1 }
出来たくせーー。 ポート番号28018のインスタンスは
> db.comments.count(); 370911
だったのが↓に減って
> db.comments.count(); 365148
ポート番号28017のインスタンスは
> db.comments.count(); 345999
だったのが↓に増えました~
> db.comments.count(); 351762
んでもって、mongos経由で件数取得してみます。
mongos> db.comments.find().count(); 716910
365148+351762で大丈夫な感じっぽいですね。 んま、一応ちゃんと偏ったデータになってんのよね、と(もっと先にやっとく事ですがw)
mongos> db.comments.find({"user_id":10000000}).count(); 25204 mongos> db.comments.find({"user_id":10000001}).count(); 10 mongos> db.comments.find({"user_id":10000002}).count(); 10 mongos> db.comments.find({"user_id":10000003}).count(); 10
printShardingStatusしてみるとチャンクの数も上記では ・shard0000 60 ・shard0001 61 だったのに対して以下のように変わってましたー。 ・shard0000 59 ・shard0001 62
mongos> db.printShardingStatus(); --- Sharding Status --- sharding version: { "_id" : 1, "version" : 3 } shards: { "_id" : "shard0000", "host" : "localhost:27018" } { "_id" : "shard0001", "host" : "localhost:27017" } databases: { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "survey", "partitioned" : true, "primary" : "shard0001" } survey.comments chunks: shard0000 59 shard0001 62
■ 最後に、、 しょっぱいところでミスったりして結果的に結構時間かかりましたがw んま、アプリ側でなんも考えずにmongos経由で叩けばよくて、 手動でなんとか出来る術もあるってのはホントお手軽だよなぁと。 今回の検証で手応え掴めたので、あとはconfigサーバーの冗長化とかくらいで、 業務でもシャーディングしないと耐え切れない~くらいの データがガツガツ入ってきても対応出来るような気がしてきた今日この頃です。