- Seiya Konno
- Works at Uniba Inc. (http://uniba.jp)
システムの規模に依らず機能を適応できること
- リクエストに対するスケーラビリティ
- アプリケーションコードに対するスケーラビリティ
言わずと知れたウェブアプリケーションフレームワーク
- 右も左もわからなかった頃 => app.js の肥大化
- メンテナビリティの低下
- アプリの規模が大きくなってもメンテナビリティを確保したい
- Express のアプリを
use
でマウントする機構 - Rails Mountable Engine のような機能
- マトリョーシカみたい
var app = express();
var sub = express();
sub.get('/hello', function(req, res) {
res.send('this is /sub/hello');
});
app.use('/sub', sub); // /sub 下にマウント
- ルーティングそのものをモジュール化
- アプリのマウントより粒度を細かく出来る
- Express 4.0 以降の新機能
var app = express();
var users = express.Router();
users.use(fn); // 特定の router に対する middleware
users.get('/', fn);
users.get('/:id', fn);
users.post('/', fn);
users.put('/:id', fn);
users.del('/:id', fn);
app.use('/users', users);
- よく言われること
- 「Node.js はシングルスレッドだから CPU 使い切れない...」
- 「Cluster でマルチプロセス化すれば性能向上できるよ」
boot.js:
var cluster = require('cluster');
module.exports = function(child, num) {
return cluster.isMaster() ? master(num) : child();
};
function master(num) {
num = num || require('os').cpus().length;
cluster.on('exit', function(worker, code, signal) {
cluster.fork();
});
for (var i = 0; i < num; ++i) cluster.fork();
};
server.js:
var app = require('./app');
// var io = require('./io');
var boot = requier('./boot');
boot(function() {
var server = app.listen(3000, function() {
// io.attach(server);
});
}, require('os').cpus().length);
- JMeter!
- 詳しくは Google で検索
ELB でバランシングする際、Node サーバに Nginx を設置しないほうがオーバーヘッドが少なく Node のプロセスを直接バランシングしたほうが高速だった。
言わずと知れたリアルタイムウェブアプリケーションフレームワーク
- 接続数
- CPU リソース
- Socket.IO / Engine.IO 自体のコスト
- ハンドシェイキング
- 接続クライアントの管理など
- アプリケーションコードのコスト
- ブロードキャストの頻度など
- 要件次第で変動
- Socket.IO / Engine.IO 自体のコスト
CPU 使用率との戦いになるため、大規模になる場合は Web アプリと一緒にせず、分けたほうが吉。
アプリケーションは Engine.IO と Socket.IO の関係性を真似るといいかも。 Socket.IO のクライアントを包含するようなクラスを作ってビジネスロジックを記述する。
クライアントサイドのコードは、DOM に依存するレイヤーを分けておくと、Node.js 側で socket.io-client を使ってベンチマークテストする時にコードが流用しやすくなる。
オープンできるファイル数 (ソケット数) の上限が設定されているので緩和する
/etc/security/limits.conf:
* soft nofile 65536
* hard nofile 65536
これだけではデーモンプロセスには適応されない
/etc/sysconfig/init:
ulimit -n 65536
設定を変更したら一旦再起動。再起動後に /proc/${PID}/limits
で上限が緩和されているか確認しましょう
Socket.IO と組み合わせた時の ELB の悲しい制約
- HTTP モードの場合
- HTTP ヘッダが書き換えられ HTTP Upgrade 出来ない
- WebSocket が使えないため XHR polling が強制される
- TCP モードの場合
- Sticky Session が使えない
- 接続するたび別のサーバが応答するため Socket.IO のハンドシェイキングに失敗する
ELB を使わず複数台をバランシングする場合、Socket.IO 接続前に、どの Nginx ホストに接続するかをサーバに問い合わせて、もらったホストに接続するように知る。
コネクション数の設定と、HTTP Upgrade が有効になっていることがキモ。ip_hash
で stickiness を有効にするのを忘れずに。
/etc/nginx/nginx.conf:
events {
worker_connections 32768;
}
worker_rlimit_nofile 65536;
/etc/nginx/conf.d/virtual.conf:
upstream io {
ip_hash; # <= Important!
server 0.0.0.0:3000;
server 0.0.0.0:3001;
server 0.0.0.0:3002;
server 0.0.0.0:3003;
}
server {
listen 80;
location / {
proxy_set_header Upgrade $http_upgrade; # <= Important!
proxy_set_header Connection "upgrade"; # <= Important!
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_pass http://io;
}
}
Redis の Pub/Sub を使って複数プロセスを協調するようにする
- https://github.com/Automattic/socket.io-redis
- 別プロセスに接続されているクライアントに対してメッセージを送信
- 特定のプロセスからのブロードキャストをプロセス全体に通知
- Emitter を使って非 Socket.IO プロセスからの通知をハンドリング出来る
var io = require('socket.io')();
var redis = require('socket.io-redis');
io.adapter(redis('redis.host:6379'));
別の Node プロセスからメッセージを送信する。(Redis Adapter に依存。)
var emitter = require('socket.io-emitter')('redis.host:6379');
emitter.emit('broadcast', 'this is broadcasting'); // broadcasting
emitter.of('/nsp').emit('broadcast', 'broadcasting to namespace');
emitter.of(socketId).emit('greeting', 'Hello, ' + socketId + '!');
Socket.IO Emitter の Ruby 版。別の Ruby プロセスからメッセージを送信する。(Redis Adapter に依存。)
Gemfile:
gem 'redis'
gem 'socket.io-emitter'
broadcast.rb:
emitter = Emitter.new(Redis.new)
emitter.emit('greeting', 'hello from ruby');
emitter.of('/nsp').emit('greeting', 'hello from ruby');
var io = require('socket.io')();
Object.keys(io.sockets.sockets).length;
io.httpServer.getConnection(function(err, count) {
console.log(count);
});
-
require('socket.io-client');
- Node.js でクライアントを大量生成
http.globalAgent.maxSockets
で同時接続数を上げておく- クライアントの動作をシミュレートする
- Browserify でクライアントとコードを共有できるかも
-
Multiple IFRAME
- クライアントと同じ振る舞いをするページを IFRAME で大量生成
- 親ページから postMesage で IFRAME を操る
- 同一ホストへの同時接続数が設定できる Firefox 限定
about:config
=>network.http.max-persistent-connections-per-server
- Chrome は同時接続数を増やせないっぽい
var iframe = document.createElement('iframe');
iframe.src = '/client.html';
接続数と CPU・ネットワーク負荷を確認しながら 1 サーバ、1 プロセスあたりの接続上限数を見極める。
- Cluster, Nginx, ELB をうまく使う
- プロセスは機能毎に思い切って分割する
- 一つのアプリで多くのことをしすぎない
- 経験上 Socket.IO と Express は密結合にならないほうが吉