技术分享之 使用 NodeLocal DNSCache 提高 clusterDNS 性能和可靠性

用 NodeLocal DNSCache 提高 clusterDNS 性能和可靠性

Posted by 董江 on Sunday, July 17, 2022

使用 NodeLocal DNSCache提高clusterDNS性能和可靠性

由于一次权威DNS故障,导致的DNS链式故障, 从中发现的各类使用问题 以及 后续可能的影响!

结论:

权威DNS是所有内外域名解析依赖,是组织内核心服务; 权威DNS故障导致的依赖故障不能避免,但可以**减少爆炸半径**,减少故障影响面

  • DNSCache本地缓存:提升dns解析性能,避免不必要forward, 减少权威DNS、CoreDNS请求压力(常见:解决 CoreDNS 的5秒超时问题
  • kubernetes搜索域Search搜索半径Ndots 优化,减少发起不必要的请求

复现步骤

整个Kubernetes集群使用CoreDNS, CoreDNS通过ConfigMap定义Corefile/etc/resolv.conf, 在权威DNS故障发生时,整体表象不正常。

不正常现象:

  • nsenter 发包只有A记录,未有AAAA记录
dongjiangdeMacBook-Pro:kubernetes $ kubectl exec -it busybox "/bin/sh"
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # ping google.com
PING google.com (142.251.43.14): 56 data bytes
64 bytes from 142.251.43.14: seq=0 ttl=37 time=9.450 ms
64 bytes from 142.251.43.14: seq=1 ttl=37 time=7.857 ms
^C
--- google.com ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 7.857/8.653/9.450 ms


dongjiangdeMacBook-Pro:kubernetes $ nsenter -t 3885 -n tcpdump -i eth0 udp port 53
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:09:11.918847 IP 10.96.0.10.domain > 10.244.2.202.38697: 11858 NXDomain*- 0/1/0 (147)
10:09:11.922468 IP 10.244.2.202.38697 > 10.96.0.10.domain: 15573+ AAAA? google.com.default.svc.cluster.local. (54)  //线上无此条记录
10:09:11.923001 IP 10.96.0.10.domain > 10.244.2.202.38697: 15573 NXDomain*- 0/1/0 (147)
10:09:11.923248 IP 10.244.2.202.43230 > 10.96.0.10.domain: 62042+ A? google.com.svc.cluster.local. (46)  
  • A记录中带有权威DNS不可识别的搜索域
10:09:11.922468 IP 10.244.2.202.38697 > 10.96.0.10.domain: 15573+ AAAA? google.com.default.svc.cluster.local.**unkownlan** //出现自定义domain

原因分析

kubernetes的容器域名解析

先从kubernetes的容器内域名解析开始分析:

dongjiangdeMacBook-Pro:kubernetes $ kubectl get cm coredns -n kube-system  -o yaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        hosts /etc/add-hosts/customer-hosts . {
           reload 5s
        }
        prometheus :9153
        forward . /etc/resolv.conf {
           max_concurrent 1000
        }
        cache 30
        loop
        reload
        loadbalance
    }    
kind: ConfigMap

CoreDNS/kube-dnsCorefile 使用正常!

dongjiangdeMacBook-Pro:kubernetes $ cat /etc/resolv.conf    
....
nameserver 10.xxx.xxx.xxx

Node节点上/etc/resolv.conf 也正常!

dongjiangdeMacBook-Pro:kubernetes $ kubectl exec -it nginx-deployment-xxxx-xxxx "/bin/sh"
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
# cat /etc/resolv.conf  
nameserver 10.xxx.xxx.xxx
//'这才是问题关键'
search default.svc.cluster.local svc.cluster.local cluster.local unkownlan
options ndots:5

Pod中的/etc/resolv.conf 才是问题关键!

POD在集群中使用了默认的dnspolicy: ClusterFirst, 全部会将AAAAA记录forward上游DNS. 并且coredns ConfigMap中配置了hosts域名本地解析,因此default.svc.cluster.localAAAA记录,本地host命中,因此只有A记录forward到上游. 然而,使用了ndots:5 ,就会拼接google.com.default.svc.cluster.local.**unkownlan** 请求上游DNS, 也解释了unkownlan段的纯在

解决方案

集群DaemonSet添加NodeLocal DNSCache

NodeLocal DNSCache 通过在集群节点上运行一个 DaemonSet 来提高 clusterDNS 性能和可靠性。处于 ClusterFirst 的 DNS 模式下的 Pod 可以连接到 CoreDNS 的 serviceIP 进行 DNS 查询。通过 kube-proxy 组件添加的 iptables 规则将其转换为 CoreDNS 端点。通过在每个集群节点上运行 DNS 缓存NodeLocal DNSCache 可以缩短 DNS 查找的延迟时间、使 DNS 查找时间更加一致,以及减少发送到 CoreDNS 的 DNS 查询次数。

NodeLocalDNSCache

安装 NodeLocal DNSCache 也非常简单,直接获取官方的资源清单即可:

dongjiangdeMacBook-Pro:kubernetes $ wget https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

该资源清单文件中包含几个变量,其中:

  • __PILLAR__DNS__SERVER__ :表示 kube-dns 这个 Service 的 ClusterIP,可以通过命令 kubectl get svc -n kube-system | grep kube-dns | awk '{ print $3 }' 获取
  • __PILLAR__LOCAL__DNS__:表示 DNSCache 本地的 IP,默认为 169.254.20.10
  • __PILLAR__DNS__DOMAIN__:表示集群域,默认就是 cluster.local
  • __PILLAR__UPSTREAM__SERVERS__: 定制的 Upstream Server 配置
  • __PILLAR__CLUSTER__DNS__: 值来源于 kube-dnsConfigMap
dongjiangdeMacBook-Pro:kubernetes $ kubectl get svc -n kube-system | grep kube-dns | awk '{ print $3 }'
10.96.0.10

dongjiangdeMacBook-Pro:kubernetes $ sed 's/__PILLAR__DNS__SERVER__/10.96.0.10/g
s/__PILLAR__LOCAL__DNS__/169.254.20.10/g
s/__PILLAR__DNS__DOMAIN__/cluster.local/g' nodelocaldns.yaml |
kubectl apply -f -
serviceaccount/node-local-dns created
service/kube-dns-upstream created
configmap/node-local-dns created
daemonset.apps/node-local-dns created
service/node-local-dns created

dongjiangdeMacBook-Pro:kubernetes $ kubectl get pods -n kube-system | grep node-local-dns    
node-local-dns-4n7fj                     1/1     Running     0          1m54s

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

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

iptables模式下 Pod 还是向原来的集群 DNS 请求,节点上有这个 IP 监听,会被本机拦截,再请求集群上游 DNS,所以不需要更改 --cluster-dns 参数。

最后修改 kubeletcluster-dns 参数进行修改

dongjiangdeMacBook-Pro:kubernetes $ kubectl exec -it busybox "/bin/sh"
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

dongjiangdeMacBook-Pro:kubernetes $ sed -i 's/10.96.0.10/169.254.20.10/g' /var/lib/kubelet/config.yaml
dongjiangdeMacBook-Pro:kubernetes $ systemctl daemon-reload && systemctl restart kubelet

dongjiangdeMacBook-Pro:kubernetes $ kubectl exec -it busybox "/bin/sh"
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # cat /etc/resolv.conf
nameserver 169.254.20.10 #转移到本地DNS Cache上
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots: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 秒超时)

优化搜索域Search搜索半径Ndots

先了解一个概念:FQDN(Fully qualified domain name) FQDN是完整域名,一般来说,域名最终以.结束表示是FQDN,例如google.com.FQDN,但google.com不是。对FQDN,操作系统会直接查询DNS server, 就要用到search和ndots了。

ndots表示的是域名中必须出现的.的个数 (需要1-15个)。 如果域名中的.的个数>=ndots数,则该域名为一个FQDN,操作系统会直接查询;如果域名中的.的个数<ndots数据,操作系统会在search搜索域中进行查询。

举例:

# cat /etc/resolv.conf  
nameserver 10.xxx.xxx.xxx
search default.svc.cluster.local svc.cluster.local cluster.local unkownlan
options ndots:5

其中前面3个搜索域是由kubernetes注入的,最后的unkownlan操作系统默认的搜索域。

kubernetes搜索域

kubernetes搜索域 源码研习

var (
	// The default dns opt strings.
	defaultDNSOptions = []string{"ndots:5"} //默认就是5个.
)
//.....
func (c *Configurer) generateSearchesForDNSClusterFirst(hostSearch []string, pod *v1.Pod) []string {
	if c.ClusterDomain == "" {
		return hostSearch
	}
    // kubernetes默认注入 3个搜索域
	nsSvcDomain := fmt.Sprintf("%s.svc.%s", pod.Namespace, c.ClusterDomain) 
	svcDomain := fmt.Sprintf("svc.%s", c.ClusterDomain)
	clusterSearch := []string{nsSvcDomain, svcDomain, c.ClusterDomain}

	return omitDuplicates(append(clusterSearch, hostSearch...))
}

func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) {
	//...
	case podDNSCluster:
		if len(c.clusterDNS) != 0 {
			dnsConfig.Servers = []string{}
			for _, ip := range c.clusterDNS {
				dnsConfig.Servers = append(dnsConfig.Servers, ip.String())
			}
			dnsConfig.Searches = c.generateSearchesForDNSClusterFirst(dnsConfig.Searches, pod)
			dnsConfig.Options = defaultDNSOptions
			break
		}
	//...
	// 是否有自定义dnsConfig 和 CustomPodDNS,走自定义
	if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) && pod.Spec.DNSConfig != nil {
		dnsConfig = appendDNSConfig(dnsConfig, pod.Spec.DNSConfig)
	}
}

ndots修改

ndots是可以被修改的,可以通过pod.Spec.DNSConfig改写

apiVersion: v1
kind: Pod
metadata:
  namespace: default
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  dnsConfig:
    options:
      - name: ndots
        value: "2"

node节点resolv.conf 挂载到pod中

apiVersion: v1
kind: Pod
metadata:
  namespace: default
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  	  volumeMounts:
      - name: resolv
      mountPath: /etc/resolv.conf
      readOnly: true
  volumes:
    - name: resolv
      hostPath:
        path: /etc/resolv.conf  #去掉`操作系统默认`的搜索域

「如果这篇文章对你有用,请随意打赏」

Kubeservice博客

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付