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

ホントは怖いMongoDB

とあるMongoDBを利用しているサービスを引き継いだら死にかけていたって話。

mongoose populate事件

まず、最初にサービスにjoinしたころ

mongoが遅い!mongoが遅い!という声が聞かれたので、試しにスロークエリーをとってみることにした

cat /var/log/slow.log

===

db.users.find({_id: {$in: [1,2,3,4,5,6,7,8,8,9,................. 
~ コンソールいっぱいのID列(略) ~
.limit(10)  // 律儀にlimitはかかっている
query not recording (too large)

mongodbのログってクエリが長いと省略されるんだーへー

じゃない!

おかしい!

しかし、調べてもソースコード上でin句を使ってるところが見当たらない

結果的には、mongooseのpopulateメソッドが犯人だった

user to user のデータにpopulateを使うべからず

Mongooseにはpopulateという、RDBでいうとこのjoinみたいな機能がある

Mongoose Query Population v4.1.11

KVSでjoinとか革新的じゃん

しかし。。このpopulateを過信して設計を誤ると大変なことになる (ふつうのDBでもuser to userデータにjoin使わないけど。。)

簡単例で、たとえばユーザーデータとして

  • ユーザーのID
  • フォローするユーザーのID

があったとする

実際のデータはこんな感じ

{
  "user_id": 1,
  "follow": [2,3,4] // ここにユーザーIDが入ることはmongooseのModelに定義済とする
},
{
  "user_id": 2,
  "follow": []
},
{
  "user_id": 3,
  "follow": []
},
{
  "user_id": 4,
  "follow": []
}

それに対して、以下のpopulteをなげると

User
  .findOne({_id: 1})
  .populate('follow')
  .exec(function (err, users) {
    // 
});
[
{
  "user_id": 1,
  "follow" : [
     {
        "user_id": 2,
        "follow": []
     }
     {
        "user_id": 3,
        "follow": []
     }
     {
        "user_id": 4,
        "follow": []
     }
  ]
}
]

データがjoinされて返って来る!便利!

この便利なメソッド、内部的にはこんなクエリが発行される

db.users.find({_id:1});
db.users.find({_id: {$in: [2,3,4]}});

ここで注意しないといけないのは、$in句は1件取得なので計算量はO(n)である。

これをそこそこ大きいサービスで運用するとどうなるかというと

計算量はユーザー数に比例してでかくなり、スロークエリが連発する。

そして何より恐ろしいのが

MongoDBのslow queryがin句によりうめつくされ、tailするとコンソールが固まり、更にはログサーバーのディスク容量が枯渇する(経験談)

解決策 データ構造を見直し、indexを利用

一度に全員引く必要が無いなら、サンプルの例はこれのほうがいい

followUsers

{
  user_id: 1,
  follow: 2
},
{
  user_id: 1,
  follow: 3
},
{
  user_id: 1,
  follow: 4
},
{
  user_id: 1,
  follow: 5
},
{
  user_id: 1,
  follow: 6
}

これに、ユニークインデックスを貼っとく

db.followUsers.exsureIndex({user_id: 1, follow:1}, {unique: true});

BtreeのオーダーはO(log n)なので、計算量は膨れ上がらないのと、limit, offsetが効くので元の構造より全然早く取れる

そもそもの話

KVSで複雑なindexが必要な状況なら、RDB使った方がいい 基本的にはタイムラインとか、シーケンシャルにIDで引けるものが一番パフォーマンスを発揮する

インスタンスがやたらつよいけど、スロークエリでまくってる事件

色々とクエリを改修して、とりあえず、障害クラスのスロークエリーがなくなり始めた頃

LAが全然低いのに、パフォーマンスが上がらない。。

という問題にぶち当たった

ちなみに環境は

  • MongoDBのバージョンは2.2
  • サーバー8コア

お気づきの方はいるかと思うが

Mongodbの2系はマルチコアが利用できないため、8コアのうちの1コアしか使えない

というもの。。

また、Databaseが一個しかない構成だった

MongoDBが適さないケース - LinuxとApacheの憂鬱

MongoDB 2.2以前はReaders-Writerロックなので、長いクエリーが走ったその瞬間から、短いクエリーもロックされる。 WebからのPVを捌く様なシステムの場合、一瞬でもロックが走れば、WEBサーバ上の全てワーカースレッドが潰されてサイトがダウンする。

MongoDBの2.2系はデータベースのグローバルロック問題を抱えている

一般的にMongoDBの2系を運用する際には、機能ごとにデータベースを分けて、ロックを分散させるように作るらしい

解決策

リリース直後のMongoの3系へのバージョンアップを決意

Mongo3系の特徴としては 

  • コレクションレベルロック(2.6系から)
  • WiredTiger
  • マルチコアサポート

そして、人柱となる覚悟を決めたのであった

Mongo3.0はいい感じなので、後でまとめたい

Tips

色々と運用してて溜まったTipsたち

MongoDBを選ぶ理由

  • スキーマレス
  • シャーディングによるスケーラビリティの高さ

MongoDBを選ばない理由

サービスの規模が予測可能、データ構造が変わりにくい場合はMySQL使った方が安定・安心

MongoDBを使う上で気をつけないといけないこと

正規化よりも、データの参照性を意識したほうがいい

  • KVS全般に言えることと思うけど、正規化しすぎると参照が苦しくなる

基本的に検索したいっていう要件には向かない

  • KVSは時系列など、並んでいるデータをとるときに真価を発揮する
  • MongoDBの場合、B-Tree Indexを使えるが、RDBに比べてアドバンテージがあるわけではない
  • 全文検索するなら別のミドルウェアを考えたほうがいい (elasticsearch + river + mongoとか)
  • トランザクションがないため、代替してデータを保護する仕組みが必要
  • Lock機構とか

データ設計で注意すること

  • 最初からシャーディングを意識した設計にする
    • シャーディングは基本_idでシャード
    • シャーディング環境のみでエラーになるクエリもあるので注意
  • 1ドキュメントがでかくなりすぎないような設計にする
  • データを重複で管理するとなってもデータの取り回しを優先した方がいい

シャーディング

  • 複数データベースをリストアするときは1データベースずつやる
  • データをロックしてしまい結果的に完了するまでの時間がかかる