SoftLayerでMongoDB環境を構築してみよう
試しにデータを入れてみる
ではさっそくデータを投入してみましょう。MongoDBの場合、一個のデータを「ドキュメント」と呼び、ドキュメントが沢山集まった、検索とか集計の単位を「コレクション」、コレクションの集まりである管理単位を「データベース」と呼びます。データベースやコレクションを作るのに特別な手続きは必要はなく、単にそこを指定してドキュメントを作成するだけです。ドキュメント形式もやはりJSONです。
では、この連載であるOSS on SoftLayer Showcaseの情報を集めたデータベースOSSonSoftLayerを作り、そこに連載記事の情報をコレクションarticlesに格納してみます。先ほどからつないでいるプライマリノードでそのまま作業します。
replSetSLDemo:PRIMARY> use OSSonSoftLayer switched to db OSSonSoftLayer replSetSLDemo:PRIMARY> db.articles.insert({title: "IBMのSoftLayerで最新のDrupal 8を試してみよう!", OSS: "Drupal", authors: [{name: "小薗井 康志", yomi: "OSONOI, Yasushi"}], URL: "http://thinkit.co.jp/story/2014/12/15/5484"}) WriteResult({ "nInserted" : 1 }) replSetSLDemo:PRIMARY> db.articles.insert({title: "OpenStack Juno on SoftLayer by RDO", OSS: "OpenStack", authors: [{name: "熊谷育朗", yomi: "KUMAGAI, Ikuo"}, {name: "鈴木智明", yomi: "SUZUKI, Toshiaki"}], URL: "http://thinkit.co.jp/story/2014/12/15/5484"}) WriteResult({ "nInserted" : 1 })
とりあえず二つのドキュメントを追加してみました。
検索してみましょう。
replSetSLDemo:PRIMARY> db.articles.findOne() { "_id" : ObjectId("551f511648f585534212d0d7"), "title" : "IBMのSoftLayerで最新のDrupal 8を試してみよう!", "OSS" : "Drupal", "authors" : [ { "name" : "小薗井 康志", "yomi" : "OSONOI, Yasushi" } ], "URL" : "http://thinkit.co.jp/story/2014/12/15/5484" } replSetSLDemo:PRIMARY> db.articles.find() { "_id" : ObjectId("551f511648f585534212d0d7"), "title" : "IBMのSoftLayerで最新のDrupal 8を試してみ よう!", "OSS" : "Drupal", "authors" : [ { "name" : "小薗井 康志", "yomi" : "OSONOI, Yasushi" } ], "URL" : "http://thinkit.co.jp/story/2014/12/15/5484" } { "_id" : ObjectId("551f511948f585534212d0d8"), "title" : "OpenStack Juno on SoftLayer by RDO", "OSS" : "OpenStack", "authors" : [ { "name" : "熊谷育朗", "yomi" : "KUMAGAI, Ikuo" }, { "name" : "鈴木智明", "yomi" : "SUZUKI, Toshiaki" } ], "URL" : "http://thinkit.co.jp/story/2014/12/15/5484" }
ちゃんと入っているようですね。
さて、レプリカセットなので、投入したデータはほかのノードにもレプリケーションされているはずです。mongo2につないで確認してみましょう。標準だとセカンダリノードからのREADはできないので、rs.slaveOk()コマンドで許可してから検索してみます。
$ mongo 〈mongo2のプライベートIP〉/OSSonSoftLayer MongoDB shell version: 2.6.9 connecting to: 〈mongo2のプライベートIP〉/OSSonSoftLayer replSetSLDemo:SECONDARY> rs.slaveOk() replSetSLDemo:SECONDARY> db.articles.find() { "_id" : ObjectId("551f511648f585534212d0d7"), "title" : "IBMのSoftLayerで最新のDrupal 8を試してみよう!", "OSS" : "Drupal", "authors" : [ { "name" : "小薗井 康志", "yomi" : "OSONOI, Yasushi" } ], " URL" : "http://thinkit.co.jp/story/2014/12/15/5484" } { "_id" : ObjectId("551f511948f585534212d0d8"), "title" : "OpenStack Juno on SoftLayer by RDO", "OSS" : "OpenStack", "authors" : [ { "name" : "熊谷育朗", "yomi" : "KUMAGAI, Ikuo" }, { "name" : "鈴木智明", "yomi" : "SUZUKI, Toshiaki" } ], "URL" : "http://thinkit.co.jp/story/2014/12/15/5484" }
ばっちりですね。
もっと大量のデータを!
せっかくのデータベースなのでもうちょっと大きなデータを入れてみましょう。日本郵便の「郵便番号データダウンロード」サービス(http://www.post.japanpost.jp/zipcode/download.html)から、ローマ字読みのデータを取ってきて展開します。
$ wget http://www.post.japanpost.jp/zipcode/dl/roman/ken_all_rome.zip ... $ unzip ken_all_rome.zip Archive: ken_all_rome.zip inflating: ken_all_rome/KEN_ALL_ROME.CSV
このデータはシフトJISなので、iconvでutf-8に変換します。
$ cd ken_all_rome $ iconv -f SJIS -t UTF-8 KEN_ALL_ROME.CSV -o KEN_ALL_ROME.utf8.csv
これを、zipsデータベースのjapanコレクションにインポートします。データファイルの仕様を参照してフィールド名を決めると、次のようなコマンドでインポートできます。
$ mongoimport --host 〈mongo1のプライベートIP〉 -d zips -c japan --type csv --fields zip,prefecture,city,town,pref-en,city-en,town-en --file KEN_ALL_ROME.utf8.csv
プライマリノードとセカンダリノードのMongoDBシェルで
$ for (;;) {print(db.japan.count(); sleep(1000)}
というスクリプトを走らせ、zipデータベースのjapanコレクションのサイズを表示するようにしてみました。ちゃんとリアルタイムで同期されているのがわかりますね(上がプライマリ、下がセカンダリ)。
自動フェイルオーバー
次はちょっとした実験をしてみましょう。以下のようなPython3スクリプトを書いてappで実行します。レプリカセットに接続し、さきほど投入した電話番号データの全件を問い合わせし、順に表示するものです。アプリケーションでキャッシュしているわけではないので、順に問い合わせているときでもデータベースとの通信は発生します。
#!/usr/bin/env python3 from pymongo import MongoClient import time # connect to replicaset client = MongoClient([u'〈mongo1のプライベートIP〉',u'〈mongo2のプライベートIP〉',u'〈mongo3のプライベートIP〉']) # access japan collection in zips database zips = client.zips japan = zips.japan # put all entries in japan collection, except Japanese field for zip in japan.find({},{"_id":0, "zip":1, "pref-en":1, "city-en":1, "town-en":1}): print(zip) time.sleep(1)
実行にはpip経由でpymongoのインストールが必要になるので別途appにインストールしておきます(python3-pipパッケージを導入してpip3 install pymongo)。
さて、これを実行するとこんなふうな画面になりますが、
$ ./mongo_test.py {'town-en': 'IKANIKEISAIGANAIBAAI', 'zip': 600000, 'city-en': 'SAPPORO SHI CHUO KU', 'pref-en': 'HOKKAIDO'} {'town-en': 'ASAHIGAOKA', 'zip': 640941, 'city-en': 'SAPPORO SHI CHUO KU', 'pref-en': 'HOKKAIDO'} {'town-en': 'ODORIHIGASHI', 'zip': 600041, 'city-en': 'SAPPORO SHI CHUO KU', 'pref-en': 'HOKKAIDO'} {'town-en': 'ODORINISHI(1-19-CHOME)', 'zip': 600042, 'city-en': 'SAPPORO SHI CHUO KU', 'pref-en': 'HOKKAIDO'} ...
ここでSoftLayerのコンソールから、さっきまでプライマリノードだったmongo1の電源を止めてしまいましょう。パワーオフ!
……ちょっとひっかかりますが、なにごともなかったように再開して処理は継続されます。これぞレプリカセットの威力! 読み込みの場合はほとんど気にならない程度の時間でスイッチします。mongo2につないで、レプリカセットのステータスを確認してみましょう。
replSetSLDemo:SECONDARY> rs.status() { "set" : "replSetSLDemo", "date" : ISODate("2015-04-04T05:23:13Z"), "myState" : 2, "syncingTo" : "〈mongo3のプライベートIP〉:27017", "members" : [ { "_id" : 0, "name" : "〈mongo1のプライベートIP〉:27017", "health" : 0, "state" : 8, "stateStr" : "(not reachable/healthy)", "uptime" : 0, "optime" : Timestamp(1428119806, 1244), "optimeDate" : ISODate("2015-04-04T03:56:46Z"), "lastHeartbeat" : ISODate("2015-04-04T05:23:05Z"), "lastHeartbeatRecv" : ISODate("2015-04-04T05:20:34Z"), "pingMs" : 0, "syncingTo" : "〈mongo3のプライベートIP〉:27017" }, { "_id" : 1, "name" : "〈mongo2のプライベートIP〉:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 11010, "optime" : Timestamp(1428119806, 1244), "optimeDate" : ISODate("2015-04-04T03:56:46Z"), "self" : true }, { "_id" : 2, "name" : "〈mongo3のプライベートIP〉:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 10885, "optime" : Timestamp(1428119806, 1244), "optimeDate" : ISODate("2015-04-04T03:56:46Z"), "lastHeartbeat" : ISODate("2015-04-04T05:23:12Z"), "lastHeartbeatRecv" : ISODate("2015-04-04T05:23:12Z"), "pingMs" : 0, "electionTime" : Timestamp(1428123061, 1), "electionDate" : ISODate("2015-04-04T04:51:01Z") } ], "ok" : 1 }
さきほどまでプライマリだったmongo1のstateStrが”(not reachable/healthy)”となっていて、mongo3がプライマリに昇格していることがわかります。ここでmongo1の電源を入れなおしたらどうなるでしょうか。
replSetSLDemo:SECONDARY> rs.status() { "set" : "replSetSLDemo", "date" : ISODate("2015-04-04T05:33:06Z"), "myState" : 2, "syncingTo" : "〈mongo3のプライベートIP〉:27017", "members" : [ { "_id" : 0, "name" : "〈mongo1のプライベートIP〉:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 250, "optime" : Timestamp(1428119806, 1244), "optimeDate" : ISODate("2015-04-04T03:56:46Z"), "lastHeartbeat" : ISODate("2015-04-04T05:33:04Z"), "lastHeartbeatRecv" : ISODate("2015-04-04T05:33:05Z"), "pingMs" : 0, "syncingTo" : "〈mongo3のプライベートIP〉:27017" }, ...
セカンダリとして復旧しました。強制パワーオフはともかく、たとえばサーバーのスケールアップを行う場合など、アプリケーション側の設定はそのままに、サービスを停止することなく作業できるのは嬉しいと思います。
MongoDBとSoftLayerは仲がいい?
MongoDBのレプリカセットの構成を作ってみました。こういう複数ノードの環境を作って試すにはSoftLayerのようなクラウドは便利でよいですね。各ノードの設定は完全に同じなので、Provision ScriptやCLIを用いれば、もっと手間を少なく構築できると思います。
最後に、ちょっとした小咄です。
つい数年前の常識では、非常にハイパフォーマンスな(MongoDB的にはメモリを大量に載せた)ハードウェアは非常に高価で、それよりコモディティハードウェアを並べたほうが(台数が増える分の管理コストを考えても)低コストということは言えたでしょう。そして、ノード数が増えてくると物理ハードウェアを並べるよりも管理が圧倒的にラクなので、クラウドとMongoDBは親和性が高い、というのが定説だったと思います。
しかし今はハイエンドのサーバーも価格は下がってきました。クラウドでもかなり高い性能のインスタンスを提供するようになってきましたので、台数を増やして管理コストが上がるスケールアウトより、スケールアップでできるところまで頑張りたい、できれば物理ハードウェアも視野に入れたい、でも自社でホスティングはしたくない……そんなニーズにMongoDB on SoftLayerはうまくハマるのではないかな、と思います。
今回はアプリケーション側の話はほとんどしませんでしたが、事前のスキーマ定義が不要で、スクリプト系言語でいうところのハッシュをほぼそのままデータベースに永続化できるMongoDBはとてもアプリケーションが作りやすいです。レプリカセット一つだけで、いや要件によってはシングルノードで始めても、MongoDBのメリットは大きいと思います。ぜひぜひ、MongoDB on SoftLayerで楽しい開発を!