コンテンツにスキップ

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

Envoy Gateway を使った gRPC のリトライを試してみました。検証で使う fake-service の fault-injection により、一定の確率でエラーを返すように設定しています。Envoy Gateway は、エラーが発生した場合にリトライを行います。

構成

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でリトライされていることがわかります。

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"

気になったこと

Envoy Gateway で Fault Injection と Retry をまとめてやろうとしたらうまくいかなかった

これまでの手順では fake-service を使って Fault Injection の設定をしていました。Envoy Gateway の機能にも Fault Injection がありますが、うまく動作しなかったので fake-service を使って Fault Injection を設定しました。 Envoy Gateway で Fault Injection と Retry を設定したら下記のような挙動になると想定していました。 しかし、実際は Fault Injection が設定されていると、Envoy Gateway での Retry が行われませんでした。

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

上記のような挙動になると想定していましたが、実際には Fault Injection が Gateway で実行される以下のような挙動になりました。

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