Node.jsで(なるべく)落ちないアプリを作ろう。

さてさて、第三回東京Node学園の自分の発表の時を見ていた人にはわかっていると思いますが、自分の発表の時に見事に発表用のアプリが見事に落ちてしまいました。

悔しかったので、NodeNinjaさんに頼んでログの調査方法を教えてもらい、調査してみました。
その結果、以下のことがわかりました。

あの時落ちた理由は、単純な問題で、socket.ioからもらった値を未検証で利用していたため、
値にnullが入った事によるNullPointer例外でした。

聴講者の誰かがJavascriptのコンソールからおそらく下記のように実行したのでしょう。

socket.emit('count up', null, null);

こうやると、nullがemitされ、サーバー側ではnullが来ることを期待していなかったので
落ちたという、新人プログラマーにありがちなミスでした。

今回の話はいくつかの教訓と実践Tipsを交えて共に振り返ろうと思います。

教訓1 検証はクライアントサイド、サーバーサイドで共にチェックするべし。

クライアント、サーバーどちらかで検証すれば良い、というものではなく、両方で検証しないと行けません。

そしてサーバーサイドJavaScriptの大きな利点の一つはクライアントとサーバーサイドで同じ実装で検証の機能を作れることです。

Jxckさんの記事にNode.jsでクライアント、サーバーサイド両方で
動作するJavascriptの書き方が載っているのでそこを参考にして、必ずチェックするようにしましょう。

自分が簡単に作ったvalidatorを紹介します。
check_args.js

//付箋の位置チェック。xとyの座標を検証。
function validate_position(x, y) {
  if (x && y && typeof x == "number" && typeof y == "number") {
    return x >= 0 && x <= 850 && y>=0 && y<= 680;
  } else {
    return false;
  }
}

//スライドIDがnullじゃない事をチェック。
function validate_slideId(data) {
  if (data && data.slideId) {
    return true;
  } else {
    return false;
  }
}

//スライドページが数字データであること、数字マイナスが入っていないことをチェック。
function validate_slideNo(data) {
  if (data && typeof data.slideno == "number") {
    return data.slideno >= 0;
  } else {
    return false;
  }  
}

//メッセージがnullじゃないこと、(本当はメッセージバイト数を制限したほうが良い。)
function validate_message(data) {
  return data && data.message;
}

//メッセージのIDがnullじゃないこと
function validate_id(data) {
  return data && data.id;
}

//ここがポイント。requireで呼んでも、ブラウザから呼んでも同じく読み込まれる。
this['validate_position'] = validate_position;
this['validate_slideId'] = validate_slideId;
this['validate_slideNo'] = validate_slideNo;
this['validate_message'] = validate_message;
this['validate_id'] = validate_id;

上に記載されていますが、下記のように記載すればnodeからrequireで呼んでもブラウザから呼んでも
等しく使えます。

this['validate_position'] = validate_position;
this['validate_slideId'] = validate_slideId;
this['validate_slideNo'] = validate_slideNo;
this['validate_message'] = validate_message;
this['validate_id'] = validate_id;

もちろん、これだけで十分だと思ってはダメで、サーバーサイド側はデータベースに格納されているデータと不整合がないか等も
検証する必要があります。

教訓2 Socket.ioのイベント名は分からないものにし、クライアントサイドはなるべく難読化しておくほうが良い。


これも同じくJxckさんの記事に書かれています。
イベント名を分からないものにしておく方が何が発生するのか分からないため、ある程度驚異は減ります。

一応難読化もしておけば、ソースから何が発生するのかを推測することも難しくなるはずです。

PHPSPOT日誌さんの記事で難読化するツールが紹介されているので、難読化も簡単にできると思います。

教訓3 万が一落ちた時の復旧策をよく考えていれるべし。


どんなに施策を施したとしても落ちるときは落ちます。
検証漏れもあるでしょうし、単純にバグを埋め込んじゃうこともあると思います。

Node.js v0.6から追加されたclusterを利用すれば、ワーカープロセスが落ちたときに再復帰することで万が一落ちても
サービスを継続させることができます。(マスタープロセスが生きていればですが。)

以下のような要領で実施すれば可能です。

var  cluster = require('cluster');

var numProcs = 1;

if (cluster.isMaster) {
  // Fork workers.
  console.log('num of worker process = ' +  numProcs);
  for (var i = 0; i < numProcs; i++) {
    cluster.fork();
  }
  // worker プロセスが死んだ時
  cluster.on('death', function(worker) {
    console.log('worker ' + worker.pid + ' died');
    // 再実行
    cluster.fork();
  });
} else {
//worker process
}

ただし、これは万能策ではなく、以下のような副作用もあるので気をつけてください。


  1. 不具合に気づきにくくなる。

  2. リソース不足で死んだ場合も再起動してしまうため、リソースの競合が起きやすくなる。

  3. マスタープロセスが死んだ場合は何もできない。

この対策だけではなく、きちんとエラー処理をすることも併せて実施する必要があります。
※1 Node NinjaはまだV0.5.10なので、上のコードは使えませんが、それは今後を期待しましょう。

※2 あと、workerプロセス数が1つしかないので、clusterによる負荷分散は上のコードではできていません。

まとめ


以下の教訓を紹介しました。
教訓1 検証はクライアントサイド、サーバーサイドで共にチェックするべし。
教訓2 Socket.ioのイベント名は分からないものにし、クライアントサイドはなるべく難読化しておくほうが良い。
教訓3 万が一落ちた時の復旧策をよく考えていれるべし。

Node.jsもセキュアコーディングを心がけていきたいですね。