Node.jsを使ったリアルタイムなエロサイトの作り方

実は、 AV女優.com は裏で Node.js が走っています。
表のウェブサーバは、

で紹介した通り、Nginxです。
では、どこでNode.jsを使っているか。
こっそりと設置された、流行っていない AV女優.com アダルト掲示板 です。

ここのところ、Node.jsはとても人気者ですが、その記事の大半は入門記事です。
今回は、取り上げられることの少ない、 本番環境 でのNode.jsの運用ノウハウをシェアします。

最後には、運用に役立ちそうなリンクもまとめてみます。

AV女優.comのサーバ構成

Node.jsを使って構成を考えるとき、真っ先に悩むのが、 データの保存先とその方法 です。

入門として良く上がる、チャットシステムであれば、データを保存する必要はありません。
リアルタイム・メッセージングを体験するのが目的であるためです。
例えば、次のNode.js入門の記事でもデータは保存していません。

Nodeで作る人工無脳
Node.jsSocket.IO を使ったNode.js入門です。
ステップ・バイ・ステップで書かれており、大変分かりやすい記事です。
しかし、データの保存に関してはノータッチです。

しかし、本番環境で運用する場合、入力されたデータを投げ捨てる訳にはいきません。
そこで、簡単に思いつくのは、Node.jsに データベース を扱わせることです。

node.jsとMySQLで割と普通のデータベースウェブアプリを作ってみるチュートリアル
Node.jsとMySQLを使った ベーシックなウェブ・アプリケーション開発 です。
この記事も、Node.jsを使ったウェブ・アプリケーション開発の入門に最適です。

しかし、これには若干、気になる点があります。

1. データのバリデーションをNode.js側で処理する必要がある
通常のウェブ・アプリケーションであれば、 Node.jsだけで構成されているのは稀 です。
その他にPHPやPythonと言った、ベーシックなサーバ・サイド・スクリプト言語で書かれたアプリケーションも存在するでしょう。
これらのアプリケーションも、データのバリデーション・ロジックを持っています。
これをNode.jsで同じように実装してしまうと、コードがDRYになりません。
2. Node.jsの開発ノウハウが足りない
データの取り扱い は、セキュリティ上、最も気を使うところです。
Node.jsを使い始めたばかりの状態で、ここをNode.js上に実装してしまうのは、少し気が引けます。
慣れない環境で、バグを作り込んでしまいそうです。

そこで、 AV女優.com では、データの保存や読み込みは全て、 CakePHP (AV女優.com はCakePHPで出来ています) に任せることにしました。
Node.jsは リアルタイム・メッセージング だけを担当します。
AV女優.com のサーバ構成です。

/img/posts/010/av_jyo_servers.png

掲示板のメッセージの書き込みは全て、 Node.js を仲介します。
図を見れば分かりますが、流れは次のようになります。

  1. Node.jsがメッセージを受け取ります。
  2. Node.jsは何も加工せず、CakePHPに渡します。
  3. CakePHPがデータのバリデーションを実行し、データベースにメッセージを書き込みます。
  4. 書き込みが成功したら、CakePHPはJSONで結果をNode.jsに渡します。
  5. 受け取った結果を、何も加工せず、Node.jsに接続しているクライアントにブロードキャストします。

この流れを作ることで、データを保存しつつ、リアルタイム・メッセージングを実現出来ます。
しかも、コードは DRY です。
もし、既にメッセージ書き込みのロジックが、 (Node.js側でない) アプリケーションに実装済みなら、Node.jsのコードとクライアントのJavaScriptを少し書くだけで、OKです。
既存のアプリケーションを簡単にリアルタイム対応出来ます。

ウェブ・サーバとNode.js間で通信を行うため、余計なコストが掛かってしまいますが、そこは開発コストとのトレード・オフですね。

Node.jsとSocket.IOを使って、メッセージング・サーバを開発する

実際に、Node.jsのコードから、先ほど紹介した構成を理解します。
AV女優.com で走っているNode.jsは、ほんの 139行のコード で動いています (ちなみにプロジェクト名は、 ohyes です)。
ウェブ・サーバとNode.jsが通信する部分のサンプル・コードを上げておきます:

// モジュールの読み込みと、settings変数を用意する。
var http = require('http'),
    io = require('socket.io'),
    settings = {
        app_host: 'localhost',
        app_port: 80,
    };

// Socket.IOを初期化し、待ち受ける。
var socket = io.listen(app);
// メッセージを受け取ったら、write()関数を呼び出す。
socket.on('connection', function(client) {
    client.on('message', function(data) {
        write(client, data);
    });
});

/**
 * 掲示板への書き込み
 *   client: Socket.IOのclientオブジェクト
 *   data: POSTでNode.jsに渡されたデータ一式
 */
function write(client, data) {
    // dataにはメッセージの他に、ユーザ認証のためのクッキーを入れてある。
    var id = data.topic_id,
        cookie = data.cookie;

    // httpモジュールを使って、ウェブ・サーバと通信する。
    // (queryString()とcookieString()は自作関数)
    var query = queryString({'data': data}),
        options = {
            host: settings.app_host,
            port: settings.app_port,
            path: '/topic/write/' + id,
            method: 'POST',
            headers: {
                'Cookie': cookieString(cookie),
                'Content-Length': query.length,
                'Content-Type': 'application/x-www-form-urlencoded',
                'X-Requested-With': 'XMLHttpRequest',
            },
        };

    // 以下の手順でウェブ・サーバと通信する。
    //   1. http.requestオブジェクト生成(http.request())
    //   2. クエリ書き込み(request.write())
    //   3. 送信(request.end())
    var request = http.request(options, function(response) {
        response.setEncoding('utf8');
        if (response.statusCode != 200) {
            client.send({result: false});
            return;
        }
        response.on('data', function(result) {
            try {
                result = JSON.parse(result);
            } catch(e) {
                result = {result: false};
            }
            client.send(result);
            if (result.result) {
                client.broadcast(result);
            }
        });
    });
    request.write(query);
    request.end();
}

解説用のサンプルなので、このままでは動きません。
雰囲気だけ掴んで頂ければ、結構だと思います。

ここでは、ユーザ認証するために、 セッション を維持する必要があります。
上記のコードでは、data変数にcookieがセットしてあります。
下記のコードをクライアント・サイドに実装することで、クッキーをNode.jsに送ることが出来ます:

var socket = new io.Socket();
socket.on('connect', function() {
    socket.send({ cookie: document.cookie });
});
socket.connect();

Node.js側でなく、既存のウェブ・アプリケーション側にロジックを寄せれば、このレベルのコードでリアルタイム・メッセージングが出来るようになります。
残りの部分は 人口無能のチュートリアル と同じです。
人口無能のチュートリアル を参考に、 Socket.IO を使って、独自に実装してみてください。

Ubuntuに本番環境を用意する

コードは端折りましたが、本番環境の用意も要点を絞って説明します。
例によって、 人口無能のチュートリアル を参考に、 npmNode.js はインストールしておいてください。

Ubuntu 10.04のリポジトリにも、Node.jsのパッケージがあるのですが、バージョン0.2.6と少し古いです。
チュートリアル通り、naveでインストールするのが無難でしょう。
ビルドには、メモリが1Gバイトは必要です。
私は、512Mバイトの仮想マシン上で、メモリ不足でマシンがハングしました .;)

本番環境は、以下のいずれかの記事を参考に準備します。

Run Node.js as a Service on Ubuntu
Ubuntuのupstartスクリプトを使った環境の準備です。
プロセスが落ちた場合も、respawnで自動復帰させます。
Keep a node.js server up with Forever
Node.jsのモジュールであるForeverを使った環境の準備です。
npmでインストール出来るのでお手軽です。

AV女優.com では upstartスクリプト を使ってNode.jsを走らせています。

基本的に、記事の通りやれば本番環境を用意出来ます。
しかし、注意する点が一点あります。
それは、 npmでインストールしたモジュールのパス です。

npmは一般ユーザ権限でインストールすると、 ~/node_modules ./node_modules にインストールされ、モジュールを読み込む場合もここから読み込みます [1]
しかし、これは本番環境では気持ち悪いのです。
本番環境では、システム全体の共通パスにインストールし、それを参照したいものです。

これを実現するには、 root権限-gオプション を付けて、モジュールをインストールします。
そうすると、 /usr/local/lib/node_modules にモジュールがインストールされます。
参照するときはどうするか?
NODE_PATH 環境変数を設定します。
つまり、Ubuntuのupstartスクリプトは次のようになります:

description "ohyes server with Node"
author      "sunomaru"

start on started mountall
stop on shutdown

respawn
respawn limit 99 5

script
    export HOME="/srv/ohyes"
    export NODE_PATH="/usr/local/lib/node_modules"
    export NODE_ENV="development"
    if [ ! -d "/var/log/node" ]; then
        mkdir /var/log/node
    fi
    exec /usr/local/bin/node /srv/ohyes/app.js >> /var/log/node/ohyes.log 2>&1
end script

post-start script
end script

もし、開発環境でも同様にモジュールを共通で使いたい場合、 npmをroot権限で使うことを心がけ、 .bashrc などにNODE_PATHを設定しておくと良いかもしれません。

Node.jsの開発/運用に役立つ記事

最後に、Node.jsを使う上で、参考になる記事をまとめておきます。

本番環境

Nodeにおけるスケールアーキテクチャ考察(Scale編)
Node.jsをスケールするための構成 が、詳しく解説されています。
より実践に近い内容で解説されているため、とても参考になります。
Web開発系のクラウド(SaaS, PaaS, IaaS)まとめ
近年、やたらと数の増えたクラウドサービスに関するまとめです。
Node.jsに対応したPaasが多く紹介されています。
ここに紹介されている 11サービス中、6サービス がNode.jsに対応しています。

テスト

Unit testing in node.js
とても分かりやすい、Node.jsにおける ユニットテストのチュートリアル です。
このチュートリアルをこなせば、Node.jsである程度のテストが書けるようになります。
1〜2時間もあれば終わるので、是非どうぞ。

TIPS

node.jsでhttp sessionを共有するsocket.ioのテストを書く
Node.jsと既存のウェブ・アプリケーション間で セッションを共有する方法 が解説されています。
また、そのテストの仕方もまとめられています。

JavaScript

Learning Advanced JavaScript
チュートリアル形式でJavaScriptが学習出来ます。
その場でコードを修正して走らせる ことができ、JavaScriptへの理解を深めることが出来ます。
JavaScript 第5版
オライリーのJavaScript本です。
私はまだ読んでないのですが、評判はかなり良いです。
JavaScriptの基礎力向上 にどうぞ。
Learning Server-Side JavaScript with Node.js
サーバ・サイドのJavaScript を学ぶための簡単なチュートリアルです。
インストールから、ツイッターのリアルタイム・ストリーミングまで、実際に手を動かしながら学べます。
JavaScriptのいろいろなコーディングルールをまとめてみた
JavaScriptのコーディングルール がまとめられています。
公式ではありませんが、Node.jsのコア・デベロッパの方が書いたNode.jsのコーディング・ルールは こちら

全般

node.jsとWebSocketの利用シーン
AjaxからWebSocketに至るまでの経緯や、WebSocketの活用方法までわかりやすく解説されています。
Node.jsとWebSocketの概要 を知るには、とても良い記事です。
Nodeにおけるスケールアーキテクチャ考察(SSP編)
Node.jsのアーキテクチャ について、細かく考察されています。
Node.jsを本格的に運用するのであれば、必読です。

いかがでしょうか。
今回紹介した構成は、アクセスが増えてきた場合にスケールしにくい構成です。
しかし、 Node.js開発の工数が少なくて済み、コードをDRYにしやすいという点 で優れています。
簡単にリアルタイム・メッセージングを導入出来ます。

AV女優.com が流行り、アクセスが増えてきたら、また構成を考える必要があるでしょう。
そのときは、またこのブログでシェアしていきます。

私自身、まだまだノウハウが少ないので、困っています。
アドバイスなどありましたら、コメント欄までお願いします。

盛り上がる兆しのない AV女優.com アダルト掲示板 にも、何か書き込んでくれると嬉しい限りです。

[1] はてなブックマークのコメントで指摘があったので、修正しました。ドキュメントには、 ./node_modules にインストールされるとあります。私の環境だと、必ず ~/node_modules にインストールされるのですが、何故でしょう?

JavaScript 第5版

ちょっと一言

sunomaru

@sunomaru

前回の@gonzoo48の記事は、はてブをたくさん付けて頂いたようで、感謝しています。このブログは真面目な内容で書いていくので、気兼ねなくシェアしてください(と言っても難しいかもしれませんが...)。