Coredns+Nodelocaldns cache解决Coredns域名解析延迟荐

目前18.6版本和之前的coredns都会出现超时5s的情况,那么为什么会出现coredns超时的情况发生?

背景

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

服务是如何G s 6 M |工作的?

在iptables模式下(默认情况下)0 8 * p i h v,每个服务的kube-proxy在主机网络名称空间的nat表中创建一些iptab% , G n Bles规则。
让我们考虑在集Y B + 7 ^ b群中具有两个DNS服务器实例的z q Ckube-dns服务。 相关规则如下:

(1) -A PREROUTING -m comment --co| E x I # S q Ymment "kubernetes service p& R r S N 8 & 0 cor2 + { V 4tals" -j KUBE-SERVICES
<...>
(2) -A KUBE-SERVICEg g  Z 7S -d 10.96.0.10/32 -ps k i s ^ udp -m comment --comment "kube-sz 8 ] ) w ` 1 Mystem/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
<...>
(9 C o3) -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -m statistic --mode random --proba/ U = E y zbility 0.50000000000 -j KUBE-SEP-LLLB6FGW = sXBLX6PZF7
(4) -A` y . L $ Q } t KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-LRVEW52VMYCOUSMZ
<...>
(5) -A KUBE-SEP-LLLB6FGXBLX6t ^ ( 9 `PZF7 -p udp -m comment --comment "kU 1 F ~ 3ube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.6:53
<...>
(6) -A KUBE-SEP-) m s ^ ) $ q &LRVEW52VMYCOUSMZ -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.7:53

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

Linux内核中的DNAT

如上所示,服务r 6 s 2 5 h(在iptables模式下)的基础是DNA- 7 T VT,它由内核执行。

DNAT的主要职责是同时更改传出数据包的目的地,答复数据包的源,并确保对所有后续数据包进行相同的修改。
后者严重依赖于连接跟踪机制,也称为conntrack,它被实现为内核模块。顾名思义,conntr@ 3 C P ^ qack会跟踪系统中正在进行的网络连接。
以一种简化的方式,conntrack中的每个连接都由两个元组表示-一个元组用于原始请求(IP_CT_DIR_ORIGINAL),另一个元组用于答复(IP_R M : F i p a ?CT_DIR_REPLY)。对于UDP,每个元组都由源IP地址,源端口以及目P f : v G X O标IP地址和目标端口组成。答复元组包含存储在src字段中的目标的真实地址。
例如,如果IP地址为10.40.0.17的Pod向kube-dns的ClusterIP发送一个请求,该请求被转换i ; O 1 ; . P # `为10.32.0.6,则将创建以下元组:

原始:src = 10.40.0.17 dst = 1W q 6 , E0.96.0.10 sport = 53378 dport = 53
回复:src = 10.32.0.6 dst = 10.40.0.S ! ~ M & r T17 sport = 53 dport = 53378

通过具有这些条目! y g i 0 9,内核可以相应地修改任何相关数据包的目的地和源地址,而无需再次遍历DNAT规则。此外,它将m e ( u知道如何修改回复以及应将回复发送给谁。
创建conntrack条目后,将首先对其进行确认。稍后,如果没有已确认的cW 7 @ Donntrack条目具有相同的3 n a 0 A ! w k Q原始元组或回复元组,则内核将尝试确认该条目。
conntrack创建和DNAT的简化流程如W m n j % 下所示:


+----------------------) ` m-----+
|                                         |      为一个给定的包创建一个conntrack,如果
|    1. nf_conntrack_inT U F Z Z Y       |     它并不存在;IP_CT_DI% , 7 $ CR_REPLY是
|                                        |      反向的IP_CT_b y $ r % i 3 bDIR_ORIGINALR f B a j y K * n元组,因此
+------------+--------------+    回- M g ; p % 0 !复元组的src还没有改变。
|
v
+---------------------------+
|                                          |
|     2. ipt_do_table             |     找到一个匹配的DNAT规则。
|                                          |
+------------+--------------+
|
v
+--------------------2 Q 0 2 n A 1 T-------+
|                                        |      根据DNAT规则更新回复元组src部分
|    3. get_unique_tuple    |     使其不被任何已经确认的连接使用。
|                                        |
+------------o w ! a i i )+--------------+
|
v
+---------------------------+
|                                        |
|     4. nf_nat_packet        |      根据应答元组打乱数据包的目的端口和地址。
|                                       |^ 7 ) W v a
+------------+--------------+
|
v
+----------------------------+
|                                                |  如果没有与相同的原w c e T I J始元组或应答元组确认的连
|  5. __nf_conntrack_confi  o [ 7 R R iirm | 则确认连接道;
|                                                |
+----M V I . Y----------1 v y i--------------+     递增insei ) ! [ p = L ?rt_failed计数器并删除数据包(如果在)。

问题

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

1、两) / d e L E个包都没有在1中找到一个确认的conntrack。nf_conntracB ^ F , Wk_in一步。为两个包创建具有相同元组的两个conntrack条目。
2、与上面的情况相同,但一n s Z 9个包的ct : - ` - [ J Sonntrack条目在另一个包调用3之前被确认。get_unique_tuple。另一个包通常在源端口更改后得到一个不同的应答元组。
3、与第一种情c I X o x H 7况相同,但是在步骤2中选择了具有不同端点的~ { 2 W两个不同规则。e * 1 t S ; = i Zipt_do_table。

竞争的结果A 9 Q }是相同的—其中一个包在步骤5中被丢弃。__nf_? x 8conntrack_confirm。

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

值得一提的是,这个问题y . m B B 6 l Z =不仅是针对K7 + : 0 M * 5 i Bubernetes的-任何并行发0 | } l O G送UDP数据6 s ~ | p W % j k包的Linux多线程进程都容易出现这* } 8 : c p种竞争情况。

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

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

缓解措施

意见建议

建议采取多种解决方法:禁用并行查找,_ S x | ? P H E K禁用IPv6以避免AAAA查找,@ } ( ~ B z s %使用TCP进行查找,改为在Pod的解析器配置文件中设置DNS服务器的真实IP地址,等等。不幸的x ~ H T f y c 3是,由于常用的容器基础映像Alpine Linux使用musl libc的限制,它们中的许多不起作用。
? # D于Weave Net用户来说似乎可靠的方法是使用tc延迟DNS数据包。

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

内核修复

无论采用哪种解决方法,都决定在内核中修复根本原因。
结果是以下内核补丁:

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

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

解决方案如下:- x A K } c

方案(一):使z r m h用 TCP 协议发送 DNS 请求
通过resolv.conf的use-vc选项来开启 TCP 协议
测试
1、修改/etc/resolv.conf文件,在最后加入一行文本:
options use-vc
2、此压测可根据下面测试的go文件进行测试,编译好后放进一个pod中,进行压测:
#200个并发,持续30秒,记录超过5+ W es的请求个数 ./dns -host {servic1 K # C fe}.{namespace} -c 200 -d 3s q - I ! h d z %0 -l 5000

方案(二):避免相同五元组 DNS 请求的L g L O r 2并发
通过resolv.conf的single-request-reopen和single-request选项来避免:
sW c q q #ingle-re; v % m mquest-reopA s Een (w n K 4 % 1 -glibc>=2.9) 发送 A 类型请求和 AAAA 类型请求使用不同的源端口。这样两个请求在 conntrack 表中不占用同一个表项,从而避免冲突。
single-request (glibc>=2./ 1 /10) 避免并发,改为串行发送 A 类型和 AAAA 类型请求,没有了并发,从而也避免了冲突。

测试 single-request-reopen
修改/etc/resolv.conf文件,在最后加入一行文本:
options single-request-reopen
此压测3 ) % 2 N } N J可根据下面测试的go文件进行测试,编译好后放进一个pod中,进行压测:
#% 5 X [ q ` ~ E ^200个并发,持续3W y S0秒,记录超过5s的请求个数 ./dns -host {service}.{namespace}$ q 1 9 g - b : -c 200 -d 30 -l 5000

测试 single-request
修改/etc/resolv.conf文件,在最后加入一行文本:
options single-requeZ ) p ] ` B 3st
此压测可根据下面测试的go文件进行测试,编译好后放进一个ph L % ~ } v f od中,进行压测:
#200个并发,持续30秒,记录超过5s的请N C = U求个数 ./dns -host {service}.{naB ` G bmespace} -c 200 -d 30 -l 5000

最后结果,如果你测试过,相信coredns的测试如果还是增加使用 TCP 协议4 y 0 ; z U发送 DNS 请求,还是避免相同五元组 DNS 请求的并发,都没有显著的解决coredns延迟的结果

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

在 Kubernetes 集群中使用 NodeLocal DNSCache

https://github.com/kubernetes/kubernetes/tree/master/clustem o u K v F I U `r/addoA $ ` 3 j dns/dns/nodelocaldns

NodeLocal DNSCache 通过在集群节点上作为* X 1 $ f ; | / DaemonSet 运行 dns 缓存代理A } / W 8 T K E来提高集群 DNS 性能。 在当今的体系结构中,处于 ClusterFirst DNS 模式的 Pod 可以连接到 kube-dns se? I 0 = M f R ^ (rviceIP 进行 DNS 查询。 通过 kube-proxy 添加的 iptables 规则将其转换为 kube-dns/CoreDNS 端点。 借助这种新架构,Pods 将可以访问在同一节点上运行的 dns 缓存代理,从而l 0 2 J d 3 E避免了 iptables DNAT 规则和连接跟踪。 本地缓存代理将查询 kube-dns 服务以获取集群主机名的缓存缺失(默m b f h 0认为 cluster.local 后缀),并有效解决5秒延迟问题

在集群中运行 NodeLocal DNSCache 有如下几个~ j : h ! L H p好处:

如果本地没有 CoreDNS 实例,则具有最高 DNS QPS 的 Pod 可能必须到另一个节点进行解析,使用 NodeLocal DNSCache 后,拥有本地_ 1 R j ` = u } [缓存将有助于改善延迟
跳过 iptables DNAT 和连接跟踪将有助7 ( d U s N |于减少 conntrack 竞争并避免 UDP DNy D XS 条目填满H l H ` C C [ / conntrack 表。(常见的5s超时问题就是这个原因造成的B 6 8 n z
从本地缓存代理到 kube-dns 服务的连接可以升级到 TCP,TCP conntrack 条目将在连接关闭时被删除,而 UDP 条目必须超时(默认 nf_conntrack_udp_timeout 是 30 秒)
将 DNS 查询从 UDP 升级到 TCP 将2 * !k : r Z b U少归因于丢弃的 UDP 数据包和 DNS 超时的尾部等待时间,通常长达 30 秒9 5 L U(3 次重试+ 10 秒超时)。
可以重新启用负缓存,从而减少对 kube-d^ 9 s lnS ~ W ? *s 服务的查询数量。

架构图

启用 NodeLoF t U ucal DNSCache 之后,这是 DNS 查询所遵循的路径:
Coredns+Nodelocaldns cache解决Coredns域名解析延迟荐

环境检查

该资源清单文件中包含几个变量,其中:
PILLARDNSSERVER :表示 j 3 v & } v 2 | kube-dns 这个 Service` U M b 3 T 的 CluI ] t nsterIP,可以通过命令 kubectl get svc -n A | grep kube-dns | awk '{ print $4 }' 获取
PILLARLOCALDNS:表示 DNSCache 本地的 IP,默认为 169.254.20.10
PILLARDNSDOMAIN:表示集群域,默认就是 clusterI g S ^.local

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

运行nod= = s oelocaldns需要进行替换以下操5 A o O作,如果下载过慢,可以直接使用下面的yaml来使用,需要替换的话,只有10.96.0.10,这个是kube-dns service的clusterIP

开始部署

wget -O_ M F 5 nodelocaldns.yaml "https://github.com/kubernetes/kubernete- c : & 5 g # 5 }s/raw/master/cluster/addons/dns/nodez - Glocaldns/nodelocaldns.yaml" && \
sed -i 's/k8s.( - &gcr.io/zhaocheng172/g' nodeloD Q : + _  C _ )caldns.yam% : 7 3 ]l && \
sed -i 's/__PILLAR__DNS__SERVER__/10.96.0.10/g' nodelocaldns.yaml &a; r _ = F C @ Qmp;& \
sed -i 's/__PILLAR__LOCAL__DNS__/169.254.20.10/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__DNS__DOMAIN__/cluster.lW 6 | F Pocal/g' nodelocaldns.yaml 

最终替换结果

#Copyright 2018 TD @ R 8 RhM O We Kubernetes Authors.
#Licensed under the Apache License, Version 2.0 3  U { Q  O0 (the "License");
#you may not use this file exce8 Q  Ppt in compliance wit~ j ] L T Ph the License.
#You may obtain a copy of the License at
#http://www.apache.org/licenses/LICENSE-2.0
#Unless required by applicable law or agreed to in writiJ z 8 y @ } bng, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language gover~ Z  = 2ning permissions and
#limitations under the License.
a. ^ W s  u A @piVersion: v1
kiu r f 1 , hnd: ServiceAccouS g  ~ ~ [ # Fnt
metadata:
ns o 1 o came: node-local-dns
namespace:c G J ^ D {  R kube-system
labels:
kubernetes.io/cluster-servicea 1 D + i z F d: "true"
addonmana( Q a ger.kubernetes.io/moB % 0 ) b Kde: Reco? A y O L @ t B &ncile
---
apiVersU a q fion: v1
kind: Service
metadata:
name: kube-dns-upstream
namespace: kube-system
labels:
k8s-app:+ h A ] 9 I B x m kube-dns
kubernetes.io/cluster-servicC ( ; ae: "true"
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/name: "KubG P eeDNSUpstream"
speK x r kc:
ports:
- name: dns
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53
selector:
k8s-app: kube-dns
--: = E 0-
apiVersin X V } ^ T g - con: v1
kind: ConfigMap
metadata:
name: node-local-dns
nam o * / Sespace: kube-W e } W r 6 V }system
labels:
addonmanager.kubernetes.io/mode: Reconcile
data:
Corefile: |
cluster.loca@ 3 ; &l:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLh 1 )AR__CLUSTER__DNS__6 q % P m ? 8 H {
force_tcp
}
prometheus :9253
health 169+ O m ` O R.254.20.10:8080
}
in-addr.arpa:53 {
err0 l Mors
cache 30
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PIQ ( H X G s K ~LLAR__CLUSTER__DNSd  t ? 0 } ` A__ {
force_tcp
}
prometheus :9253
}
ip6.arpa:53 {
errors
caK d F R - x p Vche 30
reload
loop
bind 169.254.20.10 10.96.0.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
promeV $ X Ntheus :925~ % w % / % ~ A3
}
.:53 {
errors
cache 30
reload
loop
bind 169.254.20.10 10.96.0.10
forwa, r n X K R r Frd . __PILLAR__UPSTREAM__SERVEY @ B j A 2 L ORS__ {
force_tcp
}
prometheus :9253
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-local-dns
naC y D m q Vmespace: kube-syW ( B N 3stem
labels:
k8s-app: node-local-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
updato T ? J : 8 MeStrategy:
rollingUpdate:
maxn D #UnavO ; q C /ailable: 10%
selector:
matchLabels:
k8s-app: node-local-dns
template:5 f U v V
metadata:f j  y
labels:
k8s-app: node-local-dnsE { / r W
annotations:
prometheus.io/port: "9253"
prometheus.io/scrape: "true"
spec:
priorityClassNaml T b ;e: system-node-critical
serviceAccountName: node~ O y-local-dns
hostNetwork: true
dnsPolicy: Default  # Don'6 w 3 | wt use cluster DNS.
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
-G X % W n O effect: "NoExecute"
operator: "Exists"
- effect: "NoSchedule"
operator: "Exists"
containers:
- name: node-cache
image: zhaoU S & M wcheng172/k8s-dns-node-cacm b  i # S O Z nhe:1.15.13
resoY D Durces:
requests:
cpu: 25m
memory: 5Mi
args: [ "-localip", "169.254.20.10,10.96.0.x $ & / d10", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-ua { 6 i L ^ 7 N Hpstream" ]
securityConR f E ( ytext:
privileged: true
ports:
- containerPort: 53
name: dns
prot$  g X ] ocol: UDP
- contaiN K +nerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9253
name: metrics
protocol: TCP
livenessProbe:
httpGet:
hosq $ = mt: 169.254.20.10
path: /health
port: 8080
inu I n q G M 0itialDelaySeconds: 60
timeoutSeconds: 5
volumeMounts:
- mountPath: /run/xtables4 M = ` J ` *.lock& ~ C 4 ~
name: xtables-lock
readOnly: false
- name: config-volume
mountPath: /u 5 H ! p Z k y metc/coredns
- name: kube-dns-config
mountPath: /etc/kube-dns
vo2 _ o d ) 1 +lumes:
- name: xtables-lock
hostPath:
path: /run/xtables.lock ; ` , ^k
type: FileOrCreate
- name: kube-dns-config
configMap:
name: kube-` R y _ r } m rdns
optional: true
- name: config-volume
configMap:
name: node-local-dns
items:
- key: Corefile
path: Corefile.base

可以通过/ + 5 +如下命令来查看对应的 Pod 是否已经启动成功:

Coredns+Nodelocaldns cache解决Coredns域名解析延迟荐
需要注意的是这里使用 DaemonSet 部署 node-local-dns 使用i i i b了 hostNetwork=true,会占用宿主机的 8` { Q ` o B - E v08M * Q I ^ : ( M D0 端口,所以需要保证该端口未被占用。

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

两种方案测试j W ] + { vnodelocaldns实效性

第一种就是定制一个pod,Kup _ N ) - ^ :bernetes Pod dnsPolicy 可, Y 1 E以针对每个Pod设置DNS的策略,通过PodSpec下的dnsPolicr h 7 h ! ~ ay字段可以指; J ( H定相应的策略
这种方式可以直接启动一个pod,Pods将直接可以访问在同一节点上运行的 dns 缓存代理,从而避免了 iptables DNATA f , ] M 规则和连接跟踪,但是这种对l ? v = Z { ` 4 e于整体集群来6 J &讲并不适合,只] } _ 6提高了, E - R 2 [ *当前pod的DNScache的命中率,这种适合定制一些dns策略

apiVersion: apps/v1
kindP a A @ r d: Deployment
metadata i e | Q b l 1 2:
creationTimestamp: null
labels:
app: web
name: web
namespac2 U @ 1 e `e: kube-system# + = L R .  + ?
spec:
replicas: 1
selector:
matchLabels:
app: web
stratI 3 X m Y L S regy: {}
template:
metadata:
creationTimestamp: null
labels:
app: web
spec:
container% v ? K ( } ! G :s:
- image: nginx
name: nginx
dnsConfig:
nameservers:
- 169.254.20.10
search4 ] L [ 2 o %es:
- public.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "x 7 t  V5"
dnsPolicy: None

第二种如_ ` k } H b果对于集群来讲,需要全部生效
需要替换每个节点的clusterDNS的地址

clusterDNJ / jS:
- 10.96.0.10

替换的话可以直接使用sed直接替* K 9 p ^ n换,另外需要所有节点替换并重启kubelet

sed -i 's/10.96.0.10/169.254.! L 120.10/g' /var/lib/kub# 7 1 b @ $ -elet/config.yaml
sD + d x ( $ystemctl daemon-reload
systemctl restart kubele5 g 1 N Z 1 Vt

待 node-local-dns 安装配置完成后,我们可以部署一个新的 Pod 来验证下:(test-node-local-dns.yaml)a T 9 K f ] I i q

apiVersion: v1
kind: Pod
metadata:
name: test-node-local-dns
spec:
containers:
- name: local-dns
image: busy= 2 ubox
command: ["/bin/sh", "-c", "sleep 60m"]

直接部署:

$ kubectl apply -f test-node-loq E % | x | @ p Lcal-dns.yaml
$ kubect* U v | X O [ Il exec -it test-node-local{ 9 $ u O ~  f #-dns /bin/sh
/ # cat /etc/resolv.conf
nameserver 169.254.20.10
search default.svc.cluster.local svc.cluster.local cluster.lo+ M P v E 1 B 3 2cal
optionsO 7 ( ] q V z & i ndots:5

需要注意
我们现在已经可以看到 nameserver 已经变成 169.254.20.10 了,当然对于之前的历史 Pod 要想使用 node-local-dns 则需要重建所有的pod

1、linux 中glibc的 resolver 的缺省超时时间是 5s,而导致超时的原因是内核cZ ) Z F R X ;onntrack模块的 bug。
2、DNS client (glibc 或 musl libc) 会并发请求 A 和 AAAA 记录,跟 DNS Serveq / i 5 ; Mr 通信自然会先 connect (建立/ U ? @ fd),后面请求报文使用这个 fd 来发送,由于 UDP 是无状h w [ F 1 K l n态协议, co] n W ( snnect 时并不会发包,也就不会创建 conntrack 表项, 而并发请求的 A 和 AAAA 记录默认使用同一个 fd 发包,send 时各自发的包它们源 Port 相同(因为用的同一个 socket 发送),当并发发包时,两个包都还没有被插入 connO c . u ltrack 表项,所以 netfilter 会为它们分别创建 conntrack 表项,而集群内请求Y + r G t J 1 ! M kube-dns 或 coredns 都是访问的 CLUSTER-IP,报文最终会. a L S _ f )被 DNAT 成m ^ , * s `一个 endpointY U = c 的 POD IP,当两个包恰好又W y p 1 4 i E被 DNAT 成同一个 POD IP 时,它1 P i x们的五元组就相同/ = % q 2了,在最终插入的时候后面那个包就会被丢掉,如果 dns 的 pod 副本只有一个实例的情况就很容易发生(始终被 DNAT 成同一个 POD IP),现象就是 dns 请求超时,client 默认策略是等待 5s 自动重试,如果重试成功,我们看到的现象k z J就是 dns 请求j } 5 3 a有 5s 的延时。

另外如果要想去跟踪 DNS 的解析过程的话可以去通过抓包来观察具体超时的最大时间。

测试coredns+nodelocaldns的效果
我们可以通过Gor A _ - ( C V %的一个测试用例来测试DNS的解析是否得到提升

首先安装一个Go的) p z M S `环境

#wget   https://dl.google.com/go/go1.12.5.linux-amd64.tar.gz
#tar -zxf go1.12.5.linux-amd64.tar.gz -C /usr/local
#export PATH=$PATH:/usr/local/go/bin
#go version

主要是要测试 dns 服务的性能,相当于压测工具只做域名解析的耗时时间

cat dns-test.go
package main
import (0 J d a X X v q !
"context"
"flag"
"fmt"
"net"
"sync/atomic"
"time"
)
var host string
var connectd i l J V [ L tions int
var dur; p ) E C Lation int64
var limit int64
var timeoutCount int64
func main() {
// os.Args = appendt u 7 v 7 m 9 x(os.Args, "-host", "www.baidu.com", "-c", "200", "-d", "30", "4 V ; ( s-lH l E @ o ` !", "5000")
flag.StringVar(&a1 w r [mp;host, "host", "", "Resolve host")
flag.IntVar(&connections, "c", 100, "ConnW N ~ F ] T d :e- _ 8 } M cctions")
flag.Int64Var(&duration, "d", 0, "Duration(s)")
flag.InR ) ht64V e 5 $ g N ?Var(&limit, "l", 0, "Limit(ms)")
flag.Parse()
var count int64 = 0
var errCount int64 = 0
pool := make(chan inter0 { R 5 gface{}, connections)
exit := make(chan bool)
var (
min int64 = 0
max int64 = 0
sum int: A q 7 @ 5 e Y w64 = 0
)
go func() {
time.Sleep(time.Second * time.Duration(duration))
exit <- true
}()
endD:
fork W G r , {
select {
case pool <- nil:
go func() {
defer func() {
<-pool
}()
resolver := &netj D z 2 r 0 ).Resolver{}
now := time.Now()
_, err := resolver.LookupIPAddr(co5 p r + ) ; _ Kntext.BackgroE b I T ^ | Dund(), host)
use := time.Since(now).Nanoseconds() / int64(timeM % 7 [ & m D +.Millisecond)
if min == 0 || use < min {
min = use
}
if use > max {
max = use
}
sum += use
if limit > 0 &&amJ C q e _p; use >= limit {
timeoutCount++
}
atom% S D * t Z pic.AddInt64(&count, 1)
if err != nil {
fmt.Println _ W o L(err.Error())
atomic.AddInt6x A , u4(&errCount, 1R m V L)
}
}()
c) r f g K Xase <-exit:
break endD
}
}
fmt.Printf("request count:%d\nerror cou@ ~ 0  ( 4 jnt:%d\n",` B # w c e % u count, errCount)
fmt.Printf(5 ( { x"Q g wrequest time:min(d o 6 a % P d y s%dms) max(%dms) avg(%dms) timeout(%dn)\n", min, max, sum/count, timeoutCount)
}

直接go build,会有一个dnK ( q i { T 1 0s-test的二进制文件

将此文件放到pod里面
#kubectl cp /root/dns-test web-7f5df76d5f-r76xx:/rou ^ D Wot -n kube-syst; j mem
下面来进行测试

进行压测
200个并发,持续30秒,记录超过5s的请求个数
/dns -host {service}.{namespace} -c 200 -d 30 -l 5000
结果如下:
1.14.3版本原生集群不加- K V j参数测试默认使用iptables性能方面可能不是那么好,不过已经没有5s延迟情况发生,最高耗时2.9s
Coredns+Nodelocaldns cache解决Coredns域名解析延迟荐

1.18.6原生集群l O y ~ b = O 3不加参数测试
结论没有延迟操作最大耗时为0.5s,默认采用ipvs,效率非常高
Coredns+Nodelocaldns cache解决Coredns域名解析延迟荐

最后总结:
通过测试结果得到以下结论
1.14.3集群使用coredns+nodelocaldns配合使用避免相同五元组 DNS 请求的并发,增加options single-requeZ Z ` Z ( st-reopen,最大耗时降低到2.25s左右,不会Q F 8 : g ^ ^出现5s超- = W } @ e时情况,效果最好
1.18.6集群使用coredns+nodelocaldns不加参数测试最大耗时降低到0.53s左右,效率明h { C显提升,效果最好