问题背景
在我们的Service-mesh系统项目中,为了适配更广泛的系统环境,也针对国产麒麟操作系统对我们的系统进行了适配改造。此次改造中的麒麟操作系统是运行在ARM架构上的,因此,我们首先对sidecar进行了基于ARM架构的编译,接着对改造后的sidecar进行了详细的功能测试。在测试过程中,我们发现一个现象,在通过sidecar连续转发请求时,会不定期出现规律性5秒卡顿的情况。为了解决该问题,我们进行了深入的研究并提出了修改建议。
问题分析
问题复现
1. 不定期出现
根据截图我们可以看到,卡顿现象是不定期出现的。没有固定的周期,也没有固定的量级区间(即不是固定的每发n个包就延迟一次)。
2. 规律性5秒
虽然卡顿的出现是随机的,但是卡顿的时间是固定的5秒。这让我们认定一定是触发了系统(不单单指我们的Service-mesh系统,也可以是操作系统等其他功能组件)中某个组件的某种机制。这为我们的修复工作带来了希望。
问题分析
1. 初步排查
5秒的卡顿让我们首先想到了在Linux中常见的DNS解析默认5秒超时问题,首先我们检查了测试用机的/etc/resolv.conf中的配置。如下所示。
; generated by /usr/sbin/dhclient-script
nameserver 100.100.2.136
nameserver 100.100.2.138
为了是否印证为DNS解析超时的影响,我们在配置文件中加入options timeout: 2
以降低延迟时间,修改后的配置如下:
options timeout:2
; generated by /usr/sbin/dhclient-script
nameserver 100.100.2.136
nameserver 100.100.2.138
修改后,延迟时间果然从规律性5秒减低到了2秒。因此可以初步断定DNS解析的延迟有很大的概率造成了5秒延迟的现象。
但为了更准确的确定问题根源,我们仍对我们系统中的sidecar以及DNS-Server进行了测试,并最终定位到问题。
2. 排除DNS-Server的影响
由于在我们的项目中,通过服务名访问时,流量首先会经过iptables的过滤将正常的DNS解析请求转发到我们所使用的DNS-Server上,然后返回给sidecar。具体流程如下图所示:
因此,我们还需要排除sidecar是否有阻塞的情况。我们使用tcpdump抓包后分析报文发现,卡顿发生时,我们的DNS-server无论是对于ipv4或ipv6地址的解析请求都能够成功处理。因此也并不是因为DNS-Server的问题。
3. 排除sidecar的影响
由于系统在RedHat7 X86的机器上是完全能够正常运行的。我们也对RedHat上的请求进行了抓包分析,入下图所示。并未出现卡顿情况,因为系统只会发送ipv4的解析请求。
根据这个情况,我们在麒麟操作系统中只发送ipv4解析请求,也不再出现卡顿的情况了。
同时,当只发送ipv6解析时,也没有卡顿的现象发生。
因此,我们也排除了我们sidecar的影响。
根源剖析
在Linux中存在已久的DNS解析延迟问题
我们从抓包中可以看到,在5秒延迟发生时前后两次的DNS请求会有以下两个现象:
-
每次DNS解析中ipv4和ipv6请求报文具有相同的TransactionsID(以下简称TXID)。关于TXID,RFC 1035 中用详细的定义:
A 16 bit identifier assigned by the program that generates any kind of query. This identifier is copied the corresponding reply and can be used by the requester to match up replies to outstanding queries. 由程序分配的一种16位标识符,用于生成任何类型的查询。此标识符被复制到相应的应答中,请求者可以使用它将应答与未完成的查询进行匹配。------- https://tools.ietf.org/html/rfc1035
每次重试时的请求是顺序发送的,并不是同时发送的。虽然他们仍有相同的TXID,但从报文的返回顺序可以看出,先发了一条然后收到返回后又发了第二条请求。为什么这么做在后文我们会解释。
根据上述两个现象,我们查找了一些资料,并最终对问题的根源有了一个清晰的认识。这需要从Linux操作系统的DNS解析过程进行分析。
1. Linux中DNS解析过程
1. 在RHEL5/CentOS5/Ubuntu 10.04等Linux下,DNS的解析请求过程如下:
- 主机从一个随机的源端口,请求 DNS的AAAA 记录
- 主机接受DNS-Server返回AAAA记录
- 主机从一个另一个随机的源端口,请求 DNS的A 记录
- 主机服DNS-Server返回A记录,
2. 在RHEL6/CentOS6及以上,交互过程有所不同,过程如下:
- 主机从一个随机的源端口,请求 DNS的A 记录
- 主机从同一个源端口,请求 DNS的AAAA 记录
- 主机接受DNS-Server返回A记录
- 主机接受DNS-Server返回AAAA记录,
2. 麒麟操作系统中DNS解析过程
虽然无法拿到准确的DNS解析过程资料,但根据捕获的报文可以判断,中标麒麟的DNS解析过程是与RHEL6/CentOS6及以上的Linux版本保持一致的。因此它也会出现使用同源端口并发发送ipv4&ipv6的地址解析请求。至于为什么有时ipv4和ipv6的解析请求TXID会相同,我们很难做出判断。
3. 造成5秒超时的原因
其实问题的根源在于netfilter conntrack
模块的设计问题。DNS client (glibc 或 musl libc) 会并发请求 A 和 AAAA 记录,跟 DNS Server 通信自然会先 connect (建立 fd),后面请求报文使用这个 fd 来发送,由于 UDP 是无状态协议, connect 时并不会创建 conntrack 表项, 而并发请求的 A 和 AAAA 记录默认使用同一个 fd 发包,这时它们源端口相同,当并发发包时,两个包都还没有被插入 conntrack 表项,所以 netfilter 会为它们分别创建 conntrack 表项,在我们的系统中,所有请求53端口的DNS报文都会经过iptables被 DNAT 到一个本地DNS-server 的 15030端口。
DNAT
的主要职责是同时更改传出数据包的目的地,响应数据包的源,并确保对所有后续数据包进行相同的修改。后者严重依赖于连接跟踪机制,也称为 conntrack
,它被实现为内核模块。conntrack
会跟踪系统中正在进行的网络连接。
conntrack
中的每个连接都由两个元组表示,一个元组用于原始请求(IP_CT_DIR_ORIGINAL),另一个元组用于答复(IP_CT_DIR_REPLY)。对于UDP,每个元组都由源 IP 地址,源端口以及目标 IP 地址和目标端口组成,答复元组包含存储在src字段中的目标的真实地址。
当两个包被 DNAT 成同一个 IP,最终它们的五元组就相同了,在最终插入的时候后面那个包就会被丢掉,现象就是dns请求超时,client 默认策略是等待 5s 自动重试,如果重试成功,我们看到的现象就是 dns 请求有 5s 的延时。
更详细的分析可以参考Weave works工程师Martynas Pumputis的文章: https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts
简而言之就是下述两点:
- 只有多个线程或进程,并发从同一个socket发送相同五元组的UDP报文时,才有一定概率会发生
- glibc, musl(alpine linux的libc库)都使用"parallel query", 就是并发发出多个查询请求,因此很容易碰到这样的冲突,造成查询请求被丢弃
对比上述的两种解析过程,我们再看下在发生延迟时的解析过程:
- 主机从一个随机的源端口,请求 DNS的A 记录,
- 主机从同一个源端口,请求 DNS的AAAA 记录,
- 主机先收到dns返回的AAAA记录
- 防火墙认为本次交互通信已经完成,关闭连接
- 于是剩下的dns服务器返回的A记录响应包被防火墙丢弃
- 等待5秒超时之后,主机因为收不到A记录的响应,重新通过新的端口发起A记录查询请求,此后的机制等同于centos5
- 主机收到dns的A记录响应
- 主机从同一端口端口发起AAAA
- 主机收到dns的AAAA记录响应
也就是在重试时,系统会默认使用顺序发送ipv4解析请求和ipv6解析请求,从而避免上述延迟的现象发生,这也是为什么重试的时候成功的概率就会加大,这种机制也是Linux对于内核的一个修复。但并没有真正解决问题,因此我们会提出一些修改建议。
解决建议
根据上述的根源剖析,我们已经可以基本断定问题产生的原因在于DNS解析过程中如果存在同源端口并发DNS ipv4&6地址解析请求时。我们所选用的麒麟操作系统一定会存在DNS解析5秒延迟的现象。那么为了避免出现该问题,我们只要根据自身系统的需求解决同源端口、并发、ip4&ipv6请求三个条件中至少任意一个前提条件就可以顺利解决该问题。
Single-request 解决并发
# single-request (since glibc 2.10)
Sets RES_SNGLKUP in _res.options. By default, glibc
performs IPv4 and IPv6 lookups in parallel since
version 2.9. Some appliance DNS servers cannot handle
these queries properly and make the requests time out.
This option disables the behavior and makes glibc
perform the IPv6 and IPv4 requests sequentially (at the
cost of some slowdown of the resolving process).
在 /etc/resolv.conf中添加下列参数:
options single-request
; generated by /usr/sbin/dhclient-script
nameserver 183.60.83.19
nameserver 183.60.82.98
发送A类型(ipv4 )请求和AAAA类型(ipv6)请求同样会使用同源端口发送,知识发送方式改为了串行,从而也避免了冲突。
Single-request-open 解决并发及同源端口
# single-request-reopen (since glibc 2.9)
Sets RES_SNGLKUPREOP in _res.options. The resolver
uses the same socket for the A and AAAA requests. Some
hardware mistakenly sends back only one reply. When
that happens the client system will sit and wait for
the second reply. Turning this option on changes this
behavior so that if two requests from the same port are
not handled correctly it will close the socket and open
a new one before sending the second request.
在 /etc/resolv.conf中添加下列参数:
options single-request-reopen
; generated by /usr/sbin/dhclient-script
nameserver 183.60.83.19
nameserver 183.60.82.98
发送A类型(ipv4 )请求和AAAA类型(ipv6)请求使用不同的源端口。这样两个请求在conntrack表中不占用同一个表项,从而避免冲突。
关闭 ipv6 解决 ipv4&6请求
其实还可以根据项目的需求进行评估是否开启ipv6解析,事实上如果不需要ipv6解析,直接完全关闭ipv6的相关组件。在只有单一A类型的请求的情况下,也可以避免出现冲突的情况。
Timeout 并不根治但可以降低延迟
如果前三种参数无法满足业务需求,那么添加Timeout
参数是可以降低延迟的,如下图所示,这样可以将延迟降低到1s,或再加上attempts
参数,使重试次数限制起来。
options timeout: 1 attempts: 1
; generated by /usr/sbin/dhclient-script
nameserver 183.60.83.19
nameserver 183.60.82.98