gRPCでのRetry設定について

<!-- 以下本文 -->
## はじめに

レアゾン・ホールディングスでエンジニアをやっている窪田です。
menuのマイクロサービスにおいてはgRPCを利用しており、menuのバックエンドを担うマイクロサービスサーバーの間ではgRPCでの通信が行われています。
今回は自分の忘備録もかねて、gRPCの[Service Config](https://grpc.io/docs/guides/service-config/)と[Retry](https://grpc.io/docs/guides/retry/)に関してまとめようと思います。
なお、gRPCに関しての実装経験を前提とした記事になります。

## [Service Config](https://grpc.io/docs/guides/service-config/)

まず、Service Configに関してです。
Service ConfigとはgRPCクライアントがgRPCサーバーとやり取りを行う際の動作に関しての「設定」です。
この設定は、各RPCごとに指定が可能であり、globalに適用されるものではありません。

gRPCの仕様によると、Service Configでは以下のような値が設定可能[^1]ということになっています。
[^1]:[gRPCの仕様のコード](https://github.com/grpc/grpc-proto/blob/2eb777aba6593c31e21f7f69a163486bdc793501/grpc/service_config/service_config.proto#L564)から確認可能です。

現状、非推奨（deprecated）になっているものを除くと以下の4つがあります。

* Load Balancing Config
* Method Conding
* Retry Throttling
* Helth Check Config

gRPC Clientの各言語の実装において、これらの設定を行えるようになっており、例えばGoの実装では以下のよう形で記述できます。


```go
var (
	// JSONの形で変数に埋め込む
	serviceConfig = `{
        "loadBalancingConfig": [ { "round_robin": {} } ],
        "healthCheckConfig": {
		    "serviceName": "grpc.examples.echo.Health"
	    },
		"methodConfig": [{
		  "name": [{"service": "grpc.examples.echo.Echo"}],
		  "retryPolicy": {
			  "MaxAttempts": 4,
			  "InitialBackoff": ".01s",
			  "MaxBackoff": ".01s",
			  "BackoffMultiplier": 1.0,
			  "RetryableStatusCodes": [ "UNAVAILABLE" ]
		  }
		}]}`
)

.
.
.
//途中省略
.
.
.

// WithDefaultServiceConfigを用いて、Clientに渡す
conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultServiceConfig(retryPolicy))
```

このようにして、gRPCリクエストに対して、healtcheckの設定やretryの設定を組み込むことが可能です。

## Retry

ここからはService Configで設定できるretryの仕組みをより細かく見ていこうと思います。
Service Configで設定できるretryに関する項目は以下の2点です。

* Retry Throttling
* Method Configの中にあるRetry Policy

それぞれを見ていきます。

### Retry Throttling

Retry ThrottlingはgRPCクライアント側において、リトライが過剰に行われるのを防ぐための仕組みを提供してくれます。

Retry Throttlingでは以下の二つの値が設定可能です。

* `max_tokens`
* `token_ratio`

gRPCクライアントは、サーバーごとに`token_count`という内部カウンターを持っています。
`token_count`の初期値は、Retry Throttlingの設定として設定できる、`max_tokens`になります。
RPCが失敗すると、`token_count`は1ずつ減ってゆき、RPCが成功すると`token_count`は`token_ratio`で指定されている値だけ増えていきます。
`token_count <= max_tokens/2`となるとRPCのリトライが停止されるように設定されています。

例えば

```json
"retryThrottling": {
  "maxTokens": 10,
  "tokenRatio": 0.1
}
```

という設定の場合、失敗が多くなると、`token_count`が減っていき、`5`を下回るとretryを停止しますが、成功が続けば、`0.1`ずつ`token_count`が増えていき、再度retry可能な状態になる。という形でgRPCのリクエストが行われるようになります。


### Retry Policy

Service Configの中のMethod Config内でRetry Policyが設定可能です。
Defaultではこの設定は空になっており、ほとんどの場合retryは実行されない状態になっています。
Retry Policyをメソッド毎に設定することで、各メソッドに対して[Exponential Backoff](https://cloud.google.com/memorystore/docs/redis/exponential-backoff?hl=ja)のロジックに従ったretryが可能になります。
Retry Policyにおいては以下の5つの値が設定可能です。

* `max_attempts`
* `initial_backoff`
* `max_backoff`
* `backoff_multiplier`
* `retryable_status_codes`

`max_attempts`はRPCの最大試行回数を示し、最初のリクエストも含んだ回数を指定します。例えば3を設定した際には、最大2回のretryが実行されることになります。
`initial_backoff`が最初のretryまでの待機時間、`max_backoff`が待機時間の最大値、`backoff_multiplier`がExponential Backoffの倍率で、この倍率分retry間の時間が伸びていくことになります。
最後の`retryable_status_codes`にはgRPCのstatus codeを指定し、どのstatusでリトライを行うかを指定できます。`"retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]`のような形になります。

### Retry Internals

gRPC RFCsとして採択されたProposalの[Retry Internals](https://github.com/grpc/proposal/blob/master/A6-client-retries.md#retry-internals)によるとgRPCのRetryは以下のように実装されているようです。

> The retry policy will be implemented in-between the channel and the load balancing policy. That way every retry gets a chance to be sent out on a different subchannel than it originally failed on.

gRPCのChannelとはgRPCクライアントとサーバの通信を抽象化した仕組みであり、SubChannelとはDNSの解決先として複数のバックエンドがある場合の論理的な接続先となります。DNSに3つのIPが紐づいている場合には、SubChannelが3つあるイメージです。
上記の記述は、Retry PolicyはChannelとLoad Balancing Policyの間にあると書かれているので図示すると以下のようになります。
```
+-----------------+
|   gRPC Client   |
+--------|--------+
         |
         v
+--------|--------+
|      Channel     |  <- アプリケーションはChannelを利用
+--------|--------+
         |
         |
+--------|--------+
|   Retry Policy  | 
+--------|--------+
         |
         |（ロードバランシング）
         |
  +------+---------+---------+
  |                |         |
  v                v         v
+-------+     +-------+  +-------+
| Subch |     | Subch |  | Subch |  <- 各Subchannelが異なるサーバーに接続
| (IP1) |     | (IP2) |  | (IP3) |
+-------+     +-------+  +-------+
```

つまり、Load Balancing Policyが`round_robin`に設定されていれば、retryが発生するたびに異なるSubChannelを用いたリクエストが行われ、直前で失敗が帰ってきたサーバとは異なるサーバにリクエストを送れることになります。
この実装は、2024年8月23日時点で、Java, .NET, Node, Go, C-Coreで実装済みです。

## まとめ

今回はgRPCのretry周りの仕様に関してまとめてみました。仕様をきちんと読んで実装しないと、retryをきちんと働かせることができなかったりするので仕様を読むのは大事ですね。


<!-- フッター -->

<hr>

### **▼採用情報**<br>
レアゾン・ホールディングスは、「世界一の企業へ」というビジョンを掲げ、「新しい"当たり前"を作り続ける」というミッションを推進しています。

現在、エンジニア採用を積極的に行っておりますので、ご興味をお持ちいただけましたら、ぜひ下記リンクからご応募ください。

https://recruit.reazon.jp/