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を表に立たせる方式を書いてあったので、その方式でも実験してみたいと思います。