ECS Service Connect で gRPC を利用した際のリトライポリシーを確認してみる

ECS Service Connect で gRPC を利用した際のリトライポリシーを確認してみる

ECS Service Connect を利用することで、特定条件のレスポンスを受け取った際に自動でリトライ処理を組み込むことができます。
この際、Envoy の管理をしなくてもサービスメッシュを組めるので非常に楽ですが、細かいカスタマイズはできません。
よりカスタマイズの幅が広い App Mesh の場合、gRPC 通信時に下記からリトライイベントを複数選択して設定することが可能です。

  • unavailable (一時的にサービスが利用できない)
  • internal (内部エラー)
  • cancelled (処理がキャンセルされた)
  • resource-exhausted (何らかのリソースが使い果たされている)
  • deadline-exceeded (操作が完了する前にタイムアウトした)

https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_GrpcRetryPolicy.html

ECS Service Connect を利用した際にこの辺りの設定値がどうなるかを確認してみました。

試してみる

下記構成で実際にリクエストを送信して試してみます。

ecs-sc.png

ECS Service Connect に属する ECS タスクを 2 つ用意します。
入力に依ってステータスコードを変更できるサーバー用の ECS タスクを用意して、クライアント用の ECS タスクから grpcurl でリクエストを送信します。
サーバー側は grpc 公式のサンプルコードを活かして下記のように作りました。

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

type server struct{
    pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.Name)

    // unavailableエラーのシミュレーション
    if in.Name == "unavailable" {
        return nil, status.Error(codes.Unavailable, "The service is currently unavailable.")
    }

    // cancelledエラーのシミュレーション
    if in.Name == "cancelled" {
        return nil, status.Error(codes.Canceled, "The operation was cancelled.")
    }

    // internalエラーのシミュレーション
    if in.Name == "internal" {
        return nil, status.Error(codes.Internal, "Internal errors occurred.")
    }

    // resource-exhaustedエラーのシミュレーション
    if in.Name == "resourceExhausted" {
        return nil, status.Error(codes.ResourceExhausted, "Some resource has been exhausted.")
    }

    // deadline-exceededエラーのシミュレーション
    if in.Name == "deadlineExceeded" {
        return nil, status.Error(codes.DeadlineExceeded, "The deadline expired before the operation could complete.")
    }

    // 入力値の検証
    if in.Name == "" {
        return nil, status.Error(codes.InvalidArgument, "名前が空です。有効な名前を指定してください")
    }

	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    reflection.Register(s)
    log.Printf("serving on :50051\n")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

クライアント側でリクエストを送った際に、リトライが行われるかどうかはサーバー側のログから判断します。

unavailable(一時的にサービスが利用できない)の場合

エラーコードが unavailable となるようにリクエストを送信します。

$ grpcurl -v -d '{"name": "unavailable"}' -plaintext app.masukawa-test-ecs-namespace:50051 helloworld.Greeter.SayHello

Resolved method descriptor:
rpc SayHello ( .helloworld.HelloRequest ) returns ( .helloworld.HelloReply );

Request metadata to send:
(empty)

Response headers received:
(empty)

Response trailers received:
content-type: application/grpc
date: Sun, 06 Apr 2025 06:03:36 GMT
server: envoy
x-envoy-upstream-service-time: 33
Sent 1 request and received 0 responses
ERROR:
  Code: Unavailable
  Message: The service is currently unavailable.

クライアント側でのリクエスト 1 回に対して、サーバー側では 3 回分のリクエストが記録されていました。

grpc-unavailable.png

ECS Service Connect を利用した場合、リトライ回数は 2 回で固定です。

Service Connect は、プロキシを通過して失敗した接続を再試行するようにプロキシを設定し、2 回目の試行では、前の接続のホストを使用しません。これにより、Service Connect を介した各接続が 1 回限りの理由で失敗することがなくなります。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-connect-concepts-deploy.html#service-connect-concepts-proxy

unavailable の場合は、丁度 2 回分のリトライが行われていると考えられます。

internal(内部エラー)の場合

エラーコードが internal となるようにリクエストを送信します。

$ grpcurl -v -d '{"name": "internal"}' -plaintext app.masukawa-test-ecs-namespace:5
0051 helloworld.Greeter.SayHello

Resolved method descriptor:
rpc SayHello ( .helloworld.HelloRequest ) returns ( .helloworld.HelloReply );

Request metadata to send:
(empty)

Response headers received:
(empty)

Response trailers received:
content-type: application/grpc
date: Sun, 06 Apr 2025 06:04:33 GMT
server: envoy
x-envoy-upstream-service-time: 1
Sent 1 request and received 0 responses
ERROR:
  Code: Internal
  Message: Internal errors occurred.

この際、クライアント側のリクエスト 1 回に対して、サーバー側では 1 回分のリクエストが記録されます。

grpc-internal.png

internal の場合では、リトライされないようです。

cancelled(処理がキャンセルされた)の場合

エラーコードが cancelled となるようにリクエストを送信します。

$ grpcurl -v -d '{"name": "cancelled"}' -plaintext app.masukawa-test-ecs-namespace:
50051 helloworld.Greeter.SayHello

Resolved method descriptor:
rpc SayHello ( .helloworld.HelloRequest ) returns ( .helloworld.HelloReply );

Request metadata to send:
(empty)

Response headers received:
(empty)

Response trailers received:
content-type: application/grpc
date: Sun, 06 Apr 2025 06:05:40 GMT
server: envoy
x-envoy-upstream-service-time: 0
Sent 1 request and received 0 responses
ERROR:
  Code: Canceled
  Message: The operation was cancelled.

cancelled の場合もリトライされないようです。

grpc-cancelled.png

resource-exhausted(何らかのリソースが使い果たされている)の場合

エラーコードが resource-exhausted となるようにリクエストを送信します。

$ grpcurl -v -d '{"name": "resourceExhausted"}' -plaintext app.masukawa-test-ecs-n
amespace:50051 helloworld.Greeter.SayHello
Too few arguments.
Try 'grpcurl -help' for more details.
bash: amespace:50051: command not found
root@ip-10-0-11-140:/app# grpcurl -v -d '{"name": "resourceExhausted"}' -plaintext app.masukawa-test-ecs-namespace:50051 helloworld.Greeter.SayHello

Resolved method descriptor:
rpc SayHello ( .helloworld.HelloRequest ) returns ( .helloworld.HelloReply );

Request metadata to send:
(empty)

Response headers received:
(empty)

Response trailers received:
content-type: application/grpc
date: Mon, 07 Apr 2025 02:02:02 GMT
server: envoy
x-envoy-upstream-service-time: 74
Sent 1 request and received 0 responses
ERROR:
  Code: ResourceExhausted
  Message: Some resource has been exhausted.

resource-exhausted の場合もリトライされないようです。

grpc-resource-exhausted.png

deadline-exceeded

エラーコードが deadline-exceeded となるようにリクエストを送信します。

$ grpcurl -v -d '{"name": "deadlineExceeded"}' -plaintext app.masukawa-test-ecs-na
mespace:50051 helloworld.Greeter.SayHello

Resolved method descriptor:
rpc SayHello ( .helloworld.HelloRequest ) returns ( .helloworld.HelloReply );

Request metadata to send:
(empty)

Response headers received:
(empty)

Response trailers received:
content-type: application/grpc
date: Mon, 07 Apr 2025 02:06:36 GMT
server: envoy
x-envoy-upstream-service-time: 4
Sent 1 request and received 0 responses
ERROR:
  Code: DeadlineExceeded
  Message: The deadline expired before the operation could complete.

deadline-exceeded の場合もリトライされないようです。

grpc-deadline-exceeded.png

まとめ

下記結果となりました。

ステータス リトライ
unavailable する(2 回)
internal しない
cancelled しない
resource-exhausted しない
deadline-exceeded しない

ECS Service Connect で GrpcRetryPolicygrpcRetryEvents に当たる設定は App Mesh のデフォルトルート再試行ポリシーと同じ UNAVAILABLE のみになっているようです。
App Mesh からの移行などを考えるとカスタマイズできると嬉しいですが、現時点では必要に応じてアプリケーション側で上手くリトライ処理を組み込むのが良いと思います。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.