以下故障环境纯属捏造,如有雷同,仅供处理参考,请以实际情况为主。

问题背景

我们有一个反向代理设备(后称 Proxy),CentOS7 系统的,他会代理请求用户发来的请求给后端服务器(后称 Server)。有用户发现,当他的请求过了这个 Proxy 之后,会慢个大概 6 秒左右,遂上报了这个故障。

请求拓扑:Client–业务域名–>Proxy–后端真实域名–>Server

问题排查

收到这个问题后,我第一时间复现了问题。我使用 Edge 访问业务域名,也会觉得非常卡顿,打开 DevTools 发现部分请求会在 6 秒左右。Waterfall 一片绿,大概是请求都耗在了等待从服务器接收第一个字节上了。

20230312012724

是我电脑到 Proxy 的网络中有什么问题吗?或者有没有可能是 Proxy 的出口带宽不足了?

我登录上了 Proxy 的服务器,开起了一个 SSH 隧道,通过这个隧道直接访问后端真实域名,发现现象依然存在,部分请求会卡顿 6 秒左右。那这个问题似乎不在 Client 和 Proxy 之间了,更像 Proxy 到 Server 间有什么问题。

那我 curl 一下后端真实域名吧,并且抓一下我 curl 后端时候的包,看看是不是 Proxy 和 Server 之间有什么影响访问的设备。

1
curl https://apis.gw.16iot.cn/get/ak -o /dev/null -s -w time_namelookup:%{time_namelookup}\ntime_connect:%{time_connect}\ntime_starttransfer:%{time_starttransfer}\ntime_total:%{time_total}\n

使用上面的命令,可以打印 curl 时候,记录几个关键的时间,包括 DNS 解析的时间,Client 和 Server(这里也就是 Proxy 和 Server)建立 TCP 连接的时间,TTFB 时间,以及总的时间。

1
2
3
4
time_namelookup:5.075034
time_connect:5.075537
time_starttransfer:5.115484
time_total: 5.115582

看上面这个测试结果,namelookup 达到了惊人的 5 秒,那这是 DNS 的问题?

为了进一步定位这个问题,我尝试在 Proxy 服务器上,dig 这个后端的真实域名,发现 DNS 服务器很快就能给予响应,响应时间一般是毫秒级的。

20230312021929

那这个不对劲,检查 DNS 结果和之前 curl 测试结果差距很大。很难说法是 DNS 出现了什么问题。

目前还定位不出问题在哪,那就对比一下 dig 和 curl 域名时候关于 namelookup 这块有什么差异吧。

对比发现,dig 在不指定查询什么类型的记录时,默认查询 A 记录,但是 curl 如果不指定 v4 还是 v6,那默认会查询 A 记录和 AAAA 记录,如下图。

dig apis.gw.16iot.cn

20230312022403

curl https://apis.gw.16iot.cn

20230312022505

已知是这个后端真实域名是没有 AAAA 记录的,不过我们还是要测试一下 dig 查询这个后端真实域名的 AAAA 记录的速度如何,到底是不是他影响了我们的访问速度。

dig AAAA apis.gw.16iot.cn 发现有明显卡顿,时间在 5 秒+,DNS 服务器会回复 NOERROR,但是没有 AAAA 记录。

照目前这个情况看,大概率是 AAAA 记录查询拖慢了访问速度。我又找了两个方法印证了一下。

  1. curl -4 https://apis.gw.16iot.cn 指定 curl 走 v4,速度正常,无卡顿
  2. 写 hosts,只保留 v4 地址,过隧道访问后端真实域名 速度正常,无卡顿

综上情况,基本确定了是 AAAA 记录查询拖慢了访问速度。

为什么

一开始以为是 Curl 的问题,我翻了一下 libcurl 的代码,发现 libcurl 域名解析的实现方式有两种,一种是 asyn-thread,一种是 asyn-ares,这个在编译 libcurl 的时候就已经确定用哪种方式做解析了,默认是 asyn-thread,即单独开启一个线程调用系统域名解析 API。顺着这个思路继续找,需要看一下 glibc 中是怎么处理域名解析的了。

名称服务转换(Name Service Switch,NSS)是 glibc 包的一部分,用于进行域名解析,搜索顺序定义在 /etc/nsswitch.conf 中,负责响应域名解析的数据库是 hosts 数据库,在 glibc 中提供了 files 和 dns 两种服务。对于 files 而言,会读取读取 /etc/hosts 文件,对于 dns 则会读取 /etc/resolv.conf 的 glibc 解析器。

无论服务器有没有 IPv6 地址,都会查询 AAAA 记录,最后只是不用罢了。

我找了一下关于 resolv.conf 的文档,发现对于 resolver configuration 来说,timeout 的时间是 5 秒。所以之前 curl 的时候,这种很稳定的 5 秒卡顿应该就是 DNS 查询超时了。

20230312115932

回过头来看一下之前抓的 curl 后端域名时候的数据包。其中 42 是 Proxy,92 是 DNS。

20230312125447

1,2 号数据包,Proxy 通过 15427 这个端口,分别使用 0x6 和 0x3 这俩 DNSID 查询了后端域名的 A 记录和 AAAA 记录。
3 号数据包,DNS 通过 53 端口回复了 DNSID 为 0x6 这个 A 记录的查询结果。
5 秒之后,因为 0x3 这个 AAAA 的查询没有结果,系统又重发了 0x6 和 0x3 这个 DNS 查询请求。这次 Proxy 收到了查询的结果,即 4,5 和 6,7 两组数据包。

很遗憾我的权限不足以登录 DNS 继续排查,所以并不知道 DNS 到底一开始会没回复 0x3 这个 DNSID 的 AAAA 查询。

我查了 glibc 发布说明,2.9 版本的 Changelog 中提到了 Unified lookup for getaddrinfo: IPv4 and IPv6 addresses are now looked up at the same time. 这个版本在做 DNS 查询时采用了并行的方式,从同一个 SrcPort 做 A 和 AAAA 记录的查询。这样做确实提高了 DNS 查询效率,端口复用率也进一步增高,节省了部分资源。

但是在这种机制下,可能有比较极端的场景。
比如同源目的 IP,同源目的端口,同样的 4 层协议的连接,会被防火墙当成一个会话,在第一次查询返回后就认为这个会话已经结束,后面的查询结果的返回包就被丢弃了。或者 DNS 服务器那边响应处理问题只响应了一次查询。再或者客户端内核(k8s 场景)丢弃了某一次回复的查询。
但是这时候客户端还在等待。就显得非常不和谐了。

为了解决这个问题,官方给了一个 option,可以通过配置 single-request-reopen,关于这个字段的说明,man 中是这么说的。意思大概就是当 DNS 查询请求没有被正确响应时,超时后会换个端口继续请求。

20230312141224

2.10 版本 glibc 提出了另外一种解决这种问题的方法。即配置single-request,其说明如下,主要解决的就是 2.9 版本并行查询 DNS 响应异常的情况。通过这个字段可以人为把 DNS 查询方式改回串行方式。

20230312141939

按照 glibc 2.10 news 中关于“DNS NSS improvement”部分的说法。他们认为并行模式是更加快速的,而且适用于大多数场景,single-request 只是一种兼容的模式,所以不做默认开启。

1
All of these work-arounds are easy to implement. Therefore there is no reason to not have the fast mode the default which in any case will work for 99% of the people.

总结:解决方法

1、联系 DNS 方和安全设备方,一起排查 AAAA 查询不回复的问题。

2、临时使用 hosts 方式,指定到后端域名走 v4 地址。

3、glibc 需要>=2.10 配置 resolv.conf 的 options,启用 single-request,将并行的 DNS 查询改成串行查询。

我选择了 1,虽然原理如此,但是我直接 dig 后端真实域名,DNS 也是回复 AAAA 记录很慢。感觉是 DNS 问题的可能性比较大。另外两种方法也留着供大家参考一下吧。

参考文档


文章封面 BY fabio on Unsplash