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;
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);
}
実験のコードは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);
}
で、サーバを再起動。そして結果はこちら。
⋊> ~/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);
}
結果はこちら。
⋊> ~/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を表に立たせる方式を書いてあったので、その方式でも実験してみたいと思います。