#作者:邓伟
文章目录
- 问题列表
- 问题分析:
- 问题分析
- 解决方案详情
- 方案验证
- 部署步骤
- 验证结论
- 回滚方案
- 回滚验证
- 注意事项
- NodeLocalDNS介绍
问题列表
近来发现K8s频繁出现5s超时问题,业务反馈收到一定影响,问题包括:
- coredns解析报5s延迟
如图
- 优化配置后,延迟有所缓解,但仍然存在5s延迟情况。
问题分析:
通过进一步查看和分析,磐基dns解析组件为coredns,支持ipv4和ipv6解析,而linux 中 glibc 的 resolver 的缺省超时时间是 5s,而导致超时的原因是内核 conntrack 模块的 bug,详解分析:
https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts,详细说明如下:
DNS client (glibc 或 musl libc) 会并发请求 A 和 AAAA 记录,跟 DNS Server 通信自然会先 connect (建立 fd),后面请求报文使用这个 fd 来发送,由于 UDP 是无状态协议, connect 时并不会发包,也就不会创建 conntrack 表项, 而并发请求的 A 和 AAAA 记录默认使用同一个 fd 发包,send 时各自发的包它们源 Port 相同(因为用的同一个 socket 发送),当并发发包时,两个包都还没有被插入 conntrack 表项,所以 netfilter 会为它们分别创建 conntrack 表项,而集群内请求 kube-dns 或 coredns 都是访问的 CLUSTER-IP,报文最终会被 DNAT 成一个 endpoint 的 POD IP,当两个包恰好又被 DNAT 成同一个 POD IP 时,它们的五元组就相同了,在最终插入的时候后面那个包就会被丢掉,如果 dns 的 pod 副本只有一个实例的情况就很容易发生(始终被 DNAT 成同一个 POD IP),现象就是 dns 请求超时,client 默认策略是等待 5s 自动重试,如果重试成功,我们看到的现象就是 dns 请求有 5s 的延时。
问题分析
通过进一步查看和分析发现:
在使用 DNS 客户端(如 glibc 或 musl libc)时,客户端会并发请求 A 记录和 AAAA 记录,并通过同一个 socket 文件描述符(fd)发送 UDP 报文。
由于 UDP 是无状态协议,connect 不会实际发包,因此初始时不会创建 conntrack 表项。当并发请求的 A 和 AAAA 记录使用同一个 fd 发送时,它们的源端口相同,且在发送时都未被插入 conntrack 表项,导致 netfilter 会为它们分别创建表项。
在 Kubernetes 集群中,DNS 请求访问 Cluster IP 后会被 DNAT 成具体的 Endpoint Pod IP。如果两个并发请求的包被 DNAT 成同一个 Pod IP,它们的五元组(源 IP、源端口、目标 IP、目标端口、协议)将完全相同,导致后一个包在插入 conntrack 表项时被丢弃。
当 DNS 服务的 Pod 只有一个实例时,这种情况尤为常见,最终表现为 DNS 请求超时。客户端默认会等待 5 秒后重试,若重试成功,则会观察到 5 秒的延迟。
解决方案详情
针对于以上问题和分析,可选的解决具体方案和优缺点如下:
-
将client dns请求由udp转换为tcp,避免出现conntrack 引发的问题,但是性能明显下降;
-
避免相同五元组 DNS 请求的并发,即配置single-request-reopen 或single-request ,可以显著降低超时,但是改动较大影响面较广;
需要说明的是1和2:不支持 alpine 基础镜像的容器,因为 apline 底层使用的 musl libc 库并不支持这些 resolv.conf 选项,所以如果使用 alpine 基础镜像构建的应用,还是无法规避超时的问题。
- 本地 DNS 缓存,也即:本地 DNS 缓存以 DaemonSet 方式在每个节点部署一个使用 hostNetwork 的 Pod,创建一个网卡绑上本地 DNS 的 IP,本机的 Pod 的 DNS 请求路由到本地 DNS,然后取缓存或者继续使用 TCP 请求上游集群 DNS 解析 (由于使用 TCP,同一个 socket 只会做一遍三次握手,不存在并发创建 conntrack 表项,也就不会有 conntrack 冲突),也即:NodeLocalDNS,详见文末介绍。
综合分析优缺点以及实际影响面,从影响和改动成本以及长期稳定性等因素考虑,选择第3项解决此问题,可做到部署即生效(k8s集群为iptables模式)。
方案验证
针对以上提出的问题,本方案有以下内容组成如下:
- 部署NodeLocalDNS
按照官方提供的K8S部署配置((根据k8s版本选择,磐基为1.23))参见:https://github.com/kubernetes/kubernetes/blob/release-1.23/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml):
镜像地址:k8s.gcr.io/dns/k8s-dns-node-cache:1.21.1;
整理后完整内容为:
apiVersion: v1
kind: ServiceAccount
metadata:name: node-local-dnsnamespace: kube-systemlabels:kubernetes.io/cluster-service: "true"addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:name: kube-dns-upstreamnamespace: kube-systemlabels:k8s-app: kube-dnskubernetes.io/cluster-service: "true"addonmanager.kubernetes.io/mode: Reconcilekubernetes.io/name: "KubeDNSUpstream"
spec:ports:- name: dnsport: 53protocol: UDPtargetPort: 53- name: dns-tcpport: 53protocol: TCPtargetPort: 53selector:k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:name: node-local-dnsnamespace: kube-systemlabels:addonmanager.kubernetes.io/mode: Reconcile
data:Corefile: |cluster.local:53 {errorscache {success 9984 30denial 9984 5}reloadloopbind 169.254.20.10 169.169.0.100forward . __PILLAR__CLUSTER__DNS__ {force_tcp}prometheus :9253health 169.254.20.10:<设定监控检查端口,确保不宿主机没占用>}in-addr.arpa:53 {errorscache 30reloadloopbind 169.254.20.10 169.169.0.100forward . __PILLAR__CLUSTER__DNS__ {force_tcp}prometheus :9253}ip6.arpa:53 {errorscache 30reloadloopbind 169.254.20.10 169.169.0.100forward . __PILLAR__CLUSTER__DNS__ {force_tcp}prometheus :9253}.:53 {errorscache 30reloadloopbind 169.254.20.10 169.169.0.100forward . __PILLAR__CLUSTER__DNS__prometheus :9253}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:name: node-local-dnsnamespace: kube-systemlabels:k8s-app: node-local-dnskubernetes.io/cluster-service: "true"addonmanager.kubernetes.io/mode: Reconcile
spec:updateStrategy:rollingUpdate:maxUnavailable: 10%selector:matchLabels:k8s-app: node-local-dnstemplate:metadata:labels:k8s-app: node-local-dnsannotations:prometheus.io/port: "9253"prometheus.io/scrape: "true"spec:priorityClassName: system-node-criticalserviceAccountName: node-local-dnshostNetwork: truednsPolicy: Default # Don't use cluster DNS.tolerations:- key: "CriticalAddonsOnly"operator: "Exists"- effect: "NoExecute"operator: "Exists"- effect: "NoSchedule"operator: "Exists"containers:- name: node-cacheimage: <可拉取的image地址>resources:requests:cpu: 25mmemory: 5Miargs: [ "-localip", "169.254.20.10,169.169.0.100", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ]securityContext:privileged: trueports:- containerPort: 53name: dnsprotocol: UDP- containerPort: 53name: dns-tcpprotocol: TCP- containerPort: 9253name: metricsprotocol: TCPlivenessProbe:httpGet:host: 169.254.20.10path: /healthport: <配置文件设置的健康检查端口>initialDelaySeconds: 60timeoutSeconds: 5volumeMounts:- mountPath: /run/xtables.lockname: xtables-lockreadOnly: false- name: config-volumemountPath: /etc/coredns- name: kube-dns-configmountPath: /etc/kube-dnsvolumes:- name: xtables-lockhostPath:path: /run/xtables.locktype: FileOrCreate- name: kube-dns-configconfigMap:name: kube-dnsoptional: true- name: config-volumeconfigMap:name: node-local-dnsitems:- key: Corefilepath: 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-dnsname: node-local-dnsnamespace: kube-system
spec:clusterIP: Noneports:- name: metricsport: 9253targetPort: 9253selector:
k8s-app: node-local-dns
以上假设
- kube-dns服务的ip地址为:169.169.0.100,且保证pod 的nameserver为169.169.0.100,否则,请按kube-dns服务实际ip更改;
- 部署前请检查169.254.20.10有无被占用,如有,可跟换169.254.20.10/24网段其他未被占用的ip地址
部署步骤
-
确定image,ip以及端口后,将以上yaml保存为nodelocaldns.yaml(文件名可以自定义);
-
在磐基管理机上部署执行:
kubectl apply -f nodelocaldns.yaml
执行输出以下内容,则表示创建和部署成功成功:
Node-cache 进程监听相应ip端口(53,9253,9353,28080),优先接收响应流量,进而代理coredns解析,起到被动缓存的作用
Pod node-local日志
以上说明,nodelocaldns在当前宿主机正常监听169.169.0.100流量 ,即代理kube-dns的解析转发到新的kube-dns-upstream服务(代理coredns服务,详解配置文件)。
验证结论
正常接收dns域名解析请求:
性能验证,经过多轮,每轮至少5次压测脚本执行,平均以及99耗时已有明显提升:
至此 nodelocaldns部署完成,正常提供解析功能。
回滚方案
相对应地,如果正常下线nodelocaldns(不可强制下线),则可以执行:
kubectl delete -f nodelocaldns.yaml
下线成功:
回滚验证
执行上述回滚命令后,宿主机已无node-cache服务监听 169.169.0.100和169.254.20.10的相关端口了:
且 iptable kube-dns规则正常生效:
回滚后,原coredns正常解析:
表明:回滚完成,nodelocaldns已正常下线
注意事项
- 确保所用ip以及相应端口不被占用,以免端口或ip冲突,造成启动失败或者启动后运行异常;
- 确保镜像为官方拉取的镜像,并部署时可正常拉取;
- 资源使用最小配置(request):cpu 35m,memory 30Mi,最大限制(limit)根据实际情况可适当放宽;
- 确保执行正常部署和混滚命令,避免强制删除,并注意部署和回滚验证;
- 确保尽可能接入监控平台并配置,并配置报警
- 确保宿主机有足够的pod额度
NodeLocalDNS介绍
NodeLocal DNSCache 通过在集群节点上运行一个 DaemonSet 来提高 clusterDNS 性能和可靠性。
处于 ClusterFirst 的 DNS 模式下的 Pod 可以连接到 kube-dns 的 serviceIP 进行 DNS 查询。通过 kube-proxy 组件添加的 iptables 规则将其转换为 CoreDNS 端点。通过在每个集群节点上运行 DNS 缓存,NodeLocal DNSCache 可以缩短 DNS 查找的延迟时间、使 DNS 查找时间更加一致,以及减少发送到 kube-dns 的 DNS 查询次数,流程示意图如下:
参考链接
1、https://github.com/kubernetes/kubernetes/blob/release-1.23/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
2、https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/nodelocaldns/
3、案例:https://www.ctyun.cn/document/10025153/10110270
4、问题剖析:https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts
5、附件物料可咨询博主