在Kubernetes中,Pod访问DNS服务器(kube-dns)的最常见方法是通过服务抽象。 因此,在尝试解释问题之前,了解服务的工作原理以及因此在Linux内核中如何实现目标网络地址转换(DNAT)至关重要。

  1. (1) -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
  2. <...>
  3. (2) -A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
  4. <...>
  5. (3) -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LLLB6FGXBLX6PZF7
  6. (4) -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-LRVEW52VMYCOUSMZ
  7. <...>
  8. (5) -A KUBE-SEP-LLLB6FGXBLX6PZF7 -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.6:53
  9. <...>
  10. (6) -A KUBE-SEP-LRVEW52VMYCOUSMZ -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.7:53

在我们的示例中,每个Pod的/etc/resolv.conf中都有填充的名称服务器10.96.0.10条目。 因此,来自Pod的DNS查找请求将发送到10.96.0.10,它是kube-dns服务的ClusterIP(虚拟IP)。
由于(1),请求进入KUBE-SERVICE链,然后匹配规则(2)最后根据(3)随机值,跳转到(5)或(6)根据规则( 负载平衡),将请求UDP数据包的目标IPv4地址修改为DNS服务器的“实际” IPv4地址。 这种修饰是由DNAT完成的。
10.32.0.6和10.32.0.7是Weave Net网络中Kubernetes DNS服务器容器的IPv4地址。

Linux内核中的DNAT

DNAT的主要职责是同时更改传出数据包的目的地,答复数据包的源,并确保对所有后续数据包进行相同的修改。
后者严重依赖于连接跟踪机制,也称为conntrack,它被实现为内核模块。顾名思义,conntrack会跟踪系统中正在进行的网络连接。
以一种简化的方式,conntrack中的每个连接都由两个元组表示-一个元组用于原始请求(IP_CT_DIR_ORIGINAL),另一个元组用于答复(IP_CT_DIR_REPLY)。对于UDP,每个元组都由源IP地址,源端口以及目标IP地址和目标端口组成。答复元组包含存储在src字段中的目标的真实地址。

当从不同线程通过同一套接字同时发送两个UDP数据包时,会出现问题。
UDP是无连接协议,因此connect(2)syscall(与TCP相反)不会发送任何数据包,因此,在调用之后没有创建conntrack条目。
该条目仅在发送数据包时创建。这导致以下可能:

1、两个包都没有在1中找到一个确认的conntrack。nf_conntrack_in一步。为两个包创建具有相同元组的两个conntrack条目。
2、与上面的情况相同,但一个包的conntrack条目在另一个包调用3之前被确认。get_unique_tuple。另一个包通常在源端口更改后得到一个不同的应答元组。
3、与第一种情况相同,但是在步骤2中选择了具有不同端点的两个不同规则。ipt_do_table。

竞争的结果是相同的—其中一个包在步骤5中被丢弃。__nf_conntrack_confirm。

这正是在DNS情况下发生的情况。 GNU C库和musl libc都并行执行A和AAAA DNS查找。由于竞争,内核可能会丢弃其中一个UDP数据包,因此客户端通常会在5秒的超时后尝试重新发送它。

值得一提的是,这个问题不仅是针对Kubernetes的-任何并行发送UDP数据包的Linux多线程进程都容易出现这种竞争情况。

另外,即使您没有任何DNAT规则,第二场竞争也可能发生-加载nf_nat内核模块足以启用对get_unique_tuple的调用就足够了。

可以使用conntrack -S获得的insert_failed计数器可以很好地指示您是否遇到此问题。

缓解措施

意见建议
建议采取多种解决方法:禁用并行查找,禁用IPv6以避免AAAA查找,使用TCP进行查找,改为在Pod的解析器配置文件中设置DNS服务器的真实IP地址,等等。不幸的是,由于常用的容器基础映像Alpine Linux使用musl libc的限制,它们中的许多不起作用。
对于Weave Net用户来说似乎可靠的方法是使用tc延迟DNS数据包。

另外,您可能想知道在ipvs模式下的kube-proxy是否可以绕过这个问题。答案是否定的,因为conntrack也是在这种模式下启用的。此外,在使用rr调度程序时,可以在DNS流量较高的集群中轻松重现第3次竞争。

内核修复

1、 “ netfilter:nf_conntrack:解决冲突以匹配conntracks”修复了第一场比赛(被接受)。
2、 “ netfilter:nf_nat:返回相同的答复元组以匹配CT”修复了第二场比赛(等待复审)。

这两个补丁解决了仅运行一个DNS服务器实例的群集的问题,同时降低了其他实例的超时命中率。
为了在所有情况下完全消除问题,需要解决第三场竞争。一种可能的解决方法是在步骤5中将冲突的conntrack条目与来自同一套接字的不同目的地合并。__nf_conntrack_confirm。但是,这会使在该步骤中更改了目的地的数据包的先前iptables规则遍历的结果无效。
另一种可能的解决方案是在每个节点上运行DNS服务器实例,并按照我的同事的建议,通过Pod查询运行在本地节点上的DNS服务器。
结论
首先,我展示了“ DNS查找需要5秒”问题的基本细节,并揭示了罪魁祸首-Linux conntrack内核模块,它本质上是不受欢迎的。有关模块中也存在其他可能的问题。

解决方案如下:

方案(一):使用 TCP 协议发送 DNS 请求
通过resolv.conf的use-vc选项来开启 TCP 协议
如果使用 TCP 发 DNS 请求,connect 时就会发包建立连接并插入 conntrack 表项,而后并发的 A 和 AAAA 记录的请求在 send 时都使用 connect 建立好的这个 fd,由于 connect 时 conntrack 表项已经建立,所以 send 时不会再建立,也就不存在并发创建 conntrack 表项,避免了冲突。
通过resolv.conf的use-vc选项来开启 TCP 协议
测试
1、修改/etc/resolv.conf文件,在最后加入一行文本:
options use-vc

  1. ifecycle:
  2. postStart:
  3. exec:
  4. command:
  5. - /bin/sh
  6. - -c
  7. - "/bin/echo 'options use-vc' >> /etc/resolv.conf"

2、此压测可根据下面测试的go文件进行测试,编译好后放进一个pod中,进行压测:

200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000

方案(二):避免相同五元组 DNS 请求的并发
通过resolv.conf的single-request-reopen和single-request选项来避免:

single-request-reopen (glibc>=2.9) 发送 A 类型请求和 AAAA 类型请求使用不同的源端口。这样两个请求在 conntrack 表中不占用同一个表项,从而避免冲突。
single-request (glibc>=2.10) 避免并发,改为串行发送 A 类型和 AAAA 类型请求,没有了并发,从而也避免了冲突。

测试 single-request-reopen

修改/etc/resolv.conf文件,在最后加入一行文本:

options single-request-reopen

此压测可根据下面测试的go文件进行测试,编译好后放进一个pod中,进行压测:

200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000

  1. lifecycle:
  2. postStart:
  3. exec:
  4. command:
  5. - /bin/sh
  6. - -c
  7. - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"

测试 single-request

修改/etc/resolv.conf文件,在最后加入一行文本:

options single-request

此压测可根据下面测试的go文件进行测试,编译好后放进一个pod中,进行压测:

200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000

  1. lifecycle:
  2. postStart:
  3. exec:
  4. command:
  5. - /bin/sh
  6. - -c
  7. - "/bin/echo 'options single-request' >> /etc/resolv.conf"

最后结果,如果你测试过,相信coredns的测试如果还是增加使用 TCP 协议发送 DNS 请求,还是避免相同五元组 DNS 请求的并发,都没有显著的解决coredns延迟的结果
那么其实 k8s 官方也意识到了这个问题比较常见,所以也给出了 coredns 以 cache 模式作为 daemonset 部署的解决方案,Pod 的 DNS 查询都通过本地的 DNS 缓存查询,避免了 DNAT,从而也绕开了内核中的竞争问题。
最后结果,如果你测试过,相信coredns的测试如果还是增加使用 TCP 协议发送 DNS 请求,还是避免相同五元组 DNS 请求的并发,都没有显著的解决coredns延迟的结果。

那么其实 k8s 官方也意识到了这个问题比较常见,所以也给出了 coredns 以 cache 模式作为 daemonset 部署的解决方案

在 Kubernetes 集群中使用 NodeLocal DNSCache

NodeLocal DNSCache 通过在集群节点上作为 DaemonSet 运行 dns 缓存代理来提高集群 DNS 性能。 在当今的体系结构中,处于 ClusterFirst DNS 模式的 Pod 可以连接到 kube-dns serviceIP 进行 DNS 查询。 通过 kube-proxy 添加的 iptables 规则将其转换为 kube-dns/CoreDNS 端点。 借助这种新架构,Pods 将可以访问在同一节点上运行的 dns 缓存代理,从而避免了 iptables DNAT 规则和连接跟踪。 本地缓存代理将查询 kube-dns 服务以获取集群主机名的缓存缺失(默认为 cluster.local 后缀),并有效解决5秒延迟问题

在集群中运行 NodeLocal DNSCache 有如下几个好处:
如果本地没有 CoreDNS 实例,则具有最高 DNS QPS 的 Pod 可能必须到另一个节点进行解析,使用 NodeLocal DNSCache 后,拥有本地缓存将有助于改善延迟
跳过 iptables DNAT 和连接跟踪将有助于减少 conntrack 竞争并避免 UDP DNS 条目填满 conntrack 表。(常见的5s超时问题就是这个原因造成的)
从本地缓存代理到 kube-dns 服务的连接可以升级到 TCP,TCP conntrack 条目将在连接关闭时被删除,而 UDP 条目必须超时(默认 nf_conntrack_udp_timeout 是 30 秒)
将 DNS 查询从 UDP 升级到 TCP 将减少归因于丢弃的 UDP 数据包和 DNS 超时的尾部等待时间,通常长达 30 秒(3 次重试+ 10 秒超时)。
可以重新启用负缓存,从而减少对 kube-dns 服务的查询数量。

启用 NodeLocal DNSCache 之后,这是 DNS 查询所遵循的路径:

环境检查

该资源清单文件中包含几个变量,其中:
PILLARDNSSERVER :表示 kube-dns 这个 Service 的 ClusterIP,可以通过命令 kubectl get svc -n A | grep kube-dns | awk ‘{ print $4 }’ 获取
PILLARLOCALDNS:表示 DNSCache 本地的 IP,默认为 169.254.20.10
PILLARDNSDOMAIN:表示集群域,默认就是 cluster.local

另外还有两个参数 PILLARCLUSTERDNS 和 PILLARUPSTREAMSERVERS,这两个参数会通过镜像 1.15.6 版本以上的去进行配置,对应的值来源于 kube-dns 的 ConfigMap 和定制的 Upstream Server 配置。直接执行如下所示的命令即可安装:

运行nodelocaldns需要进行替换以下操作,如果下载过慢,可以直接使用下面的yaml来使用,需要替换的话,只有172.26.0.1,这个是kube-dns service的clusterIP

  1. wget -O nodelocaldns.yaml "https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml" && \
  2. sed -i 's/k8s.gcr.io/zhaocheng172/g' nodelocaldns.yaml && \
  3. sed -i 's/__PILLAR__DNS__SERVER__/172.26.0.10/g' nodelocaldns.yaml && \
  4. sed -i 's/__PILLAR__LOCAL__DNS__/169.254.20.10/g' nodelocaldns.yaml && \
  5. sed -i 's/__PILLAR__DNS__DOMAIN__/cluster.local/g' nodelocaldns.yaml
  1. cat nodelocaldns.yaml
  2. # Copyright 2018 The Kubernetes Authors.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. apiVersion: v1
  17. kind: ServiceAccount
  18. metadata:
  19. name: node-local-dns
  20. namespace: kube-system
  21. labels:
  22. kubernetes.io/cluster-service: "true"
  23. addonmanager.kubernetes.io/mode: Reconcile
  24. ---
  25. apiVersion: v1
  26. kind: Service
  27. metadata:
  28. name: kube-dns-upstream
  29. namespace: kube-system
  30. labels:
  31. k8s-app: kube-dns
  32. kubernetes.io/cluster-service: "true"
  33. addonmanager.kubernetes.io/mode: Reconcile
  34. kubernetes.io/name: "KubeDNSUpstream"
  35. spec:
  36. ports:
  37. - name: dns
  38. port: 53
  39. protocol: UDP
  40. targetPort: 53
  41. - name: dns-tcp
  42. port: 53
  43. protocol: TCP
  44. targetPort: 53
  45. selector:
  46. k8s-app: kube-dns
  47. ---
  48. apiVersion: v1
  49. kind: ConfigMap
  50. metadata:
  51. name: node-local-dns
  52. namespace: kube-system
  53. labels:
  54. addonmanager.kubernetes.io/mode: Reconcile
  55. data:
  56. Corefile: |
  57. cluster.local:53 {
  58. errors
  59. cache {
  60. success 9984 30
  61. denial 9984 5
  62. }
  63. reload
  64. loop
  65. bind 169.254.20.10 172.26.0.10
  66. forward . __PILLAR__CLUSTER__DNS__ {
  67. force_tcp
  68. }
  69. prometheus :9253
  70. health 169.254.20.10:8080
  71. }
  72. in-addr.arpa:53 {
  73. errors
  74. cache 30
  75. reload
  76. loop
  77. bind 169.254.20.10 172.26.0.10
  78. forward . __PILLAR__CLUSTER__DNS__ {
  79. force_tcp
  80. }
  81. prometheus :9253
  82. }
  83. ip6.arpa:53 {
  84. errors
  85. cache 30
  86. reload
  87. loop
  88. bind 169.254.20.10 172.26.0.10
  89. forward . __PILLAR__CLUSTER__DNS__ {
  90. force_tcp
  91. }
  92. prometheus :9253
  93. }
  94. .:53 {
  95. errors
  96. cache 30
  97. reload
  98. loop
  99. bind 169.254.20.10 172.26.0.10
  100. forward . __PILLAR__UPSTREAM__SERVERS__
  101. prometheus :9253
  102. }
  103. ---
  104. apiVersion: apps/v1
  105. kind: DaemonSet
  106. metadata:
  107. name: node-local-dns
  108. namespace: kube-system
  109. labels:
  110. k8s-app: node-local-dns
  111. kubernetes.io/cluster-service: "true"
  112. addonmanager.kubernetes.io/mode: Reconcile
  113. spec:
  114. updateStrategy:
  115. rollingUpdate:
  116. maxUnavailable: 10%
  117. selector:
  118. matchLabels:
  119. k8s-app: node-local-dns
  120. template:
  121. metadata:
  122. labels:
  123. k8s-app: node-local-dns
  124. annotations:
  125. prometheus.io/port: "9253"
  126. prometheus.io/scrape: "true"
  127. spec:
  128. priorityClassName: system-node-critical
  129. serviceAccountName: node-local-dns
  130. hostNetwork: true
  131. dnsPolicy: Default # Don't use cluster DNS.
  132. tolerations:
  133. - key: "CriticalAddonsOnly"
  134. operator: "Exists"
  135. - effect: "NoExecute"
  136. operator: "Exists"
  137. - effect: "NoSchedule"
  138. operator: "Exists"
  139. containers:
  140. - name: node-cache
  141. image: harbor.lianhang.jetair/k8s/k8s-dns-node-cache:1.15.14
  142. resources:
  143. requests:
  144. cpu: 25m
  145. memory: 5Mi
  146. args: [ "-localip", "169.254.20.10,172.26.0.10", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ]
  147. securityContext:
  148. privileged: true
  149. ports:
  150. - containerPort: 53
  151. name: dns
  152. protocol: UDP
  153. - containerPort: 53
  154. name: dns-tcp
  155. protocol: TCP
  156. - containerPort: 9253
  157. name: metrics
  158. protocol: TCP
  159. livenessProbe:
  160. httpGet:
  161. host: 169.254.20.10
  162. path: /health
  163. port: 8080
  164. initialDelaySeconds: 60
  165. timeoutSeconds: 5
  166. volumeMounts:
  167. - mountPath: /run/xtables.lock
  168. name: xtables-lock
  169. readOnly: false
  170. - name: config-volume
  171. mountPath: /etc/coredns
  172. - name: kube-dns-config
  173. mountPath: /etc/kube-dns
  174. volumes:
  175. - name: xtables-lock
  176. hostPath:
  177. path: /run/xtables.lock
  178. type: FileOrCreate
  179. - name: kube-dns-config
  180. configMap:
  181. name: kube-dns
  182. optional: true
  183. - name: config-volume
  184. configMap:
  185. name: node-local-dns
  186. items:
  187. - key: Corefile
  188. path: Corefile.base

需要注意的是这里使用 DaemonSet 部署 node-local-dns 使用了 hostNetwork=true,会占用宿主机的 8080 端口,所以需要保证该端口未被占用。

另外我们还需要修改 kubelet 的 —cluster-dns 参数,将其指向 169.254.20.10,Daemonset 会在每个节点创建一个网卡来绑这个 IP,Pod 向本节点这个 IP 发 DNS 请求,缓存没有命中的时候才会再代理到上游集群 DNS 进行查询。

两种方案测试nodelocaldns实效性

第一种就是定制一个pod

Kubernetes Pod dnsPolicy 可以针对每个Pod设置DNS的策略,通过PodSpec下的dnsPolicy字段可以指定相应的策略
这种方式可以直接启动一个pod,Pods将直接可以访问在同一节点上运行的 dns 缓存代理,从而避免了 iptables DNAT 规则和连接跟踪,但是这种对于整体集群来讲并不适合,只提高了当前pod的DNScache的命中率,这种适合定制一些dns策略

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. creationTimestamp: null
  5. labels:
  6. app: web
  7. name: web
  8. namespace: kube-system
  9. spec:
  10. replicas: 1
  11. selector:
  12. matchLabels:
  13. app: web
  14. strategy: {}
  15. template:
  16. metadata:
  17. creationTimestamp: null
  18. labels:
  19. app: web
  20. spec:
  21. containers:
  22. - image: nginx
  23. name: nginx
  24. dnsConfig:
  25. nameservers:
  26. - 169.254.20.10
  27. searches:
  28. - public.svc.cluster.local
  29. - svc.cluster.local
  30. - cluster.local
  31. options:
  32. - name: ndots
  33. value: "5"
  34. dnsPolicy: None
第二种如果对于集群来讲,需要全部生效

需要替换每个节点的clusterDNS的地址

clusterDNS:

  • 10.96.0.10
    替换的话可以直接使用sed直接替换,另外需要所有节点替换并重启kubelet

sed -i ‘s/10.96.0.10/169.254.20.10/g’ /var/lib/kubelet/config.yaml
systemctl daemon-reload
systemctl restart kubelet
待 node-local-dns 安装配置完成后,我们可以部署一个新的 Pod 来验证下:(test-node-local-dns.yaml)

文档更新时间: 2020-10-23 14:38   作者:张尚