处理GRPC连接"failed reaching server: context deadline exceeded"报错

408 阅读3分钟

最近遇到个 grpc 连接报错问题,主要现象是:

  1. 有3个容器服务 A,B,C
  2. 宿主机是 ubuntu 系统时,A 服务用 grpc 连接 C 服务能成功,B 服务用 grpc 连接 C 服务也能成功
  3. 宿主机是 Centos7 系统时,A 服务用 grpc 连接 C 服务能成功,B 服务用 grpc 连接 C 服务失败,报错:“ failed reaching server: context deadline exceeded ”。

接下来记录一下解决该问题的过程

排查过程

  1. 确定三个服务都在同个 docker network 下
  2. 对比 GRPC 连接配置,确认连接地址是同一个
  3. 尝试将域名 + 端口的连接地址修改为ip + 端口,发现 B 服务能正常连上 C 服务了。确认是 dns 解析这一块问题。
  4. 用 B 服务镜像在同一个网络下单独启动一个新容器,并进入容器终端。
docker run --network univer -it --rm service-b-image:tag /bin/bash
  1. 在这个终端里安装 tcpdump 工具
apt-get update
apt-get install tcpdump
  1. 新开个命令行,连接上这个终端容器
docker exec -it xxxxxxx /bin/bash
  1. 现在有2个终端环境,一个先启动 tcpdump 监听 dns 解析,一个再启动 B 服务,查看 dns 请求日志
tcpdump -i eth0 port 53 -nn -vvv

可以看到有个 TXT 解析请求,就感觉比较奇怪为什么 grpc 建立连接要解析 TXT 记录? 图片.png

  1. 在终端中安装 dig 命令,验证一下这个 TXT 请求是什么情况
apt-get install dnsutils
dig TXT _grpc_config.univer-temporal.

可以看到查询状态为 status: SERVFAIL,查询时间为 4008 毫秒 图片.png

  1. 可能是系统原因?我重新在那个一切正常的 ubunutu 系统下,启动了相同服务,并监听 dns 解析,一样能看到相同的 TXT 请求记录。用 dig 验证了一下,它的响应很快才 43 毫秒,而且状态为 status: NXDOMAIN 为空

图片.png

解决问题

  1. 通过网上查找资料和查看代码,知道 grpc.NewClient 建立连接时,默认使用 grpc 官方的 dns resolver

图片.png

它解析域名的时候,会通过 TXT 记录获取一个 service config

图片.png

  1. 那 A 服务为什么能连接成功?因为 A 服务用的是旧的 grpc.Dial 连接方式,它会设置一个 passthrought 的 resolver 而不是用默认 dns resolver

图片.png

它的作用是直接将域名返回,并不解析,交给系统底层自己去做网络解析

图片.png

  1. 所以一共有 4 种解决方法:
  • 建立连接时添加个官方连接配置 grpc.WithDisableServiceConfig() 禁止掉 TXT 查询,比较推荐。

图片.png

  • 改用官方废弃的 grpc.Dial 方式
  • 自定义一个 resolver, 去掉 TXT 查询请求
package clients

import (
	"context"
	"net"

	"google.golang.org/grpc/resolver"
)

type noTxtResolverBuilder struct{}

func (*noTxtResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	r := &noTxtResolver{
		target: target,
		cc:     cc,
	}
	r.start()
	return r, nil
}

func (*noTxtResolverBuilder) Scheme() string { return "no-txt" }

type noTxtResolver struct {
	target resolver.Target
	cc     resolver.ClientConn
}

func (r *noTxtResolver) start() {
	endpoint := r.target.Endpoint()
	host, port, err := net.SplitHostPort(endpoint)
	if err != nil {
		host = endpoint
	}
	ips, err := net.DefaultResolver.LookupHost(context.Background(), host)
	if err != nil {
		r.cc.ReportError(err)
		return
	}
	addrs := make([]resolver.Address, len(ips))
	for i, ip := range ips {
		addr := ip
		if port != "" {
			addr = net.JoinHostPort(ip, port)
		}
		addrs[i] = resolver.Address{Addr: addr}
	}
	r.cc.UpdateState(resolver.State{Addresses: addrs})
}

func (r *noTxtResolver) ResolveNow(o resolver.ResolveNowOptions) {}
func (r *noTxtResolver) Close()                                  {}

func init() {
	resolver.Register(&noTxtResolverBuilder{})
}
  • 改用 ip 建立 grpc 连接

好了,上述便是我这期分析的内容,希望对你有帮助,我们有缘再见