gRPC の通信の中身が気になったので,Wireshark でパケットキャプチャして観察しました.
Wireshark のインストール
M1 MacOS 用の Wireshark が公開されているのでダウンロードしてインストールします.権限が不足している場合は "Install ChmodBPF.pkg" を実行します.
環境
- M1 Macbook Air
- macOS Ventura 13.2.1
- Python 3.11.2
- grpc 1.51.3
- Unary RPCs で通信
結果
最も単純な,1 リクエスト送信し,1 レスポンス受信する Unary RPC の通信内容を見てみます.サーバーの動作は,非同期でリクエストを受信して 1 秒待機してからレスポンスを返すだけです.
1 リクエスト送信,1 レスポンス受信
以下のような Python スクリプトを作成し,実行しました.サーバー,クライアント両方で, grpc.aio を使用しています.
async def main(query: str): channel = insecure_channel(target) search = channel.unary_unary( "/Search/search", request_serializer=main_pb2.SearchParam.SerializeToString, response_deserializer=main_pb2.SearchStatus.FromString, ) await sub("Banana", search)
sub
関数で非同期に通信をします.
サーバーとクライアントは両方とも localhost で,順に 8100, 51590 ポートを使用しています.
まずは TCP コネクションを確立します.gRPC は TCP を使用していることが分かります.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
1 | 0.000000 | ::1 | ::1 | TCP | 88 | 51590 → 8100 [SYN] Seq=0 Win=65535 Len=0 |
2 | 0.000141 | ::1 | ::1 | TCP | 88 | 8100 → 51590 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 |
3 | 0.000168 | ::1 | ::1 | TCP | 76 | 51590 → 8100 [ACK] Seq=1 Ack=1 Win=407744 Len=0 |
4 | 0.000189 | ::1 | ::1 | TCP | 76 | [TCP Window Update] 8100 → 51590 [ACK] Seq=1 Ack=1 Win=407744 Len=0 |
次にクライアントからサーバーにデータを送信します.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
5 | 0.000217 | ::1 | ::1 | TCP | 158 | 51590 → 8100 [PSH, ACK] Seq=1 Ack=1 Win=407744 Len=82 |
6 | 0.000245 | ::1 | ::1 | TCP | 76 | 8100 → 51590 [ACK] Seq=1 Ack=83 Win=407680 Len=0 |
7 | 0.000256 | ::1 | ::1 | TCP | 122 | 8100 → 51590 [PSH, ACK] Seq=1 Ack=83 Win=407680 Len=46 |
8 | 0.000273 | ::1 | ::1 | TCP | 76 | 51590 → 8100 [ACK] Seq=83 Ack=47 Win=407744 Len=0 |
9 | 0.000274 | ::1 | ::1 | TCP | 85 | 8100 → 51590 [PSH, ACK] Seq=47 Ack=83 Win=407680 Len=9 |
10 | 0.000299 | ::1 | ::1 | TCP | 76 | 51590 → 8100 [ACK] Seq=83 Ack=56 Win=407744 Len=0 |
11 | 0.000391 | ::1 | ::1 | TCP | 359 | 51590 → 8100 [PSH, ACK] Seq=83 Ack=56 Win=407744 Len=283 |
12 | 0.000423 | ::1 | ::1 | TCP | 76 | 8100 → 51590 [ACK] Seq=56 Ack=366 Win=407424 Len=0 |
13 | 0.000455 | ::1 | ::1 | TCP | 93 | 8100 → 51590 [PSH, ACK] Seq=56 Ack=366 Win=407424 Len=17 |
14 | 0.000480 | ::1 | ::1 | TCP | 76 | 51590 → 8100 [ACK] Seq=366 Ack=73 Win=407680 Len=0 |
15 | 0.000499 | ::1 | ::1 | TCP | 93 | 51590 → 8100 [PSH, ACK] Seq=366 Ack=73 Win=407680 Len=17 |
16 | 0.000515 | ::1 | ::1 | TCP | 76 | 8100 → 51590 [ACK] Seq=73 Ack=383 Win=407360 Len=0 |
サーバーで処理が完了したらクライアントにレスポンスを返します.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
17 | 1.001834 | ::1 | ::1 | TCP | 216 | 8100 → 51590 [PSH, ACK] Seq=73 Ack=383 Win=407360 Len=140 |
18 | 1.001901 | ::1 | ::1 | TCP | 76 | 51590 → 8100 [ACK] Seq=383 Ack=213 Win=407552 Len=0 |
19 | 1.002013 | ::1 | ::1 | TCP | 93 | 51590 → 8100 [PSH, ACK] Seq=383 Ack=213 Win=407552 Len=17 |
20 | 1.002044 | ::1 | ::1 | TCP | 76 | 8100 → 51590 [ACK] Seq=213 Ack=400 Win=407360 Len=0 |
21 | 1.002091 | ::1 | ::1 | TCP | 93 | 8100 → 51590 [PSH, ACK] Seq=213 Ack=400 Win=407360 Len=17 |
22 | 1.002125 | ::1 | ::1 | TCP | 76 | 51590 → 8100 [ACK] Seq=400 Ack=230 Win=407552 Len=0 |
クライアントで処理が完了したとサーバーに伝えます.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
23 | 1.002487 | ::1 | ::1 | TCP | 76 | 51590 → 8100 [FIN, ACK] Seq=400 Ack=230 Win=407552 Len=0 |
24 | 1.002527 | ::1 | ::1 | TCP | 76 | 8100 → 51590 [ACK] Seq=230 Ack=401 Win=407360 Len=0 |
非同期で 4 リクエスト送信
クライアントから同一の channel を使用して非同期で 4 リクエストを送信し,上記の内容と比較してみます.
async def main(query: str): channel = insecure_channel(target) search = channel.unary_unary( "/Search/search", request_serializer=main_pb2.SearchParam.SerializeToString, response_deserializer=main_pb2.SearchStatus.FromString, ) await asyncio.gather(*[sub(f"Banana(index={u})", search) for u in range(4)])
TCP コネクションを確立するリクエストは全く同じです.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
1 | 0.000000 | ::1 | ::1 | TCP | 88 | 51498 → 8100 [SYN] Seq=0 Win=65535 Len=0M |
2 | 0.000180 | ::1 | ::1 | TCP | 88 | 8100 → 51498 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 |
3 | 0.000216 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=1 Ack=1 Win=407744 Len=0 |
4 | 0.000235 | ::1 | ::1 | TCP | 76 | [TCP Window Update] 8100 → 51498 [ACK] Seq=1 Ack=1 Win=407744 Len=0 |
4 リクエストを送信したら,リクエスト数が増えるのではないかと予想しましたが,No.11 のリクエストのサイズが増えただけでした.4 リクエストは全てまとめて送信されたようです.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
5 | 0.000278 | ::1 | ::1 | TCP | 158 | 51498 → 8100 [PSH, ACK] Seq=1 Ack=1 Win=407744 Len=82 |
6 | 0.000313 | ::1 | ::1 | TCP | 76 | 8100 → 51498 [ACK] Seq=1 Ack=83 Win=407680 Len=0 |
7 | 0.000320 | ::1 | ::1 | TCP | 122 | 8100 → 51498 [PSH, ACK] Seq=1 Ack=83 Win=407680 Len=46 |
8 | 0.000330 | ::1 | ::1 | TCP | 85 | 8100 → 51498 [PSH, ACK] Seq=47 Ack=83 Win=407680 Len=9 |
9 | 0.000341 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=83 Ack=47 Win=407744 Len=0 |
10 | 0.000355 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=83 Ack=56 Win=407744 Len=0 |
11 | 0.000470 | ::1 | ::1 | TCP | 542 | 51498 → 8100 [PSH, ACK] Seq=83 Ack=56 Win=407744 Len=466 |
12 | 0.000495 | ::1 | ::1 | TCP | 76 | 8100 → 51498 [ACK] Seq=56 Ack=549 Win=407232 Len=0 |
13 | 0.000551 | ::1 | ::1 | TCP | 93 | 8100 → 51498 [PSH, ACK] Seq=56 Ack=549 Win=407232 Len=17 |
14 | 0.000585 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=549 Ack=73 Win=407680 Len=0 |
15 | 0.000615 | ::1 | ::1 | TCP | 93 | 51498 → 8100 [PSH, ACK] Seq=549 Ack=73 Win=407680 Len=17 |
16 | 0.000641 | ::1 | ::1 | TCP | 76 | 8100 → 51498 [ACK] Seq=73 Ack=566 Win=407232 Len=0 |
サーバーで処理が完了したリクエストは No. 17, 18 の 2個に分割されて返されました.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
17 | 1.002172 | ::1 | ::1 | TCP | 216 | 8100 → 51498 [PSH, ACK] Seq=73 Ack=566 Win=407232 Len=140 |
18 | 1.002221 | ::1 | ::1 | TCP | 114 | 8100 → 51498 [PSH, ACK] Seq=213 Ack=566 Win=407232 Len=38 |
19 | 1.002241 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=566 Ack=213 Win=407552 Len=0 |
20 | 1.002245 | ::1 | ::1 | TCP | 114 | 8100 → 51498 [PSH, ACK] Seq=251 Ack=566 Win=407232 Len=38 |
21 | 1.002268 | ::1 | ::1 | TCP | 114 | 8100 → 51498 [PSH, ACK] Seq=289 Ack=566 Win=407232 Len=38 |
22 | 1.002273 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=566 Ack=251 Win=407488 Len=0 |
23 | 1.002302 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=566 Ack=289 Win=407488 Len=0 |
24 | 1.002312 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=566 Ack=327 Win=407424 Len=0 |
25 | 1.002415 | ::1 | ::1 | TCP | 93 | 51498 → 8100 [PSH, ACK] Seq=566 Ack=327 Win=407424 Len=17 |
26 | 1.002449 | ::1 | ::1 | TCP | 76 | 8100 → 51498 [ACK] Seq=327 Ack=583 Win=407168 Len=0 |
27 | 1.002493 | ::1 | ::1 | TCP | 93 | 8100 → 51498 [PSH, ACK] Seq=327 Ack=583 Win=407168 Len=17 |
28 | 1.002531 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [ACK] Seq=583 Ack=344 Win=407424 Len=0 |
最後は全く同じです.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
29 | 1.003262 | ::1 | ::1 | TCP | 76 | 51498 → 8100 [FIN, ACK] Seq=583 Ack=344 Win=407424 Len=0 |
30 | 1.003302 | ::1 | ::1 | TCP | 76 | 8100 → 51498 [ACK] Seq=344 Ack=584 Win=407168 Len=0 |
2 個の channel で非同期で それぞれ 1 リクエスト送信
クライアントのソースコードの一部は以下の通りです.あえて channel を2個作成しています.
async def main(query: str): channel1 = insecure_channel(target) search1 = channel1.unary_unary( "/Search/search", request_serializer=main_pb2.SearchParam.SerializeToString, response_deserializer=main_pb2.SearchStatus.FromString, ) channel2 = insecure_channel(target) search2 = channel2.unary_unary( "/Search/search", request_serializer=main_pb2.SearchParam.SerializeToString, response_deserializer=main_pb2.SearchStatus.FromString, ) await asyncio.gather( *[ sub("channel1", search1), sub("channel2", search2), ] )
クライアントは複数のポートを使用するだろうと予想しましたが,全て同じポートを使用しました.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
5 | 0.000238 | ::1 | ::1 | TCP | 158 | 51908 → 8100 [PSH, ACK] Seq=1 Ack=1 Win=407744 Len=82 |
6 | 0.000273 | ::1 | ::1 | TCP | 76 | 8100 → 51908 [ACK] Seq=1 Ack=83 Win=407680 Len=0 |
7 | 0.000280 | ::1 | ::1 | TCP | 122 | 8100 → 51908 [PSH, ACK] Seq=1 Ack=83 Win=407680 Len=46 |
8 | 0.000294 | ::1 | ::1 | TCP | 85 | 8100 → 51908 [PSH, ACK] Seq=47 Ack=83 Win=407680 Len=9 |
9 | 0.000302 | ::1 | ::1 | TCP | 76 | 51908 → 8100 [ACK] Seq=83 Ack=47 Win=407744 Len=0 |
10 | 0.000316 | ::1 | ::1 | TCP | 76 | 51908 → 8100 [ACK] Seq=83 Ack=56 Win=407744 Len=0 |
11 | 0.000435 | ::1 | ::1 | TCP | 352 | 51908 → 8100 [PSH, ACK] Seq=83 Ack=56 Win=407744 Len=276 |
12 | 0.000460 | ::1 | ::1 | TCP | 76 | 8100 → 51908 [ACK] Seq=56 Ack=359 Win=407424 Len=0 |
13 | 0.000483 | ::1 | ::1 | TCP | 143 | 51908 → 8100 [PSH, ACK] Seq=359 Ack=56 Win=407744 Len=67 |
14 | 0.000500 | ::1 | ::1 | TCP | 93 | 8100 → 51908 [PSH, ACK] Seq=56 Ack=359 Win=407424 Len=17 |
15 | 0.000506 | ::1 | ::1 | TCP | 76 | 8100 → 51908 [ACK] Seq=73 Ack=426 Win=407360 Len=0 |
16 | 0.000515 | ::1 | ::1 | TCP | 76 | 51908 → 8100 [ACK] Seq=426 Ack=73 Win=407680 Len=0 |
17 | 0.000537 | ::1 | ::1 | TCP | 93 | 51908 → 8100 [PSH, ACK] Seq=426 Ack=73 Win=407680 Len=17 |
18 | 0.000571 | ::1 | ::1 | TCP | 76 | 8100 → 51908 [ACK] Seq=73 Ack=443 Win=407296 Len=0 |
サーバーからのレスポンスも同じポートへ送信されました.
No. | Time | Source | Destination | Protocol | Length | Info |
---|---|---|---|---|---|---|
19 | 1.001927 | ::1 | ::1 | TCP | 214 | 8100 → 51908 [PSH, ACK] Seq=73 Ack=443 Win=407296 Len=138 |
20 | 1.001972 | ::1 | ::1 | TCP | 112 | 8100 → 51908 [PSH, ACK] Seq=211 Ack=443 Win=407296 Len=36 |
21 | 1.001993 | ::1 | ::1 | TCP | 76 | 51908 → 8100 [ACK] Seq=443 Ack=211 Win=407552 Len=0 |
22 | 1.002016 | ::1 | ::1 | TCP | 76 | 51908 → 8100 [ACK] Seq=443 Ack=247 Win=407552 Len=0 |
23 | 1.002122 | ::1 | ::1 | TCP | 93 | 51908 → 8100 [PSH, ACK] Seq=443 Ack=247 Win=407552 Len=17 |
24 | 1.002154 | ::1 | ::1 | TCP | 76 | 8100 → 51908 [ACK] Seq=247 Ack=460 Win=407296 Len=0 |
25 | 1.002197 | ::1 | ::1 | TCP | 93 | 8100 → 51908 [PSH, ACK] Seq=247 Ack=460 Win=407296 Len=17 |
26 | 1.002228 | ::1 | ::1 | TCP | 76 | 51908 → 8100 [ACK] Seq=460 Ack=264 Win=407488 Len=0 |
27 | 1.002830 | ::1 | ::1 | TCP | 76 | 51908 → 8100 [FIN, ACK] Seq=460 Ack=264 Win=407488 Len=0 |
28 | 1.002870 | ::1 | ::1 | TCP | 76 | 8100 → 51908 [ACK] Seq=264 Ack=461 Win=407296 Len=0 |
最後は全く同じなので省略します.
解釈
gRPC の通信を観察した結果,以下の事項が分かりました.
- gRPC は TCP を使用する.
- 非同期で関数を複数回実行しても 1 個のコネクションを利用して,なるべく同一のパケットにまとめて送信される.
- 複数の channel を作成しても,可能ならば同じコネクションが使われる.
ドキュメントとの比較
Always re-use stubs and channels when possible. *1
スタブとチャンネルは可能な限り再利用してください. (拙訳: みー)
上記のように説明されていますが,Python の grpc.aio ライブラリを使用する場合は,別の channel を作成しても可能ならば再利用されるように設計されている*2ようで,気が向くままに channel を作成すればよいみたいです.
*1:Performance Best Practices | gRPC より引用
*2:Underlying our Python wrapper, the Channel object uses C-Core's Channel which will reuse existing connections. E.g. if you create 100 channel object to the same destination with same options, there will be only 1 outstanding TCP connection. Question: Python GRPC client, is there a way to create multiple connections with in the same channel? · Issue #20985 · grpc/grpc · GitHub より引用