コンテンツにスキップ

Envoy Gateway を使った gRPC のリトライ

Envoy Gateway v1.3.0 のBackendTrafficPolicyを使って、gRPC 通信のリトライを試してみました。検証に使う fake-service には、一定の確率で意図的にエラーを返す fault-injection を設定し、エラー発生時に Envoy Gateway が再試行するかを確認します。

構成

本検証における通信フローを、Envoy Gateway の有無による 2 つのパターンで比較します。

Envoy Gateway を使わない場合

まず、Envoy Gateway を用いずにリトライする場合の構成を確認します。 この構成ではクライアントがサービスにリクエストを送信し、サービスがエラーを返すとクライアントがリトライします。

sequenceDiagram
  participant C as Client
  participant S as Service
  C->>S: Request
  S->>C: Response 500
  C->>S: Request
  S->>C: Response 500
  C->>S: Request
  S->>C: Response 200

no-gateway

Envoy Gateway を使った場合

次に、Envoy Gateway を経由するリトライ構成を確認します。 この構成ではクライアントからのリクエストを Envoy Gateway がサービスへ転送し、サービスがエラーを返すと Envoy Gateway がリトライします。クライアント側がリトライを意識しなくてよい点が、Envoy Gateway を使わない構成との違いです。

sequenceDiagram
  participant C as Client
  participant G as Gateway
  participant S as Service
  C->>G: Request
  G->>S: Request
  S->>G: Response 500
  G->>S: Request
  S->>G: Response 500
  G->>S: Request
  S->>G: Response 200
  G->>C: Response 200

with-gateway

以降の手順では、Envoy Gateway を使ったリトライの構成を検証します。

セットアップ

クラスターに Envoy Gateway をインストールします。envoyproxies.gateway の CRD がインストールされていることを確認します。

kubectl get crd | grep envoyproxies.gateway

インストールされていない場合は、次のコマンドでインストールします。

helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.3.0 -n envoy-gateway-system --create-namespace

サンプルの gRPC アプリケーションをデプロイします。

kubectl apply -f https://raw.githubusercontent.com/Nishikoh/envoy-sandbox/refs/heads/main/grpc-retry/grpc-routing.yaml
コードの簡単な解説
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: custom-envoy-proxy
  namespace: envoy-gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService: # (2)
        name: grpc-gateway # (1)
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg-example
  labels:
    example: grpc-routing
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
  parametersRef:
    group: gateway.envoyproxy.io
    kind: EnvoyProxy # (3)
    name: custom-envoy-proxy
    namespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg-example
  labels:
    example: grpc-routing
spec:
  gatewayClassName: eg-example
  listeners: # (4)
    - name: http
      protocol: HTTP
      port: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: fake
    example: grpc-routing
  name: fake
spec:
  selector:
    matchLabels:
      app: fake
  replicas: 1
  template:
    metadata:
      labels:
        app: fake
    spec:
      containers:
        - name: grpcsrv
          image: nicholasjackson/fake-service:v0.26.2
          env:
            - name: ERROR_RATE
              value: "0.5" # (5)
            - name: ERROR_TYPE
              value: "http_error"
            - name: ERROR_CODE
              value: "14"
            - name: SERVICE_TYPE
              value: "grpc"
            - name: LISTEN_ADDR
              value: "0.0.0.0:9000"
          ports:
            - containerPort: 9000
              protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: fake
    example: grpc-routing
  name: fake
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 9000
      protocol: TCP
      targetPort: 9000
  selector:
    app: fake
---
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute # (6)
metadata:
  name: fake
  labels:
    example: grpc-routing
spec:
  parentRefs:
    - name: eg-example # (7)
  hostnames:
    - "grpc-example.com"
  rules:
    - backendRefs: # (8)
        - group: ""
          kind: Service
          name: fake
          port: 9000
          weight: 1
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: retry-policy
  namespace: default
spec:
  # faultInjection:
  #   abort:
  #     grpcStatus: 14
  #     percentage: 50
  retry: # (9)
    numRetries: 5
    perRetry:
      backOff:
        baseInterval: 100ms
        maxInterval: 10s
      timeout: 250ms
    retryOn:
      triggers:
      - unavailable
      - retriable-status-codes
  targetRefs: # (10)
  - group: gateway.networking.k8s.io
    kind: GRPCRoute
    name: fake
  1. この指定がないと Gateway を新しく作る際にランダムな文字列が付与されて識別しにくいため、指定を強く推奨
  2. Service の設定以外に、HPA や Deployment などの設定も可能
  3. GatewayClass に EnvoyProxy を紐付け
  4. port 80 の HTTP リクエストを受け付ける設定(他の port や TCP などのプロトコルも設定可能)
  5. リトライ検証のため、fake-service のエラー率を 50% に設定
  6. GRPCRoute の設定
  7. Gateway との紐付け
  8. Service との紐付け
  9. リトライの設定
  10. リトライの対象の指定
各リソースの依存関係
graph TD
  subgraph Gateways
    envoyProxy[EnvoyProxy: custom-envoy-proxy]
    gatewayClass[GatewayClass: eg-example]
    gateway[Gateway: eg-example]
  end

  subgraph Deployments & Services
    fake[Deployment: fake]
    fakeService[Service: fake]
  end

  subgraph Routing & Policies
    grpcRoute[GRPCRoute: fake]
    faultInjection[BackendTrafficPolicy: retry-policy]
  end

  gatewayClass --> envoyProxy 
  gateway --> gatewayClass
  faultInjection --> grpcRoute
  grpcRoute --> gateway

  grpcRoute --> fakeService

リトライの動作検証

Envoy Gateway の gRPC リトライを検証します。

export GATEWAY_HOST=$(kubectl get gateway/eg-example -o jsonpath='{.status.addresses[0].value}')
grpcurl -plaintext -authority=grpc-example.com ${GATEWAY_HOST}:80 FakeService.Handle

結果は次のようになります。

1
2
3
{
  "Message": "{\n  \"name\": \"Service\",\n  \"type\": \"gRPC\",\n  \"ip_addresses\": [\n    \"x.y.z.32\"\n  ],\n  \"start_time\": \"2025-02-19T06:06:12.655413\",\n  \"end_time\": \"2025-02-19T06:06:12.655454\",\n  \"duration\": \"40.876µs\",\n  \"body\": \"Hello World\",\n  \"code\": 0\n}\n"
}
kubectl logs svc/fake -f

上記のコマンドでログを見ると、error_injector によってエラーが発生しています。x-request-id valueが同じであることから、同じリクエストに対して Envoy Gateway がリトライし、この例では最終的に成功したことがわかります。

2025-02-19T06:06:12.653Z [INFO]  Handling request gRPC request:
  context=
  | key: :authority value: [grpc-example.com]
  | key: content-type value: [application/grpc]
  | key: grpc-accept-encoding value: [gzip]
  | key: x-forwarded-for value: [xx.yy.zz.1]
  | key: user-agent value: [grpcurl/1.9.2 grpc-go/1.61.0]
  | key: x-forwarded-proto value: [http]
  | key: x-envoy-external-address value: [xx.yy.zz.1]
  | key: x-request-id value: [b93fa578-5291-4023-9f2a-63feca3d07ea]

2025-02-19T06:06:12.653Z [INFO]  error_injector: Injecting error: request_count=2 error_percentage=0.5 error_type=http_error
2025-02-19T06:06:12.653Z [ERROR] Error handling request: error="Service error automatically injected"
2025-02-19T06:06:12.653Z [INFO]  Finished handling request: duration="125.503µs"
2025-02-19T06:06:12.655Z [INFO]  Handling request gRPC request:
  context=
  | key: user-agent value: [grpcurl/1.9.2 grpc-go/1.61.0]
  | key: grpc-accept-encoding value: [gzip]
  | key: x-request-id value: [b93fa578-5291-4023-9f2a-63feca3d07ea]
  | key: :authority value: [grpc-example.com]
  | key: x-forwarded-for value: [xx.yy.zz.1]
  | key: x-forwarded-proto value: [http]
  | key: x-envoy-external-address value: [xx.yy.zz.1]
  | key: content-type value: [application/grpc]

2025-02-19T06:06:12.655Z [INFO]  Finished handling request: duration="52.877µs"

同じ namespace からのアクセス確認

default の namespace にある fake へ、同じ名前空間から Envoy Gateway を通してアクセスする場合は次のようになります。 grpcurl を使って Envoy Gateway のサービスにリクエストを送信します。

kubectl run grpcurl --image=fullstorydev/grpcurl:latest --namespace default -it --rm -- -plaintext -authority=grpc-example.com grpc-gateway.envoy-gateway-system.svc.cluster.local:80 FakeService.Handle

クライアントのログを確認すると通信が成功していることがわかります。

kubectl logs grpcurl
1
2
3
{
  "Message": "{\n  \"name\": \"Service\",\n  \"type\": \"gRPC\",\n  \"ip_addresses\": [\n    \"xxx.yyy.zzz.34\"\n  ],\n  \"start_time\": \"2025-02-20T07:55:33.765312\",\n  \"end_time\": \"2025-02-20T07:55:33.765457\",\n  \"duration\": \"145.169µs\",\n  \"body\": \"Hello World\",\n  \"code\": 0\n}\n"
}

サーバーのログを確認すると、先ほどと同様に失敗した後、同じx-request-idで Envoy Gateway がリトライしていることがわかります。

fake-74cb56bd56-kfx9h grpcsrv 2025-02-20T07:58:17.985Z [INFO]  Handling request gRPC request:
fake-74cb56bd56-kfx9h grpcsrv   context=
fake-74cb56bd56-kfx9h grpcsrv   | key: x-forwarded-for value: [xxx.yyy.zzz.42]
fake-74cb56bd56-kfx9h grpcsrv   | key: x-forwarded-proto value: [http]
fake-74cb56bd56-kfx9h grpcsrv   | key: :authority value: [grpc-example.com]
fake-74cb56bd56-kfx9h grpcsrv   | key: content-type value: [application/grpc]
fake-74cb56bd56-kfx9h grpcsrv   | key: x-envoy-external-address value: [xxx.yyy.zzz.42]
fake-74cb56bd56-kfx9h grpcsrv   | key: x-request-id value: [6e8c6e70-0133-475c-a070-6aeb3638de5c]
fake-74cb56bd56-kfx9h grpcsrv   | key: user-agent value: [grpcurl/v1.9.2 grpc-go/1.61.0]
fake-74cb56bd56-kfx9h grpcsrv   | key: grpc-accept-encoding value: [gzip]
fake-74cb56bd56-kfx9h grpcsrv
fake-74cb56bd56-kfx9h grpcsrv 2025-02-20T07:58:17.985Z [INFO]  error_injector: Injecting error: request_count=36 error_percentage=0.5 error_type=http_error
fake-74cb56bd56-kfx9h grpcsrv 2025-02-20T07:58:17.986Z [ERROR] Error handling request: error="Service error automatically injected"
fake-74cb56bd56-kfx9h grpcsrv 2025-02-20T07:58:17.986Z [INFO]  Finished handling request: duration=1.097642ms
fake-74cb56bd56-kfx9h grpcsrv 2025-02-20T07:58:18.052Z [INFO]  Handling request gRPC request:
fake-74cb56bd56-kfx9h grpcsrv   context=
fake-74cb56bd56-kfx9h grpcsrv   | key: content-type value: [application/grpc]
fake-74cb56bd56-kfx9h grpcsrv   | key: grpc-accept-encoding value: [gzip]
fake-74cb56bd56-kfx9h grpcsrv   | key: x-request-id value: [6e8c6e70-0133-475c-a070-6aeb3638de5c]
fake-74cb56bd56-kfx9h grpcsrv   | key: user-agent value: [grpcurl/v1.9.2 grpc-go/1.61.0]
fake-74cb56bd56-kfx9h grpcsrv   | key: x-forwarded-for value: [xxx.yyy.zzz.42]
fake-74cb56bd56-kfx9h grpcsrv   | key: x-forwarded-proto value: [http]
fake-74cb56bd56-kfx9h grpcsrv   | key: x-envoy-external-address value: [xxx.yyy.zzz.42]
fake-74cb56bd56-kfx9h grpcsrv   | key: :authority value: [grpc-example.com]
fake-74cb56bd56-kfx9h grpcsrv
fake-74cb56bd56-kfx9h grpcsrv 2025-02-20T07:58:18.052Z [INFO]  Finished handling request: duration="210.211µs"

Gateway の Fault Injection と Retry の併用

本検証では、バックエンドの fake-service 側で Fault Injection を設定しました。Envoy Gateway 自身にも Fault Injection の機能がありますが、Gateway 上でリトライと同時に設定したところ、期待した再試行は行われませんでした。

Gateway 上で Fault Injection を有効化したとき、想定していた動作は次のシーケンスです。

sequenceDiagram
  participant C as Client
  participant G as Gateway
  participant S as Service
  C->>G: Request
  G->>S: Request
  S->>G: Response 500
  G->>S: Request
  S->>G: Response 500
  G->>S: Request
  S->>G: Response 200
  G->>C: Response 200

しかし実際に試すと、次のように Gateway 自体がエラーを生成してクライアントへ応答し、バックエンドへの再試行は行われませんでした。

sequenceDiagram
  participant C as Client
  participant G as Gateway
  participant S as Service
  C->>G: Request
  G->>C: Response 500

このため、Gateway のリトライ挙動を検証したい場合、疑似障害は Gateway 自身ではなくバックエンド側で注入します。

まとめ

Envoy Gateway を導入したことで、gRPC クライアント側で再試行ロジックを実装しなくても、インフラ層で透過的にリトライを制御できました。

  • GRPCRoute を対象にした BackendTrafficPolicy のretryOn.triggers(本記事ではunavailableとretriable-status-codes)で、再試行の条件を宣言的に定義できる
  • バックエンドが一時的にエラーを返しても、numRetriesで指定した回数(本記事では 5 回)以内の再試行で成功すれば、クライアントには成功レスポンスが返る
  • Gateway の Fault Injection と Retry を併用するとリトライが行われなかったため、障害注入はバックエンドサービス側で行う

クライアントのコードに再試行ロジックを持ち込まずに通信の信頼性を高めたい場合、Envoy Gateway によるリトライ制御は有効な選択肢です。