ひよくあブログ

Node.jsとか。

Sails.jsを使ってハマったところとか

この記事はNode.js Advent Calendar 2013の17日目の記事です。

先日勉強がてらNode.jsを使った簡単なWebページを作ってみようということで、ちょうど話題に上がっていたSails.jsを使ってみました。

その中で最初よくわからなかったことやハマったことを書こうと思います。

Sails.jsについて

Sails.jsはRuby on RailsのMVCを模倣して作られたNode.js用のフレームワークです。
ただRailsのようにフルスタックというわけではなく、Model-View-Controllerやそれぞれのジェネレータについては標準でサポートされていますが、テストフレームワークなどは提供されていません。
個人的には、特筆すべきものは以下の2つかと思います。

Realitime機能

ControllersとPoliciyが普通のHTTPリクエストだけでなく、Socket.io / WebSocketを自動的にハンドル可能となっていて、そういったアプリ開発と親和性が高いです。

i18nや認証やアクセスコントロールをビルドインでサポート

この辺はエンタープライズっぽい利用もちゃんと考えているなあという印象。

Modelの使い方

最初に詰まったのはModelの使い方でした。
Sails.jsのgithubのwikiにドキュメントがありますが、Sails.jsのORM部分はwaterlineという別モジュールとして開発をされていて、そちらの方のREADMEやテストを読んだ方が参考になります。
アダプターでMySQLやMongoDBなどを共通のインターフェースで使えるようにする、というコンセプトは面白いですが、まずはその共通インターフェースの学習をする必要があります。

DB操作系のAPIについてはcallbackとdeferredとpromiseと3つのインターフェースが提供されているのですが、最初それに気づかずにドキュメントを読む箇所によって書き方が変わっていて混乱をしてしまいましたw

よくわからなかったのが、Modelへのstaticメソッドとインスタンスメソッドの追加方法でした。試してみたところ、以下のように行うようです。

// api/models/User.js
module.exports = {

  attributes: {
    _id:         'integer', 
    name:     'string',
    // インスタンスメソッド
    getNickname: function() {
        return this.name || '名無し';
    },
  },
  // staticメソッド
  findNewUsers: function(cb) {
    this.find({
          limit: 20,
          sort: '_id desc'
    }, cb);
  }

};

勝手にエンドポイントを追加しないようにしたい

CRUDなエンドポイントなどを自動で作ってくれるのは便利なのですが、外部に公開するアプリの場合は忘れているとproductionで穴を開けたままリリースしてしまう可能性があるので、まず最初にオフにしました。
config/controllers.jsを開くとそれぞれのオプションについてコメントで解説をしてあるので、必要の無いものはオフにしておきましょう。

- shortcuts: true,
+ shortcuts: false,

- rest: true,
+ rest: false,

- expectIntegerId: false
+ expectIntegerId: false

expressのmiddlewareを追加したい

app.jsで変更できるののかと書いてみたのですが、反映されずググってみたところ、config/express.jsというファイルを追加して以下のような記述をすることでexpressのmiddlewareを追加することができました。

module.exports.express = {
  customMiddleware: function (app) {
    // 圧縮してレスポンスを返す
    app.use(require('../node_modules/sails/node_modules/express').compress());
  }
};

production環境と開発環境で設定を変えたい

見た感じconfig/local.jsでポートなどは変更できるようなのですが、DBの接続先などの変更方法がないように見えました。
production環境ではNODE_ENV=productionに環境変数を設定するので、例えばconfig/adapters.jsを以下のように切り替えています。

var dev = {
    'default': 'mongo',
    mongo: {/* development setting */}
};
var prod = {
    'default': 'mongo',
    mongo: {/* production setting */}
};
module.exports.adapters = (process.env.NODE_ENV == "production") ? prod : dev;

sails-mongoの罠

Sails.jsは標準だとMySQLですが、sails-mongoというMongoDB用のアダプタが提供されています。sails-mongoをnpm installしてconfigを書き換えれば、MySQLとも共通のインターフェースでさくっと使うことができます。
で、このアダプタで一つハマりました。 MySQLではデフォルトではアルファベットの大文字と小文字をSELECT時に区別しない(case-insentiveである)のですが、MongoDBは区別をされます。なので、例えば"Hatena"と保存をしていると"hatena"でMongoDBで検索しても引っ掛かりません。
簡単な回避策としては、MongoDBがクエリに正規表現が使えるので、db.collection.find({name: /^hatena$/i})のようにiオプションを使うことです。ただ、iオプションを使った場合はインデックスが使われなくなってしまうため、ある程度の規模のサイトを作るのであればこのやり方は適当ではありません。 で、sails-mongoなのですがMySQLとの互換性を保つために、findの条件の値にストリングを渡したときに/^hatena$/iを勝手にやってくれるという罠がありました。MongoDBでプロファイリングをやっていて、見覚えのないクエリが発行されていて調べていたら気づきました。こういうところが、ORMの怖いところですね。
対処法としては以下のように自前で正規表現でクエリを発行します。前方一致かつcase-insentiveでなければちゃんとインデックスが使われます。

User.find({
    name: new RegExp('^' + name + '$')
}, cb);

ユーザからの入力を受け付ける場合は上記のままだと正規表現に使えないものがあったりするので、以下のような感じでエスケープをする必要があります。(replaceの正規表現はsail-mongoのものを流用しています)

User.find({
    name: new RegExp('^' + name.replace(/[-[\]{}()+?*.\/,\\^$|#]/g, "\\$&") + '$')
}, cb);

使ってみての感想

Railsに比べてしまうと足りない機能は多いですが、なかなか完成度が高くてさくっとWebアプリケーションが作れるフレームワークになっているな、という印象です。ただ、設定やモジュールの実装方法などはSails.js独自のものが多いので、ちゃんと使うにはその辺の学習をする必要があるのですが、まだドキュメント不足な感じは否めないです。
sails-mongoというかModelのアダプターの考え方は嫌いではないのですが、MongoDBならではのクエリやAPIはたくさんあるので、ガチでMongoDBを使うことを考えると困ることが多そうです。なので、Sails.js+MongoDBでちゃんと作るなら、自前でORMもどきを作るか、Mongooseを使う事になりそうです。
今回はRealtimeな使い方はしなかったので次の機会には試してみたいと思っています。

ということで、これから使う人や現在ハマっている人にこの記事が参考になれば幸いです。

MongoDBを真面目に使ってみた

MongoDB Advent Calendar 2013の10日目の記事です。

2つほどNode.js + MongoDBで作ったサービスを最近リリースする機会があったので、そこで得られた個人的な知見や感想についてつらつら書きまとめようと思います。

前提

今回はアプリ側の開発を担当していて、インフラ周りはインフラチームやMongoDBのプロバイダーに御任せでしたので、そちらの設定などの話はノータッチです。

元々の業務ではMySQLを使っていて、この2つのプロジェクトで初めてMongoDBを勉強しました。

永続化にMongoDBを使う理由

MySQLでなくMongoDBを使う理由はMongoDB自体の利便性などもあげることができるかもしれませんが、個人的には運用面での利点が強いと思っています。(特にサンフランシスコのような人材採用が激しい地域の場合)

シャーディングやフェイルオーバーが大規模な運用では必須になりますが、MySQLの場合はMySQL自体が提供している機能ではないので、運用する会社ごとに独自の運用方法を持っています。運用方法を標準化できないと、運用メンバーの流動性が高いと学習コストが馬鹿にならなかったり、サービスの規模や数が増える際に運用メンバーの数がボトルネックになってくる可能性があります。 MongoDBはこの辺りが標準の機能として提供されているのは、運用メンバーのスケールアウトを考えると一つ大きな利点だと思っています。

とはいえ、実際にMongoDBを運用した話をググったことがある方はわかると思うのですが、この辺の機能を使おうとして失敗した話や苦労した話は無数に転がっています。残念ながら、気軽に使うには少し難易度が高く、ある程度の知識や経験が求められています。

そこを踏まえてのもう一つの理由は、有力なMongoDBプロバイダ(DaaSを提供する会社)がいくつか存在することです(MongoHQ, MongoLab, ObjectRocketなど)。現在の人材採用の戦線でMongoDBのエキスパートを会社に何人も雇うことは大変なことですが、こういったプロバイダを利用することで、そういった人材不足をカバーでき、運用面のコストや人的リソースの削減を行うことができます。

日本にもこういった有力なプロバイダが出てくると、もっとMongoDBの利用事例が増えていくのではないかと思います。

MongoDBが適するケース、適さないケース

個人的に、適するケース、適さないケースと思ったことについて述べます。

適するケース

複数のコレクション同士の関連が薄いもの、または多少の不整合が許容されるものの場合にMongoDBは適しています。

ブログや掲示板などの場合、同時に複数のコレクションに挿入や更新を行うことが少なく、また失敗しても多少の不整合は許容される場合が多いため、適した例の一つです。

FoursquareがMongoDBを使っていることは有名かと思いますが、実際の機能から推測するしかないですが、この場合もコレクションの関連が薄く、多少の不整合が許されるものの良い例だと思います。

その他にも、不整合がクリティカルになりにくい社内ツールやコレクションの独立性の高いちょっとしたログデータのような場合も適した例と言えます。

適さないケース

逆に複数のコレクションに跨いで更新が必要なものの場合、MongoDBは利用が難しくなります。

例えばPVP対戦機能を持ったMMO RPGなどで使うことを考えると、複数のコレクションに更新、挿入をすることが予想されます。あるコレクションの更新に失敗した後に別のコレクションの更新に失敗した場合に不整合が起きる可能性があります。開発者は、自前で不整合を解消する方法や複数のコレクションにまたがらないようにデータ設計の改善などの工夫が必要となります。

また、不整合を避けるために1つのドキュメントに複数のドキュメントをまとめることがありますが、最大で1ドキュメント16MBまでは許容はされますが、成長を続けるドキュメントはパフォーマンスの低下やデータの断片化を招くため、あまり良い状態とは言えません。

ドキュメント設計について

ドキュメント設計で考えるべきポイントは、ドキュメントを成長させない形に設計することです。

最も効率の良いMongoDBのドキュメントの使い方としては、ドキュメントに必要なフィールドがプリアロケートできるケースです。

データが変わると、MongoDBは新しい領域を確保してそこにデータを移動します。この処理はコストが高く、頻発するとMongoDBのパフォーマンスの劣化を招きます。MongoDBではこれを避けるために、データの移動が起きるとpaddingFactorの値を増やし、そのコレクションに新しいドキュメントを保存する際に少し大きめの領域を確保するようになります。これはパフォーマンスの観点からは良いことなのですが、実際に必要なデータのサイズ以上にデータ領域を確保してしまうため、データサイズの肥大化を招きます。

あるコレクションが持つドキュメントの構造があらかじめわかっている場合には、仮の値を入れておくとドキュメントのために必要な領域が最初に確保することができ、データの移動も起きないので、最も効率よくMongoDBを使うことができます。(この辺りの話については「Scheme design in MongoDB.」という記事の「ドキュメントを成長させない」を読むとよく理解できます)

ただ、実際のユースケースを考えると、ある程度のドキュメントの成長は避けられない場合も多いです。この場合、1つのドキュメントが大きくなりすぎるとデータの移動のコストも高くなるので、1ドキュメントが大きくなりすぎないように適切にコレクションを分割するように設計をするのが良いと思います。

MongoDBの使用サイズを減らす

MongoDBを使っていてアプリ開発側が考慮すべき一つのポイントはドキュメントやコレクションのサイズです。

MongoDBはMySQLに比べるとデータのサイズが肥大化しがちで、サービスを運営する側から見るとコストの増加につながっていきます。 ここはアプリ側の設計や工夫の余地がある所になります。

実サイズを確認する

既に開発中や運用中のデータがある場合、以下のやり方でデータベースやコレクションやドキュメントのサイズを確認可能です。

データベースとコレクションのサイズを確認するのは以下を実行します。1024*1024はMBで表示をするために指定しています。

# データベースのサイズを確認
> db.stats(1024*1024);
{
    "collections" : 2,
    "objects" : 227397,
    "avgObjSize" : 183.91343280251579,
    "dataSize" : 110,
    "storageSize" : 154,
    "numExtents" : 27,
    "indexes" : 18,
    "indexSize" : 47,
    "fileSize" : 496,
    "nsSizeMB" : 16,
    "dataFileVersion" : {
        "major" : 4,
        "minor" : 5
    },
    "ok" : 1
}

# コレクションのサイズを確認
> db.user.stats(1024*1024)
{
    "ns" : "test.user",
    "count" : 4408,
    "size" : 85,
    "avgObjSize" : 0.00009644043132718926,
    "storageSize" : 118,
    "numExtents" : 11,
    "nindexes" : 6,
    "lastExtentSize" : 35,
    "paddingFactor" : 1,
    "systemFlags" : 1,
    "userFlags" : 0,
    "totalIndexSize" : 62,
    "indexSizes" : {
        "_id_" : 5,
        "id_1" : 0
    },
    "ok" : 1
}

読み方は以下を参照。

http://docs.mongodb.org/manual/reference/command/dbStats/

http://docs.mongodb.org/manual/reference/command/collStats/

変なインデックスを作っていてインデックスサイズが大きくなりすぎていないかや、paddingFactorが1になっていない場合はドキュメントの移動(前述)が起きていることの確認ができます。

では、実際にドキュメントがどのくらいサイズを利用しているのかはBSONのサイズで確認します。

# 渡したオブジェクトのサイズを確認
Object.bsonsize({"_id" : ObjectId("11271c0c3b6cd10db106eeef"), name: "John"})

# findしてきたドキュメントのサイズを確認
Object.bsonsize(db.collection.findOne( {"_id" : ObjectId("52242092732483cb06a444fc")}));

サイズを減らす方法

実際にドキュメントのサイズを減らす方法ですが、データ構造やインデックスの見直しなどもありますが、一番手っ取り早いのはドキュメントのフィールド名を短くすることです。MongoDBはドキュメントベースのデータベースであるため、フィールド名も各ドキュメントで持っているため、フィールド名を数文字減らすだけでもドキュメントの数が数千万とかになってくると差が出てきます。

ドキュメントを設計する時点で意識的に短い名前にする(userIdをuidにするとか)のも良いですし、MongoDBのライブラリでフィールド名にエイリアスを付ける(実際にはuidという名前だけどuserIdとしてアクセスできる)ような機能を持つものを利用するのも良いかもしれません。

このあたりはMySQLとは事情が異なるのでRaisの慣習でフィールド名を決めると(テーブル名+IDとか)後々になって後悔するかもしれません。

以下の記事がわかりやすかったです。

MongoDB Collectionの手軽な省サイズ化

終わりに

ということで、色々書きましたが、この記事が今後MongoDBを使う方の参考になれば幸いです。

MongoDBのスロークエリの最適化をするまでの流れ

前回New Relicを入れてデータを見ているとちょいちょい response timeが遅くなってることがわかりました。

やはりというかなんと言うか、 アプリのデータの永続化に使っているMongoDBで遅いクエリが発行されているようなので、これを調査して対応をするまで流れをメモしました。

ここではクエリと呼んでますが、mongo的には正確にはオペレーションと呼ぶのが正しいのかな?

スロークエリを調べる

コンソールからmongoシェルに入って以下を実行すればスロークエリのロギングが始まります。

db.setProfilingLevel(1,20)

これを実行すると20ms以上掛かってるクエリがsystem.profileというcollectionに保存されて、普通のcollectionと同じくfindで確認することができます。

system.profile.find();

setProfillingLevelの第一引数は出力する内容のレベルを指定するものです。 0がプロファイリングを行わない(ログを何も記録しない)、1がスロークエリの記録、2が全てのクエリの記録です。

第二引数はslowmsで、スロークエリとして扱う閾値をミリ秒で指定をします。

実際に使うときには、1か2で必要なだけ記録して、必要がなくなったら0を指定して元に戻す感じですね。

コンソールからさくっとプロファイリングして、実際のスロークエリをcollectionの一つとしてすぐに見えるようになるのはすばらしいですね。

setProfillingLevelに関する公式ドキュメントはこちら

で、スロークエリはわかったので、MySQLとかと同じくexplainをします。

db.colleciton.find({遅い条件}).explain();

結果の項目の見方は公式ドキュメントを参照のこと。

クエリの最適化

explainの結果、私の場合、インデックスが意図したものが使われていないことがわかりました。 MySQLのforce index的なことをやるには、hintを使います。

db.collection.find().hint({_id: 1});

descでensureIndexをしている場合は、1の代わりに-1を使います。 存在しないインデックスを指定するとエラーになります。

地味にわからなかったのが複合インデックスの場合の指定の方法です。 例えば、tags: 1, updatedAt: -1で複合インデックスを作っている場合、以下のように指定します。

db.collection.find().hint({tags: 1, updatedAt: -1});

インデックスを作ったときの順序と値で指定する必要があるので注意がいります。

自分が作ったインデックスを確認するときは以下です。

db.collection.getIndexes();

結果

というところで、意図したインデックスを使ってみたのですが、私の場合むしろhintを付けるよりも遅くなるという事態に。

原因としては、普通に大小比較とソートが別々のフィールドを指定しているというよくあるインデックスが効果的に使われないパターンでした。

面白いのは、絞り込みの条件にインデックスを使うよりも、ソートの方にインデックスが使われるようにした方が速度が向上したことでした。

データ件数的には40万件ぐらいだったのですが、条件によっては絞り込まれる数が多すぎてその後のソートに時間が掛かっていたようで、むしろソートで使うフィールドだけをhintでインデックスを使った方が早くなりました。外から見た挙動から察するに多分ソートをした後に絞り込みを行っているんじゃないかなあ。

ということで、一旦hintを使ってソートにのみインデックスを使うようにして、後日データ構造を見直して大小比較を条件に使わないように更新を行いました。

今回もそうですが、基本的にはインデックスはb-treeで実装されているので、MongoDBもMySQLも遅くなる原因が似通っていて、対応も似たり寄ったりなので、MySQLの知識が使えることが多い気がしますね。

New Relicが思った以上に便利だった件

趣味で作ったサービスをNodejitsuで動かしているのですが、ぼちぼち監視を入れたいなーと思っていたので、前々から興味が会ったNew Relicを入れてみました。

Nodejitsu自体はPaaSでサーバの監視はできないので、Webアプリケーションの監視を試しました。

何かコードに色々埋め込んだりする必要があるのかなー、と思い込んでいたのですが、想像以上に簡単に導入することがわかってびっくりしました。アカウントを登録した後に、"Get started with Web App Monitoring"のステップを順番に実行すれば5分もあれば作業が完了しました。

"Reveal your licence key"を押してLicense keyをコピーして、

cd APP_DIR/
# newrelic moduleをpackage.jsonに追加
npm install newrelic --save
# 設定ファイルをコピペ
cp node_modules/newrelic/newrelic.js .
vi newrelic.js
# app_nameを自分のアプリ名に、license_keyをさっきコピーした値に変更して、
vi app.js
# 最後にrequireを追加して
+ require('newrelic');
# デプロイすれば完了
jitsu deploy

で、数分待つとログの集計結果がNew Relicのサイトから見られるようになりました。

これだけで、WebアプリだけじゃなくてDB(私の場合はMongoDB)のreponse timeやthoughputのグラフが見られるようになったり、メールアドレスを登録すれば、アラートのメールが受け取れるようになりました。

サーバのパフォーマンスの監視を自前でやって色々面倒くさかった経験があったので、この立ち上がりの早さでいい感じのアウトプットが出てきて圧倒されました。

台数増やしてちゃんと使おうとすると結構お高いんですが、スタートアップ向けのプランがあったり、aws利用者の場合以下から登録するとStandard planがfreeで使えたりするので、久しぶりにこれはすごい!と思わされたサービスでした。

http://newrelic.com/aws

まだ機能の全体を把握できてないので、その辺を調べつつ、次はEC2(CloudWatch)の監視を試そうかなと思ってます。

久しぶりのはてな

はてブのホットエントリなどはチェックしてたけれど、最近環境が変わって調べ物が増えてきたので、またメモがてら技術ブログを再開することにしてみた。

せっかくなのでダイアリーじゃなくてブログの方を試してみる。