gRPC 的错误处理


gRPC code


当发起 gRPC 调用时,客户端会接收成功状态的响应或带有对应错误状态的错误;在编写客户端应用程序时需处理所以潜在的错误和错误条件,编写服务端应用程序也需要处理错误并生成适当的错误状态码。

当发生错误时,gRPC 会返回一个错误状态码并附带一条可选的错误消息,该消息提供错误条件的更多细节。类似于 HTTP 定义了一套响应状态码,gRPC 也定义有一些状态码,Go 语言中此状态码由 codes 定义,本质上是一个 uint32 类型,使用时需导入 google.golang.org/grpc/codes 包。

type Code uint32
import "google.golang.org/grpc/codes"

gRPC 使用一组定义好的专用状态码,目前已经定义的状态码有如下几种:

Code含义
OK0请求成功,成功状态
Canceled1操作已被(被调用)取消
Unknown2未知错误,如果从另一个地址空间接收到的状态值属于在该地址空间中未知的错误空间,则可以返回此错误的示例,没有返回足够的错误信息的 API 引发的错误也可能会转换为此错误
InvalidArgument3表示客户端指定的参数无效(注:这与 FailedPrecondition 不同),它表示无论系统状态如何都有问题的参数(如格式错误的文件名)
DeadlineExceeded4表示操作在完成之前已过期,超过截止时间,对于改变系统状态的操作,即使操作成功完成,也可能会返回此错误(如来自服务器的成功响应可能已延迟足够长的时间以使截止日期到期)
NotFound5表示未找到某些请求的实体(如文件或目录)
AlreadyExists6创建实体的尝试失败,因为实体已经存在
PermissionDenied7表示调用者没有权限执行指定的操作, 它不能用于拒绝由耗尽某些资源引起的(使用 ResourceExhausted ), 如果无法识别调用者,也不能使用它(使用 Unauthenticated )
ResourceExhausted8表示某些资源已耗尽,可能是每个用户的配额,或者整个文件系统空间不足
FailedPrecondition9指示操作被拒绝,因为系统未处于操作执行所需的状态(如要删除的目录可能是非空的,rmdir 操作应用于非目录等)
Aborted10表示操作被中止,通常是由于并发问题,如排序器检查失败、事务中止等
OutOfRange11表示尝试超出有效范围的操作
Unimplemented12表示此服务中未实施或不支持/启用操作
Internal13意味着底层系统预期的一些不变量已被破坏,若看到这个错误,则说明问题很严重
Unavailable14表示服务当前不可用,这很可能是暂时的情况,可以通过回退重试来纠正(请注意:重试非幂等操作并不总是安全的)
DataLoss15表示不可恢复的数据丢失或损坏
Unauthenticated16表示请求没有用于操作的有效身份验证凭据
_maxCode17-

gRPC Status


RPC 服务的方法应该返回 nil 或来自 status.Status 类型的错误,客户端可以直接访问错误,Go 语言使用 gRPC Status 时需导入 google.golang.org/grpc/status 包。

import "google.golang.org/grpc/status"

当遇到错误时,gRPC 服务的方法函数应该创建一个 status.Status ,通常调用 status.New() 函数并传入适当的 status.Code 和错误描述来生成一个 status.Status ,调用 status.Err() 方法便能将一个 status.Status 转为 error 类型或者调用 status.Error() 方法直接生成 error ,下面是两种方式的比较:

// 创建 status.Status
st := status.New(codes.NotFound, "some description")
err := st.Err()  // 转为error类型

// vs

err := status.Error(codes.NotFound, "some description")

在某些情况下,可能需要为服务器端的特定错误添加详细信息 ,status.WithDetails() 方法可以添加任意多个 proto.Message ,可以使用 google.golang.org/genproto/googleapis/rpc/errdetails 包中的定义或自定义的错误详情,例如以下的代码形式:

st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
ds, _ := st.WithDetails(
		// proto.Message
)
return nil, ds.Err()

客户端可以通过首先将普通 error 类型转换回 status.Status ,然后调用 status.Details() 方法来读取这些详细信息,例如以下的代码形式:

s := status.Convert(err)
for _, d := range s.Details() {
		// ...
}

gRPC 程序示例


假如为 hello 服务设置访问限制,每个 name 只能调用一次 SayHello() 方法,超过此限制就返回一个请求超过限制的错误,编写实现程序的步骤如下:

(1)在任意目录下创建 serverclient 项目并初始化( go mod init server / client ),分别在服务端和客户端目录下创建一个 proto 目录,编写 hello.proto 文件如下内容:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本

option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径

package pb; // 包名


// 定义服务
service Greeter {
    	// SayHello 方法
    	rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
    string name = 1;
}

// 响应消息
message HelloResponse {
    string reply = 1;
}

此时,项目的目录结构如下所示:

ErrorHandle
├── client
│   ├── go.mod
│   ├── go.sum
│   └── proto
│       └── hello.proto
└── server
    ├── go.mod
    ├── go.sum
    └── proto
        └── hello.proto

生成 gRPC 源码程序,分别在客户端和服务端项目下的 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

成功生成后项目的目录结构如下所示:

ErrorHandle
├── client
│   ├── go.mod
│   ├── go.sum
│   └── proto
│       ├── hello_grpc.pb.go
│       ├── hello.pb.go
│       └── hello.proto
└── server
    ├── go.mod
    ├── go.sum
    └── proto
        ├── hello_grpc.pb.go
        ├── hello.pb.go
        └── hello.proto

(2)编写 server 端程序使用 map 存储每个 name 的请求次数,超过 1 次则返回错误,并且记录错误详情,该程序的具体代码如下:

package main

import (
        "context"
        "fmt"
        pb "server/proto"
        "net"
        "sync"
        "google.golang.org/genproto/googleapis/rpc/errdetails"
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/status"
)

// grpc server

type server struct {
        pb.UnimplementedGreeterServer
        mu    sync.Mutex     // count 的并发锁
        count map[string]int // 记录每个 name 的请求次数
}

// SayHello 是需要实现的方法对外提供的服务
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
        s.mu.Lock()
        defer s.mu.Unlock()
        s.count[in.Name]++ // 记录用户的请求次数
        // 超过 1 次就返回错误
        if s.count[in.Name] > 1 {
                st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
                ds, err := st.WithDetails(
                        &errdetails.QuotaFailure{
                                Violations: []*errdetails.QuotaFailure_Violation{{
                                        Subject:     fmt.Sprintf("name:%s", in.Name),
                                        Description: "限制每个 name 只能调用一次!",
                                }},
                        },
                )
                if err != nil {
                        return nil, st.Err()
                }
                return nil, ds.Err()
        }
        // 正常返回响应
        reply := "hello " + in.GetName()
        return &pb.HelloResponse{Reply: reply}, nil
}

func main() {
        // 启动服务
        l, err := net.Listen("tcp", ":8972")
        if err != nil {
                fmt.Printf("failed to listen, err:%v\n", err)
                return
        }
        s := grpc.NewServer() // 创建 gRPC 服务
        // 注册服务,注意初始化 count
        pb.RegisterGreeterServer(s, &server{count: make(map[string]int)})
        // 启动服务
        err = s.Serve(l)
        if err != nil {
                fmt.Printf("failed to serve,err:%v\n", err)
                return
        }
}

(3)编写 client 端程序,当服务端返回错误时,尝试从错误中获取 detail 信息,该程序的具体代码如下:

package main

import (
        "context"
        "flag"
        "fmt"
        "google.golang.org/grpc/status"
        pb "client/proto"
        "log"
        "time"
        "google.golang.org/genproto/googleapis/rpc/errdetails"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

// gRPC 客户端
// 调用 server 端的 SayHello() 方法

var name = flag.String("name", "cqupthao", "通过 -name 告诉 server 你是谁")

func main() {
        flag.Parse() // 解析命令行参数

        // 连接 server
        conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
                if err != nil {
                        log.Fatalf("grpc.Dial failed,err:%v", err)
                        return
                }
                defer conn.Close()
                // 创建客户端
                c := pb.NewGreeterClient(conn) // 使用生成的 Go 代码
                // 调用 RPC 方法
                ctx, cancel := context.WithTimeout(context.Background(), time.Second)
                defer cancel()
                resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
                if err != nil {
                        s := status.Convert(err)        // 将 err 转为 status
                        for _, d := range s.Details() { // 获取 details
                                switch info := d.(type) {
                                        case *errdetails.QuotaFailure:
                                                fmt.Printf("Quota failure: %s\n", info)
                                        default:
                                                fmt.Printf("Unexpected type: %s\n", info)
                                }
                        }
                fmt.Printf("c.SayHello failed, err:%v\n", err)
                return
        }
        // 获取到 RPC 响应
        log.Printf("resp:%v\n", resp.GetReply())
}

(4)分别执行服务端和客户端的程序,多次调用服务会输出如下的结果:

// 第一次调用服务
2023/02/15 22:02:41 resp:hello cqupthao
// 第二次调用服务
Quota failure: violations:{subject:"name:cqupthao"  description:"限制每个 name 只能调用一次!"}
c.SayHello failed, err:rpc error: code = ResourceExhausted desc = Request limit exceeded.

  • 参考链接:gRPC教程

  • 参考链接:gRPC 官网

  • 参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在使用 gRPC Java 进行通信时,我们需要考虑异常处理以确保我们的应用程序具有适当的容错能力。下面是一些常见的 gRPC 异常及其处理方法: 1. RpcException:这是一个通用的 gRPC 异常,表示 RPC 调用过程中出现了错误。可以通过捕获 RpcException 来处理 gRPC 的任何异常。例如: ```java try { // Make a gRPC call } catch (StatusRuntimeException e) { // Handle the exception } ``` 2. StatusRuntimeException:这是一个特殊的 RpcException 子类,表示 RPC 调用失败并返回了一个错误状态码。可以通过 getStatus() 方法来获取状态码并进行相应的处理。例如: ```java try { // Make a gRPC call } catch (StatusRuntimeException e) { if (e.getStatus().getCode() == Status.Code.NOT_FOUND) { // Handle the "not found" error } else if (e.getStatus().getCode() == Status.Code.PERMISSION_DENIED) { // Handle the "permission denied" error } else { // Handle any other errors } } ``` 3. DeadlineExceededException:这是一个表示 RPC 超时的异常。可以通过捕获 DeadlineExceededException 来处理超时异常。例如: ```java try { // Make a gRPC call } catch (DeadlineExceededException e) { // Handle the timeout error } ``` 4. CancellationException:这个异常表示 RPC 调用已被取消。可以通过捕获 CancellationException 来处理取消异常。例如: ```java try { // Make a gRPC call } catch (CancellationException e) { // Handle the cancellation error } ``` 以上是一些常见的 gRPC 异常及其处理方法,当然还有其他更详细的异常类型,可以在 gRPC Java 的官方文档中找到。在编写 gRPC 应用程序时,我们应该根据实际情况选择合适的异常类型并进行适当的处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

물の韜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值