5.7 KiB
5.7 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.grpc-microservices.grpc-proto | gRPC 和 Protobuf:IDL、四种调用方式、错误码、超时、拦截器 | lesson | backend |
|
|
intermediate |
|
3 |
|
|
1 |
gRPC 和 Protobuf:IDL、四种调用方式、错误码、超时、拦截器
解释说明
- gRPC:基于 HTTP/2 的 RPC 框架。先用 Protobuf 写接口定义(
.proto),再由工具生成各语言的客户端和服务端代码,调用远程方法像调用本地函数。 - Protobuf:二进制序列化格式。比 JSON 体积小、解析快,有强类型约束;缺点是不可读,浏览器不能直接调用(需要 gRPC-Web 或网关转 HTTP/JSON)。
- 字段编号:序列化时用编号而不是字段名。已发布的编号不能改、不能复用;删除字段时用
reserved保留编号和名字。新增字段是向后兼容的。 - 四种调用方式:一元(一问一答)、服务端流(一次请求、多次返回,例如推送进度)、客户端流(多次上传、一次返回,例如上传分片)、双向流(聊天、实时协作)。
- 错误码:gRPC 有自己的状态码,常用
InvalidArgument、NotFound、AlreadyExists、PermissionDenied、Unauthenticated、DeadlineExceeded、Unavailable、Internal。可重试的一般只有Unavailable(以及部分DeadlineExceeded、ResourceExhausted),由业务判断。 - Deadline(截止时间):客户端每次调用都要设置;截止时间会随请求传到服务端,服务端的
ctx到期后应该停止工作,并继续传给下游调用。 - 拦截器(interceptor):gRPC 的中间件,用来做日志、鉴权、指标、恢复 panic。元数据(metadata)相当于 HTTP 头,用来传 token、请求 ID。
和 REST 怎么选:对外开放的接口、浏览器直接调用用 REST/JSON;内部服务之间、对性能和强类型契约有要求时用 gRPC。
案例
订单服务需要查用户信息:定义 UserService.GetUser,用户服务实现,订单服务通过生成的客户端调用,每次调用带 300ms 截止时间;用户不存在返回 NotFound,订单服务据此返回业务错误,而不是 500。
代码示例
接口定义(examples/grpc-user/user.proto):
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User); // 服务端流
}
message GetUserRequest { int64 id = 1; }
message ListUsersRequest { int32 page_size = 1; }
message User {
int64 id = 1;
string name = 2;
string email = 3;
reserved 4; // 已删除字段的编号,不能复用
reserved "phone";
}
Go 服务端和客户端的关键写法(需要先用 protoc --go_out --go-grpc_out 生成 userv1 包;安装和生成命令以官方 Go 快速开始为准:https://grpc.io/docs/languages/go/quickstart/ ):
// 服务端:实现生成的接口
type userServer struct {
userv1.UnimplementedUserServiceServer // 嵌入它,接口新增方法时不会编译失败
repo UserRepo
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
if req.GetId() <= 0 {
return nil, status.Error(codes.InvalidArgument, "id 必须大于 0")
}
u, err := s.repo.Find(ctx, req.GetId()) // 把 ctx 继续往下传,截止时间才能生效
if errors.Is(err, ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "用户 %d 不存在", req.GetId())
}
if err != nil {
return nil, status.Error(codes.Internal, "查询失败") // 不把内部错误细节暴露给调用方
}
return &userv1.User{Id: u.ID, Name: u.Name, Email: u.Email}, nil
}
// 一元拦截器:记录方法、耗时和状态码
func logging(ctx context.Context, req any, info *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) {
start := time.Now()
resp, err := h(ctx, req)
log.Printf("%s %s %v", info.FullMethod, status.Code(err), time.Since(start))
return resp, err
}
// 客户端:每次调用都带截止时间
func getUser(cli userv1.UserServiceClient, id int64) (*userv1.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
u, err := cli.GetUser(ctx, &userv1.GetUserRequest{Id: id})
if status.Code(err) == codes.NotFound {
return nil, ErrUserNotFound
}
return u, err
}
可直接运行的完整演示用 Python(不需要单独安装 protoc):examples/grpc-user/demo.py,覆盖一元调用、服务端流、NOT_FOUND 和 DEADLINE_EXCEEDED。
易错点
- 修改或复用字段编号:老客户端解析出错乱的数据,而且不会报错。
- 客户端不设截止时间:下游卡住时请求无限等待,连接和 goroutine 堆积。
- 服务端不把
ctx传给下游:客户端已经放弃了,服务端还在查数据库。 - 所有错误都返回
Unknown/Internal:调用方无法区分“参数错”和“服务挂了”,也就无法决定是否重试。 - 每次调用都新建连接:gRPC 连接(
grpc.ClientConn)应该复用,它内部会管理 HTTP/2 连接。 - 把 proto 文件复制到各个仓库:容易不一致,放在统一的仓库或用 Buf 等工具管理版本。
面试常问
见 gRPC 与微服务面试题:gRPC 和 REST 的区别、为什么快、四种调用方式、字段编号规则、deadline 传递。