3.5 进行服务间内调

在上一个章节中,我们运行了一个最基本的 gRPC 服务,那么在实际上的应用场景,我们的服务是会有多个的,并且随着需求的迭代拆分重合,服务会越来越多,到上百个也是颇为常见的。因此在这么多的服务中,最常见的就是 gRPC 服务间的内调行为,再细化下来,其实就是客户端如何调用 gRPC 服务端的问题,那么在本章节我们将会进行使用和做一个深入了解。

3.5.1 进行 gRPC 调用

理论上在任何能够执行 Go 语言代码,且网络互通的地方都可以进行 gRPC 调用,它并不受限于必须在什么类型应用程序下才能够调用。接下来我们在项目下新建 client 目录,创建 client.go 文件,编写一个示例来调用我们先前所编写的 gRPC 服务,如下代码:

  1. package main
  2. import (
  3. ...
  4. pb "github.com/go-programming-tour-book/tag-service/proto"
  5. )
  6. func main() {
  7. ctx := context.Background()
  8. clientConn, _ := GetClientConn(ctx, "localhost:8004", nil)
  9. defer clientConn.Close()
  10. tagServiceClient := pb.NewTagServiceClient(clientConn)
  11. resp, _ := tagServiceClient.GetTagList(ctx, &pb.GetTagListRequest{Name: "Go"})
  12. log.Printf("resp: %v", resp)
  13. }
  14. func GetClientConn(ctx context.Context, target string, opts []grpc.DialOption) (*grpc.ClientConn, error) {
  15. opts = append(opts, grpc.WithInsecure())
  16. return grpc.DialContext(ctx, target, opts...)
  17. }

在上述 gRPC 调用的示例代码中,一共分为三大步,分别是:

  • grpc.DialContext:创建给定目标的客户端连接,另外我们所要请求的服务端是非加密模式的,因此我们调用了 grpc.WithInsecure 方法禁用了此 ClientConn 的传输安全性验证。
  • pb.NewTagServiceClient:初始化指定 RPC Proto Service 的客户端实例对象。
  • tagServiceClient.GetTagList:发起指定 RPC 方法的调用。

3.5.2 grpc.Dial 做了什么

常常有的人会说在调用 grpc.Dialgrpc.DialContext 方法时,客户端就已经与服务端建立起了连接,但这对不对呢,这是需要细心思考的一个点,客户端真的是一调用 Dial 相关方法就马上建立了可用连接吗,我们一起尝试一下,示例代码:

  1. func main() {
  2. ctx := context.Background()
  3. clientConn, _ := GetClientConn(ctx, "localhost:8004", nil)
  4. defer clientConn.Close()
  5. }

在上述代码中,我们只保留了创建给定目标的客户端连接的部分代码,然后执行该程序,接着马上查看抓包工具的情况下,竟然提示一个包都没有,那么这算真正连接了吗?

实际上,如果你真的想在调用 DialContext 方法时就马上打通与服务端的连接,那么你需要调用 WithBlock 方法来进行设置,那么它在发起拨号连接时就会阻塞等待连接完成,并且最终连接会到达 Ready 状态,这样子在此刻的连接才是正式可用的,代码如下:

  1. func main() {
  2. ctx := context.Background()
  3. clientConn, _ := GetClientConn(
  4. ctx,
  5. "localhost:8004",
  6. []grpc.DialOption{grpc.WithBlock()},
  7. )
  8. defer clientConn.Close()
  9. }

再次进行抓包,查看效果,如下:

image

3.5.2.1 源码分析

那么在调用 grpc.Dialgrpc.DialContext 方法时,到底做了什么事情呢,为什么还要调用 WithBlock 方法那么“麻烦”,接下来我们一起看看正在调用时运行的 goroutine 情况,如下:

image

我们可以看到有几个核心方法一直在等待/处理信号,通过分析底层源码可得知。涉及如下:

  1. func (ac *addrConn) connect()
  2. func (ac *addrConn) resetTransport()
  3. func (ac *addrConn) createTransport(addr resolver.Address, copts transport.ConnectOptions, connectDeadline time.Time)
  4. func (ac *addrConn) getReadyTransport()

在这里主要分析所提示的 resetTransport 方法,看看都做了什么。核心代码如下:

  1. func (ac *addrConn) resetTransport() {
  2. for i := 0; ; i++ {
  3. if ac.state == connectivity.Shutdown {
  4. return
  5. }
  6. ...
  7. connectDeadline := time.Now().Add(dialDuration)
  8. ac.updateConnectivityState(connectivity.Connecting)
  9. newTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline)
  10. if err != nil {
  11. if ac.state == connectivity.Shutdown {
  12. return
  13. }
  14. ac.updateConnectivityState(connectivity.TransientFailure)
  15. timer := time.NewTimer(backoffFor)
  16. select {
  17. case <-timer.C:
  18. ...
  19. }
  20. continue
  21. }
  22. if ac.state == connectivity.Shutdown {
  23. newTr.Close()
  24. return
  25. }
  26. ...
  27. if !healthcheckManagingState {
  28. ac.updateConnectivityState(connectivity.Ready)
  29. }
  30. ...
  31. if ac.state == connectivity.Shutdown {
  32. return
  33. }
  34. ac.updateConnectivityState(connectivity.TransientFailure)
  35. }
  36. }

通过上述代码可得知,在该方法中会不断地去尝试创建连接,若成功则结束。否则不断地根据 Backoff 算法的重试机制去尝试创建连接,直到成功为止。

3.5.2.2 小结

因此单纯调用 grpc.DialContext 方法是异步建立连接的,并不会马上就成为可用连接了,仅处于 Connecting 状态(需要多久则取决于外部因素,例如:网络),正式要到达 Ready 状态,这个连接才算是真正的可用。

我们再回顾到前面的示例中,为什么抓包时一个包都抓不到,实际上连接立即建立了,但 main 结束的很快,因此可能刚建立就被销毁了,也可能还处于 Connecting 状态,没来得及产生具体的网络活动,自然也就抓取不到任何包了。

本图书由 煎鱼©2020 版权所有,所有文章采用知识署名-非商业性使用-禁止演绎 4.0 国际进行许可。

3.5 进行服务间内调 - 图3