乐趣区

关于go:写给go开发者的gRPC教程超时控制

本篇为【写给 go 开发者的 gRPC 教程系列】第六篇

第一篇:protobuf 根底

第二篇:通信模式

第三篇:拦截器

第四篇:错误处理

第五篇:metadata

第六篇:超时管制 👈

本系列将继续更新,欢送关注👏获取实时告诉


导言

一个正当的超时工夫是十分必要的,它能进步用户体验,进步服务器的整体性能,是服务治理的常见伎俩之一

为什么要设置超时

用户体验:很多 RPC 都是由用户侧发动,如果申请不设置超时工夫或者超时工夫不合理,会导致用户始终处于白屏或者申请中的状态,影响用户的体验

资源利用:一个 RPC 会占用两端(服务端与客户端)端口、cpu、内存等一系列的资源,不合理的超时工夫会导致 RPC 占用的资源迟迟不能被开释,因此影响服务器稳定性

综上,一个正当的超时工夫是十分必要的。在一些要求更高的服务中,咱们还须要针对 DNS 解析、连贯建设,读、写等设置更精密的超时工夫。除了设置动态的超时工夫,依据以后零碎状态、服务链路等设置自适应的动静超时工夫也是服务治理中一个常见的计划。

客户端的超时

连贯超时

还记得咱们怎么在客户端创立连贯的么?

conn, err := grpc.Dial("127.0.0.1:8009",
    grpc.WithInsecure(),)
if err != nil {panic(err)
}

// c := pb.NewOrderManagementClient(conn)

// // Add Order
// order := pb.Order{
//     Id:          "101",
//     Items:       []string{"iPhone XS", "Mac Book Pro"},
//     Destination: "San Jose, CA",
//     Price:       2300.00,
// }
// res, err := c.AddOrder(context.Background(), &order)
// if err != nil {//     panic(err)
// }

如果指标地址 127.0.0.1:8009 无奈建设连贯,grpc.Dial()会返回谬误么?这里间接放论断:不会的,grpc 默认会异步创立连贯,并不会阻塞在这里,如果连贯没有创立胜利会在上面的 RPC 调用中报错。

如果咱们想管制连贯创立时的超时工夫该怎么做呢?

  • 异步转成同步:首先咱们须要应用 grpc.WithBlock() 这个选项让连贯的创立变为阻塞式的
  • 超时工夫:应用 grpc.DialContext() 以及 Go 中 context.Context 来管制超时工夫

于是实现如下,当然应用 context.WithDeadline() 成果也是一样的。连贯如果在 3s 内没有创立胜利,则会返回 context.DeadlineExceeded 谬误

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

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.Context 来管制服务调用的超时工夫,而后应用 status 来判断是否是超时报错,对于 status 能够回顾之前讲过的错误处理

ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

// Add Order
order := pb.Order{
    Id:          "101",
    Items:       []string{"iPhone XS", "Mac Book Pro"},
    Destination: "San Jose, CA",
    Price:       2300.00,
}
res, err := c.AddOrder(ctx, &order)
if err != nil {st, ok := status.FromError(err)
    if ok && st.Code() == codes.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
}

服务端的超时

连贯超时

服务端也能够管制连贯创立的超时工夫,如果没有在设定的工夫内建设连贯,服务端就会被动断连,避免浪费服务端的端口、内存等资源

s := grpc.NewServer(grpc.ConnectionTimeout(3*time.Second),
)

服务实现中的超时

服务实现函数的第一个参数也是 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, 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 来说曾经超时了,那么就没有必要持续把申请传递上来了。这样能够最大限度的防止后续服务的资源节约,进步零碎的整体性能。

grpc-go实现了这一个性,咱们要做的就是一直的把 context.Context 传下去

// 服务 A
func main(){ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    
    client.ServiceB(ctx)
}
// 服务 B
func ServiceB(ctx context.Context){client.ServiceC(ctx)
}
// 服务 C
func ServiceC(ctx context.Context){client.ServiceD(ctx)
}

在每一次的 context.Context 透传中,timeout 都会减去在本过程中耗时,导致这个 timeout 传递到下一个 gRPC 服务端时变短,当在某一个过程中曾经超时,申请不会再持续传递,这样即实现了所谓的 超时传递

对于超时传递的实现能够参考上面的参考资料中的链接

总结

通过应用context.Context,咱们能够精细化的管制 gRPC 中服务端、客户端两端的建连,调用,以及在拦截器中的超时工夫。同时 gRPC 还提供了超时传递的能力,让超时的申请不持续在链路中往下传递,进步链路整体的性能。

参考

  • 以上代码示例均位于仓库: https://github.com/liangwt/grpc-example
  • Golang gRPC 学习(04): Deadlines 超时限度
  • gRPC 系列——grpc 超时传递原理

✨ 微信公众号【凉凉的知识库】同步更新,欢送关注获取最新最有用的后端常识 ✨

退出移动版