Files

5.7 KiB
Raw Permalink Blame History

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
grpc-microservices
go
gRPC
Protobuf
HTTP/2
拦截器
deadline
intermediate
backend.go.errors-concurrency
3
backend.grpc-microservices.governance
backend.grpc-microservices.interview
14
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 传递。