- 一个合理的超时时间是非常必要的,它能提高用户体验,提高服务器的整体性能,是服务治理的常见手段之一。
- 为什么要设置超时?
- 用户体验:很多RPC都是由用户侧发起,如果请求不设置超时时间或者超时时间不合理,会导致用户一直处于白屏或者请求中的状态,影响用户的体验。
- 资源利用:一个RPC会占用两端(服务端与客户端)端口、cpu、内存等一系列的资源,不合理的超时时间会导致RPC占用的资源迟迟不能被释放,因而影响服务器稳定性。
客户端#
连接超时#
- 如果我们想控制连接创建时的超时时间该怎么做呢?(在之前版本中)
- 异步转成同步:首先我们需要使用grpc.WithBlock()这个选项让连接的创建变为阻塞式的
- 超时时间:使用grpc.DialContext()以及Go中context.Context来控制超时时间
- 于是实现如下,当然使用context.WithDeadline()效果也是一样的。连接如果在3s内没有创建成功,则会返回context.DeadlineExceeded错误。
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// DialContext 已被弃用
conn, err := grpc.DialContext(ctx, "127.0.0.1:8009",
grpc.WithInsecure(),
grpc.WithBlock(), // 已被弃用
)
if err != nil {
if err == context.DeadlineExceeded {
panic(err)
}
panic(err)
}
拦截器中#
- 普通RPC还是流式RPC拦截器函数签名第一个参数也是context.Context,我们也可以在拦截器中修改超时时间。错误处理也是和服务调用是一样的。
- 需要注意的是context.WithTimeout(context.Background(), 100*time.Second)。因为Go中context.Context向下传导的效果,我们需要基于context.Background()创建新的context.Context,而不是基于入参的ctx。
func unaryClientInterceptor(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
defer cancel()
// Invoking the remote method
err := invoker(ctx, method, req, reply, cc, opts...)
if err != nil {
st, ok := status.FromError(err)
if ok && st.Code() == codes.DeadlineExceeded {
panic(err)
}
panic(err)
}
return err
}
服务端#
连接超时#
- 服务端也可以控制连接创建的超时时间,如果没有在设定的时间内建立连接,服务端就会主动断连,避免浪费服务端的端口、内存等资源。
func main() {
// ...
// 开启端口
listen, _ := net.Listen("tcp", ":9090")
// 创建grpc服务
grpcServer := grpc.NewServer(
grpc.Creds(creds),
grpc.ConnectionTimeout(2*time.Second), // 连接超时
grpc.UnaryInterceptor(interceptor.UnaryServerInterceptor()),
grpc.StreamInterceptor(interceptor.StreamServerInterceptor()))
// ...
}
服务实现中#
- 服务实现函数的第一个参数也是context.Context,所以我们可以在一些耗时操作前对context.Context进行判断:如果已经超时了,就没必要继续往下执行了。此时客户端也会收到上文提到过的超时error。
func (s *server) AddOrder(ctx context.Context, orderReq *pb.Order) (*wrapperspb.StringValue, error) {
log.Printf("Order Added. ID : %v", orderReq.Id)
// 超时时间来自客户端调用方法传入的时间
select {
case <-ctx.Done():
return nil, status.Errorf(codes.Canceled, "Client cancelled, abandoning.")
default:
}
orders[orderReq.Id] = *orderReq
return &wrapperspb.StringValue{Value: "Order Added: " + orderReq.Id}, nil
}
- 很多库都支持类似的操作,我们要做的就是把context.Context透传下去,当context.Context超时时就会提前结束操作了。
db, err := gorm.Open()
if err != nil {
panic("failed to connect database")
}
db.WithContext(ctx).Save(&users)
拦截器中#
- 在服务端的拦截器里也可以修改超时时间。
func unaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// ctx 超时时间来自客户端传入的超时时间
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Invoking the handler to complete the normal execution of a unary RPC.
m, err := handler(ctx, req)
return m, err
}
超时传递#
- 一个正常的请求会涉及到多个服务的调用。从源头开始一个服务端不仅为上游服务提供服务,也作为下游的客户端。
- 如上的链路,如果当请求到达某一服务时,对于服务A来说已经超时了,那么就没有必要继续把请求传递下去了。这样可以最大限度的避免后续服务的资源浪费,提高系统的整体性能。
- 写给go开发者的gRPC教程-超时控制