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 最佳实践】。。。