• 你知道gRPC框架的错误处理原理吗?
  • 发布于 1个月前
  • 93 热度
    0 评论
基本错误处理
首先回顾下gRPC的pb文件和生成出来的client与server端的接口
service OrderManagement {
    rpc getOrder(google.protobuf.StringValue) returns (Order);
}
type OrderManagementClient interface {
 GetOrder(ctx context.Context, 
           in *wrapperspb.StringValue, opts ...grpc.CallOption) (*Order, error)
}
type OrderManagementServer interface {
 GetOrder(context.Context, *wrapperspb.StringValue) (*Order, error)
 mustEmbedUnimplementedOrderManagementServer()
}
可以看到,虽然我们没有在pb文件中的接口定义设置error返回值,但生成出来的go代码是包含error返回值的。这非常符合Go语言的使用习惯:通常情况下我们定义多个error变量,并且在函数内返回,调用方可以使用errors.Is()或者errors.As()对函数的error进行判断
var (
 ParamsErr = errors.New("params err")
 BizErr    = errors.New("biz err")
)

func Invoke(i bool) error {
 if i {
  return ParamsErr
 } else {
  return BizErr
 }
}

func main() {
 err := Invoke(true)

 if err != nil {
  switch {
  case errors.Is(err, ParamsErr):
   log.Println("params error")
  case errors.Is(err, BizErr):
   log.Println("biz error")
  }
 }
}
但,在RPC场景下,我们还能进行error的值判断么?
// 堆代码 duidaima.com
// common/errors.go
var ParamsErr = errors.New("params is not valid")
// server/main.go
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) {
 return nil, common.ParamsErr
}
// client/main.go
retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})

if err != nil && errors.Is(err, common.ParamsErr) {
  // 不会走到这里,因为err和common.ParamsErr不相等
  panic(err)
}
很明显,server和client并不在同一个进程甚至都不在同一个台机器上,所以errors.Is()或者errors.As()是没有办法做判断的

业务错误码
那么如何做?在http的服务中,我们会使用错误码的方式来区分不同错误,通过判断errno来区分不同错误
{
    "errno": 0,
    "msg": "ok",
    "data": {}
}

{
    "errno": 1000,
    "msg": "params error",
    "data": {}
}
类似的,我们调整下我们pb定义:在返回值里携带错误信息
service OrderManagement {
    rpc getOrder(google.protobuf.StringValue) returns (GetOrderResp);
}

message GetOrderResp{
    BizErrno errno = 1;
    string msg = 2;
    Order data = 3;
}

enum BizErrno {
    Ok = 0;
    ParamsErr = 1;
    BizErr = 2;
}

message Order {
    string id = 1;
    repeated string items = 2;
    string description = 3;
    float price = 4;
    string destination = 5;
}
于是在服务端实现的时候,我们可以返回对应数据或者错误状态码
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.GetOrderResp, error) {
 ord, exists := orders[orderId.Value]
 if exists {
  return &pb.GetOrderResp{
   Errno: pb.BizErrno_Ok,
   Msg:   "Ok",
   Data:  &ord,
  }, nil
 }

 return &pb.GetOrderResp{
  Errno: pb.BizErrno_ParamsErr,
  Msg:   "Order does not exist",
 }, nil
}
在客户端可以判断返回值的错误码来区分错误,这是我们在常规RPC的常见做法
// Get Order
resp, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""})
if err != nil {
  panic(err)
}

if resp.Errno != pb.BizErrno_Ok {
  panic(resp.Msg)
}

log.Print("GetOrder Response -> : ", resp.Data)
 但,这么做有什么问题么?很明显,对于clinet侧来说,本身就可能遇到网络失败等错误,所以返回值(*GetOrderResp, error)包含error并不会非常突兀。但再看一眼server侧的实现,我们把错误枚举放在GetOrderResp中,此时返回的另一个error就变得非常尴尬了,该继续返回一个error呢,还是直接都返回nil呢?两者的功能极度重合!

那有什么办法既能利用上error这个返回值,又能让client端枚举出不同错误么?一个非常直观的想法:让error里记录枚举值就可以了!但我们都知道Go里的error是只有一个string的,可以携带的信息相当有限,如何传递足够多的信息呢?gRPC官方提供了google.golang.org/grpc/status的解决方案

使用 Status处理错误
gRPC 提供了google.golang.org/grpc/status来表示错误,这个结构包含了 code 和 message 两个字段,code是类似于http status code的一系列错误类型的枚举,所有语言 sdk 都会内置这个枚举列表,虽然总共预定义了16个code,但gRPC框架并不是用到了每一个code,有些code仅提供给业务逻辑使用
Code Number Description
OK 0 成功
CANCELLED 1 调用取消
UNKNOWN 2 未知错误
... ... ...
message就是服务端需要告知客户端的一些错误详情信息
package main

import (
 "errors"
 "fmt"
 "log"

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

func Invoke() {
 ok := status.New(codes.OK, "ok")
 fmt.Println(ok)

 invalidArgument := status.New(codes.InvalidArgument, "invalid args")
 fmt.Println(invalidArgument)
}
Status 和语言 Error 的互转
上文提到无论是server和client返回的都是error,如果我们返回Status那肯定是不行的。但 Status 提供了和Error互转的方法

所以在服务端可以利用.Err()把Status转换成error并返回,或者直接创建一个Status的error:status.Errorf(codes.InvalidArgument, "invalid args")返回
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) {
 ord, exists := orders[orderId.Value]
 if exists {
  return &ord, status.New(codes.OK, "ok").Err()
 }

 return nil, status.New(codes.InvalidArgument,
  "Order does not exist. order id: "+orderId.Value).Err()
}
到客户端这里我们再利用status.FromError(err)把error转回Status
order, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""})
if err != nil {
  // 转换有可能失败
  st, ok := status.FromError(err)
  if ok && st.Code() == codes.InvalidArgument {
    log.Println(st.Code(), st.Message())
  } else {
    log.Println(err)
  }

  return
}

log.Print("GetOrder Response -> : ", order)
但,status真的够用么?类似于HTTP 状态码code的个数也是有限的。有个很大的问题就是 表达能力非常有限,所以我们需要一个能够额外传递业务错误信息字段的功能。

Richer error model
Google 基于自身业务, 有了一套错误扩展 https://cloud.google.com/apis/design/errors#error_model
// The `Status` type defines a logical error model that is suitable for
// different programming environments, including REST APIs and RPC APIs.
message Status {
  // A simple error code that can be easily handled by the client. The
  // actual error code is defined by `google.rpc.Code`.
  int32 code = 1;

  // A developer-facing human-readable error message in English. It should
  // both explain the error and offer an actionable resolution to it.
  string message = 2;

  // Additional error information that the client code can use to handle
  // the error, such as retry info or a help link.
  repeated google.protobuf.Any details = 3;
}
可以看到比标准错误多了一个 details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展。

使用示例
由于 Golang 支持了这个扩展, 所以可以看到 Status 直接就是有 details 字段的.所以使用 WithDetails 附加自己扩展的错误类型, 该方法会自动将我们的扩展类型转换为 Any 类型,WithDetails 返回一个新的 Status 其包含我们提供的details。WithDetails 如果遇到错误会返回nil 和第一个错误
func InvokRPC() error {
 st := status.New(codes.InvalidArgument, "invalid args")

 if details, err := st.WithDetails(&pb.BizError{}); err == nil {
  return details.Err()
 }

 return st.Err()
}
前面提到details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展。同时,Google API 为错误详细信息定义了一组标准错误负载,您可在 google/rpc/error_details.proto 中找到这些错误负载。它们涵盖了对于 API 错误的最常见需求,例如配额失败和无效参数。与错误代码一样,开发者应尽可能使用这些标准载荷。

下面是一些示例 error_details 载荷:
ErrorInfo 提供既稳定又可扩展的结构化错误信息。
RetryInfo:描述客户端何时可以重试失败的请求,这些内容可能在以下方法中返回:Code.UNAVAILABLE 或 Code.ABORTED
QuotaFailure:描述配额检查失败的方式,这些内容可能在以下方法中返回:Code.RESOURCE_EXHAUSTED

BadRequest:描述客户端请求中的违规行为,这些内容可能在以下方法中返回:Code.INVALID_ARGUMENT


服务端
package main

import (
 "fmt"

 pb "github.com/liangwt/note/grpc/error_handling/error"
 epb "google.golang.org/genproto/googleapis/rpc/errdetails"
 "google.golang.org/grpc/codes"
 "google.golang.org/grpc/status"
)

func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) {
 ord, exists := orders[orderId.Value]
 if exists {
  return &ord, status.New(codes.OK, "ok").Err()
 }

 st := status.New(codes.InvalidArgument,
  "Order does not exist. order id: "+orderId.Value)

 details, err := st.WithDetails(
  &epb.BadRequest_FieldViolation{
   Field:       "ID",
   Description: fmt.Sprintf("Order ID received is not valid"),
  },
 )
 if err == nil {
  return nil, details.Err()
 }

 return nil, st.Err()
}
客户端
// Get Order
order, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""})
if err != nil {
 st, ok := status.FromError(err)
 if !ok {
  log.Println(err)
   return
 }

 switch st.Code() {
 case codes.InvalidArgument:
  for _, d := range st.Details() {
   switch info := d.(type) {
   case *epb.BadRequest_FieldViolation:
    log.Printf("Request Field Invalid: %s", info)
   default:
    log.Printf("Unexpected error type: %s", info)
   }
  }
 default:
  log.Printf("Unhandled error : %s ", st.String())
 }

 return
}

log.Print("GetOrder Response -> : ", order)
引申问题
如何传递这个非标准的错误扩展消息呢?或许可以在下一章可以找到答案。

总结
我们先介绍了gRPC最基本的错误处理方式:返回error。之后我们又介绍了一种能够携带更多错误信息的方式:Status,它包含code、message、details等信息,通过Status与error的互相转换,利用error来传输错误
用户评论