zrpc = gRPC + 服务发现 + 负载均衡 + 超时/重试/熔断。本文把一次 RPC 调用的全链路拆开讲,配代码、配时序图。
zrpc 是对 gRPC 的封装:你写 proto,它帮你生成服务端骨架与「自带服务发现」的客户端,调用时像调本地函数一样简单。
自动 etcd 注册发现、内置负载均衡、超时控制、熔断、限流、链路追踪——这些裸 gRPC 都要自己拼,zrpc 默认接好。
Server:实现 proto 里定义的 service 方法;Client:通过生成的 client 调用对端。一个服务可以同时是两者。
proto 是服务之间的「接口文档」,双方按它生成代码,强类型、不会调错字段。
syntax = "proto3"; package user; // go_package 决定生成代码的位置 option go_package = "./user;user"; service User { rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc Login(LoginRequest) returns (LoginResponse); } message GetUserRequest { int64 id = 1; } message GetUserResponse { int64 id = 1; string name = 2; } message LoginRequest { string username = 1; string password = 2; } message LoginResponse { string token = 1; }
goctl 会生成 pb.go(消息)、grpc.pb.go(gRPC 桩)、以及 zrpc 的 server/client 封装。
goctl rpc protoc user.proto \
--go_out=. \
--go-grpc_out=. \
--zrpc_out=. \
-m # -m 顺带生成 yaml 配置与 main
产物结构:
user/ user.pb.go # 消息结构体 user_grpc.pb.go # gRPC 接口 + 客户端 internal/server/ userserver.go # 服务端注册 internal/logic/ getuserlogic.go # 你的实现 loginlogic.go user.go # main etc/user.yaml
func (l *GetUserLogic) GetUser(in *user.GetUserRequest) (*user.GetUserResponse, error) { u, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id) if err != nil { return nil, err } return &user.GetUserResponse{Id: u.Id, Name: u.Name}, nil }
调用方不写连接、不写拨号。在 config 配 etcd,svc 建 client,logic 直接方法调用。
type Config struct { rest.RestConf UserRpc zrpc.RpcClientConf // 关键:下游 RPC 配置 }
type ServiceContext struct { Config config.Config UserRpc user.User } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)), } }
resp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.GetUserRequest{Id: 1})
if err != nil {
return nil, err
}
// resp.Name 即可使用
从「发起调用」到「拿到响应」,中间经过服务发现、负载均衡、超时控制等。时序图如下。
想在每次调用前后统一做事(日志、鉴权、追踪)?用拦截器,不用改业务。
srv := zrpc.MustNewServer(c.RpcServerConf, func(s *grpc.Server) { s.AddUnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { start := time.Now() resp, err := handler(ctx, req) logx.Infof("%s cost=%v err=%v", info.FullMethod, time.Since(start), err) return resp, err }) }) defer srv.Stop() if err := srv.Add(user.RegisterUserServer, userServer.NewUserServer(ctx)); err != nil { logx.Fatal(err) } srv.Start()
client := zrpc.MustNewClient(c.UserRpc, zrpc.WithUnaryClientInterceptor(
func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = metadata.AppendToOutgoingContext(ctx, "x-trace-id", traceId())
return invoker(ctx, method, req, reply, cc, opts...)
}))
RPC 调用一定要设防护,否则一个慢下游能把整条链路拖死。
UserRpc:
Etcd:
Hosts: [127.0.0.1:2379]
Key: user.rpc
Timeout: 3000 # 单此调用超时(ms)
err := breaker.Do("user-rpc:GetUser", func() error { _, e := l.svcCtx.UserRpc.GetUser(l.ctx, req) return e }, nil)
UserRpc:
Timeout: 3000
Retries: 1 # 失败重试次数(谨慎,需幂等)