错误处理
本章介绍 gRPC 的错误处理机制,包括标准错误码、错误详情和最佳实践。
gRPC 错误模型
gRPC 定义了一套标准的错误模型,使用状态码和错误消息来表示不同类型的错误:
标准错误码
错误码列表
| 错误码 | 值 | 说明 | HTTP 映射 |
|---|---|---|---|
OK | 0 | 成功 | 200 |
CANCELLED | 1 | 操作被取消 | 499 |
UNKNOWN | 2 | 未知错误 | 500 |
INVALID_ARGUMENT | 3 | 客户端传入无效参数 | 400 |
DEADLINE_EXCEEDED | 4 | 操作超时 | 504 |
NOT_FOUND | 5 | 请求资源不存在 | 404 |
ALREADY_EXISTS | 6 | 资源已存在 | 409 |
PERMISSION_DENIED | 7 | 权限不足 | 403 |
RESOURCE_EXHAUSTED | 8 | 资源耗尽(如配额) | 429 |
FAILED_PRECONDITION | 9 | 前置条件不满足 | 400 |
ABORTED | 10 | 操作中止(如并发冲突) | 409 |
OUT_OF_RANGE | 11 | 超出有效范围 | 400 |
UNIMPLEMENTED | 12 | 服务未实现 | 501 |
INTERNAL | 13 | 服务内部错误 | 500 |
UNAVAILABLE | 14 | 服务不可用 | 503 |
DATA_LOSS | 15 | 数据丢失或损坏 | 500 |
UNAUTHENTICATED | 16 | 未认证 | 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
}
小结
本章我们学习了:
- 标准错误码:gRPC 定义的 17 种错误码
- 返回错误:使用 status 包创建错误
- 错误详情:传递结构化的错误信息
- 错误处理:客户端解析和处理错误
- 重试策略:处理临时性错误
- 最佳实践:使用具体错误码、提供有意义消息
掌握错误处理是构建健壮 gRPC 服务的关键。