进行服务间内调

3.5 进行服务间内调

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

3.5.1 进行 gRPC 调用

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

package main

import (
	...
	pb "github.com/go-programming-tour-book/tag-service/proto"
)

func main() {
	ctx := context.Background()
	clientConn, _ := GetClientConn(ctx, "localhost:8004", nil)
	defer clientConn.Close()
  
	tagServiceClient := pb.NewTagServiceClient(clientConn)
	resp, _ := tagServiceClient.GetTagList(ctx, &pb.GetTagListRequest{Name: "Go"})
	
	log.Printf("resp: %v", resp)
}

func GetClientConn(ctx context.Context, target string, opts []grpc.DialOption) (*grpc.ClientConn, error) {
	opts = append(opts, grpc.WithInsecure())
	return grpc.DialContext(ctx, target, opts...)
}

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

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

3.5.2 grpc.Dial 做了什么

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

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

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

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

func main() {
	ctx := context.Background()
	clientConn, _ := GetClientConn(
		ctx,
		"localhost:8004",
		[]grpc.DialOption{grpc.WithBlock()},
	)
	defer clientConn.Close()
}

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

image

3.5.2.1 源码分析

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

image

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

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

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

func (ac *addrConn) resetTransport() {
	for i := 0; ; i++ {
		if ac.state == connectivity.Shutdown {
			return
		}
		...
		connectDeadline := time.Now().Add(dialDuration)
		ac.updateConnectivityState(connectivity.Connecting)
		newTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline)
		if err != nil {
			if ac.state == connectivity.Shutdown {
				return
			}
			ac.updateConnectivityState(connectivity.TransientFailure)
			timer := time.NewTimer(backoffFor)
			select {
			case <-timer.C:
				...
			}
			continue
		}

		if ac.state == connectivity.Shutdown {
			newTr.Close()
			return
		}
		...
		if !healthcheckManagingState {
			ac.updateConnectivityState(connectivity.Ready)
		}
		...

		if ac.state == connectivity.Shutdown {
			return
		}
		ac.updateConnectivityState(connectivity.TransientFailure)
	}
}

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

3.5.2.2 小结

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

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



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