背景
gRPCを利用していると、デプロイを含む一時的なネットワーク断で以下のようなエラーが発生することがあります。
rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp xxx: connect: connection refused"
rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp: lookup xxx: i/o timeout"
これはgRPCがfail fastの思想になっており、ChannelがTRANSIENT_FAILURE
の状態だとRPCを即座に失敗させるようにしているためです。
Channelの詳細は過去の記事を参考にしてください。
しかしローリングアップデートのようにChannelの状態がすぐに復旧する(再接続される)状況下であれば、すぐにエラーとせずに待機して正常系レスポンスを返せた方が良さそうです。
今回はその対応方法を紹介します。
環境
- Go v1.20.2
- grpc-go v1.53.0
方法
WaitForReadyを使う
gRPCにはWait for Readyという仕様があり、これをtrueにすることでTRANSIENT_FAILURE
でもすぐにエラーを返さず、READY
になるまでRPCを待ってくれます。
Goだとgrpc.WaitForReadyが用意されています。
context canceledであったり、deadline exceeded(=context timeout)であればエラーとして終了されるので、永続的に残ることはありません。
逆に言うと↑をきちんと対応しておかないと、もしコネクションが復旧しない場合に永続的にRPCがキューに残るので、リソースリークが起きOOMが発生する可能性があります。
実装
CallOptionなので、1つ1つのRPCに設定するか、
resp, err := c.SayHello(ctx, req, grpc.WaitForReady(true))
もしくはWithDefaultCallOptionsで一律設定することで対応できます。
conn, err := grpc.Dial(address,
grpc.WithInsecure(),
grpc.WithDefaultCallOptions(
grpc.WaitForReady(true),
),
)
動作確認
サーバとクライアントを1台ずつ用意し、クライアントは1秒毎にリクエストを送ります。
func main() { conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithDefaultCallOptions( grpc.WaitForReady(true), ), ) if err != nil { log.Fatal(err) } defer conn.Close() c := pb.NewGreeterClient(conn) for { hello(c) time.Sleep(1 * time.Second) } }
その状態でサーバを落としたり起動したりして検証します。
またGRPC_GO_LOG_SEVERITY_LEVEL
を付けてChannelの状態を確認します。
$ export GRPC_GO_LOG_SEVERITY_LEVEL=info
WaitForReadyなしの場合
2023/04/01 06:22:07 Reply: Hello alice 2023/04/01 06:22:08 Reply: Hello alice # サーバを停止 2023/04/01 06:22:08 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to IDLE 2023/04/01 06:22:08 INFO: [core] [Channel #1] Channel Connectivity change to IDLE 2023/04/01 06:22:09 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to CONNECTING 2023/04/01 06:22:09 INFO: [core] [Channel #1 SubChannel #2] Subchannel picks a new address "localhost:8080" to connect 2023/04/01 06:22:09 INFO: [core] [Channel #1] Channel Connectivity change to CONNECTING 2023/04/01 06:22:09 WARNING: [core] [Channel #1 SubChannel #2] grpc: addrConn.createTransport failed to connect to { "Addr": "localhost:8080", "ServerName": "localhost:8080", "Attributes": null, "BalancerAttributes": null, "Type": 0, "Metadata": null }. Err: connection error: desc = "transport: Error while dialing: dial tcp [::1]:8080: connect: connection refused" 2023/04/01 06:22:09 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:22:09 INFO: [core] [Channel #1] Channel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:22:09 rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp [::1]:8080: connect: connection refused"
timeoutに関わらず、即座にUnavailable
エラーが返ります。
WaitForReadyありの場合
2023/04/01 06:24:12 Reply: Hello alice 2023/04/01 06:24:13 Reply: Hello alice 2023/04/01 06:24:14 Reply: Hello alice # サーバを停止 2023/04/01 06:24:14 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to IDLE 2023/04/01 06:24:14 INFO: [core] [Channel #1] Channel Connectivity change to IDLE 2023/04/01 06:24:15 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to CONNECTING 2023/04/01 06:24:15 INFO: [core] [Channel #1 SubChannel #2] Subchannel picks a new address "localhost:8080" to connect 2023/04/01 06:24:15 INFO: [core] [Channel #1] Channel Connectivity change to CONNECTING 2023/04/01 06:24:15 WARNING: [core] [Channel #1 SubChannel #2] grpc: addrConn.createTransport failed to connect to { "Addr": "localhost:8080", "ServerName": "localhost:8080", "Attributes": null, "BalancerAttributes": null, "Type": 0, "Metadata": null }. Err: connection error: desc = "transport: Error while dialing: dial tcp [::1]:8080: connect: connection refused" 2023/04/01 06:24:15 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:15 INFO: [core] [Channel #1] Channel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:16 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to IDLE 2023/04/01 06:24:16 INFO: [core] [Channel #1] Channel Connectivity change to IDLE 2023/04/01 06:24:16 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to CONNECTING 2023/04/01 06:24:16 INFO: [core] [Channel #1 SubChannel #2] Subchannel picks a new address "localhost:8080" to connect 2023/04/01 06:24:16 INFO: [core] [Channel #1] Channel Connectivity change to CONNECTING 2023/04/01 06:24:16 WARNING: [core] [Channel #1 SubChannel #2] grpc: addrConn.createTransport failed to connect to { "Addr": "localhost:8080", "ServerName": "localhost:8080", "Attributes": null, "BalancerAttributes": null, "Type": 0, "Metadata": null }. Err: connection error: desc = "transport: Error while dialing: dial tcp [::1]:8080: connect: connection refused" 2023/04/01 06:24:16 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:16 INFO: [core] [Channel #1] Channel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:18 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to IDLE 2023/04/01 06:24:18 INFO: [core] [Channel #1] Channel Connectivity change to IDLE 2023/04/01 06:24:18 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to CONNECTING 2023/04/01 06:24:18 INFO: [core] [Channel #1 SubChannel #2] Subchannel picks a new address "localhost:8080" to connect 2023/04/01 06:24:18 INFO: [core] [Channel #1] Channel Connectivity change to CONNECTING # サーバを起動 2023/04/01 06:24:18 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to READY 2023/04/01 06:24:18 INFO: [core] [Channel #1] Channel Connectivity change to READY 2023/04/01 06:24:18 Reply: Hello alice 2023/04/01 06:24:19 Reply: Hello alice
このようにクライアントでエラーは発生せずに待機し、再接続後してChannelがREADY
になったら成功するようになります。
WaitForReadyありでtimeoutした場合
今回5秒のtimeoutを入れているので、
2023/04/01 06:24:20 Reply: Hello alice 2023/04/01 06:24:21 Reply: Hello alice 2023/04/01 06:24:22 Reply: Hello alice # サーバを停止 2023/04/01 06:24:22 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to IDLE 2023/04/01 06:24:22 INFO: [core] [Channel #1] Channel Connectivity change to IDLE 2023/04/01 06:24:23 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to CONNECTING 2023/04/01 06:24:23 INFO: [core] [Channel #1 SubChannel #2] Subchannel picks a new address "localhost:8080" to connect 2023/04/01 06:24:23 INFO: [core] [Channel #1] Channel Connectivity change to CONNECTING 2023/04/01 06:24:23 WARNING: [core] [Channel #1 SubChannel #2] grpc: addrConn.createTransport failed to connect to { "Addr": "localhost:8080", "ServerName": "localhost:8080", "Attributes": null, "BalancerAttributes": null, "Type": 0, "Metadata": null }. Err: connection error: desc = "transport: Error while dialing: dial tcp [::1]:8080: connect: connection refused" 2023/04/01 06:24:23 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:23 INFO: [core] [Channel #1] Channel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:24 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to IDLE 2023/04/01 06:24:24 INFO: [core] [Channel #1] Channel Connectivity change to IDLE 2023/04/01 06:24:24 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to CONNECTING 2023/04/01 06:24:24 INFO: [core] [Channel #1 SubChannel #2] Subchannel picks a new address "localhost:8080" to connect 2023/04/01 06:24:24 INFO: [core] [Channel #1] Channel Connectivity change to CONNECTING 2023/04/01 06:24:24 WARNING: [core] [Channel #1 SubChannel #2] grpc: addrConn.createTransport failed to connect to { "Addr": "localhost:8080", "ServerName": "localhost:8080", "Attributes": null, "BalancerAttributes": null, "Type": 0, "Metadata": null }. Err: connection error: desc = "transport: Error while dialing: dial tcp [::1]:8080: connect: connection refused" ... 2023/04/01 06:24:27 INFO: [core] [Channel #1 SubChannel #2] Subchannel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:27 INFO: [core] [Channel #1] Channel Connectivity change to TRANSIENT_FAILURE 2023/04/01 06:24:28 rpc error: code = DeadlineExceeded desc = latest balancer error: connection error: desc = "transport: Error while dialing: dial tcp [::1]:8080: connect: connection refused"
このようにDeadlineExceeded
エラーがクライアントに返りました。
その他
サンプルコード
今回のサンプルコードはこちらです。
なぜfail fastなのか
fail fastの考え方は、ネットワークエラーやタイムアウトなどの問題が発生した場合、その原因をすぐに特定して接続を早期に断つことで、リソースを保護しシステムの可用性を向上させることを目的としています。
特に大規模トラフィックを扱う場合、ネットワークエラーなどで大量のリクエストが滞留すると、1つ1つのリクエストのメモリ消費は少なくてもそれが積み重なってOOMが発生することがあります。
なのでこの考え方自体は実用的と言えます。
リトライは効かないのか
Interceptorでリトライ処理を入れたとして、Channelの状態はTRANSIENT_FAILURE
なので即座にエラーとなるリトライがN回続くだけになります。
またEnvoyやIstioのVirtualServiceのような自動リトライは、そもそもそのレイヤまでRPCが飛んで行かない状況なので効果がありません。
まとめ
gRPCで一時的なネットワーク断が起きた際に、transportエラーを回避する方法を紹介しました。