patorashのブログ

方向性はまだない

とりあえずIsolateを使って並列化してパフォーマンスを見た

Dartの強みといえば、非同期処理と並列化の簡単さだと思います(本を読みかじった感じだと)。

そこで、ちょっと気になったのでIsolateでサーバを並列化して、パフォーマンスを見てみることにしました。本に書いてあるけれど、実際にやってみた感じです。まぁ環境は自分はrikulo streamを使っているので、それに置き換えてやってます。

私のマシンはMacBook Pro Late 2013のcore i5でコア数は2ですが仮想コアで考えると4になりますので、4プロセスまで実験できそうです。

wrkを入れる

まずはベンチマークツールのwrkを入れます。全然詳しくないのですが、luaで拡張したりできるとかなんとか。自分の環境はMacなので、Homebrewで入れました。

brew install wrk

main.dartの書き換え

Isolateを使った実装にあたって、以下のスライドを参考にやってみました。

rikulo streamにも、sharedオプションがあったので、それをtrueにしています。また、実験のため、ひとまずIsolateを使うところはコメントアウト

import 'dart:io';
import 'dart:async';
import "dart:convert" show JSON;
import 'dart:isolate';
import "package:stream/stream.dart";
import "package:rikulo_commons/io.dart" show getContentType;

//URI mapping
var _mapping = {
  "/": serverInfo
};

void serverInfo(HttpConnect connect) {
  final info = {"name": "Rikulo Stream", "version": connect.server.version};
  connect.response
    ..headers.contentType = getContentType("json")
    ..write(JSON.encode(info));
}

void addServer(int i) {
  print("Launch server: $i");
  new StreamServer(
    homeDir: 'client',
    uriMapping: _mapping,
  ).start(shared: true);
}

void main() {
  addServer(0);
  // Isolate.spawn(addServer, 1);
  // Isolate.spawn(addServer, 2);
  // Isolate.spawn(addServer, 3);
}

実験のコードはrikuloのチュートリアルでやったやつでjsonを返すコードです。

1プロセスで実験

まずは1プロセスで実験です。以下のコマンドでrikulo streamを起動します。

dart web/webapp/main.dart

そして、wrkでベンチマーク

wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080

1スレッド、256コネクション、30秒で、http://127.0.0.1:8080にアクセスするという設定です。

結果はこちら

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    31.76ms   21.07ms 412.23ms   97.24%
    Req/Sec     8.57k     1.56k    9.56k    86.67%
  255918 requests in 30.03s, 73.46MB read
Requests/sec:   8521.67
Transfer/sec:      2.45MB

1秒あたり8521のリクエストを捌いているようです。

2プロセスで実験

つぎは2プロセス。コメントアウトを外します。

main() {
  addServer(0);
  Isolate.spawn(addServer, 1);
  // Isolate.spawn(addServer, 2);
  // Isolate.spawn(addServer, 3);
}

で、サーバを再起動。そして結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    23.50ms   43.48ms 753.46ms   98.57%
    Req/Sec    13.33k     2.51k   15.23k    93.33%
  398198 requests in 30.06s, 114.31MB read
  Socket errors: connect 0, read 116, write 6, timeout 0
Requests/sec:  13248.59
Transfer/sec:      3.80MB

13248に増えています。1.6倍くらいでしょうか?

3プロセスで実験

つぎは3プロセス。

main() {
  addServer(0);
  Isolate.spawn(addServer, 1);
  Isolate.spawn(addServer, 2);
  // Isolate.spawn(addServer, 3);
}

結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    18.16ms   34.03ms 743.47ms   98.14%
    Req/Sec    17.19k     3.64k   20.26k    92.33%
  513436 requests in 30.05s, 147.38MB read
  Socket errors: connect 0, read 70, write 5, timeout 0
Requests/sec:  17088.10
Transfer/sec:      4.91MB

17088まで増えました!2倍くらいですね。

4プロセスで実験

いよいよ4プロセス。

main() {
  addServer(0);
  Isolate.spawn(addServer, 1);
  Isolate.spawn(addServer, 2);
  Isolate.spawn(addServer, 3);
}

結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    15.78ms   13.58ms 135.88ms   88.92%
    Req/Sec    18.67k     1.99k   21.86k    81.33%
  557971 requests in 30.06s, 160.17MB read
Requests/sec:  18562.11
Transfer/sec:      5.33MB

18562。思ったほど伸びませんでしたが、増えてますね!

もっとプロセス数を増やしたらどうなるのか?

どうせ実験なので、コア数以上にIsolateを増やしてみよう!と思い、やってみました。

5つの結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    24.38ms   32.29ms 537.93ms   87.20%
    Req/Sec    17.16k     4.44k   21.31k    88.26%
  509821 requests in 30.05s, 146.35MB read
  Socket errors: connect 0, read 2, write 0, timeout 0
Requests/sec:  16966.75
Transfer/sec:      4.87MB

なんと、16996に減りました。

そして、6つの結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    22.90ms   34.82ms 595.32ms   88.45%
    Req/Sec    16.56k     4.60k   21.48k    88.51%
  493331 requests in 30.08s, 141.61MB read
  Socket errors: connect 0, read 22, write 10, timeout 0
Requests/sec:  16402.62
Transfer/sec:      4.71MB

16402に減りました。

やはりCPUコア数以上に増やしてもよくなさそうですね。

チャートにしてみる

Online Chart Maker | amChartsを使って、チャートにしてみました。仮想コア数と同じ4プロセスがもっとも良い結果となりました。

Isolateを増やしても伸び悩んだ原因は、Mac上で色々動かしている状態でテストしたからかな〜と思います。wrk自体の動作でも30%程度CPUを使っていたりもしたので。Dockerとか、その他もろもろのアプリを停止してやってみればよかったですね。

自動的にCPUコア数だけIsolateを起動するコード

先のスライドでは、コア数を変数にハードコードしていましたが、英語のDart本をみたところ、Platform.numberOfProcessorsでCPUコア数が取得できるようです。そこで、以下のように書き換えてみました。

main() {
  addServer(0);
  for(int i = 1; i < Platform.numberOfProcessors; i++) {
    Isolate.spawn(addServer, i);
  }
}

これで4コア分のIsolateが動きます。本番環境の場合はこうすることでCPUコアを全て使うことができます。

編集後記

サクッとCPUコア数だけ並列化できるのはすごいなーと思います。Default Isolateとのやりとりが発生しないようにしているので、その辺りをちゃんとやろうと思ったら、もう少し複雑なコードになるとは思いますが、英語のDart本だとDefault IsorateでServerSocketを表に立たせる方式を書いてあったので、その方式でも実験してみたいと思います。