重试调用

到目前为止,我们只在服务器端工作。现在,让我们看看客户端上的一个重要功能——根据状态码重试失败的调用。这在网络不稳定的情况下可能非常有用。如果我们遇到 Unavailable 错误码,我们将使用指数增长的等待时间进行重试。这样做是因为我们不希望过于频繁地重试,导致网络过载。

gRPC 本身支持重试,而无需第三方库。然而,配置过程相对冗长,且文档并不完善。如果您有兴趣尝试,可以查看以下示例: gRPC Go 示例

接下来,我们需要获取所需的依赖(在 client 文件夹中):

$ go get github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry

然后,我们可以定义一些重试选项。我们将定义重试次数和在何种错误码下进行重试。我们希望重试 3 次,使用指数回退(从 100 毫秒开始),并且错误码为 Unavailable

retryOpts := []retry.CallOption{
    retry.WithMax(3),
    retry.WithBackoff(retry.BackoffExponential(100 * time.Millisecond)),
    retry.WithCodes(codes.Unavailable),
}

接着,我们将这些选项传递给 retry 包提供的拦截器:

import (
    //...
    "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
)

func main() {
    //...
    retryOpts := []retry.CallOption{
        //...
    }
    opts := []grpc.DialOption{
        //...
        grpc.WithChainUnaryInterceptor(
            retry.UnaryClientInterceptor(retryOpts...),
            //...
        ),
        grpc.WithChainStreamInterceptor(
            retry.StreamClientInterceptor(retryOpts...),
            //...
        ),
        //...
    }
    //...
}

重试对于客户端流式 RPC 是不可用的。如果您尝试在这种 RPC 接口上进行重试,您将得到以下错误:rpc error: code = Unimplemented desc = grpc_retry: cannot retry on ClientStreams, set grpc_retry.Disable(). 因此,添加 retry.StreamClientInterceptor 是有风险的。我们仅想展示一些流式操作也可以进行重试。

一旦我们完成了这些配置,问题就来了。我们的 API 正在本地运行,并且几乎不可能遇到 Unavailable 错误。因此,为了测试和演示,我们将暂时让 AddTask 直接返回这样的错误。在 server/impl.go 中,我们可以注释掉其余的代码并添加以下内容:

func (s *server) AddTask(_ context.Context, in *pb.AddTaskRequest) (*pb.AddTaskResponse, error) {
    return nil, status.Errorf(
        codes.Unavailable,
        "unexpected error: %s",
        "unavailable",
    )
}

然后,我们运行服务器:

$ go run ./server 0.0.0.0:50051 0.0.0.0:50052
metrics server listening at 0.0.0.0:50052
gRPC server listening at 0.0.0.0:50051

接着运行我们的客户端:

$ go run ./client 0.0.0.0:50051
--------ADD--------
rpc error: code = Unavailable desc = unexpected error: unavailable

我们得到一个错误。虽然看起来只进行了一个查询,但如果你查看服务器日志,应该能看到如下信息:

INFO :started call todo.v2.TodoService AddTask
WARN :finished call todo.v2.TodoService AddTask
INFO :started call todo.v2.TodoService AddTask
WARN :finished call todo.v2.TodoService AddTask
INFO :started call todo.v2.TodoService AddTask
WARN :finished call todo.v2.TodoService AddTask

实际上,这是进行了三次请求。

Bazel

和往常一样,您需要运行 gazelle-update-reposgazelle 来获取新的依赖并将其链接到您的库:

$ bazel run //:gazelle-update-repos
$ bazel run //:gazelle

现在,您应该能够正确运行客户端:

$ bazel run //client:client 0.0.0.0:50051
--------ADD--------
rpc error: code = Unavailable desc = unexpected error: unavailable

总结一下,在这一节中,我们展示了如何根据某些条件进行重试,使用指数回退,并且设定重试的次数。重试是一个重要的功能,因为网络通常不可靠,我们不希望每次出现问题时都让用户手动重试。