首先让我们先复现这个问题。

1.创建一个lxc 容器develop,容器的网络类型为veth.

2.在容器里面安装curl以及tcpdump.

3.用tcpdump对以下3个地方进行抓包:

  a.容器的网络接口;

  b.host这边的bridge接口;

  c.host的对外接口;

4.在容器中用curl来访问www.163.com

5.杀掉tcpdump进程停止抓包

 

那么在实验中的现象就是curl访问网络会有几秒钟的延迟。

[develop] / # time curl www.163.com
<html><head><title>ERROR: ACCESS DENIED</title></head><body><center><h1>ERROR: ACCESS DENIED</h1></center><hr>
<center>Thu, 19 Mar 2020 05:45:28 GMT (taikoo/BC110_dx-jiangsu-zhenjiang-1-cache-7)</center></BODY></HTML>
<!-- web cache -->
real    0m 2.69s
user    0m 0.01s
sys     0m 0.01s

我们可以看到这条指令执行了将近3秒钟。

 

为什么会需要3秒呢,我们来看看用tcpdump在容器的网络接口上抓包结果。

 

 

我们从上面的抓包来看,curl在访问www.163.com的时候,会同时发出2个DNS query报文, 1个type A,用来获取服务器的IPv4的地址,一个type AAAA,用来获取服务器的IPv6的地址。发出请求后,很快就收到了type A的response。但是没有收到type AAAA的response, 于是,2.5秒后容器重发了一遍AAAA的DNS query,这一次很快就收到了AAAA类型的response。之后curl才开始向服务器建立TCP链接,发送HTTP请求。

 

那么host上面的bridge接口呢,我们可以观察到跟上面一样的现象。

 

但是如果我们观察host一侧的WAN口上的tcpdump发现第一个AAAA DNS response并没有延迟,也就是说第一个AAAA DNS response被host drop掉了。

 

关于这个问题的分析,可以见下面这篇文章。

https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts

 

总的意思是linux的net conntrack模块同时在处理DNS A response和DNS AAAA response有冲突问题,所以就丢掉了一个response.那么容器里面的DNS client就会timeout,然后重发。

 

这个问题至今没有在linux kernel里面fix掉,那么我们该怎么解决掉呢。既然是因为2个DNS response同时处理时有问题,可不可以让2个request不同时发呢?

 

关于这个问题的解决,有下面几种解决办法。

一. 我们可以配置在/etc/resolve.conf里面对dns resolve进行配置:

 **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).

这样的话就会一前一后发送2个DNS query. 但是这个对于musl libc来说是不生效的。

二.还有一个办法是通过在内核里对AAAA DNS query进行延迟发送,思路如下:

1. 用iptables对AAAA DNS query报文进行标记

2. 通过tc filter 根据标记进行分类,然后在tc队列里用netem模块的延时功能对AAAA DNS query进行延时发送。

 

首先我们要重新编译内核,加入kmod-netem模块。

然后在容器的名字空间里配置如下:(记得一定是在容器的名字空间里面,所以下面的配置脚本可以放在容器的钩子脚本lxc.hook.pre-mount里面)

tc qdisc add dev eth0 root handle 1: prio bands 2 priomap 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
tc qdisc add dev eth0 parent 1:2 handle 12: fq_codel
tc qdisc add dev eth0 parent 1:1 handle 11: netem delay 5ms 1ms
tc filter  add dev eth0 protocol all parent 1: prio 1 handle 0x123 fw flowid 1:1
iptables -A POSTROUTING -t mangle -p udp --dport 53 -m string --hex-string "|00001C0001|" --algo bm --from 40 -j MARK --set-mark 0x123

"|00001C0001|"用来匹配AAAA DNS query报文,匹配上之后会对报文标记为0x123。之后在tc filter中对标记进行匹配,匹配到flowid 1:1。对于flow 1:1, netem会对报文进行4ms~6ms之间的延迟。

 

当然如果应用程序完全不需要要IPv6,那我们就没有必要大费周折,只需要在编写应用程序代码时,禁止程序进行AAAA DNS query.

 

如果你使用curllib进行网络编程,那么你可以设置option,让他只进行DNS A Query.

curl_easy_setopt(handle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

结束。