Carpe Diem

備忘録

gRPCのChannelについて

背景

gRPCにはクライアントとサーバとの通信を抽象化したChannelという仕組みがあります。

GRPC_GO_LOG_SEVERITY_LEVEL=infoを有効にした際に出てくる

[core] Channel Created
[core] parsed scheme: ""
[core] scheme "" not registered, fallback to default scheme
[core] ccResolverWrapper: sending update to cc: {[{localhost:8080  <nil> 0 <nil>}] <nil> <nil>}
[core] Resolver state updated: {Addresses:[{Addr:localhost:8080 ServerName: Attributes:<nil> Type:0 Metadata:<nil>}] ServiceConfig:<nil> Attributes:<nil>} (resolver returned new addresses)
[core] ClientConn switching balancer to "pick_first"
[core] Channel switches to new LB policy "pick_first"
[core] Subchannel Created
[core] Subchannel(id:2) created
[core] Subchannel Connectivity change to CONNECTING
[core] Subchannel picks a new address "localhost:8080" to connect
[core] Channel Connectivity change to CONNECTING
[core] CPU time info is unavailable on non-linux or appengine environment.
[core] Subchannel Connectivity change to READY
[core] Channel Connectivity change to READY
...
[core] Channel Connectivity change to SHUTDOWN
[core] Subchannel Connectivity change to SHUTDOWN
[core] Subchannel Deleted
[core] Subchanel(id:2) deleted
[core] Channel Deleted

Channel、Subchannelのような概念もあるので、理解のため一度整理します。

環境

  • grpc-go v1.38.1

Channelの状態

Channelの状態は以下の5つがあります。

状態 説明
CONNECTING 接続を試みている状態。
名前解決、TCP接続の確立、TLSハンドシェイクなどが含まれる
READY 接続を確立し、その後のすべての通信試行が成功している
TRANSIENT_FAILURE 接続に失敗している状態。
接続の再試行が行われ成功するまでCONNECTING↔TRANSIENT_FAILUREの状態を繰り返す。
再試行は指数関数的なバックオフで行われるためこの状態にいる時間がどんどん長くなっていく。
IDLE 新しいRPCや保留中のRPCがないため、Channelが接続を作成しようともしない状態。
デフォルトのIDLE_TIMEOUTは300秒(5分)
SHUTDOWN Channelがシャットダウンを開始。
保留中のRPCはキャンセルされるまで続行されるが
新規のRPCは即座に失敗する

grpc/connectivity-semantics-and-api.md at master · grpc/grpc · GitHub

Channelの状態遷移図

Channelの状態は以下のように遷移します。

f:id:quoll00:20210708015857p:plain

gRPC Channelz

gRPCの接続に関して、gRPC Channelzというデバッグの仕様があります。

理解しておくべき概念は以下です。

Channel

ChannelはRPCを開始したり完了したりすることができる機能を抽象化した概念です。
有向非巡回グラフ(DAG)で、内部のチャネルはChannelとSubhannelのみを持ち、リーフチャネルのみがSocketを持ちます。

f:id:quoll00:20210708023318p:plain

ref: grpc/connectivity-semantics-and-api.md at master · grpc/grpc · GitHub

grpc-goでは一番上のChannelがClientConnに当たります。

ClientConn作成時にChannelが登録されます。

Subchannel

Subchannelはロードバランスのための接続の抽象化です。

DNSで名前解決した際に複数のバックエンドが返った場合、各バックエンドへの(論理的な)接続はSubchannelとして表現されます。

f:id:quoll00:20210708030038p:plain

grpc-goでは、SubConnはSubchannelとして見ることができます。

ResolverState.Addresses毎にSubchannelを登録します。

Socket

Socketはファイルディスクリプタとほぼ同等で、一般的には2つのエンドポイント間のTCPコネクションと考えることができます。
gRPCではTCPコネクション上でHTTP/2のstreamが多重化されますが、Socketは各streamに関する情報を保持しています。

grpc-goではhttp2Clienthttp2Serverに当たります。

例えばクライアントはhttp2Clientを作成したタイミングで登録されます。

Channelzの可視化

Channelzの情報を可視化するには

といったツールがあります。

今回は後者のgrpcdebugを使って可視化してみます。

インストールは以下。

$ go install -v github.com/grpc-ecosystem/grpcdebug@latest

実装

Channelzはサーバ側もクライアント側もメトリクスを出せます。

サーバ側はChannelよりも概念的にシンプルなので、先程のような階層モデルはありません。ロードバランシングや名前解決の必要もありません。各Subchannelは1つのSocketに結びついているので、SubchannelとSocketはフラット化されてSocketだけになります。

なので今回はクライアント側のみ表示できるようにします。

以下のようにChannelzのRPCを叩けるgRPCサーバを内部で起動します。クライアント側の処理をブロックしないよう非同期で起動しておきます。

import (
        "log"
        "net"

        "google.golang.org/grpc"
        "google.golang.org/grpc/channelz/service"
        "google.golang.org/grpc/keepalive"
        "google.golang.org/grpc/reflection"
)

...

go func() {
        s := grpc.NewServer()
        reflection.Register(s)
        service.RegisterChannelzServiceToServer(s)

        lis, err := net.Listen("tcp", ":8000")
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }

        if err := s.Serve(lis); err != nil {
                log.Fatalf("err %v\n", err)
        }

}()

Channel

一覧取得と

$ grpcdebug localhost:8000 channelz channels
Channel ID   Target           State     Calls(Started/Succeeded/Failed)   Created Time
1            localhost:8080   READY     1/0/0                             13 seconds ago

ID指定による詳細取得があります。

$ grpcdebug localhost:8000 channelz channel 1
Channel ID:        1
Target:            localhost:8080
State:             READY
Calls Started:     2
Calls Succeeded:   1
Calls Failed:      0
Created Time:      46 seconds ago
---
Subchannel ID   Target           State     Calls(Started/Succeeded/Failed)   CreatedTime
2               localhost:8080   READY     2/1/0                             46 seconds ago
---
Severity   Time             Child Ref                      Description                                                        
CT_INFO    46 seconds ago                                  Channel Created                                                    
CT_INFO    46 seconds ago                                  parsed scheme: ""                                                  
CT_INFO    46 seconds ago                                  scheme "" not registered, fallback to default scheme               
CT_INFO    46 seconds ago                                  ccResolverWrapper: sending update to cc: {[{localhost:8080  <nil> 0 <nil>}] <nil> <nil>}
CT_INFO    46 seconds ago                                  Resolver state updated: {Addresses:[{Addr:localhost:8080 ServerName: Attributes:<nil> Type:0 Metadata:<nil>}] ServiceConfig:<nil> Attributes:<nil>} (resolver returned new addresses)
CT_INFO    46 seconds ago                                  ClientConn switching balancer to "pick_first"                      
CT_INFO    46 seconds ago                                  Channel switches to new LB policy "pick_first"                     
CT_INFO    46 seconds ago   subchannel(subchannel_id:2 )   Subchannel(id:2) created                                           
CT_INFO    46 seconds ago                                  Channel Connectivity change to CONNECTING                          
CT_INFO    46 seconds ago                                  Channel Connectivity change to READY                               

Subchannel

Channelで表示されていたSubchannel IDを指定することで取得できます。

$ grpcdebug localhost:8000 channelz subchannel 2
Subchannel ID:     2
Target:            localhost:8080
State:             READY
Calls Started:     3
Calls Succeeded:   2
Calls Failed:      0
Created Time:      1 minute ago
---
Socket ID   Local->Remote             Streams(Started/Succeeded/Failed)   Messages(Sent/Received)
5           [::1]:52197->[::1]:8080   3/2/0                               3/2

Socket

Subchannelで表示されていたSocket IDを指定することで取得できます。

$ grpcdebug localhost:8000 channelz socket 5
Socket ID:                       5
Address:                         [::1]:52197->[::1]:8080
Streams Started:                 4
Streams Succeeded:               4
Streams Failed:                  0
Messages Sent:                   4
Messages Received:               4
Keep Alives Sent:                11
Last Local Stream Created:       33 seconds ago
Last Remote Stream Created:      a long while ago
Last Message Sent Created:       33 seconds ago
Last Message Received Created:   11 seconds ago
Local Flow Control Window:       65535
Remote Flow Control Window:      65535

先に述べたようにstreamの情報があります。またKeepaliveの送信数なども見れます。

TRANSIENT_FAILUREの場合

接続に問題が発生してChannelがTRANSIENT_FAILUREになった場合、

Channel

$ grpcdebug localhost:8000 channelz channel 1
Channel ID:        1
Target:            localhost:8080
State:             TRANSIENT_FAILURE
Calls Started:     55
Calls Succeeded:   0
Calls Failed:      103
Created Time:      54 seconds ago
---
Subchannel ID   Target           State               Calls(Started/Succeeded/Failed)   CreatedTime
2               localhost:8080   TRANSIENT_FAILURE   7/0/7                             54 seconds ago

Subchannel

$ grpcdebug localhost:8000 channelz subchannel 2
Subchannel ID:     2
Target:            localhost:8080
State:             TRANSIENT_FAILURE
Calls Started:     7
Calls Succeeded:   0
Calls Failed:      7
Created Time:      28 seconds ago

となりSocketは存在しません。

まとめ

Channelの概念を知ることでgRPCのコネクション周りの理解が少し深くなりました。

参考