読者です 読者をやめる 読者になる 読者になる

「ニフティクラウドでMongoDBは使えるか?」を読んで、僕なりの考察を書いてみた

MongoDB

@です。2011年5月19日付けのニフティクラウドさんのブログエントリーが「ニフティクラウドでMongoDBは使えるか?」と称してニフティクラウド上でMongoDBのパフォーマンス比較を行うという、非常に興味深いものでしたので、このエントリーを読んで僕なりの考察をまとめてみました。完全な検証環境がわからないので一部推測を含み、誤解などもあるかもしれませんが、それらのフィードバックも期待して、ブログにすることにしました。ですのでまずはこのエントリーを読んで下さいね。

ニフティクラウドでMongoDBは使えるか? : NIFTY Cloud ユーザーブログ

それではエントリーの中盤、「Benchmark」以降を部分的に引用しながら考察を述べていきたいと思います。

shardKeyを考慮に入れる事の重要性

■ Benchmark 下記のような条件およびプロセスでベンチマークを行い、プロセス完了までの時間を計測しました。
データ twitterのtweetを1.4GB (delete含めて707,259tweet)
条件
・shardingの有無(1台=無 vs. 3台)
・stripingの有無(有 vs. 無)
ニフティクラウドスペック(mini vs large16)

注:stripingについてはニフティクラウドにMySQLは載せられるのか?パフォーマンス大検証!をご覧ください

まず提示された条件については、以下の情報が不足しているように感じました。

shardingを構成する際に、shardKeyとして何を設定したか?

MongoDB のshardingでは、1番始めに行う「shardKeyの設定」が非常に重要です。このshardKeyの設定次第では、shard間の偏りやデータの非効率な取得が生じてしまう原因になります。また、find()やinsert()などの処理に関してもそのshardKeyを条件に含めて実行するかどうかで効率性が大きく異なってきます。shard環境の有無でfind()やinsert()の性能比較を行う場合には、shardKeyを条件に含めた場合と含めない場合の両方でさらに比較する必要があります。このshardKeyが不明ですとこの切り分けができません。

ここでは前提としてstatus_idをshardKeyにすることにします

今回のインプットはTwitterデータということで、最も一般的なstatus_idをshardKeyにした場合で考えることにします。実はstatus_idはMongoDBのshardKeyを考える上で非常に良い例となってくれます(最後のスライドの2枚目をご覧下さい)。
ここでのstatus_idは時間増加する性質をもったものと仮定しています。つまり時間が進みにつれてstatus_idが単調増加する性質です。一時期Cassandraを採用するためにこのstatus_idが時間増加にならないようにするといった話題が出ましたが、Cassandraを採用しなかった今、結局どうなったのでしょうか…(^_^;)

find() の条件にshardKeyを含むか否か、キーにインデックスを作成しているか否かの切り分けは比較としてとても重要なファクターです

プロセス
...
find
 コレクションからデータを取得するコマンドです。MongoDBの場合は、ドキュメントを縦横無尽に条件付けてフィルタリングができるのでこの性能も無視できない重要な性能です。

・1条件 find({source:'web'})
・ネスト条件 find({'user.lang':'ja'})
・否定 find({'geo':{$ne:null}})
・サイズ find({'entities.hashtags:{$size:2}})
・比較 find({'retweet_cnt':{$gt:1}})

MongoDBのパフォーマンス比較においては、これらのfindの条件が

  1. shardKey を含むかどうか
  2. インデックスを作成しているかどうか

を明示的に考慮に入れた方が良いと感じます。

検索条件にshardKeyが含まれている場合とそうでない場合

まず前者について考えていきます。sharding環境のfind()においては、その条件にshardKeyを含ませるか否かでデータ取得の方法が異なってきます。shardKeyが含まれている場合、mongosは該当のshardKeyを持つshardがどこにあるのかを知っていますので、そのshardにだけクエリを実行することができます。

逆に、shardKeyを含まない場合にはmongosは全てのshardに対してクエリを実行して取得したデータをマージしてクライアントに返してくれます。

つまりsharding環境において効率の良いクエリが実行できるのは、find()の条件にshardKeyが含まれていた場合です。ですので性能比較の際にはshardKeyを含ませるかどうかの違いも検証することが重要です。

インデックスを作成しているか、smallインスタンスにインデックスを作り過ぎていないか

後者に関しては、もちろんインデックスを作成した方が速いのは明らかですが、インデックスサイズが大きくなりがちなMongoDBに関しては、元のメモリ量が少ないsmallインスタンスに大量のインデックスを作成した場合に、逆にパフォーマンスが落ちてしまう可能性もあるのではないかと考えたからです。

(余談)mapreduce はcount()やdistinct()との比較もあれば

mapreduce
 MongoDBを十二分に活用するのであれば、避けられないコマンドです。簡単な解析に利用することが出来ます(簡単、と書きましたがかなり使えます)。


・単純なグループ集計(source集計)
・単純なグループ集計(5分ごとのtweet数集計)
・2条件のグループ集計(1時間当たり・1人当たりのtweet数集計)
・少し複雑な集計(1時間当たりのhashtag集計)
・少し複雑な集計(共起するhashtag集計)

count() や distinct() は高速

余談ですが、sharding環境でのMongoDBにおけるmapreduceの比較については、単純なmapreduceを記述する場合にはcount()やdistinct()といった組み込み関数との比較を行うのも面白いと思います。例えばtweet数集計や、sourceの集計、あるいはUUの計算などはsharding環境でもcount()やdinstinct()コマンドが全shardを対象に行ってくれます(正しい値を返さない場合もありますが…)。そしてこれらは実はmapreduceよりも高速に結果を返してくれます。例えば24shard環境で130GB、1億2千万レコードのuseridが特定できているアクセスログに対してUU計算を行った場合、mapreduceで記述した場合が350secに対してdistinct()を用いた場合には200secでした。これは後者がインデックスを利用して効率良く集計くれているからだと思います。

結果に対する考察

それではエントリーに記述されたベンチマーク条件での比較結果について、今まで述べた性質を考慮しながら考察してみたいと思います。



























スペックminilarge16
sharding
striping
mongoimport549.201 520.438 630.101 690.042 259.671 104.757 263.050 193.803
find38.378 42.293 13.618 14.998 1.044 1.030 1.124 1.132
mapreduce226.64 229.46 107.39 106.85 52.53 52.10 26.08 26.08

全体

全体的にざっくりと見ると、当然といえば当然ですが、VMのスペックがimport/find/mapreduceに大きなパフォーマンスの差に影響を及ぼしています。
 また、IO性能に関わるstripingに関しては、import、特にlarge16以外ではほとんど意味のある効果を発揮していませんでした。これはminiの場合はIOよりもmemory/cpuがボトルネックになっているためだと考えられます。

 データサイズが1.4GBであるため、mini(memory=512MB)においてはfindやmapreduceにおいてIO性能の及ぼす影響が強いと予想していましたが、ほとんど見られませんでした。これは今後追求していければと思っています。

findに関しては検索に使用したキーにインデックスを使用していたかがポイントになってくるような気がしました。mapreduceに関しては基本的にドキュメント全件走査しますが、findの場合はインデックスが使用できますので作成しているキーが条件に指定された場合には全件走査をする必要がありません。インデックスを作成した場合とそうで無い場合でもボトルネックはmemory/cpuであったのかも試されると良かったのかもしれません。(恐らく同じボトルネックであったと思いますが。)
また、インデックスを作成した場合、MongoDBはインデックスサイズが大きくなりやすい傾向があります。今回の場合、miniインスタンスでさらにインデックス作成の有無でさらに比較をしたとすると、findにインデックスを作成することの高速化する一方で、大量のメモリ消費の影響がどのくらいありそうなのかが気になりました。

mongos

 上記と重複しますが、スペックにおける影響が非常に強く、また、次に、large16のときにstripingの影響が出ていました。このimport処理では書き込み性能に影響が出るため、shardingの影響を予測していましたが、shardingによる性能の改善は見られないばかりか、1台で行った場合よりも劣化するという結果でした。

sharding環境の有無でmongoimportに差が出なかったのは恐らくshardKeyの問題ではないでしょうか。前述しましたが、shardingは事前に設定したshardKeyに従ってデータを各shardに分割しています。例えばこのshardKeyの値が "_id" を始めとした時間軸に対して増加するキーであった場合には、連続して挿入されるデータは分散されずに同じchunk、つまり同じshardに入ってしまいます。この場合にはshardingしていながら、実際にはシングルサーバーへ書き込みを行っているのと同じ、さらにはmongosサーバーを介することによってシングルよりも無駄なステップを消費しています。今回の結果の原因は、status_idをshardKeyとして使用しているような場合にはこれによるものと考えられそうです。

 この結果は、mongosまたはconfigサーバがボトルネックとなりうるという要因が予測されます。今後、例えばmongosを分割し、それぞれに対して同時にimportを行い、全体としてのパフォーマンスの改善が見られるかを検討する必要があるかと思います。

mongosもconfigサーバーもそれほど負荷のかかる役割を担う処理を行いませんし、複数用意するのは主にレプリケーション目的であって、スケール目的ではありません。ですのでこれらはボトルネックにはなりにくいはずです。
ただ、一度に大量なインサートを行う場合には、mongosを複数用意してデータを分割して、各mongosに並列insertすることによって多少の負荷を軽減することができそうですし、書き込みにゆとりがある場合には並列insertの方が高速に処理してくれそうです。
また、クライアントとのデータ入出力の窓口を増やすことで1つのmongosがダウンしてしまった際にも安定稼働させる上で有用だと思います。

find

IO性能に関わるstripingはほぼ、findにおいては影響を見せませんでした。またshardingによるfindの性能は、miniでは、およそ2倍?3倍程度の改善を示していましたが、large16ではほとんど効果が見られませんでした。同時に、stripingも影響があまり見られませんでした。

 今回の程度のデータサイズであれば、cpu/memory性能が十分に高ければ分散させてIO性能を稼ぐことにあまり意義が無いと考えられます。

find処理においては、検索条件にshardKeyが含まれていたかどうかが重要になります。もしshardKeyが検索条件に含まれていた場合には、mongosはどのshardに該当するデータが存在するかを把握していますので必要なshardに対してのみ検索を行うので効率良く高速に実行できます。しかし、shardKeyが検索条件に含まれていない場合は、全てのshardにfind()を実行し、結果をマージするということを行いますので効率があまりよくありません。
今回差が出なかったのも検索条件にshardKeyが含まれていなかったので全部のshardを確認していったからでは無いでしょうか?
(もちろん、全てのクエリがいつもshardKeyを含んでいるとは限りません、MongoDBはメモリをうまく利用することによって、この場合でもそれなりに速く実行してくれます。)

あるいはsharding環境においては、バックグラウンドでchunkのマイグレーションが行われているかどうかによってパフォーマンスが変わってきます。chunkのマイグレーション中はデータの移動がバックグラウンドで行われており、その際にchunkは丸々メモリにコピーされています。マイグレーションが行われている場合にはこのメモリの消費とサーバーへの負担のせいでパフォーマンスが一時的に下がってしまう可能性があります。
ただ、今回はその状況は起きていないと考えられます。デフォルトのchunkサイズは200MBであり、balancing機能によってマイグレーションが発動するタイミングは、現在アクセスされているshardのchunk数と、最もchunk数の少ないshardとの数の差違で10以上になったときです。すなわち2GB以上の差違が生じた時に発動します。今回のデータは1.4GB、MongoDBに格納した場合に2.5GB程度になると考えられますのでそもそもマイグレーションが発生していたかどうか、またそれが起きたとしても少ないマイグレーション回数でbalancingできるので、検証実行時には既にそのマイグレーションが完了しており、その状況に出くわさなかったことが考えられます。

それでは、なぜminiではshard環境の有無で結果に差が出たかというと、はやりシングルサーバーの場合はmemory/cpuがボトルネックになっていて、それが3つのmemory/cpuにデータを分散できたことで解消されたので、shardKeyが条件指定されておらず、全部のshardを見に行くという処理になっても高速に実行できたのではないでしょうか?

mapreduce

 今回、一番ベンチマークを取った甲斐があるプロセスでした。stripingについては性能にほとんど影響が見られませんでしたが、shardingの効果がよく出ており、簡単とはいえ集計や計算がcpu/memory依存で行われ、mapreduceを複数台で実現することの意味が見出せたものと考えられます。

map/reduceに関してはsharding環境を活用できますので高速化が最も期待できるところです。ただし、ここでもshardKeyの設定によって、集計対象とするデータが1つのshardに偏っている場合などは、その効果があまり期待できません。
また、map/reduceの実行は基本的に1shardあたり1スレッドしか使えません。このことは例えば8coreと1coreのCPUをそれぞれ積んだサーバーで比較しても、1サーバーごとに1shardしか配置されていない場合には大きな差違が出ないということです。
MongoDBのmapreduceはshuffle機能が無いなど、Hadoop等のmapreduceに比べてやや簡素化された仕組みでありますが、この1core/shard制約は最も大きな違いであり、MongoDBのmapreduceがスケールしない、遅いといわれているゆえんであります。
これらを考慮して、データの均等分散に気を遣い、かつコア数だけshardを同居させるといった(これは非推奨です)環境で実行した場合にはより大きな差を生むことが出来るはずです。

mapreduceの特性を理解して環境を構築する重要性

今回、すべてのデータについては、分散はほとんど見られなかったこともあり、ざっくりとした考察だけを行っていますが、いかがでしたでしょうか。実際に使う上でshardingを適切に配置し「Largeシリーズ」を使っていただければ。「Largeシリーズ」を使っていただければ、性能がスケールすることも含め、比較的性能的にもよいパフォーマンスが得られたのではないかと思っております。

1つのサーバーに1shardしか組まないという安全性を重視した設計を行う場合は、例えばメモリが4MBと同じでCPUのコア数のみが異なる「large」と「small4」ならばsmall4を選択してもそれほど問題が無いと思われます。しかしその金額の差違は、月額プランにして1サーバーあたり¥48,300-¥25,410=¥22,980。仮に10台のshardとそのreplicationとしてさらに10台を使用する場合には、月額で ¥22,980 × 20台 = ¥457,800 になります。このようにMongoDBのmapreduceの特性を理解せずに高スペックマシンの方が高速に動作すると思い込んで毎月数十万を無駄する可能性があります。
ニフティクラウドのインスタンス毎の金額設定は以下を参照して下さい:
クラウド 料金 | ニフティクラウド

逆に、あくまで試験的ですが、フルコアを使いきるためにlargeの4コアサーバーに擬似的に4shard構成にすることも可能です。この場合には必要なサーバー台数が1/4にできる一方で、月額は2倍高いだけなので、結果的にsmall4に比べて1/2の金額で済む事になります。ただ、これはmapreduceのみを考慮した話であり、同サーバーでその他の処理も行われていたりする場合などで状況は変わってくるので注意して下さい。

感想

まず、ニフティさん、エントリーに対して少し批判的な内容を含む記事になってしまったことをお詫びいたします。また、誤解や間違いもあるかもしれませんので、その際は指摘して頂けると幸いです。
今回の感想ですが、サービス提供側が、今回のような比較記事を積極的に示してくれることによって、そこから僕たちは非常にたくさんの情報を読み取ることができますので、非常に良いエントリーだと思いました。是非とも今後の継続的なレポートも期待しています。
また、ニフティクラウドを代表するクラウドサービスでMongoDBをメインに使用する環境を構築する場合には、MongoDBの特性を十分に考慮した上で構築しないと、無駄に高い構成になってしまったり、きちんとMongoDBを設定・チューニングできていないことによる低パフォーマンスをスペックのせいにして高スペックプランに乗り換えてしまうといった事態が生じてしまいがちです。十分に注意して下さい。
僕はクラウド上でMongoDBを活用してサービスや解析を展開される方を心より応援しています。気軽に声を掛けて下さい。そしてニフティさん、僕にクラウド環境を貸して頂ければMongoDBの性能検証などを詳細に行うことが可能です。いかがでしょうか?

※最後の最後に、shardKeyは重要ですという話をしましたので、悪いshardKey3例と良いshardKey1例を挙げておきます。