最近遇到个 grpc 连接报错问题,主要现象是:
- 有3个容器服务 A,B,C
- 宿主机是 ubuntu 系统时,A 服务用 grpc 连接 C 服务能成功,B 服务用 grpc 连接 C 服务也能成功
- 宿主机是 Centos7 系统时,A 服务用 grpc 连接 C 服务能成功,B 服务用 grpc 连接 C 服务失败,报错:“ failed reaching server: context deadline exceeded ”。
接下来记录一下解决该问题的过程
排查过程
- 确定三个服务都在同个 docker network 下
- 对比 GRPC 连接配置,确认连接地址是同一个
- 尝试将
域名 + 端口的连接地址修改为ip + 端口,发现 B 服务能正常连上 C 服务了。确认是 dns 解析这一块问题。 - 用 B 服务镜像在同一个网络下单独启动一个新容器,并进入容器终端。
docker run --network univer -it --rm service-b-image:tag /bin/bash
- 在这个终端里安装 tcpdump 工具
apt-get update
apt-get install tcpdump
- 新开个命令行,连接上这个终端容器
docker exec -it xxxxxxx /bin/bash
- 现在有2个终端环境,一个先启动 tcpdump 监听 dns 解析,一个再启动 B 服务,查看 dns 请求日志
tcpdump -i eth0 port 53 -nn -vvv
可以看到有个 TXT 解析请求,就感觉比较奇怪为什么 grpc 建立连接要解析 TXT 记录?
- 在终端中安装 dig 命令,验证一下这个 TXT 请求是什么情况
apt-get install dnsutils
dig TXT _grpc_config.univer-temporal.
可以看到查询状态为 status: SERVFAIL,查询时间为 4008 毫秒
- 可能是系统原因?我重新在那个一切正常的 ubunutu 系统下,启动了相同服务,并监听 dns 解析,一样能看到相同的 TXT 请求记录。用 dig 验证了一下,它的响应很快才 43 毫秒,而且状态为
status: NXDOMAIN为空
解决问题
- 通过网上查找资料和查看代码,知道 grpc.NewClient 建立连接时,默认使用 grpc 官方的 dns resolver
它解析域名的时候,会通过 TXT 记录获取一个 service config
- 那 A 服务为什么能连接成功?因为 A 服务用的是旧的 grpc.Dial 连接方式,它会设置一个 passthrought 的 resolver 而不是用默认 dns resolver
它的作用是直接将域名返回,并不解析,交给系统底层自己去做网络解析
- 所以一共有 4 种解决方法:
- 建立连接时添加个官方连接配置 grpc.WithDisableServiceConfig() 禁止掉 TXT 查询,比较推荐。
- 改用官方废弃的 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 连接
好了,上述便是我这期分析的内容,希望对你有帮助,我们有缘再见