Skip to content

Instantly share code, notes, and snippets.

@nulltask
Last active September 13, 2020 09:08
Show Gist options
  • Select an option

  • Save nulltask/89e6f36e194c951697a0 to your computer and use it in GitHub Desktop.

Select an option

Save nulltask/89e6f36e194c951697a0 to your computer and use it in GitHub Desktop.

Revisions

  1. nulltask revised this gist Jan 16, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 20140826.md
    Original file line number Diff line number Diff line change
    @@ -223,7 +223,7 @@ io.adapter(redis('redis.host:6379'));
    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('/nsp').emit('broadcast', 'broadcasting to namespace');
    emitter.of(socketId).emit('greeting', 'Hello, ' + socketId + '!');
    ```

  2. nulltask revised this gist Jan 12, 2015. No changes.
  3. nulltask revised this gist Aug 26, 2014. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion 20140826.md
    Original file line number Diff line number Diff line change
    @@ -262,6 +262,7 @@ io.httpServer.getConnection(function(err, count) {
    - require('socket.io-client');
    - Node.js でクライアントを大量生成
    - `http.globalAgent.maxSockets` で同時接続数を上げておく
    - クライアントの動作をシミュレートする
    - Browserify でクライアントとコードを共有できるかも
    @@ -285,4 +286,4 @@ iframe.src = '/client.html';
    - Cluster, Nginx, ELB をうまく使う
    - プロセスは機能毎に思い切って分割する
    - 一つのアプリで多くのことをしすぎない
    - 経験上 Socket.IO と Express は密結合にならないほうが吉
    - 経験上 Socket.IO と Express は密結合にならないほうが吉
  4. nulltask revised this gist Aug 26, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 20140826.md
    Original file line number Diff line number Diff line change
    @@ -127,7 +127,7 @@ CPU 使用率との戦いになるため、大規模になる場合は Web ア
    アプリケーションは Engine.IO と Socket.IO の関係性を真似るといいかも。
    Socket.IO のクライアントを包含するようなクラスを作ってビジネスロジックを記述する。

    クライアントサイドのコードは、DOM に依存するレイヤーを分けておくと、ベンチマークテスト時に流用しやすくなる
    クライアントサイドのコードは、DOM に依存するレイヤーを分けておくと、Node.js 側で socket.io-client を使ってベンチマークテストする時にコードが流用しやすくなる


    ## 同時接続数の上限緩和
  5. nulltask revised this gist Aug 26, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 20140826.md
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    # Express / Socket.IO をスケールアウトしてみよう

    - Seiya Konno
    - Works at Uniba Inc.
    - Works at Uniba Inc. (http://uniba.jp)

    ![](https://pbs.twimg.com/profile_images/378800000408779760/0a2ddc361da5f31dae5133130e9dc1e9_200x200.png)

  6. nulltask revised this gist Aug 26, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 20140826.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # Express / Socket.IO をスケールアウトしてみよう

    - nulltask
    - Seiya Konno
    - Works at Uniba Inc.

    ![](https://pbs.twimg.com/profile_images/378800000408779760/0a2ddc361da5f31dae5133130e9dc1e9_200x200.png)
  7. nulltask revised this gist Aug 26, 2014. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions 20140826.md
    Original file line number Diff line number Diff line change
    @@ -204,6 +204,7 @@ Redis の Pub/Sub を使って複数プロセスを協調するようにする
    - https://github.com/Automattic/socket.io-redis
    - 別プロセスに接続されているクライアントに対してメッセージを送信
    - 特定のプロセスからのブロードキャストをプロセス全体に通知
    - Emitter を使って非 Socket.IO プロセスからの通知をハンドリング出来る

    ```javascript
    var io = require('socket.io')();
  8. nulltask renamed this gist Aug 26, 2014. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  9. nulltask created this gist Aug 26, 2014.
    287 changes: 287 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,287 @@
    # Express / Socket.IO をスケールアウトしてみよう

    - nulltask
    - Works at Uniba Inc.

    ![](https://pbs.twimg.com/profile_images/378800000408779760/0a2ddc361da5f31dae5133130e9dc1e9_200x200.png)

    - https://twitter.com/nulltask
    - https://github.com/nulltask
    - https://fb.me/nulltask

    # スケーラビリティとは

    システムの規模に依らず機能を適応できること

    - リクエストに対するスケーラビリティ
    - アプリケーションコードに対するスケーラビリティ

    # Express

    - https://github.com/strongloop/express

    言わずと知れたウェブアプリケーションフレームワーク

    - 右も左もわからなかった頃 => app.js の肥大化
    - メンテナビリティの低下
    - アプリの規模が大きくなってもメンテナビリティを確保したい

    ## Mounting Sub Application

    - Express のアプリを `use` でマウントする機構
    - Rails Mountable Engine のような機能
    - マトリョーシカみたい

    ```javascript
    var app = express();
    var sub = express();

    sub.get('/hello', function(req, res) {
    res.send('this is /sub/hello');
    });

    app.use('/sub', sub); // /sub 下にマウント
    ```

    ## express.Router();

    - ルーティングそのものをモジュール化
    - アプリのマウントより粒度を細かく出来る
    - Express 4.0 以降の新機能

    ```javascript
    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);
    ```

    ## Cluster

    - よく言われること
    - 「Node.js はシングルスレッドだから CPU 使い切れない...」
    - 「Cluster でマルチプロセス化すれば性能向上できるよ」

    _boot.js_:
    ```javascript
    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_:
    ```javascript
    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);
    ```

    ## Benchmarking

    - JMeter!
    - 詳しくは Google で検索

    ELB でバランシングする際、Node サーバに Nginx を設置しないほうがオーバーヘッドが少なく Node のプロセスを直接バランシングしたほうが高速だった。

    # Socket.IO

    - https://github.com/Automattic/socket.io

    言わずと知れたリアルタイムウェブアプリケーションフレームワーク

    - 接続数
    - CPU リソース
    - Socket.IO / Engine.IO 自体のコスト
    - ハンドシェイキング
    - 接続クライアントの管理など
    - アプリケーションコードのコスト
    - ブロードキャストの頻度など
    - 要件次第で変動

    CPU 使用率との戦いになるため、大規模になる場合は Web アプリと一緒にせず、分けたほうが吉。

    アプリケーションは Engine.IO と Socket.IO の関係性を真似るといいかも。
    Socket.IO のクライアントを包含するようなクラスを作ってビジネスロジックを記述する。

    クライアントサイドのコードは、DOM に依存するレイヤーを分けておくと、ベンチマークテスト時に流用しやすくなる。


    ## 同時接続数の上限緩和

    オープンできるファイル数 (ソケット数) の上限が設定されているので緩和する

    _/etc/security/limits.conf_:
    ```
    * soft nofile 65536
    * hard nofile 65536
    ```

    これだけではデーモンプロセスには適応されない

    _/etc/sysconfig/init_:
    ```
    ulimit -n 65536
    ```

    設定を変更したら一旦再起動。再起動後に `/proc/${PID}/limits` で上限が緩和されているか確認しましょう

    ## Socket.IO + ELB の制約

    Socket.IO と組み合わせた時の ELB の悲しい制約

    - HTTP モードの場合
    - HTTP ヘッダが書き換えられ HTTP Upgrade 出来ない
    - WebSocket が使えないため XHR polling が強制される
    - TCP モードの場合
    - Sticky Session が使えない
    - 接続するたび別のサーバが応答するため Socket.IO のハンドシェイキングに失敗する

    ELB を使わず複数台をバランシングする場合、Socket.IO 接続前に、どの Nginx ホストに接続するかをサーバに問い合わせて、もらったホストに接続するように知る。

    ## 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;
    }
    }
    ```

    ## Socket.IO Redis Adapter

    Redis の Pub/Sub を使って複数プロセスを協調するようにする

    - https://github.com/Automattic/socket.io-redis
    - 別プロセスに接続されているクライアントに対してメッセージを送信
    - 特定のプロセスからのブロードキャストをプロセス全体に通知

    ```javascript
    var io = require('socket.io')();
    var redis = require('socket.io-redis');

    io.adapter(redis('redis.host:6379'));
    ```

    ## Socket.IO Emitter

    別の Node プロセスからメッセージを送信する。(Redis Adapter に依存。)

    - https://github.com/Automattic/socket.io-emitter

    ```javascript
    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 for Ruby

    Socket.IO Emitter の Ruby 版。別の Ruby プロセスからメッセージを送信する。(Redis Adapter に依存。)

    - https://github.com/nulltask/socket.io-ruby-emitter

    _Gemfile_:
    ```ruby
    gem 'redis'
    gem 'socket.io-emitter'
    ```

    _broadcast.rb_:
    ```ruby
    emitter = Emitter.new(Redis.new)
    emitter.emit('greeting', 'hello from ruby');
    emitter.of('/nsp').emit('greeting', 'hello from ruby');
    ```

    ## 接続数を知る

    ```javascript
    var io = require('socket.io')();

    Object.keys(io.sockets.sockets).length;

    io.httpServer.getConnection(function(err, count) {
    console.log(count);
    });
    ```

    ## Benchmarking

    - require('socket.io-client');
    - Node.js でクライアントを大量生成
    - クライアントの動作をシミュレートする
    - Browserify でクライアントとコードを共有できるかも


    - Multiple IFRAME
    - クライアントと同じ振る舞いをするページを IFRAME で大量生成
    - 親ページから postMesage で IFRAME を操る
    - 同一ホストへの同時接続数が設定できる Firefox 限定
    - `about:config` => `network.http.max-persistent-connections-per-server`
    - Chrome は同時接続数を増やせないっぽい

    ```javascript
    var iframe = document.createElement('iframe');
    iframe.src = '/client.html';
    ```

    接続数と CPU・ネットワーク負荷を確認しながら 1 サーバ、1 プロセスあたりの接続上限数を見極める。

    ## まとめ

    - Cluster, Nginx, ELB をうまく使う
    - プロセスは機能毎に思い切って分割する
    - 一つのアプリで多くのことをしすぎない
    - 経験上 Socket.IO と Express は密結合にならないほうが吉