• 一个合理的超时时间是非常必要的,它能提高用户体验,提高服务器的整体性能,是服务治理的常见手段之一。
  • 为什么要设置超时?
    • 用户体验:很多RPC都是由用户侧发起,如果请求不设置超时时间或者超时时间不合理,会导致用户一直处于白屏或者请求中的状态,影响用户的体验。
    • 资源利用:一个RPC会占用两端(服务端与客户端)端口、cpu、内存等一系列的资源,不合理的超时时间会导致RPC占用的资源迟迟不能被释放,因而影响服务器稳定性。

客户端

连接超时

  1. 如果我们想控制连接创建时的超时时间该怎么做呢?(在之前版本中)
    • 异步转成同步:首先我们需要使用grpc.WithBlock()这个选项让连接的创建变为阻塞式的
    • 超时时间:使用grpc.DialContext()以及Go中context.Context来控制超时时间
  2. 于是实现如下,当然使用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)
}

拦截器中

  1. 普通RPC还是流式RPC拦截器函数签名第一个参数也是context.Context,我们也可以在拦截器中修改超时时间。错误处理也是和服务调用是一样的。
  2. 需要注意的是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
}

服务端

连接超时

  1. 服务端也可以控制连接创建的超时时间,如果没有在设定的时间内建立连接,服务端就会主动断连,避免浪费服务端的端口、内存等资源。
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()))

    // ...
}

服务实现中

  1. 服务实现函数的第一个参数也是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
}
  1. 很多库都支持类似的操作,我们要做的就是把context.Context透传下去,当context.Context超时时就会提前结束操作了。
db, err := gorm.Open()
if err != nil {
    panic("failed to connect database")
}

db.WithContext(ctx).Save(&users)

拦截器中

  1. 在服务端的拦截器里也可以修改超时时间。
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
}

超时传递

  1. 一个正常的请求会涉及到多个服务的调用。从源头开始一个服务端不仅为上游服务提供服务,也作为下游的客户端。
  2. 如上的链路,如果当请求到达某一服务时,对于服务A来说已经超时了,那么就没有必要继续把请求传递下去了。这样可以最大限度的避免后续服务的资源浪费,提高系统的整体性能。

参考

  1. 写给go开发者的gRPC教程-超时控制