SoftLayerでMongoDB環境を構築してみよう
[PR]
MongoDBはMongoDB Inc.という会社が中心となって開発しているオープンソースのデータベース管理システムです。NoSQLと呼ばれるデータベースの中でも、特にドキュメント指向データベースといわれるものです。
さまざまに特徴はありますが:
- JSON的な階層型データをどんどんと押し込める動的スキーマ
- インデキシングや豊富なクエリーといったアプリケーションから使いやすい豊富な機能
- シンプルな考え方で高可用性やバックアップなどを実現できるレプリカセット
- データサイズやアクセス数が増えたときに水平スケールで容易にパフォーマンスアップ可能なオートシャーディング
といったところがよく挙げられるところでしょうか。
筆者としては、アプリケーションの作りやすさと、可用性をカンタンに担保できるレプリカセットは、やはりMongoDBのわかりやすい嬉しさだと思います。ということで、MongoDBのレプリカセットをSoftLayer上に構築してみましょう。
レプリカセットとは
MongoDBのレプリカセットというのは、MongoDBが標準で採用しているレプリケーションの仕組みです。検索すれば沢山の解説ページが出てくるので詳しくは省略しますが:
- 通常3台のノードから構成
- 書き込みはプライマリノードに対して行い、複数のセカンダリノードに自動的にレプリケーション
- 原則はプライマリノードから読み書きすることでConsistency(データの整合性)を担保するが、負荷分散やレイテンシーを考慮して明示的にセカンダリノードからデータを読むことができる(Eventual Consistency)
- あるノードがダウンしたときでも2ノードで暫定的に動きつづけることが可能 ・プライマリノードがダウンした場合でも、セカンダリノードの一つが自動的にプライマリノードに昇格しサービス継続できる
といった、いろいろ美味しいことがあります。
SoftLayerではノード間の通信は内部ネットワークになり、拠点間のネットワークも回線が太いので、レプリカセットのノードの一部を国外のデータセンターに配置してディザスタ・リカバリ対策にする、といったときにも威力を発揮できると思います。
MongoDBのインストール方法の選択
SoftLayerにMongoDBをインストールする方法として、ぱっと思いつくのは次の方法でしょうか。
- SoftLayerのサーバーデプロイ時にデータベースオプションで「MongoDB」を選ぶ
- デプロイ後にOS標準パッケージでインストールする
- デプロイ後にMongoDBで配布しているパッケージでインストールする
- MongoDB Management Service(MMS)でインストールする
どれでやってもよいといえばそうなのですが、今回はSoftLayerならではのお気軽さということで、1.を選んでみましょう。
SoftLayerでサーバー払い出し
さきほど説明したとおり、レプリカセットは最低3台のノードが必要です。ついでに、アプリケーションサーバーを模したサーバーを1台用意します。ので、さくっと3台+1台のサーバーを振り出しましょう。
今回はお試しなので、構成としては一番安い仮想サーバーにします。
データセンター | 香港 |
---|---|
COMPUTING INSTANCE | 1x2.0GHz |
RAM | 1GB |
OPERATING SYSTEM | Ubuntu Linux 14.04 LTS Trusty Tahr - Minimal Install (64 bit) |
FIRST DISK | 25GB(SAN) |
DATABASE SOFTWARE | MongoDB Community Edition |
OSはWindows Serverでなければ大丈夫です(MongoDBの実体は実行ファイル一個だけなので、Windowsでも可能は可能なのですが、積極的に選ぶ理由はないです)。本番構成なら、SECOND DISKとしてSANのディスクをMongoDBのデータ格納用に別途確保するのがよいでしょう。
アプリケーションサーバーにはMongoDBはいらないのですが、別にデプロイするのが面倒という理由と、MongoDBのクライアントシェルが欲しいので入れてしまいます。
マシン名は仮にこんな感じにしましょう。
アプリケーションサーバー | app |
---|---|
MongoDB ノード1 | mongo1 |
MongoDB ノード2 | mongo2 |
MongoDB ノード3 | mongo3 |
構成としては次の図のようになります。
デプロイされたら基本的な設定をしておきましょう。アプリケーションサーバーappとMongoDBの3ノード間の通信はすべて内部ネットワークです。sshもappからするとして、app以外はufwなどでグローバルからのアクセスを拒否するとよいでしょう。
内部ネットワーク側については、MongoDBは基本27017ポートをLISTENするのでここは解放しましょう。またMongoDBはノード間の死活監視にpingを用いるので、ICMPも許可する必要があります。
本来なら、このあたりはProvisioning ScriptでやるのがSoftLayer流でしょうか。
MongoDB レプリカセットの構築
ではいよいよレプリカセットの構築です。app経由でmongo1にsshで接続しましょう。
まずは動いているmongodbをチェック。
$ mongo --version MongoDB shell version: 2.6.9 $ ps aux | grep mongo[d] mongodb 796 0.1 3.6 349760 36548 ? Ssl Apr02 1:46 /usr/bin/mongod --config /etc/mongod.conf
バージョン2.6.9が動いていて、設定ファイルは/etc/mongod.confの下にあるということがわかります。
ではレプリカセットに関する設定をします。
$ sudo vi /etc/mongod.conf
まずはlocalhost外からの接続ができるようにbind_ipの設定をコメントアウトし、
# Listen to local interface only. Comment out to listen on all interfaces. bind_ip = 127.0.0.1 ↓ # Listen to local interface only. Comment out to listen on all interfaces. #bind_ip = 127.0.0.1
レプリカセットの名前を指定します。ここでは「replSetSLDemo」にしましょう。
# Replication Options # in replicated mongo databases, specify the replica set name here #replSet=setname ↓ # Replication Options # in replicated mongo databases, specify the replica set name here replSet=replSetDLDemo
これでサービスを再起動しましょう。
$ sudo service mongod restart
同じ設定をmongod2、mongod3にも行ってください。これでMongoDBノードの設定はOKです。ではappに戻って、mongo1のMongoDBに接続しましょう。
$ mongo 〈mongo1のプライベートIP〉 >
mongoコマンドはMongoDBのシェルです。JavaScriptインタプリタが動いています。まずはレプリカセットに自分自身だけを追加しましょう。MongoDBシェルではあらゆる情報がJSON形式なので、設定もJSONで書きます。
> config = { _id: "replSetSLDemo", members: [{_id:0, host:"〈mongo1のプライベートIP〉"}] }
_idは先ほど設定ファイルに書いたレプリカセット名と同じにします。
この設定を使ってレプリカセットを初期化します。レプリカセット関係のコマンドはrs.で始まることになっていて、初期化はrs.initiate()です。
> rs.initiate(config) { "info" : "Config now saved locally. Should come online in about a minute.", "ok" : 1 }
ちょっと間をおいてエンターキーを叩いてみると、プロンプトが次のように変わるはずです。
replSetSLDemo:PRIMARY>
これは、「今つないでいるMongoDBノードは、レプリカセットreplSetSLDemoに所属している、プライマリノード」という意味です。レプリカセットの立ち上げに成功しました。
では残りのノードも追加しましょう。それにはrs.add()コマンドを使います。
replSetSLDemo:PRIMARY> rs.add("〈mongo2のプライベートIP〉") { "ok" : 1 } replSetSLDemo:PRIMARY> rs.add("〈mongo3のプライベートIP〉") { "ok" : 1 } replSetSLDemo:PRIMARY> rs.config() { "_id" : "replSetSLDemo", "version" : 3, "members" : [ { "_id" : 0, "host" : "〈mongo1のプライベートIP〉:27017" }, { "_id" : 1, "host" : "〈mongo2のプライベートIP〉:27017" }, { "_id" : 2, "host" : "〈mongo3のプライベートIP〉:27017" } ] }
これでレプリカセットの構築は終わりです。
試しにデータを入れてみる
ではさっそくデータを投入してみましょう。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で楽しい開発を!