Tkrzw-RPCの基本機能を実装したので、バージョン0.5.0をリリースした。簡易的な性能測定をして、gRPCのオーバーヘッドについて考えてみる。
gRPCの性能測定をしよう。典型的なユースケースを再現すべく、データベースは200万バケットのHashDBMを用いる。よって、以下のようにサーバを立てる。サーバとクライアントは同一のマシンで動かして、ネットワーク層には、最速であるUNIXドメインソケットを用いる。
$ tkrzw_server --address "unix:$HOME/tkconn" "casket.tkh#num_buckets=2m"
クライアントは以下のコマンドを実行する。実際にはデータベースにはアクセスせずに、8バイトのデータを送ったらそれをそのまま送り返すEchoというメソッドを実行する。それに加えて、8バイトのキーと8バイトの値を持つレコードを格納するSetメソッドと、個々のレコードを取り出すGetメソッドと、個々のレコードを参照するイテレータ作ってキーと値を取得するIterateメソッドと、レコードを削除するRemoveメソッドの性能を測定する。個々の操作は100万回行う。クライアント側のスレッド数は1、2、4と変えるが、複数スレッドの場合には、スレッド間で同一コネクションを使い回す場合と別々のコネクションを使う場合に分けて測定する。
$ tkrzw_dbm_remote_perf sequence --address "unix:$HOME/tkconn" --threads 1 --iter 1000000 $ tkrzw_dbm_remote_perf sequence --address "unix:$HOME/tkconn" --threads 2 --iter 500000 $ tkrzw_dbm_remote_perf sequence --address "unix:$HOME/tkconn" --threads 2 --iter 500000 -separate $ tkrzw_dbm_remote_perf sequence --address "unix:$HOME/tkconn" --threads 4 --iter 250000 $ tkrzw_dbm_remote_perf sequence --address "unix:$HOME/tkconn" --threads 4 --iter 250000 -separate
結果は以下のようになる。単位はQPS(クエリ毎秒)である。
Echo | Set | Get | Iterate | Remove | |
1スレッド | 22271 | 21113 | 20671 | 13249 | 20161 |
2スレッド(同ソケット) | 27400 | 26804 | 26190 | 15009 | 24942 |
2スレッド(別ソケット) | 28233 | 27060 | 25449 | 16110 | 25232 |
4スレッド(同ソケット) | 31293 | 29761 | 28792 | 13648 | 24885 |
4スレッド(別ソケット) | 31351 | 29690 | 27522 | 16044 | 25362 |
IPv4でも同様の実験を行う。サーバ側、クライアント側とも、アドレス指定を "127.0.0.1:1978" に変えたコマンドを実行する。
Echo | Set | Get | Iterate | Remove | |
1スレッド | 18412 | 17370 | 17862 | 11807 | 17634 |
2スレッド(同ソケット) | 27262 | 22440 | 23176 | 13139 | 22670 |
2スレッド(別ソケット) | 26240 | 21816 | 23236 | 13332 | 22666 |
4スレッド(同ソケット) | 28071 | 25192 | 19548 | 12150 | 20694 |
4スレッド(別ソケット) | 26730 | 22232 | 23214 | 13158 | 19505 |
IPv6でも同様の実験を行う。サーバ側、クライアント側とも、アドレス指定を "[::1]:1978" に変えたコマンドを実行する。
Echo | Set | Get | Iterate | Remove | |
1スレッド | 18108 | 16754 | 17019 | 11577 | 16648 |
2スレッド(同ソケット) | 27370 | 22008 | 23601 | 12574 | 23177 |
2スレッド(別ソケット) | 27372 | 22667 | 23515 | 12148 | 22451 |
4スレッド(同ソケット) | 25661 | 22147 | 20092 | 11185 | 19207 |
4スレッド(別ソケット) | 25576 | 22341 | 19496 | 13052 | 20867 |
結果を考察しよう。まず、Echoの結果から、ネットワーク層だけの性能を比較すると、UNIXドメインソケットの方がIPv4やIPv6より速いことが分かる。1スレッドの結果から言えば、UNIXドメインソケットのラウンドトリップはIPv4やIPv6よりも20%ほど高速だと言えそうだ。ラウンドトリップのレイテンシは、UNIXドメインソケットで44マイクロ秒、TCP/IPで54マイクロ秒ということになろうか。スレッド数を増やすと、スループットの差は小さくなるが、それでも視認できる差は存在する。よって、ローカルで完結するサービスの場合にはUNIXドメインソケットを使うのが有用と言えそうだ。IPv4とIPv6の差は誤差以上のものは見られないので、どちらを使ってもよいだろう。
EchoとSet/Get/Removeを比べると、当然、Echoの方が速い。この差がデータベース処理によるオーバーヘッドと言える。ローカルの性能測定結果から考えると、データベース操作のオーバーヘッドはもっと小さいことを予想していたが、意外にも視認できる差が出た。とはいえ、1スレッドだとSetのスループットはEchoのそれの95%であり、RPCのオーバーヘッドの方が支配的ではある。同じ理由で、SetとGetとRemoveの差は小さい。Iterateが他に比べて遅いのは、ストリームAPIの中で、JumpとGetという2つのメソッドを呼んでいるからである。
スレッド数を増やすと、どの実験でも微妙にだけスループットが向上する。サーバとクライアントが同じマシンで動いているので、クライアントのスレッド数を増やすとサーバにとっての負荷も上がってしまうのが、微増しかしない理由の一つだろう。実際のサーバの限界スループットを引き出すには、クライアント用のマシンを多数用意しなければならないが、それについては追って考える。
スレッド間で同じコネクション(RemoteDBMオブジェクト)を使い回すのと、スレッド毎に異なるコネクションを使うのとで差が出るかということだが、4スレッド程度だと有意な差は見られなかった。全体のコネクションの数が増えればサーバ側の負荷も増えるため、クライアント側ではできるだけコネクションを使い回す方がよいだろう。
今回使ったAPIは、最も単純である、同期かつユナリーな実装である。それでも2万QPSくらいでデータベース操作ができることは分かった。これが早いと捉えるか遅いと捉えるかはユースケース次第だが、個人的には遅いと捉えている。ローカルでは400万QPSが出せるデータベースなのに、リモートだと2万QPSしか出せていないというのは勿体無い。実験方法がしょぼいというのが遅い理由の一つではあるが、APIの設計も原因の一つである。gRPCのPerformance Best Practicesに述べてあるように、より高いスループットを得るには、非同期やストリームの方式のAPIを検討する価値があるだろう。
まとめ。Tkrzw-RPCの簡易的な性能測定をして、同一マシン上なら2万QPSくらいのスループットが出せることが分かった。これをベースラインにして、今後、より高いスループットを出す方法について検討していくことになる。