5月 19

mongoDB
「第3回 MongoDB 勉強会 in Tokyo」 : ATND
先週土曜日(2011年5月14日)にフューチャーアーキテクトさんのセミナールームにて行われたMongoDBの勉強会に行ってきました。


MongoDBの仕様・機能などの体系的な話からチューニングの話、開発に利用する話、プロダクションとして運用している話と、バランスよくまとまっていてとても勉強になりました。
RDBとの違いや特有の挙動をきちんと把握して利用すれば、十分プロダクションで利用出来るというビジョンが見えました。

詳細は各発表者の方のスライドを見るとわかると思いますが、当日会場でメモしたものでダイジェスト的にまとめました。
MongoDB勉強会は毎月開催(予定)しているそうなので、興味を持たれた方は是非参加して見てください。
MongoDBコミュニティー:MongoDB JP | Google グループ

【発表1】MongoDB全機能解説1

MongoDB特有の仕様や機能など、プロダクションで使う場合には特に注意が必要な部分を中心に説明されていました。

ロック

Read/Writeの各オペレーション時にDBに対してロックがかかる。
MongoDBをフロントエンドで使う場合、DBがロックして応答が返って来なくなるなどの問題が起きる可能性があるので、時間がかかる処理には注意が必要とのこと。

実際、単体のRead/Writeは十分高速でロックの待ち時間で問題が起こることはないが、複数のWrite処理をアトミックで行なったり({atomic: ture})、MapReduceで重い処理をさせるケース(ロックはかからないが処理が重いので)が問題になる。

mongos

mongosサーバーは協調する複数のmongodサーバーがある場合、それを一つのクラスターとして扱うためのルーティングサーバーで、それ自体ではストレージする機能はない。
アプリケーションは複数あるmongodサーバーの中でどれががマスターでどれがスレーブなのかを知らなくてもmongosサーバに問い合わせればいい。

database

32bit環境ではDBサイズが2.5GBまで扱えないので、要64bit環境とのこと。
参考:32-bit limitations

BSON

データはBSONという形式で保存されている。データ型が複数あり、どのデータ型で保存されるのかはドライバーに依存する。
→ 例えば、PHPの場合はMongoInt32やMongoInt64などのデータ型用のクラスが用意されてる。
オブジェクト内の順序もみているので、順番が入れ替わるだけで別のオブジェクトととされてしまう。
→ { ‘key1’: ‘value1’, ‘key2’: ‘value2’ } ≠ { ‘key2’: ‘value2’, ‘key1’: ‘value1’ }

Capped Collection

サイズを指定してCollectionを作成するとCapped Collectionになる。
サイズが固定なのでサイズを越えた場合は古いものから自動的に削除される。
とにかく書き込みが速い。でも削除できない。
_idにすらindexがつかない。indexを張ることもできるが遅くなる(それでも速い方)。
shardingすることはできない。
用途 → ロギング(ディスクサイズを圧迫しない)
   → キャッシング
   → MapReduceの一時的なCollectinとして

Insert周り

Bulk Insert

複数のdocumentを配列に入れて一括でインサートする「Bulk Insert」が速い。
ただ、どのくらいのサイズまでが最適なのかはスペック依存で、経験上16MBを超えるとエラーが出るそう。

fire and forget

インサートはデフォルトではデータベースからの応答を待たない
応答を確認するには、各種ドライバーでインサート時に応答を待つ処理を明示的にオプションとして指定する必要がある。
→ PHPではarray(‘safe’=>true)

トランザクション

RDBのように複数のオペレーションをトランザクション処理することは出来ないので、連続インサートする場合、途中でエラーになっても一連のインサートをロールバックすることは出来ない

getLastErrorメソッド
db.getLastError();

でオペレーションの最後に起こったエラーがわかるので、このメソッドは重要。

fsyncコマンド

メモリ上のデータを強制的にストレージにフラッシュするコマンド。

db.runCommand({fsync:1});

fsyncは自動的に60秒に1回走るようになっていて、mongod起動時に、–syncdelayオプションで変更可能だが10genとしては非推奨とのこと。
つまり、最長60秒間に行われたデータの更新は喪失する可能性がある。

Journaling(v1.7.5)

オペレーション前にJournalingファイルに保存し、オペレーションに失敗した場合、次回実行される。
100ms毎にジャーナリングされる。つまり、100ms間のオペレーションは喪失する可能性がある仕様。

Query

printjson

シェル(mongo)でデータを確認するときには

db.collection.find().forEach(printjson);

とするとJSON形式にフォーマットされ見やすくなる。

入れ子

入れ子になったオブジェクトをサブオブジェクト形式でfindする場合は、完全に一致したオブジェクトでないと取得できない。
サブオブジェクト内のあるキーだけを検索対象としたい場合は、dot notation形式({author.name: ‘Jane’})で問い合わせる。

var doc = { title: 'タイトル', body: '本文', author: { first_name: '太郎', family_name: '山田'} };
db.collection.insert(doc);
db.collection.find({author.first_name: '太郎'}); //◯ヒットする
db.collection.find({author: { first_name: '太郎'} }); //×ヒットしない

Update

Update

デフォルトでは最初にヒットしたオブジェクトのみを更新する。
→ ヒットした全てを更新対象としたい場合は{ ‘multi’: true } オプションを付ける。
→ 複数の更新をアトミックに行うには { ‘$atomic’: true } オプションを付ける。

Save

同じ_idのオブジェクトがあれば自動的にドキュメント全体を更新する。

modifier オペレーション

アップデートのオプションとして$inc/$set/$push/$popなどのオペレーションがある。
modifierオペレーションを指定しないでアップデートを行うとオブジェクト(ドキュメント)をまるごと上書きするのでとても怖い。

in-place update

データサイズが変わらない更新の場合は高速。(例えば、{ ‘$inc’; { key: 1 } } など。)
また、データの更新を高速に行うため、インサートされた際に実データサイズよりも大きめにストレージを使って保存している。
→ 多少のサイズの増加も高速に更新出来る。逆に言うと無駄にストレージを使う。

Snapshot

MongoDBはデータサイズが大きく増えた場合、データベースファイルの新しい領域にデータを格納し直す。この瞬間にfind()すると同じドキュメントが2個取れたりする可能性がある
find()で取り出した複数のドキュメントをupdate()するようなケースでは問題になるので、重複を排除して結果を返すsnapshot()を使う。

db.collection.find({name:'Jane'}).snapshot();

Indexについて

B-Tree indexes

インデックスを生成するオペレーションensureIndex()はデフォルトではDBをブロックする。
{ background: true } オプションを付けるとロックしないので、基本的にこれを必ず付けること。

db.collection.ensureIndex({name: 1}, {background:true});

参考:Indexing as a Background Operation

Sparse Index(v 1.7.4)

インデックスの設定されたフィールドを持っていないドキュメントは無視される。
→ コレクション全体的の中でSparse Indexに設定されたフィールドに値を持っているドキュメントがが少ない場合にパフォーマンスが出るインデックス。

Unique Index

インデックスを張ったキーが存在しないドキュメントを登録するとnullが挿入され、nullがユニークの対象となる。
→ インデックスを張ったキーが存在しないドキュメントをもう一つ登録すると重複エラーになる。
ただし、sharding環境では重複する可能性がある

Replication

  • Master-Slave:一般的なM/S構成
  • Replica Sets:自動フェイルオーバ機能などを提供
Master-Slave

Master->Slaveは非同期でコピーされる。
mongodをそれぞれ、–master、 –slaveを付けて起動するだけ。
→ slaveには直接書き込みできない。(mongos経由)

同期は各データベースがそれぞれローカルに自動的に作成するoplog(operation log)コレクションを介して行われる。

  1. スレーブ(セカンダリ)はマスターのoplogを監視している。 
  2. マスター(プライマリ)は更新系のオペレーションをoplogに書き込み、自分のデータベースに実行する。
  3. スレーブはマスターとの差分のoplogを自分のoplogへコピーし、そのオペレーションを自分のデータベースに実行する。

MySQLのバイナリログと同じようなイメージ。

Replica Sets

3台〜12台で構成でき、自動フェイルオーバー機能/自動リカバリ機能を持つ。
ノードタイプには、Standard(データあり。Primaryになれる。)、Passive(データあり。Primaryになれない。)、Arbiter(データなし。投票権はある。)がある。
フェイルオーバーは、起動時に設定するprimary値の高いもの > 最も新鮮なデータを持っているもの > 投票数の多いもの の順で順番付けされ、決定したセカンダリーがプライマリになる。
ダウンしているサーバーが多かったり、最終更新データが古すぎたりするとどのセカンダリーもプライマリにならないことがある。→ 手動で対応。

【発表2】ソーシャルアプリのプロトタイプ制作にMongoDBを活用

Sleepy.Mongoose

アプリケーションとデータベースの通信はSleepy.Mongoose(pythonで実装されたRestインターフェース)を利用して、curlで実装している。
→ Sleepy.Mongooseにはcount()が実装されていないのでforkしたものをGitHubに置いてあるとのこと。
bibrost / sleepy.mongoose

Cray Model

自動車開発のプロセスで粘土細工(Cray Model)で何度も何度も型を作ってから金型を作るように、プロトタイプの開発がしたい。
RDBのようにスキーマを定義して、チーム内で変更を共有して、不整合が起きないようにCREATE TABLEしたりALTER TABLEしたりするのは面倒。
MongoDBはアプリケーション上でいくらでもデータ構造を変更できるので、ソースさえ共有すればよく、途中でドキュメント構造が変わっても、手を止めることなくcollection名を変えるだけでいい。→まさに粘土細工のように失敗したら壊してまた作ればいい。

なぜ最終的にRDBにするのか

  • MongoDBを保守できるエンジニアがいない。
  • MongoDBをクリティカルなプロダクションで使うことが難しい(トランザクションがない)。

【発表3】MongoDBチューニング 〜スロークエリ撲滅までの道筋〜

オプティマイザ

RDBで多く採用されている一般的なオプティマイザは統計情報を元にしたコストベースで、本番環境と検証環境で統計情報が異なっていると実行計画が違ってきたり、悪い実行計画も行われる。
MongoDBのオプティマイザはコストベースではなく、新しいクエリー実行がある度に計算する仕様。(オーバーヘッド・CPUコストが高いが、最適な計画が行われる)

  • 新しいクエリー実行時に複数の実行計画を並行に実施して一番速いのを採用・学習し、他のものは途中で止める。
  • 100回データ更新が行われ時やindexが変更された時、最初の評価時からスキャンするドキュメントが大きく増えた場合(nscannedの値が10倍になったら)再評価を実施する。

explainメッソド

explain()で実行計画を確認できる。

db.collection.find(query).explain();

勘所としてはBasicCursorは全件スキャンなので×、millisが長いと処理時間が遅いので×、nscannedが多いとスキャン対象のドキュメント数が多いので×、など。

スロークエリの特定

  • slowmsオプションをつけて起動する。
    → ログファイルに保存される。
  • db.setProfilingLevel()を実行する。
    → db.system.profileコレクションに保存されていく。(デフォルト100ms)
  • db.currentOp()を実行する
    → 現在実行中のクエリが見られる。
  • 複合インデックス

    順序が大事。
    db.collection.find({x: 10}).sort({ y: 1});でも { x: 1, y: 1 }のインデックスが使われる。

    hintメソッド

    hintメソッドを使うと特定のインデックスを使わせることができる。
    インデックス設定されていれば、
    db.collection.find().hint({uid:1}); より db.collection.find().hint({uid:1, path:1}); の方がより絞られて高速になる。

    mongostat

    mongoやmongodなどと一緒にインストールされるmongostatコマンド。
    ロックされている割合やインデックスが利用されなかった割合などの統計情報が確認できる。

    killメソッド

    db.killOp(opId)で実行中のクエリをkillできる。

    【発表4】MongoDBを使用したモバイルゲーム開発

    東京ガールズスナップの構成

    Java(appサーバー2台)+ MongoDB(Replica Setで3台)で運用している。
    MongoDBに対応したSpring Data Documentを利用して開発した。

    MongoDBで良かったこと

    階層型にデータが持てるのでkey-value的に使えた。
    → ドキュメント内に配列を持つことができるので、RDBのようなリレーションや連結などしなくても直接$pushや$popで複数のデータを出し入れできる。

    バイナリデータが保存しやすい
    当初の想定していた利用法ではなかったが画像データのキャッシュストレージとして利用できた。
    → 4MBまでだったらバイナリデータをそのまま格納できる。

    MongoDBが問題になったこと

    • オペレーションミスが大変なことになる(update時に$setで指定しないと全部上書きする)
    • アプリ経由ではなくコンソールでデータを更新したらデータ型が変わってしまった(intがdoubleに)
    • ドキュメント設計の最適化に時間がかかった(ベストプラクティスがわからない)
    Tagged with:
    8月 19

    Twitter Client Ranking by Streaming APIの構成は、
    PHP5.3.3(with APC) + MongoDB1.6.1 + Apache2.2.3で、フレームワークにsyfmony1.4.6を利用しています。この構成が動く最低限の環境を構築した際のメモです。
    さくらのVPSはサービスはほとんど入っていませんでしたが、gccやautoconfは既に入っていました。

    #Apache
    sudo yum install httpd.x86_64
    
    #PHPのコンパイルに必要(パラメータによる)
    sudo yum install httpd-devel.x86_64
    sudo yum install libxml2-devel.x86_64
    sudo yum install openssl-devel.x86_64
    sudo yum install curl-devel.x86_64
    
    #最新版PHP取得→コンパイル→インストール
    wget http://ve2.php.net/get/php-5.3.3.tar.gz/from/jp2.php.net/mirror
    tar vfxz php-5.3.3.tar.gz
    cd php-5.3.3
    ./configure \
    --enable-mbstring \
    --with-apxs2=/usr/sbin/apxs \
    --with-curl \
    --with-openssl \
    --with-config-file-path=/etc  \
    --enable-dom \
    --with-libdir=lib64
    meke
    sudo make install
    
    #symfonyのインストール
    sudo pear channel-discover pear.symfony-project.com
    sudo pear install symfony/symfony
    
    #PHP拡張のインストール
    sudo pecl install apc
    sudo pecl install mongo
    
    #PHP拡張の反映
    sudo vi /etc/php.ini
    
    extension = mongo.so
    extension = apc.so
    apc.enabled=1
    

    PHPもRemiのリポジトリを利用すると簡単に最新版がインストールできますが、cli版がバックグラウンドで実行できない不具合があるのでソース版を利用しました。

    参考

    MongoDBをインストール

    下記URLを参考にMongoDBのリポジトリを設定してインストールします。
    参考:CentOS and Fedora Packages

    sudo vi /etc/yum.repos.d/10gen.repo 
    
    [10gen]
    name=10gen Repository
    baseurl=http://downloads.mongodb.org/distros/centos/5.4/os/x86_64/
    gpgcheck=0
    
    name=10gen Repository
    baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/x86_64
    gpgcheck=0
    
    sudo yum install mongo-stable* --enablerepo=10gen
    sudo yum install  mongo-10gen* --enablerepo=10gen
    

    最新のstableバージョン1.6.1がインストールされます(2010年8月19日現在)最新のstableバージョン1.8.2がインストールされます(2011年7月21日現在)。デフォルトの設定は以下の通りです。
    設定ファイル:/etc/mongod.conf
    ログファイル:/var/log/mongo/mongod.log
    データディレクトリ:/var/lib/mongo/

    起動は起動スクリプトでOK

    sudo /etc/init.d/mongod start
    

    2011-7-21修正 yumリポジトリの修正。最新バージョンの修正。

    Tagged with:
    8月 17

    はじめに

    さくらのVPSをベータ*1として使わせてもらっているのに何もしないのはもったいない&申し訳ないので、Twitter Client Ranking by Streaming APIというのを、さくらのVPSのパフォーマンスとMongoDBのパフォーマンス・使用感をテーマに実験的に作ってみました。

    TwitterのStreaming APIからツイートを取得、保存、解析してどんなクライアントが使われているかをランキング形式に表示するという単純なものです。

    解析結果をブログで公開している例があるものの、なかなか直近で連続的なデータを公開しているところがなかったので、勢いで作りました。

    さくらのVPS

    さくらのVPSの仕様は以下の通りです。

    メモリ 512MB
    HDD 20GB
    回線 100Mbps
    OS CentOS 5 x86_64
    グローバル IP アドレス IPv4 アドレス×1 個
    データ転送量 無制限
    管理者権限 root 権限付与

    データを継続的に保存するのにHDDが20GBと心許ないので古くなったデータはどんどん削除していく必要がありそうです。一方でデータ転送量が無制限となっているので、APIからの取得に(料金的な意味で)気を使う必要はなさそうです。
    OSは最新のCentOS5.5で、サービスはsendmailとssh以外は何も入っていないので、httpdなどを自由に追加して利用します。また、ポートはすべて公開状態なのでiptablesを設定、有効にすることも必要です。

    Twitter Streaming API

    Twitter Streaming APIのstatuses/sampleメソッドを利用します。sampleメソッドは公開タイムラインの1%未満がサンプリングされたデータとなります。実際にアクセスをしてみると1分間に350〜650ツイート取得でき、量にかなりムラがあることがわかりました。
    HDDの容量を考えて、データはcronで6分毎に1分間取得することにしたので、サンプルは1/6%、全ツイートの約0.167%未満になりそうです。数日間取得してみたところ、1日で約10万〜12万ツイートをサンプリングできています。

    MongoDB

    MongoDBはドキュメント指向DBと呼ばれる仲間で、Key-Value StoreにRDBMSの色を少し足したようなイメージです。スキーマはダイナミックに変更できる一方で、SQLこそ使えないものの、where, sort, limit, group(by), max, min,などが用意されていて、RDBMSに慣れている人にもとっつきやすいと思います。
    さらに、スケールアウトを容易にするSharding機能を標準装備していて、分散したデータベースから複雑な集計ができるMap/Reduceが利用できます。

    RDBMSでDBやテーブルにデータを入れようと思った時はあらかじめCREATEしておく必要がありますが、MongoDBは必要ないので存在しないDBやコレクション(RDBMSでいうテーブル)にいきなりデータを投入してもエラーは出ず、コレクションやDBすらダイナミックに作られます。この辺りの挙動はファイル操作に近いので、日付ごとにダイナミックにコレクションを作ることもでき、今回のログの保管などの用途にも適しています。

    日付ごとにコレクションを作り、値にはtimestamp, date, source, source_url, time_zone, utc_offset, langを保存することにしました。Sharding環境ではないのでMap/Reduceの力を発揮することもできないのですが、実験的な意味で、データの集計にはMap/Reduce*2を利用しています。

    使い方

    デフォルトは1日前のサンプリングしたすべてのデータが表示されます。そこから、日付、Lang、Time zone、UTC offsetで絞り込みすることで様々な形で抽出することができます。
    例えば、「8月12日のツイートの中から東京(time_zone=Tokyo)で日本語(lang=ja)のツイートをするのに利用されているクライアント」などを抽出・集計することができます。

    ランキングだけでは味気ないので、TOP10を円グラフで表すのと、各クライアントが時間ごとにどのくらい利用されているのか3つまで比較出来る機能を用意してみました。住んでいるところやクライアントの種類で利用されている時間外が全然違うのがわかります。

    現状、なかなかもっさりとした感じなので、イライラするかもしれません。
    実験なので、負荷の問題でサービスを終了するかもしれませんし、逆に、作っただけではなくMongoDBのTipsやチューニングなどで改善し、それをさらにネタにして行きたいとも思っています。

    *1 7月15日〜8月31日までクローズドベータとして会員限定でプレリリースされたホスティングサービス。2010年9月1日から初期費用無料、月額980円の「さくらのVPS 980」として正式リリースされる。

    *2 今回の実験ではmapReduce()よりgroup()を使った集計の方が速度が速いことが多かったので、当日分のリアルタイム集計ではgroup()を利用しています。mapReduce()の結果は新しくコレクションとして保存することができるので、過去分に関してはmapReduce()で抽出→コレクションとして保存→そこから再集計という実装にしています。

    Tagged with:
    preload preload preload