跳到主要内容

错误处理

本章介绍 gRPC 的错误处理机制,包括标准错误码、错误详情和最佳实践。

gRPC 错误模型

gRPC 定义了一套标准的错误模型,使用状态码和错误消息来表示不同类型的错误:

标准错误码

错误码列表

错误码说明HTTP 映射
OK0成功200
CANCELLED1操作被取消499
UNKNOWN2未知错误500
INVALID_ARGUMENT3客户端传入无效参数400
DEADLINE_EXCEEDED4操作超时504
NOT_FOUND5请求资源不存在404
ALREADY_EXISTS6资源已存在409
PERMISSION_DENIED7权限不足403
RESOURCE_EXHAUSTED8资源耗尽(如配额)429
FAILED_PRECONDITION9前置条件不满足400
ABORTED10操作中止(如并发冲突)409
OUT_OF_RANGE11超出有效范围400
UNIMPLEMENTED12服务未实现501
INTERNAL13服务内部错误500
UNAVAILABLE14服务不可用503
DATA_LOSS15数据丢失或损坏500
UNAUTHENTICATED16未认证401

错误码选择指南

Go 错误处理

返回错误

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

// 简单错误
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.db.GetUser(req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "用户不存在")
}
return nil, status.Error(codes.Internal, "数据库错误")
}
return user, nil
}

// 格式化错误消息
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
if req.Email == "" {
return nil, status.Errorf(codes.InvalidArgument, "邮箱不能为空")
}
// ...
}

错误详情

import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)

func (s *server) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
// 验证请求
var violations []*errdetails.BadRequest_FieldViolation

if req.ProductId == "" {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "product_id",
Description: "产品 ID 不能为空",
})
}

if req.Quantity <= 0 {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "quantity",
Description: "数量必须大于 0",
})
}

if len(violations) > 0 {
badRequest := &errdetails.BadRequest{
FieldViolations: violations,
}

st, err := status.New(codes.InvalidArgument, "参数验证失败").
WithDetails(badRequest)
if err != nil {
return nil, status.Error(codes.Internal, "创建错误详情失败")
}
return nil, st.Err()
}

// 创建订单...
return order, nil
}

客户端处理错误

import (
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)

func callService(client pb.OrderServiceClient) {
order, err := client.CreateOrder(ctx, req)
if err != nil {
// 解析 gRPC 状态
st, ok := status.FromError(err)
if !ok {
log.Printf("非 gRPC 错误: %v", err)
return
}

// 根据错误码处理
switch st.Code() {
case codes.InvalidArgument:
// 解析错误详情
for _, detail := range st.Details() {
switch t := detail.(type) {
case *errdetails.BadRequest:
for _, violation := range t.GetFieldViolations() {
log.Printf("字段错误: %s - %s", violation.Field, violation.Description)
}
}
}
case codes.NotFound:
log.Println("资源不存在")
case codes.Unauthenticated:
log.Println("请先登录")
case codes.DeadlineExceeded:
log.Println("请求超时")
default:
log.Printf("错误: %s - %s", st.Code(), st.Message())
}
return
}

log.Printf("订单创建成功: %s", order.Id)
}

Python 错误处理

返回错误

from grpc import StatusCode
from grpc_interceptor.exception import GrpcException

class OrderService:
def CreateOrder(self, request, context):
if not request.product_id:
context.abort(StatusCode.INVALID_ARGUMENT, "产品 ID 不能为空")

try:
order = create_order(request)
except ProductNotFoundError:
context.abort(StatusCode.NOT_FOUND, "产品不存在")
except Exception as e:
context.abort(StatusCode.INTERNAL, f"内部错误: {e}")

return order

处理错误

import grpc

def call_service(client):
try:
order = client.CreateOrder(request)
print(f"订单创建成功: {order.id}")
except grpc.RpcError as e:
code = e.code()
message = e.details()

if code == grpc.StatusCode.INVALID_ARGUMENT:
print(f"参数错误: {message}")
elif code == grpc.StatusCode.NOT_FOUND:
print(f"资源不存在: {message}")
else:
print(f"错误: {code} - {message}")

错误重试策略

可重试的错误码

以下错误码通常可以安全重试:

  • CANCELLED - 操作被取消
  • DEADLINE_EXCEEDED - 超时
  • RESOURCE_EXHAUSTED - 资源耗尽
  • ABORTED - 操作中止
  • UNAVAILABLE - 服务不可用

客户端重试配置

// Go 客户端重试配置
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithDefaultServiceConfig(`{
"methodConfig": [{
"name": [{"service": ""}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}]
}`),
)

手动重试

func callWithRetry(client pb.GreeterClient, req *pb.HelloRequest) (*pb.HelloReply, error) {
var lastErr error

for i := 0; i < 3; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

resp, err := client.SayHello(ctx, req)
if err == nil {
return resp, nil
}

st, ok := status.FromError(err)
if !ok {
return nil, err
}

// 检查是否可重试
switch st.Code() {
case codes.Unavailable, codes.DeadlineExceeded:
lastErr = err
time.Sleep(time.Second * time.Duration(i+1))
continue
default:
return nil, err
}
}

return nil, lastErr
}

最佳实践

1. 使用具体的错误码

// ❌ 不好:所有错误都用 INTERNAL
return nil, status.Error(codes.Internal, "错误")

// ✅ 好:使用具体的错误码
if user == nil {
return nil, status.Error(codes.NotFound, "用户不存在")
}
if !hasPermission {
return nil, status.Error(codes.PermissionDenied, "无权访问")
}

2. 提供有意义的错误消息

// ❌ 不好:错误消息太笼统
return nil, status.Error(codes.InvalidArgument, "参数错误")

// ✅ 好:详细的错误信息
return nil, status.Errorf(codes.InvalidArgument, "邮箱格式不正确: %s", req.Email)

3. 使用错误详情传递结构化信息

// 使用标准错误详情
st, _ := status.New(codes.FailedPrecondition, "操作失败").
WithDetails(&errdetails.PreconditionFailure{
Violations: []*errdetails.PreconditionFailure_Violation{{
Type: "Stock",
Subject: "product_123",
Description: "库存不足",
}},
})
return nil, st.Err()

4. 区分客户端和服务端错误

func (s *server) Process(ctx context.Context, req *pb.Request) (*pb.Response, error) {
// 客户端错误(4xx)
if err := validateRequest(req); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// 服务端错误(5xx)
result, err := s.processInternal(req)
if err != nil {
log.Printf("内部处理错误: %v", err)
return nil, status.Error(codes.Internal, "处理失败")
}

return result, nil
}

小结

本章我们学习了:

  1. 标准错误码:gRPC 定义的 17 种错误码
  2. 返回错误:使用 status 包创建错误
  3. 错误详情:传递结构化的错误信息
  4. 错误处理:客户端解析和处理错误
  5. 重试策略:处理临时性错误
  6. 最佳实践:使用具体错误码、提供有意义消息

掌握错误处理是构建健壮 gRPC 服务的关键。