golang|grpc 最佳实践

grpc 最常见的使用场景是:微服务框架下。多种语言只见的交互,将手机服务、浏览器连接至后台。产生高校的客户端库。(维基百科)
低延迟,高可用,分布式系统;移动客户端和云端通讯;跨语言协议;独立组件方便扩展,例如认证,负载均衡,和监控(来自grpc官方文档,最后一项翻译可能不准确)。
grpc的创建是非常简单的:
1. proto文件

math.protoService Math { rpc Div (Request) returns (Response)() }message Request { int64 divedend =1 ; int64 divisor =2; }message Response { int64 quotient =1 ; int64 remainder =2; }

2.生成服务端代码
math.gotype server struct{}func (s *server) Div (ctx context.Context, in *pb.Request) (*pb.Response,error) {n,d := in.Dividened, in.Divisor if d == 0 { return nil , status. Errof(codes.InvalidArgumnet, "division by 0") } return &pb.Response{ Quotient: int64(n / d) Reainder: int64(n % d) },nil }func main() { lis, _ := net.Listen("tcp",port) s:= grpc.NewServer() pb.RegisterMathServer(s,&server()) s.Serve(lis) }

服务的创建是非常容易的但是,真正需要在生产环境中用好还是非常有挑战的:
  1. 可靠性
  2. 安全性
  3. 性能
  4. 错误处理
  5. 监控
  6. 负载均衡
  7. 日志
  8. QOS
  9. ...
API 设计之幂等
需要实现接口的可重入。
例如转钱:
不好的设计:
message Request { string from =1; string to= 2; float amount = 3; }message Response { int64 confirmations= 1; }

好的设计:
message Request { string from =1; string to= 2; float amount = 3; int64 timestamp =4 ; }message Response { //每次相同的请求的到相同的结果 int64 confirmations= 1; }

API设计之性能
重复的地方:
Requests: 有可能有无限多的请求, 需要设置限制;
Response: 需要支持分页;
避免耗时较的操作:
时间越长,重试概率越大;
在后台处理,异步发送执行结果。(callback , email , pubsub, etc), 或者tracking token。
API设计之默认值
定义更加敏感的默认信息。
尽量将未知的,未定义的 作为默认值
向后兼容
API设计之错误处理
错误:
是grpc独立的一个类别,不要把错误信息放在响应内容中。否则判断是否成功的逻辑会非常复杂。因为需要读出响应内容进行判断。不如一开始就将错误统一定义好。
避免批量运行独立的操作:
例如: 一次性更新多个表。
错误处理非常复杂。
如果确实需要,使用stream 或者multiple call。
错误处理之 - DON'T PANIC
尽最大努力优雅的处理错误
panic只适合机器内部故障:内存泄露,内存用完,imminetn data corruption
除了上述error, 剩下都返回i给调用者。
当心空指针。使用proto的getter方式是nil安全的。
错误处理之-合理转换
别直接把其他服务的错误返回。这样不利于调试
res , err := client.Call(...) if err!= nil { s,ok := status.FromError(err) if !ok { return status.Errorf(codes.Internal, "client.Call:unkown error:%v",err) } switch s.Code() { case code.InvalidArgument: return .... } }

DeadLines
客户端:
一般需要设置,这样客户端才能知道什么时候放弃操作。一定要使用DEADLINE
使用带有deadline的ctx
res , err := client.Call( ctx, req)
服务端:
也比较关注DEADLINE
超市时间太短:不够执行对应操作,过早失败;
超时时间太长:消耗用户的其他资源。
func (s *Server) MyRequestHandler(ctx contes.Contex, ...) (*Res,error) { d,ok := ctx.Deadline() if !ok { return staus.Error {...} } timeout := d.Sub(time.Now()) if timeout < 5*time.Second || timeout > 30*time.Secone { return status.Error (...) } }

如果可以的话,尽量为不同请求创建各自的ctx 和 ctx的超时时间。
限流
服务端“
import "golang.org/x/time/rate"... s := grpc.NewServer (grpc.InTapHadle(rateLimiter)) ...func rateLimiter (ctx context.Context, info *tap.Info) (contex.Context, error) { if m[user] == nil { // m[user] = rate.newLimiter(5,1) } if !m[user].Allow() { return nil, status.Eoorof(codes....) } return ctx, nil }

好的客户端也需要限流
import "golang.org/x/time/rate"... s := grpc.NewServer (grpc.InTapHadle(rateLimiter)) ...func Myhandler (ctx contex.Context , req Request) (Response,err) { if err := limiter.Wait(ctx); err != nil { return nil , err } return c.Call(ctx, req) }

重试
官方特性说明: gRRFC A6
. 可以通过服务端的配置
支持: 按失败顺序重试 或者 并发重试。
同样需要考虑ctx中过期时间的问题。
通用的框架
func (c *client) ChildRpc (ctx contex.Context , name string , f func(contes.Contex) error){ for attempts := 1; attempts <= c.maxAttempts; attempts++ { if err := c.limiter.Wait(ctx); err != nil { return c.limiterErr(...) } if err := f(ctx); err == nil { return nil } else if !c.retry (err) { return c.convertErr (name , err, attensm) } } return c.TomanyRetry })var res Responseerr := c.ChildRpc (ctx , "SendMony", func(ctx context.Contex)(err error){ res , err := sendMondClient.SendMoney(ctx, req) return })

内存管理
方法一:
import "golang.org/x/net/netutil"listener := netutil.LimitListener(listener, connectionLimit)grpc.NewServer(grpc.MaxConcurrentStream(streamsLimit))


方法二:
user Tap Handler, 当过多的rpc连接或者内存比较低。
方法三:
health 报告机制
限制服务的请求数据大小:
grpc.NewServer(grpc.MaxRecvMsgSize(4096 /* bytes*/))
小的请求可能有大的数据响应:
eg : database query
api design issue:
使用 streaming response
按照数据最大限制进行分页。
日志
多打印吧,方便调试,以及监控发出警告
监控
【golang|grpc 最佳实践】。。。

    推荐阅读