豪鬼メモ

ピンチはチャンス

gRPCクライアントの各言語の性能比較

Tkrzw-RPCのクライアントがC++、Python、Ruby、Goで出揃ったので、簡単な性能テストをしてみた。以下は1スレッドのスループットをグラフにしたものだ。単位はQPS。
f:id:fridaynight:20211011171320p:plain


サーバ側は、gRPCのC++コアライブラリを使って実装されている。動作環境は i7-8550U 1.80GHz(4コア)のLinux機(ノートPC)だ。最もスループットが出るとされる設定として、非同期APIモードを使い、コア数に合わせて4つのキューを使うようにする。クライアントとサーバが同一マシンで動作するマルチプロセスのユースケースを想定し、ネットワークインターフェイスにはUNIXドメインソケットを用いる。

$ tkrzw_server --address "unix:$HOME/sock" --async --threads 4

クライアント側では、各言語で同じ仕様で実装したパフォーマンス測定用のコマンドを用いる。ソースはこちら(C++、Python、Ruby、Go)をご覧いただきたい。単にメッセージをエコーバックするEchoメソッドと、データベースにレコードを設定するSetメソッドと、データベースのレコードを検索するGetメソッドを、それぞれ10万回実行して、その時間を測り、逆算してスループットを導く。スレッド数は1、2、4と変えて実行する。クライアント側は同期APIを用いている。

C++ : $ tkrzw_dbm_remote_perf sequence --address unix:$HOME/sock --iter 100000 --threads 1
Python : $ perf.py --address unix:$HOME/sock --iter 100000 --threads 1
Ruby : $ perf.rb --address unix:$HOME/sock --iter 100000 --threads 1
Go : $ perf --address unix:$HOME/sock --iter 100000 --threads 1

1スレッドのスループットは以下。

Echo Get Set
C++ 26776 26269 24523
Python 10258 9680 10181
Ruby 11343 10802 10881
Go 18394 17769 17926

2スレッドのスループットは以下。

Echo Get Set
C++ 31165 30527 29906
Python 14828 13672 13933
Ruby 16597 15827 15594
Go 27271 26609 25160

4スレッドのスループットは以下。

Echo Get Set
C++ 35600 33070 32481
Python 10705 9648 9360
Ruby 15146 14420 14152
Go 31963 30426 30734

まず、EchoとGetとSetのスループットはほとんど変わらないことが確認できる。つまり、サーバ内のデータベース周りの処理は十分に高速で、ほとんど負荷がかかっていない。つまり、サーバ側のgRPC層、ネットワーク層、クライアント側のgRPC層、クライアントのビジネスロジックが負荷のほとんどを占める。よって、以後の考察はEchoの結果だけを見ればよいことになる。

1スレッドの結果を各言語で比較する。予想通り、C++が最速で、次点がGoだ。RubyとPythonはほぼ同じだ。最も遅いPythonでも1万QPSは出るので、ほとんどのユースケースではボトルネックにならないだろう。同期呼び出しのレイテンシに換算すれば0.1ミリ秒であり、100回呼び出しても10ミリ秒しかかからない。なお、短期間に100回も呼びだすならおそらくバッチ呼び出しをするだろう。100クエリをバッチするなら、スループットはEchoで70倍、Setã‚„Getでも20倍以上になる。

4コアのマシンなので、並列化による性能向上は2スレッドくらいで頭打ちになる。クライアント側とサーバ側の負荷が同じくらいと仮定すると、クライアント側で2コア、サーバ側で2コアを使い切ると最大効率になる。PythonとRubyはグローバルインタプリタロック(GIL)を備えているので、PythonやRubyで書かれた実装は並列化しないが、gRPCのクライアントライブラリのC++コア実装はGILの外で呼ばれるので、並列化の恩恵が得られている。Goは下馬評通り並列化の恩恵を強く得られていて、4スレッドでC++と遜色ない性能になる。


結論としては、どの言語での実装も性能に問題はなさそうだ。わざわざマルチプロセスやマルチマシンで並列処理や分散処理をするなら、その処理内容自体がそこそこ重いのが普通だ。その入力や出力をデータベースサーバで扱うとして、そのスループットが1万QPSであれば、クライアント側でRPC層がボトルネックになることはあまりないだろう。

ここで留意されたいのは、この実験は、単一マシンにクライアントとサーバを同居させて動かす「マルチプロセス」ユースケースの全体のスループットを測るものであり、典型的なクライアント・サーバ構成におけるサーバ側のスループットを測るものではないということだ。クライアントが分散している場合、クライアント側のRPCの性能は問題にならず、サーバ側の性能のみが問題になる。サーバ側はC++で最善と思われる実装をしているが、その限界性能がどれくらいかは、別途測定せねばならない。