作者: 洪志国
超时问题
客户反馈从pod中访问服务时,总是有些请求的响应时延会达到5秒。正常的响应只需要毫秒级别的时延。
DNS 5秒延时
在pod中(通过nsenter -n tcpdump)抓包,发现是有的DNS请求没有收到响应,超时5秒后,再次发送DNS请求才成功收到响应。
在kube-dns pod抓包,发现是有DNS请求没有到达kube-dns pod, 在中途被丢弃了。
为什么是5秒? man resolv.conf
可以看到glibc的resolver的缺省超时时间是5s。
丢包原因
经过搜索发现这是一个普遍问题。
根本原因是内核conntrack模块的bug。
Weave works的工程师Martynas Pumputis对这个问题做了很详细的分析:
https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts
相关结论:
- 只有多个线程或进程,并发从同一个socket发送相同五元组的UDP报文时,才有一定概率会发生
- glibc, musl(alpine linux的libc库)都使用”parallel query”, 就是并发发出多个查询请求,因此很容易碰到这样的冲突,造成查询请求被丢弃
- 由于ipvs也使用了conntrack, 使用kube-proxy的ipvs模式,并不能避免这个问题
问题的根本解决
Martynas向内核提交了两个patch来fix这个问题,不过他说如果集群中有多个DNS server的情况下,问题并没有完全解决。
其中一个patch已经在2018-7-18被合并到linux内核主线中: netfilter: nf_conntrack: resolve clash for matching conntracks
目前只有4.19.rc 版本包含这个patch。
规避办法
规避方案一:使用TCP发送DNS请求
由于TCP没有这个问题,有人提出可以在容器的resolv.conf中增加options use-vc
, 强制glibc使用TCP协议发送DNS query。下面是这个man resolv.conf中关于这个选项的说明:
1 | use-vc (since glibc 2.14) |
笔者使用镜像”busybox:1.29.3-glibc” (libc 2.24) 做了试验,并没有见到这样的效果,容器仍然是通过UDP发送DNS请求。
规避方案二:避免相同五元组DNS请求的并发
resolv.conf还有另外两个相关的参数:
- single-request-reopen (since glibc 2.9)
- single-request (since glibc 2.10)
man resolv.conf中解释如下:
1 | single-request-reopen (since glibc 2.9) |
笔者做了试验,发现效果是这样的:
- single-request-reopen
发送A类型请求和AAAA类型请求使用不同的源端口。这样两个请求在conntrack表中不占用同一个表项,从而避免冲突。 - single-request
避免并发,改为串行发送A类型和AAAA类型请求。没有了并发,从而也避免了冲突。
要给容器的resolv.conf加上options参数,有几个办法:
1) 在容器的”ENTRYPOINT”或者”CMD”脚本中,执行/bin/echo 'options single-request-reopen' >> /etc/resolv.conf
2) 在pod的postStart hook中:
1 | lifecycle: |
3) 使用template.spec.dnsConfig (k8s v1.9 及以上才支持):
1 | template: |
4) 使用ConfigMap覆盖POD里面的/etc/resolv.conf
configmap:
1 | apiVersion: v1 |
POD spec:
1 | volumeMounts: |
5) 使用MutatingAdmissionWebhook
MutatingAdmissionWebhook 是1.9引入的Controller,用于对一个指定的Resource的操作之前,对这个resource进行变更。
istio的自动sidecar注入就是用这个功能来实现的。 我们也可以通过MutatingAdmissionWebhook,来自动给所有POD,注入以上3)或者4)所需要的相关内容。
以上方法中, 1)和2)都需要修改镜像, 3)和4)则只需要修改POD的spec, 能适用于所有镜像。不过还是有不方便的地方:
- 每个工作负载的yaml都要做修改,比较麻烦
- 对于通过helm创建的工作负载,需要修改helm charts
方法5)对集群使用者最省事,照常提交工作负载即可。不过初期需要一定的开发工作量。
规避方案三:使用本地DNS缓存
容器的DNS请求都发往本地的DNS缓存服务(dnsmasq, nscd等),不需要走DNAT,也不会发生conntrack冲突。另外还有个好处,就是避免DNS服务成为性能瓶颈。
使用本地DNS缓存有两种方式:
- 每个容器自带一个DNS缓存服务
- 每个节点运行一个DNS缓存服务,所有容器都把本节点的DNS缓存作为自己的nameserver
从资源效率的角度来考虑的话,推荐后一种方式。
实施办法
条条大路通罗马,不管怎么做,最终到达上面描述的效果即可。
POD中要访问节点上的DNS缓存服务,可以使用节点的IP。 如果节点上的容器都连在一个虚拟bridge上, 也可以使用这个bridge的三层接口的IP(在TKE中,这个三层接口叫cbr0)。 要确保DNS缓存服务监听这个地址。
如何把POD的/etc/resolv.conf中的nameserver设置为节点IP呢?
一个办法,是设置POD.spec.dnsPolicy为”Default”, 意思是POD里面的/etc/resolv.conf, 使用节点上的文件。缺省使用节点上的/etc/resolv.conf(如果kubelet通过参数–resolv-conf指定了其他文件,则使用–resolv-conf所指定的文件)。
另一个办法,是给每个节点的kubelet指定不同的–cluster-dns参数,设置为节点的IP,POD.spec.dnsPolicy仍然使用缺省值”ClusterFirst”。 kops项目甚至有个issue在讨论如何在部署集群时设置好–cluster-dns指向节点IP: https://github.com/kubernetes/kops/issues/5584