gRPC 进阶——异常处理
gRPC 异常处理是指在 gRPC 通信过程中有效捕获和处理错误和异常的机制,以确保客户端和服务器之间的通信能够在出现错误时正确处理并提供有意义的反馈。gRPC 使用状态码(Status Codes)来表示各种类型的错误,这些状态码涵盖了从客户端错误到服务器错误的广泛范围。通过状态码,开发者可以明确错误的类型和原因,从而采取相应的措施进行处理。
gRPC状态码
这一小节我们将介绍一下gRPC内置的状态码,详细请见官方文档。
当然在我们自己的项目中,也可以自定义状态码,不过需要在多端维护。
gRPC 使用一组明确定义的状态代码作为 RPC API 的一部分。这些状态定义如下:
Code | Number | Description |
---|---|---|
OK | 0 | 不是错误;成功返回。 |
CANCELLED | 1 | 操作通常被调用者取消。 |
UNKNOWN | 2 | 未知错误。例如,当从另一个地址空间接收到的 Status 值属于该地址空间中未知的错误空间时,可能会返回此错误。此外,由未返回足够错误信息的 API 引发的错误也可能会转换为此错误。 |
INVALID_ARGUMENT | 3 | 客户端指定了无效参数。请注意,这与 FAILED_PRECONDITION 不同。 INVALID_ARGUMENT 表示无论系统状态如何都有问题的参数(例如,格式错误的文件名)。 |
DEADLINE_EXCEEDED | 4 | 操作完成之前截止日期已过。对于更改系统状态的操作,即使操作已成功完成,也可能会返回此错误。例如,服务器的成功响应可能会延迟很长时间 |
NOT_FOUND | 5 | 找不到某些请求的实体(例如文件或目录)。服务器开发人员请注意:如果整个类别的用户的请求被拒绝,例如逐步推出功能或未记录的许可名单,则可以使用 NOT_FOUND 。如果一类用户中的某些用户的请求被拒绝,例如基于用户的访问控制,则必须使用 PERMISSION_DENIED 。 |
ALREADY_EXISTS | 6 | 客户端尝试创建的实体(例如文件或目录)已存在。 |
PERMISSION_DENIED 没有权限 | 7 | 调用者没有执行指定操作的权限。 PERMISSION_DENIED 不得用于因耗尽某些资源而导致的拒绝(对于这些错误,请使用 RESOURCE_EXHAUSTED 代替)。如果无法识别调用者,则不得使用 PERMISSION_DENIED (对于这些错误,请使用 UNAUTHENTICATED 代替)。此错误代码并不意味着请求有效或请求的实体存在或满足其他先决条件。 |
RESOURCE_EXHAUSTED | 8 | 某些资源已耗尽,可能是每个用户的配额,或者可能是整个文件系统空间不足。 |
FAILED_PRECONDITION | 9 | 操作被拒绝,因为系统未处于操作执行所需的状态。例如,要删除的目录非空、对非目录应用 rmdir 操作等。服务实现者可以使用以下准则来决定 FAILED_PRECONDITION 和 ABORTED 和 UNAVAILABLE :(a) 如果客户端可以仅重试失败的调用,则使用 UNAVAILABLE 。 (b) 如果客户端应在更高级别重试(例如,当客户端指定的测试和设置失败时,指示客户端应重新启动读取-修改-写入序列),请使用 ABORTED 。 © 如果客户端在系统状态明确修复之前不应重试,请使用 FAILED_PRECONDITION 。例如,如果“rmdir”由于目录非空而失败,则应返回 FAILED_PRECONDITION ,因为除非从目录中删除文件,否则客户端不应重试。 |
ABORTED | 10 | 操作被中止,通常是由于并发问题,例如定序器检查失败或事务中止。请参阅上面的指南来决定 FAILED_PRECONDITION 、 ABORTED 和 UNAVAILABLE 。 |
OUT_OF_RANGE | 11 | 尝试的操作超出了有效范围。例如,查找或读取文件末尾之后的内容。与 INVALID_ARGUMENT 不同,此错误指示如果系统状态发生变化则可能会修复的问题。例如,如果要求读取不在 [0,2^32-1] 范围内的偏移量,32 位文件系统将生成 INVALID_ARGUMENT ,但它会生成 OUT_OF_RANGE 和 OUT_OF_RANGE 之间有相当多的重叠。我们建议在应用时使用 OUT_OF_RANGE (更具体的错误),以便迭代空间的调用者可以轻松查找 OUT_OF_RANGE 错误来检测它们何时完成。 |
UNIMPLEMENTED | 12 | 此服务未实现或不支持/启用该操作。 |
INTERNAL | 13 | 内部错误。这意味着底层系统所期望的一些不变量已经被打破。该错误代码是为严重错误保留的。 |
UNAVAILABLE | 14 | 该服务目前不可用。这很可能是一种瞬态情况,可以通过后退重试来纠正。请注意,重试非幂等操作并不总是安全的。 |
DATA_LOSS | 15 | 不可恢复的数据丢失或损坏。 |
UNAUTHENTICATED | 16 | 该请求没有用于该操作的有效身份验证凭据。 |
在客户端启动的所有 RPC 都会返回一个由整数 code
和字符串 message
组成的 status
对象。服务器端可以选择为给定 RPC 返回的状态。应用程序应仅使用上面定义的值。遇到超出此范围的值的 gRPC 库必须直接传播它们或将它们转换为 UNKNOWN。
当发生错误时,gRPC 客户端和服务器端实现也可能自行生成并返回 status
。 gRPC 库仅生成预定义状态代码的子集。这允许应用程序确保它看到的任何其他代码实际上是由应用程序返回的(尽管服务器端也可以返回 gRPC 库生成的代码之一)。
下表列出了 gRPC 库(在客户端或服务器端)可能返回的代码,并总结了它们的生成情况。
Case | Code | Generated at Client or Server |
---|---|---|
客户端应用程序取消了请求 | CANCELLED | Both |
服务器返回状态之前截止日期已过 | DEADLINE_EXCEEDED | Both |
在服务器上找不到方法 | UNIMPLEMENTED | Server |
服务器关闭 | UNAVAILABLE | Server |
服务器端应用程序抛出异常(或者执行除返回状态代码之外的其他操作来终止 RPC) | UNKNOWN | Server |
截止日期到期前未收到回复。当客户端无法向服务器发送请求或服务器未能及时响应时,可能会发生这种情况。 | DEADLINE_EXCEEDED | Both |
连接中断之前传输的一些数据(例如,写入 TCP 连接的请求元数据) | UNAVAILABLE | Client |
无法解压,但支持压缩算法(客户端 -> 服务器) | INTERNAL | Server |
无法解压,但支持压缩算法(服务器 -> 客户端) | INTERNAL | Client |
服务器不支持客户端使用的压缩机制 | UNIMPLEMENTED | Server |
服务器暂时缺少资源(例如,达到流量控制资源限制) | RESOURCE_EXHAUSTED | Server |
客户端没有足够的内存来保存服务器响应 | RESOURCE_EXHAUSTED | Client |
违反流量控制协议 | INTERNAL | Both |
解析返回状态时出错 | UNKNOWN | Client |
身份验证元数据不正确(凭据无法获取元数据、通道和调用上设置的凭据不兼容、 :authority 元数据中设置的主机无效等) | UNAUTHENTICATED | Both |
请求基数违规(方法只需要一个请求,但客户端发送了其他数量的请求) | UNIMPLEMENTED | Server |
响应基数违规(方法只需要一个响应,但服务器发送了其他数量的响应) | UNIMPLEMENTED | Client |
解析响应原型时出错 | INTERNAL | Client |
解析请求原型时出错 | INTERNAL | Server |
发送或接收的消息大于配置的限制 | RESOURCE_EXHAUSTED | Both |
看门狗超时 | UNAVAILABLE | Both |
gRPC库永远不会生成以下状态代码:
- INVALID_ARGUMENT
- NOT_FOUND
- ALREADY_EXISTS
- FAILED_PRECONDITION
- ABORTED
- OUT_OF_RANGE
- DATA_LOSS
可能希望重试失败的 RPC 的应用程序必须决定重试哪些状态代码。如上表所示,gRPC 库可以针对不同情况生成相同的状态代码。服务器应用程序也可以返回相同的状态代码。因此,不存在适合在所有应用程序中重试的固定状态代码列表。因此,各个应用程序必须自行确定哪些状态代码应导致 RPC 重试。
gRPC错误处理
这一小节的代码示例我们还是按照gRPC入门的代码作为示例:
0.目录树如下
.
├── client
│ └── client.go
├── proto
│ ├── helloworld_grpc.pb.go
│ ├── helloworld.pb.go
│ └── helloworld.proto
└── server
└── server.go
1. 编写proto/helloworld.proto代码
syntax = "proto3";
option go_package = ".;proto";
service Hello {
rpc Hello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string response = 1;
}
利用命令生成对应的go文件
cd proto/
protoc helloworld.proto --go_out=. --go-grpc_out=.
2. 编写server/server.go代码
package main
import (
"context"
"google.golang.org/grpc"
"net"
"go-try/grpc_error/proto"
)
type HelloServer struct {
proto.UnimplementedHelloServer
}
func (s *HelloServer) Hello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
return &proto.HelloResponse{
Response: "Hello " + request.Name,
}, nil
}
func main() {
server := grpc.NewServer()
proto.RegisterHelloServer(server, &HelloServer{})
listen, err := net.Listen("tcp", "0.0.0.0:8080")
if err != nil {
panic("faild to listen : " + err.Error())
}
err = server.Serve(listen)
if err != nil {
panic("faild to start grpc : " + err.Error())
}
}
3.编写client/client.go代码
package main
import (
"context"
"fmt"
"go-try/grpc_error/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
func main() {
conn, err := grpc.NewClient("0.0.0.0:8080", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
cli := proto.NewHelloClient(conn)
msg, err := cli.Hello(context.Background(), &proto.HelloRequest{Name: "张三"})
if err != nil {
st, ok := status.FromError(err)
if !ok {
panic("解析失败")
}
fmt.Println("code: ", st.Code(), " message: ", st.Message())
}
fmt.Println(msg)
}
4.分别运行server.go和client.go
运行结果如下:
response:"Hello 张三"
这是运行成功的示例。但是我们需要了解如何进行错误处理,于是我们修改server端代码。
5. 修改server/server.go代码
我们将Hello()函数修改
func (s *HelloServer) Hello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
return nil, status.Error(codes.NotFound, "数据库中不存在 "+request.Name)
}
再次分别运行分别运行server.go和client.go,
运行结果如下:
code: NotFound , message: 数据库中不存在 张三
这说明我们已经成功完成了这个错误处理的小demo。相信你已经学会了如何使用gRPC错误处理。
更多推荐
所有评论(0)