TOC
CoreDNS是Kubernetes集群中负责DNS解析的组件,能够支持解析集群内部自定义服务域名和集群外部域名。CoreDNS具备丰富的插件集,在集群层面支持自建DNS、自定义hosts、CNAME、rewrite等需求。K8S集群中默认部署形态在DNS QPS较高场景下,可能会出现解析压力,导致部分查询失败。本文介绍如何优化集群DNS性能。
合理的dns副本数
调整CoreDNS副本数与集群节点数到合适比率有助于提升集群服务发现的性能,该比值推荐为1:8,即一个CoreDNS Pod支撑8个集群节点。
- 当集群节点无需大规模扩缩容时,执行以下命令调整目标副本数到目标值。
kubectl scale –replicas={target} deployment/coredns -n kube-system
- 集群节点需要大规模扩缩容时,推荐部署以下YAML模板,使用集群水平伸缩器cluster-proportional-autoscaler动态调整副本数量。不建议使用针对QPS、CPU或MEM指标的水平伸缩器,实测效果较差.
apiVersion: apps/v1
kind: Deployment
metadata:
name: dns-autoscaler
namespace: kube-system
labels:
k8s-app: dns-autoscaler
spec:
selector:
matchLabels:
k8s-app: dns-autoscaler
template:
metadata:
labels:
k8s-app: dns-autoscaler
spec:
serviceAccountName: admin
containers:
- name: autoscaler
image: registry.cn-hangzhou.aliyuncs.com/ringtail/cluster-proportional-autoscaler-amd64:v1.3.0
resources:
requests:
cpu: "200m"
memory: "150Mi"
command:
- /cluster-proportional-autoscaler
- --namespace=kube-system
- --configmap=dns-autoscaler
- --target=Deployment/coredns
- --default-params={"linear":{"coresPerReplica":2,"nodesPerReplica":1,"min":2,"max":100,"preventSinglePointFailure":true}}
- --logtostderr=true
- --v=2
上述使用线程伸缩策略中,CoreDNS副本数的计算公式为replicas = max (ceil (cores × 1/coresPerReplica), ceil (nodes × 1/nodesPerReplica) ),且CoreDNS副本数受到max,min限制。 线程伸缩策略参数如下。
{ "coresPerReplica": 2, # 每个coredns pod可以处理的cpu核心数,和nodesPerReplica比较取最大值。 "nodesPerReplica": 1, # 每个coredns pod 可以处理的node节点数,和coresPerReplica比较取最大值。 "min": 2, "max": 100, "preventSinglePointFailure": false, # 设置为true时,如果有多个节点,controller会确保至少有2个副本。 "includeUnschedulableNodes": false # 设置为true时,副本将根据节点总数进行伸缩。否则,副本只会根据可调度节点的数量进行扩展 }
例如,给定的群集具有4个节点和13个核心。 使用上述参数,每个副本可以处理1个节点。 因此,我们需要4/1 = 4个副本来照顾所有4个节点。 每个副本可以处理2个核心。 我们需要ceil(13/2)= 7个副本来处理所有13个内核。 结果,控制器将选择较大的一个,即7。
部署NodeLocaldns
NodeLocal DNSCache在集群的上运行一个dnsCache daemonset来提高clusterDNS性能和可靠性。在K8S集群上的一些测试表明:相比于纯coredns方案,nodelocaldns + coredns方案能够大幅降低DNS查询timeout的频次,提升服务稳定性,能够扛住1倍多的QPS。
nodelocaldns通过添加iptables规则能够接收节点上所有发往169.254.20.10的dns查询请求,把针对集群内部域名查询请求路由到coredns;把集群外部域名请求直接通过host网络发往集群外部dns服务器。
架构图
部署模版:
# 保存以下脚本 命名为install-nodelocaldns.sh
#!/env/bin/bash
localDnsIp="169.254.20.10"
upstreanDnsIp=$(kubectl get svc -n kube-system | grep kube-dns | awk '{ print $3 }')
yaml=$(cat <<-END
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: node-local-dns
namespace: kube-system
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:
name: kube-dns-upstream
namespace: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/name: "KubeDNSUpstream"
spec:
ports:
- name: dns
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53
selector:
k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:
name: node-local-dns
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
data:
Corefile: |
cluster.local:53 {
errors
cache {
success 9984 300
denial 9984 5
}
reload
loop
bind $localDnsIp $upstreanDnsIp
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health $localDnsIp:8080
}
in-addr.arpa:53 {
errors
cache 30
reload
loop
bind $localDnsIp $upstreanDnsIp
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
ip6.arpa:53 {
errors
cache 30
reload
loop
bind $localDnsIp $upstreanDnsIp
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
.:53 {
errors
cache 30
reload
loop
bind $localDnsIp $upstreanDnsIp
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-local-dns
namespace: kube-system
labels:
k8s-app: node-local-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
updateStrategy:
rollingUpdate:
maxUnavailable: 10%
selector:
matchLabels:
k8s-app: node-local-dns
template:
metadata:
labels:
k8s-app: node-local-dns
annotations:
prometheus.io/port: "9253"
prometheus.io/scrape: "true"
spec:
priorityClassName: system-node-critical
serviceAccountName: node-local-dns
hostNetwork: true
dnsPolicy: Default # 不使用cluster DNS解析.
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- effect: "NoExecute"
operator: "Exists"
- effect: "NoSchedule"
operator: "Exists"
containers:
- name: node-cache
image: k8s.gcr.io/dns/k8s-dns-node-cache:1.17.0
resources:
requests:
cpu: 25m
memory: 5Mi
args: [ "-localip", "$localDnsIp,$upstreanDnsIp", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ]
securityContext:
privileged: true
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9253
name: metrics
protocol: TCP
livenessProbe:
httpGet:
host: $localDnsIp
path: /health
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
volumeMounts:
- mountPath: /run/xtables.lock
name: xtables-lock
readOnly: false
- name: config-volume
mountPath: /etc/coredns
- name: kube-dns-config
mountPath: /etc/kube-dns
volumes:
- name: xtables-lock
hostPath:
path: /run/xtables.lock
type: FileOrCreate
- name: kube-dns-config
configMap:
name: kube-dns
optional: true
- name: config-volume
configMap:
name: node-local-dns
items:
- key: Corefile
path: Corefile.base
---
# A headless service is a service with a service IP but instead of load-balancing it will return the IPs of our associated Pods.
# We use this to expose metrics to Prometheus.
apiVersion: v1
kind: Service
metadata:
annotations:
prometheus.io/port: "9253"
prometheus.io/scrape: "true"
labels:
k8s-app: node-local-dns
name: node-local-dns
namespace: kube-system
spec:
clusterIP: None
ports:
- name: metrics
port: 9253
targetPort: 9253
selector:
k8s-app: node-local-dns
END
)
echo "$yaml" > nodelocaldns-ds.yaml
kubectl apply -f nodelocaldns-ds.yaml
$ sh install-nodelocaldns.sh
此时已经完成了nodelocaldns的部署。通过查看日志会发现nodelocaldns已经正常参与解析。
$ kubectl logs node-local-dns-8x3o5 -n kube-system
[INFO] 172.31.3.217:41770 - 20309 "A IN www.baidu.com.default.svc.cluster.local. udp 57 false 512" NXDOMAIN qr,aa,rd 150 0.002431573s
[INFO] 172.31.3.217:41770 - 20650 "AAAA IN www.baidu.com.default.svc.cluster.local. udp 57 false 512" NXDOMAIN qr,aa,rd 150 0.002533574s
[INFO] 172.31.3.217:58177 - 3493 "AAAA IN www.baidu.com.svc.cluster.local. udp 49 false 512" NXDOMAIN qr,aa,rd 142 0.001126744s
[INFO] 172.31.3.217:58177 - 3232 "A IN www.baidu.com.svc.cluster.local. udp 49 false 512" NXDOMAIN qr,aa,rd 142 0.001107933s
[INFO] 172.31.3.217:51909 - 6463 "AAAA IN www.baidu.com.cluster.local. udp 45 false 512" NXDOMAIN qr,aa,rd 138 0.001056258s
[INFO] 172.31.3.217:51909 - 6211 "A IN www.baidu.com.cluster.local. udp 45 false 512" NXDOMAIN qr,aa,rd 138 0.00108577s
[INFO] 172.31.3.217:60395 - 42129 "A IN www.baidu.com.ap-southeast-1.compute.internal. udp 63 false 512" NXDOMAIN qr,rd,ra 186 0.003109582s
[INFO] 172.31.3.217:60395 - 42426 "AAAA IN www.baidu.com.ap-southeast-1.compute.internal. udp 63 false 512" NXDOMAIN qr,rd,ra 186 0.004827469s
[INFO] 172.31.3.217:54473 - 18014 "A IN www.baidu.com. udp 31 false 512" NOERROR qr,rd,ra 181 0.001185881s
[INFO] 172.31.3.217:54473 - 18249 "AAAA IN www.baidu.com. udp 31 false 512" NOERROR qr,rd,ra 207 0.001337411s
解析search域影响查询响应
目前,在ClusterFirst模式下,2次(1次IPv4,1次IPv6)集群外部域名查询产生10次(5次IPv4,5次IPv6)查询请求。(这里有aws域名,如果裸机部署k8s集群将为4次)例如,解析www.baidu.com域名,会先分别携带三个集群主域名后缀,产生八次无效查询请求,这样会导致集群DNS QPS放大四倍,影响性能。
查看pod里search域
$ kubectl exec test-pod -- cat /etc/resolv.conf
nameserver 10.100.0.10
search default.svc.cluster.local svc.cluster.local cluster.local ap-southeast-1.compute.internal
options ndots:5
通过在pod中cat /etc/resolv.conf
可以发现有4个search域,默认还有一个.,故有5次,nodots:5
表示要经过5次查询。这将严重影响dns解析效果。
我们将Pod的search改为(这里直接在pod里修改,后续通过后可通过dnsConfig修改):
search default.svc.cluster.local
options ndots:2
deployment的dnsConfig配置:
dnsPolicy: None
dnsConfig:
nameservers:
- 169.254.20.10 # 启用谁也nodelocaldns解析
searches:
- svc.cluster.local # 这样设置,要求服务名称调用时,严格svcname.namespace格式
options:
- name: ndots
value: "2"
- name: single-request-reopen
- name: timeout
value: "1"
在配置生效后,我们验证:
$ curl -s -IL https://www.ipyker.com && curl -s -IL helloworld.hw:5000/hello
HTTP/2 200
Content-Length: 60
...
$ kubectl logs node-local-dns-8x3o5 -n kube-system
[INFO] 172.31.3.217:47392 - 38526 "AAAA IN www.ipyker.com. udp 32 false 512" NOERROR qr,rd,ra 76 0.000413585s
[INFO] 172.31.3.217:47392 - 38307 "A IN www.ipyker.com. udp 32 false 512" NOERROR qr,rd,ra 204 0.000494763s
[INFO] 172.31.3.217:37782 - 60764 "AAAA IN helloworld.hw.svc.cluster.local. udp 49 false 512" NOERROR qr,aa,rd 142 0.000102695s
[INFO] 172.31.3.217:37782 - 60470 "A IN helloworld.hw.svc.cluster.local. udp 49 false 512" NOERROR qr,aa,rd 96 0.000050289s
可以发现不管内外地址,请求一次就命中。
为什么ndots:5会对应用程序性能产生负面影响?
如果您的应用程序进行了大量外部流量,则对于已建立的每个TCP连接(或更具体而言,对于每个已解析的名称),它将在正确解析名称之前发出5个DNS查询,因为它将通过4个DNS查询。 首先是本地搜索域,最后将发布绝对名称解析查询。
single-request-reopen 选项
since glibc 2.9中有说明:在_res.options中设置RES_SNGLKUPREOP。 解析程序对A和AAAA请求使用相同的套接字
。 某些硬件错误地仅发送回一个答复。 发生这种情况时,客户端系统将等待第二次答复。 启用此选项将更改此行为,以便如果未正确处理来自同一端口的两个请求,它将在发送第二个请求之前关闭套接字并打开一个新请求。
timeout 选项
一般/etc/resolv.conf
还有两个默认的值至关重要,一个是超时的 timeout,一个是重试的 attempts,默认情况下,前者是 5s 后者是 2 次。理解该两个字段:每个 nameserver 等待 timeout 的时间,如果存在多个namespace都没结果,resolver 会重复上面的步骤 (attempts – 1) 次。设置的目的是加快nodelocaldns不能处理的解析快速转发到coredns。
ipv6解析影响服务查询时间
通过上面的记录,可以发现每一次请求都会有AAAA解析,即ipv6,我们可以根据自己业务性质是否有使用到ipv6特性来关闭该查询。如果K8S集群宿主机没有关闭IPV6内核模块
的话,容器请求coredns时的默认行为是同时发起IPV4和IPV6解析。
此处暂时不涉及主机层面禁用ipv6的步骤。
DNS缓存命中和延时
首先,我们环境当前存在部分延迟,所以本次直接演示:
- 准备一个curl请求文本
cat > reuqest-time.txt <<EOF
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_redirect: %{time_redirect}\n
time_pretransfer: %{time_pretransfer}\n
time_starttransfer: %{time_starttransfer}\n
----------\n
time_total: %{time_total}\n
EOF
- 测试请求,可多执行几次 发现dns解析非常耗时,216ms,总耗时才391ms。
$ curl -w "@reuqest-time.txt" -o /dev/null -s -L helloworld.hw:5000/hello
time_namelookup: 0.216190
time_connect: 0.217433
time_appconnect: 0.233136
time_redirect: 0.000000
time_pretransfer: 0.233272
time_starttransfer: 0.390998
----------
time_total: 0.391311
修改Coredns的配置文件,修改缓存和ttl时间
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
log
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
ttl 300 # 修改ttl的值为300秒,默认为5秒
}
prometheus :9153
forward . /etc/resolv.conf
cache {
success 9984 300 120
denial 9984 5
}
loop
reload
loadbalance # 随机A,AAAA,MX解析的顺序
}
-
插件cache的语法为
cache [TTL] [ZONES...]
,最大TTL,单位为秒。如果未指定,则使用最大TTL, NOERROR响应为3600,不存在响应为1800。 例如:设置TTL为300:意思为缓存300将缓存记录长达300秒。ZONES应该缓存的区域。如果为空,则使用配置块中的区域。(一个xxx:53{}
块即为一个zone)cache {success CAPACITY TTL MINTTL}
: 覆盖用于缓存成功响应的设置。 “CAPACITY”表示开始驱逐(随机)之前缓存的最大数据包数。 TTL会覆盖缓存的最大TTL。 MINTTL会覆盖缓存的最小TTL(默认值为5),这对于将查询限制到后端很有用。cache {denial CAPACITY TTL MINTTL}
: 覆盖用于缓存拒绝存在响应的设置。 “CAPACITY”表示开始驱逐(LRU)之前缓存的最大数据包数。 TTL会覆盖缓存的最大TTL。 MINTTL会覆盖缓存的最小TTL(默认值为5),这对于将查询限制到后端很有用。 第三类(错误),但是这些响应永远不会被缓存。 -
插件Kubernetes TTL字段允许您为响应设置自定义TTL。 默认值为5秒。 允许的最小TTL为0秒,最大为3600秒。 将TTL设置为0将防止记录被缓存。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付

comments powered by Disqus