node.js + mongodbで気をつけたほうがいいこと

今まで主にクライアントサイドのプログラミングをしていたのですが新規サービスでサーバサイドを任されたため、その時に多少ノウハウがあったnodeとnode使うならmongodbだろと安直な考えでサービスを作成して2ヶ月半が経った。備忘録も兼ねて気をつけたほうが良かったと思うことをまとめておこうかと思う。

ちなみに以下のサービス。android版もリリースされたので良かったら使ってみてね♡

タップル誕生 – https://tapple.me

mongodbでindex設計は超重要

サービス初期段階ではデータ数が少なくて問題なかったがデータが多くなるにつれてindexがついていないクエリがどんどん重くなった。100万レコードを超えたあたりからindexを使わずに引いてしまうとかなり遅くなってしまう。しかし安直にindexを貼ってしまうとupdate/insertで時間がかかってしまう。なので、indexは極力貼らずに_idのみで引っ張ってこれるようにデータをいろいろな場所に置いておくのが一番良いと今のところ思っている。具体的にどういうことかというと、userが記事を投稿するとuserは記事idを、記事はuseridを持つように設計する。こうすることで、userが投稿した記事一覧を取得するときにはユーザが持っている記事idを$inでmongodbに渡してあげればkey検索となり激速で取得することが出来る。ソート等も$inでid一覧を渡してあげればそこまで性能劣化は起こらなかった。当たり前っちゃー当たり前の話。

大量のデータを書き込む場合は小分けにする

mongodbのクソ仕様としてwrite lock中はすべての処理が止まるというのがある。あるcollectionにデータを書込していれば他のcollectionのreadだろうがなんだろうがwriteが終わるまで待たされる。なんでそんな設計なのか小一時間説教したいが仕様なので置いとくとして、この動作のせいで大量のデータを数msで書き込もうとするとwrite lockが雪だるま式に膨らみmongodbが固まることになってしまう。今のサービスでは数千件のレコードを更新する処理を一気に流していたため、その処理が走ると数分サービスの全てのAPIが待たされるという事になってしまった。対策としては単純で分割してゆっくり書き込むようにする。write/readで一過性がないと問題がある処理は除くとしてそれ以外の処理は書き込みは遅いものと割り切ってしまいゆっくりと書き込んでいくほうが全体的なパフォーマンスは上がる。

nodeで重い処理はしない

nodeはsigle threadのイベント駆動で動いている。なので、1つのeventで重い処理をしてしまうと他の処理がすべて止まる。数千件のデータをforでsortとかしてしまうとその処理がnodeを専有してしまうことになり、その後に控えている軽い処理も全て遅くなる。なのでnodeで重い処理はやってはいけない。やるならasyncモジュール等を使用してeventを阻害しない程度の小さなループを大量に行うようにすればいい。そうすると処理と処理の切れ目で他の軽い処理が走るので全体的に見るとパフォーマンスは上がることになる。

まとめ

こうやって書いていくとあたりまえのことが多い。けど、最初の舵がずれているとどんどんずれていって軌道修正するのが時間が経てば経つほど難しくなるので最初から気をつけておくといいのかと思う。

node-validatorの使い方が変わってるから注意しなはれやっ!

「node validator」で検索すると↓が引っかかるけど、これバージョン2系の話です。3系だと違うんで注意が必要です。

http://codedehitokoto.blogspot.jp/2012/04/node-validator.html

2系だとcheck関数にcheckする値を渡して、関数でValidatorして、引っかかったらThrowされるのでそれを拾って処理する

	var check = require('validator').check;
	try {
		check(text).notEmpty();
	} catch(e) {
		console.log(e.message);
	}

3系だとvalidatorに関数があるので関数にValidateしたい値を渡してtrue/falseが返答される

	var validator = require('validator');
	if(!validator.isNull(text)) console.log('this is error message!');

関数は↓のリンクに詳細が記載されているので確認を。

https://github.com/chriso/validator.js

2系で作ってて3系に上がるとtry catchをifに変更しないといけないのが死ぬほどだるいので中止しなはれやっ!

3系の書き方のほうがきめ細やかなエラー処理ができて僕はいいと思うけどね。try catchで拾いたかったらifで判定した後にthrowすればいいんだし。

APIのテストにはSuperTestが便利

node.js + expressでのテスト環境を探していて見つかったんだけど、これは便利

概要

色々なhttpリクエスト投げて、結果をMochaで表示できる。

リンク先

https://github.com/visionmedia/supertest

http://visionmedia.github.io/mocha/

導入

両方共グローバルにしてもいいかも。SuperTestはグローバルにする必要もないかと思ってしてない。

npm install supertest npm install -g mocha

使い方

以下のテストコードを走らせるとhttp://localhost:3000につないでrequest.get()などで接続したホストに対してリクエストを投げて、レスポンスをテストすることができる。

/**
 * テスト雛形
 * hostにつないで、各メソッド事のテストを書く
 * @author sato_shinichiro
 */ 
var request = require('supertest');
var host = 'http://localhost:3000';

describe('テスト種別1', function () {

	before(function() {
		console.log('テスト前に動く');
	});

	after(function() {
		console.log('テスト後に動く');
	});

	it('テストする内容(ステータスエラー)', function (done) {
		var status = 300;
		var res = {
			meta: {
				code: 200,
				message: "ok"
			},
			data: null
		};
		request(host)
			.get('/test')
			.expect(status, res, function(err) {
				console.log('テスト1終了');
				done(err);
			});
	});
	it('テストする内容(レスポンス内容エラー)', function (done) {
		var status = 200;
		var res = {
			meta: {
				code: 300,
				message: "ok"
			},
			data: null
		};
		request(host)
			.get('/test')
			.expect(status, res, function(err) {
				console.log('テスト2終了');
				done(err);
			});
	});
});

describe('テスト種別2', function () {
	it('テストする内容(成功)', function (done) {
		var status = 200;
		var res = {
			meta: {
				code: 200,
				message: "ok"
			},
			data: null
		};
		request(host)
			.get('/test')
			.expect(status, res, function(err) {
				console.log('テスト3終了');
				done(err);
			});
	});
});

テストを走らせるには以下のコマンド。./testにテスト用のjsがいる。mochaはフォルダを指定すると配下のjsファイルを実行してくれる。

mocha test

テスト結果はこんな感じ。

 テスト前に動く
テスト1終了
․テスト2終了
․テスト後に動く
テスト3終了
․

  ✖ 2 of 3 tests failed:

  1) テスト種別1 テストする内容(ステータスエラー):
     Error: expected 300 "Multiple Choices", got 200 "OK"
      stacktrace...

  2) テスト種別1 テストする内容(レスポンス内容エラー):
      
      actual expected
      
      1 | {
      2 |   "meta": {
      3 |     "code": 300200,
      4 |     "message": "ok"
      5 |   },
      6 |   "data": null
      7 | }

      stacktrace...

stacktraceって書いてる部分はパスが書いてあるから削った。本来はちゃんとstacktraceが出る。

もし、サーバを立てずにexpressだけで動かしたかったら以下の様なコードを書いてexportしたappをhostの代わりに指定してあげれば動く

var express = require('express');
var app = express.createServer();
exports.module = app;

まとめ

最初Express用のテストフレームワークかと思ったけど、一般的なサーバのAPIならこれで全て叩いてテストできるんじゃなかろうか。SuperTestは便利。

mongooseで配列を連想配列でネストした時につく_idを消したい

悩んだので覚書

ユーザが書いたコメントを参照として引っ張ってきて欲しくて、それと同時に記載した時間も取りたいっていう場合を想定。

単純にスキーマを書くとこうなる。

var userSchema = {
	name: String,
	comments : [{
		comment: {type: Number, ref: 'comment'},
		updated: {type: Date, default: Date.now}
	}]
};

そうするとなぜか、commentsの要素たちに_idが勝手に入ってしまう。

非常に_idが邪魔

var userSchema = {
	name: String,
	comments : [Schema.Types.Mixed]
};

こう書くとと、commentsに_idは入らないのでmongooseが勝手に挿入してるっぽい。

調べるとどうも連想配列を新しいスキーマとして認識してるから_idが勝手に入るよう。

なので、スキーマとして_idを消すような処理を入れる

var userSchema = {
	name: String,
	comments : [new Schema({
		comment: {type: Schema.Types.ObjectId, ref: 'comment'},
		updated: {type: Date, default: Date.now}
	}, {_id: false})]
};

こうすると、_idが消える。めでたしめでたし。

node.js + expressを調べてみた

触ってみたかったnode.jsを少しだけ触ってみた。

学んだことを箇条書きに

  • exports.hogehogeで設定したものはrequire()で変数(hugahuga)に指定してhugahuga.hogehogeみたいに使える
  • require()で指定するファイルは拡張子(.js)を省略できる。フォルダ名で指定した場合はindex.jsをdefaultで取得する模様
  • app.get()などでは正規表現でマッチするかを判別できる。数字だけならこんな感じ([0-9]+)
  • res.renderも同じように拡張子とか指定できる。
  • res.renderで取得するフォルダのデフォルトはapp.set(‘views’, hogehoge)で指定された場所
  • viewに指定するEngineはejs使ってるサンプルが多いけど僕はjadeが好き。

ぐらい。もうちょっと勉強が必要。