みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

gRPC の通信を Wireshark で観察する

gRPC の通信の中身が気になったので,Wireshark でパケットキャプチャして観察しました.

Wireshark のインストール

Wireshark · Go Deep

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 より引用