Think Deep,Work Lean

k8s之dns一探究竟

Posted on By zack

前言

对于k8s dns的故障排查,自己感觉已经有点心得了,大部份感觉已经有了一个套路,但是最近翻了了一次车,突然感觉不奏效了。其实每个翻车的背后,本质上都是一些知识点不足的表现,如果当时只是把这次翻车当作偶然因素,任其一闪而过,那么这些知识点的盲区就会一直成为“隐蔽的角落”。

环境

本次实验,用的是ks安装的环境,两主两从。

[root@node1 ~]# kubectl get nodes -o wide
NAME    STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION           CONTAINER-RUNTIME
node1   Ready    master,worker   13d   v1.17.6   192.168.0.2   <none>        CentOS Linux 7 (Core)   3.10.0-1062.el7.x86_64   docker://19.3.12
node2   Ready    master,worker   13d   v1.17.6   192.168.0.3   <none>        CentOS Linux 7 (Core)   3.10.0-1062.el7.x86_64   docker://19.3.12
node3   Ready    worker          13d   v1.17.6   192.168.0.4   <none>        CentOS Linux 7 (Core)   3.10.0-1062.el7.x86_64   docker://19.3.12
node4   Ready    worker          13d   v1.17.6   192.168.0.5   <none>        CentOS Linux 7 (Core)   3.10.0-1062.el7.x86_64   docker://19.3.12

svc ip pool : 10.233.0.0/18
pod ip pool : 10.233.64.0/18
cni: calico

coredns service ip: 10.233.0.3
coredns pod ip: 10.233.74.65 10.233.71.1

[root@node1 ~]# kubectl -n kube-system get po -l k8s-app=kube-dns -o wide
NAME                       READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
coredns-79c6f6447f-6f9xp   1/1     Running   0          13d   10.233.74.65   node4   <none>           <none>
coredns-79c6f6447f-hp7ng   1/1     Running   0          13d   10.233.71.1    node3   <none>           <none>

一般人查dns的处理方法

对于查dns很多人都喜欢新创建个容器,然后在容器里面去Ping某个域名,我刚开始也是这么做的,但有个问题就是:

这个命令有时经常敲错,因为有那么长串,然后还依赖网络,容器下载完后,ping下dns的域名就完事了。

然后就喜欢进到负载pod中去,ping某个域名,或是curl某一特定的服务。但是如果这个容器中没有ping/curl命令就GG了。

如:

  1. 使用alpine镜像 在容器中执行类似于 curl 的请求,来访问某个服务;或是直接Ping某个域名。

我的镜像里面装了curl 及 bind-utils,你也可以用官方的alpine,然后apk add curl apk addbind-utils

kubectl --rm --restart=Never run -i -t --image=zackzhangkai/alpine console /bin/sh
kubernetes# wget -O - -q http://hello-world:8080/

这个方法要下载镜像,还要执行个命令进入容器,麻烦!

  1. 使用busybox镜像 镜像busybox封装了大量这种nslookup/dig这些命令,缺陷是没有curl,跟上面的类似

  2. 使用nslookup

# 可以用nslookup这个命令根据ip反查dns
nslookup 10.233.0.3
3.0.233.10.in-addr.arpa name = coredns.kube-system.svc.cluster.local.
# 也可以用dig +x 到达同样的效果
/ # dig -x 10.233.0.3
;; ANSWER SECTION:
3.0.233.10.in-addr.arpa. 30     IN      PTR     coredns.kube-system.svc.cluster.local.
;; Query time: 1 msec
;; SERVER: 169.254.25.10#53(169.254.25.10)
# 容器中也可以直接运行这个,它的解析出来的是10.233.0.1。它后面没有加.svc.cluster.local,原因为
# 是同一个namespace下的。
nslookup kubernetes.default

感觉nslookup是个弱鸡,不想用,稍微复杂点就处理不了,貌似nslooup也可以指定域名服务器。因为凡是nslooup能够解决的,dig都能解决。

root@master1:~# nslookup 10.233.0.1 10.233.0.3
1.0.233.10.in-addr.arpa name = kubernetes.default.svc.cluster.local.

终极解决方法

首先dig大法好,如果没有装就装下:

yum install bind-utils -y
apk install bind-utils -y
apt-get install bind-utils -y #没有试,猜的

1) 用ptr来查:(最简单的)

root@node1:/root # dig -x 10.233.0.1 @10.233.0.3
1.0.233.10.in-addr.arpa. 30	IN	PTR	kubernetes.default.svc.cluster.local.

注意:如果dns这个解析失败,就用coredns的pod来直接解析,上面这个用的是coredns的svc的ip来解析,用svc ip来解析,本质上最终也会走向pod ip。

2) 如果用pod ip成功,用svc ip失败,就可以看下coredns的metrics来看下状态。curl 10.233.0.3:9153/metrics或是 直接用pod ip加9153端口。如果这个失败,可以用ipvsadm -lnc | grep 9153看包的状态(establish表示连接成功,sync_send或是wait_recv)。

看包的状态,其实可以用conntrack -L | grep 9153来直接看,更直接。如果这个也失败,就可以开始抓这个端口的包。(这个的思路是把问题引向http或是tcp中。 有明确的端口号就好解决了,因为可以用抓包来看,当然也可以dns来抓包,但这样的包太多不好过滤也不好定位。)
对于flannel,还需要抓flannel.1这个网卡上9153端口的包。如果用的vxlan,还需要抓eth0的8472端口的包(这个是vxlan的udp的目的端口,问题vxlan的udp的默认 端口是4789,那么如何来确定它的目的端口?ip -d link show flannl.1可以看到)。

因为coredns本质上是提供了metrics的。

[root@node1 ~]# kubectl -n kube-system get svc coredns
NAME      TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
coredns   ClusterIP   10.233.0.3   <none>        53/UDP,53/TCP,9153/TCP   13d

3) 最后,如果只是在其中任意一个节点上执行了步骤1的方法成功,而现在想知道,在这个Pod里面到底能不能正确解析成功?那就可以使用nsenter这个命令:

# 注意:如果用nsenter,对于dns需要指明解析的server,否则用的是node的/etc/resolve.conf。就会有问题
nsenter -n -t 15620 dig  kubernetes.default.svc.cluster.local  # 这样有问题
nsenter -n -t 15620 dig @10.233.0.3 kubernetes.default.svc.cluster.local  # 这个正常
nsenter -n -t 15620 dig  @10.233.0.3 -x 10.233.0.1  # dig直接用IP, ptr也可以查到,-x后面一定要紧跟nameserver ip。
# nsenter -n -t 15620 dig -x @10.233.0.3 10.233.0.1 # 这个就有问题
nsenter -n -t 31654 dig -x 10.233.0.1 @10.233.0.3 #这个是正常的。
# nsenter一定要注意,用的网络是namespace的,但是文件是本的(包括配置文件)。

现在抓包看下

[root@node1 ~]# tcpdump -i any port 9153 -nnc 6 -w 01.pcap

然后访问9153端口

[root@node1 ~]# curl 10.233.71.35:9153/metrics

然后看下这这个包:

[root@node1 ~]# tcpdump -r 01.pcap
reading from file 01.pcap, link-type LINUX_SLL (Linux cooked)
10:22:23.272221 IP node1.56174 > 10.233.71.1.9153: Flags [S], seq 4036166206, win 43690, options [mss 65495,sackOK,TS val 1829192312 ecr 0,nop,wscale 7], length 0
10:22:23.272719 IP 10.233.71.1.9153 > node1.56174: Flags [S.], seq 2263089655, ack 4036166207, win 27760, options [mss 1400,sackOK,TS val 1827969692 ecr 1829192312,nop,wscale 7], length 0
10:22:23.272757 IP node1.56174 > 10.233.71.1.9153: Flags [.], ack 1, win 342, options [nop,nop,TS val 1829192313 ecr 1827969692], length 0
10:22:23.272800 IP node1.56174 > 10.233.71.1.9153: Flags [P.], seq 1:87, ack 1, win 342, options [nop,nop,TS val 1829192313 ecr 1827969692], length 86
10:22:23.272990 IP 10.233.71.1.9153 > node1.56174: Flags [.], ack 87, win 217, options [nop,nop,TS val 1827969693 ecr 1829192313], length 0
10:22:23.274020 IP 10.233.71.1.9153 > node1.56174: Flags [.], seq 1:2777, ack 87, win 217, options [nop,nop,TS val 1827969694 ecr 1829192313], length 2776

分析下这个包,看下flag, [S]表示 req , [S.] 表示 req+ack, [.] 表示ack 。这个就是tcp的三次握手。 [P.]表示push,此时三次连接成功后,就是http传输数据。

把它scp到本地后,用wireshark打开。

ssh root@node1 cat /root/01.pcap > /tmp/01.pcap && hugo@zack:/Users/hugo $ open /Applications/Wireshark.app /tmp/01.pcap

coredns只是个kube-system下的deploy,如果所有的副本pods分布在了一个node上,然后这个node挂了,这个就容易出问题了,当然后很快会新建Pods,只不过有个故障时间。

关于nodeLocalDns

它主要是缓存dns记录用,过期时间为30s(成功时),在configmap里面有corefile能查。在每个node上有daemonSet。cluster.local的dns解析用的是coreDns,其余的用的是nodeLocalDns自己的配置,可从cm中来配置。

root@node3:~# kubectl -n kube-system get cm nodelocaldns  -o yaml
...
Corefile: |
    cluster.local:53 {  # 这个匹配 cluster.local域
        errors
        cache {
            success 9984 30
            denial 9984 5
        }
        reload
        loop
				# kubectl -n kube-system describe po nodelocaldns-2chf7 | grep -i ip -C 3
				# Args:
				#      -localip
				#      169.254.25.10
				# root     31545 31511  0 Jun18 ?        01:21:59 /node-cache -localip 169.254.25.10 -conf /etc/coredns/Corefile -upstreamsvc coredns
        
				bind 169.254.25.10 # nodeLocalDns的地址 , 意思是程序在运行的时候,绑定这个IP


        forward . 10.233.0.3 {  # 配置转发到coreDns
            force_tcp  # dns的端口是53,既可以用tcp也可以用udp。这里指明强制使用tcp  k get svc -A | grep dns  53/UDP,53/TCP
        }
        prometheus :9253
        health 169.254.25.10:9254  
    }
    in-addr.arpa:53 { # 这个查ptr记录 反向域:arpa
        errors
        cache 30
        reload
        loop
        bind 169.254.25.10
        forward . 10.233.0.3 {
            force_tcp   
        }
        prometheus :9253
    }
    ip6.arpa:53 { 
        errors
        cache 30
        reload
        loop
        bind 169.254.25.10
        forward . 10.233.0.3 {
            force_tcp
        }
        prometheus :9253
    }
		.:53 {  # 其余的从配置文件中的规则转发
		        errors
		        cache 30
		        reload
		        loop
		        bind 169.254.25.10
		        forward . /etc/resolv.conf
		        prometheus :9253
		    }

如果在node上创建一个Pod,默认它的dns的配置如下:

root@node2:~# kubectl run --rm -ti --restart=Never console --image zackzhangkai/alpine -- /bin/sh
If you don't see a command prompt, try pressing enter.
/ # cat /etc/resolv.conf
nameserver 169.254.25.10 # 指向nodelocaldns
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

root@node2:~# kubectl -n kube-system exec nodelocaldns-2chf7 -- cat /etc/resolv.conf
# nodelocaldns dns配置文件
nameserver 100.64.11.3  # 这个是zone的内网dns
nameserver 114.114.114.114 #公网的dns 114dns.com 南京信风
nameserver 119.29.29.29  # 这个是腾讯dns

有个奇怪的现象:同样是在coredns pod里面执行了cat命令,报错

root@node2:~# kubectl -n kube-system exec coredns-878d96d96-fg6px -- cat /etc/resolv.conf
OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"cat\": executable file not found in $PATH": unknown

不对,nodelocaldns可以执行命令,但是coredns里面没有这个命令。

通过把pod找到containerID,去相应的node上通过把docker cp把它拷备出来看下

root@node3:~# cat resolv.conf
nameserver 100.64.11.3
nameserver 114.114.114.114
nameserver 119.29.29.29

竟然跟nodelocaldns是一样的。。。

那是什么时候指定这个文件的呢?

看kubelete, kubelete是负责创建容器的,看下里面是不是有指定。也不一定是这个地方。

... /usr/local/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --cgroup-driver=cgroupfs --network-plugin=cni --pod-infra-container-image=kubesphere/pause:3.2 --resolv-conf=/run/systemd/resolve/resolv.conf

可以看到里面确实指定了。

root@node3:~# cat /run/systemd/resolve/resolv.conf
nameserver 100.64.11.3
nameserver 114.114.114.114
nameserver 119.29.29.29
# Too many DNS servers configured, the following entries may be ignored.
nameserver 1.2.4.8

此时涉及到kubelet的启动参数。其实这个参数控制的是dnsPolicy的策略( Default/None/ClusterFirst)。

如果 Pod 的 dnsPolicy 设置为 “default”,则它将从 Pod 运行所在节点上的配置中继承名称解析配置。

Pod 的 DNS 解析应该与节点相同。如果您不想这样做,或者想要为 Pod 使用其他 DNS 配置,则可以 使用 kubelet 的 –resolv-conf 标志。 将此标志设置为 “” 以避免 Pod 继承 DNS

有点奇怪的是 pod的ip跟node的ip一样 对于一个pod,如果这两个Ip一样,一般是它的hostnetwork=true设置的,原因是 这个参数的意思就是说让pod使用node的ip,可以理解为它们使用同一张网卡。此时需要保证的是 这个Pod里面的进程的端口如果暴露出来的话,不能跟Node上的端口冲突。

root@node3:~# kubectl -n kube-system get po nodelocaldns-2chf7 -o yaml | less -i
hostIP: 192.168.0.2  #node3的ip不0.2,而是0.7 ,不对,这个pod本来就是跑在192.168.0.2上的。
  phase: Running
  podIP: 192.168.0.2
  podIPs:
  - ip: 192.168.0.2

关于POD 的 DnsPolicy

  • Default: 用的Node的/etc/resolv.con配置,可以通过kubelet启动命令时加 --resolv-conf来指定pod里面的/etc/resov.conf文件,此时通过这个文件来解析。
  • None: 从dnsConfig里面来读取配置,这个暂时还不是很清楚。
  • ClusterFirst:cluster.local的域用coredns来解析;否则,再用上游upstream的dns配置,即nodeLocalDns的,可以通过kubelet启动时指定 --cluster-dns 来指定。
  • ClusterFirstWithHostNet: 一般对daemonSet的有用

记录&待解决:

1) 我们知道 CNAME(本质就是另一个域名的别名) 、 A记录()、 AAAA记录()、 PTR记录(由IP找域名),那么什么是SRV记录?

2) 对于headless的svc,如何解析?

spec:
  clusterIP: None
  ports:

如果一个Service的spec.clusterIP是None,则是headless。

[root@node1 ~]# kubectl -n kubesphere-system get svc redis-ha
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)              AGE
redis-ha   ClusterIP   None         <none>        6379/TCP,26379/TCP   13d

对于headless的service,它的域名为redis-ha.kubesphere-system.svc.cluster.local,它的ip为一组pod的ip

[root@node1 ~]# dig redis-ha.kubesphere-system.svc.cluster.local @10.233.0.3
redis-ha.kubesphere-system.svc.cluster.local. 30 IN A 10.233.102.133
redis-ha.kubesphere-system.svc.cluster.local. 30 IN A 10.233.74.71
redis-ha.kubesphere-system.svc.cluster.local. 30 IN A 10.233.75.4

如果现在用这个headless service的域名,它会轮巡使用后端的地址。

[root@node1 ~]# dig -x 10.233.102.133 @10.233.0.3
133.102.233.10.in-addr.arpa. 30 IN      PTR     10-233-102-133.redis-ha-announce-1.kubesphere-system.svc.cluster.local.

通过这个我们知道域名跟IP并不是绝对的一一对应的,对正常的service实用,但是对headless无头service就不实用。

可以看到使用headless service最大的好处就是:该service没有clusterIP,查询这个serviceName,可以直接返回真实的后端,因此我们就可以使用后端了。而正常的service有clusterIP,查询它的serviceName,只能返回它的这个ClusterIP,而无法返回所有的后端。

通过上面的示例我们知道,使用headless的最大的好处是可以给后端绑定一个dns域名,这样我们就可以通过这个域名来访问指定的后端(pod),即使这个Pod的ip变了,它的域名不变。

对于stafulSet这类有状态的应用,即Pod是有启动顺序的,会根据名字的顺序来顺序启动。

[root@node1 ~]# kubectl -n kubesphere-system get po |grep redis
redis-ha-server-0                           2/2     Running   0          13d
redis-ha-server-1                           2/2     Running   0          13d
redis-ha-server-2                           2/2     Running   0          13d

headless service有个好处,可以给它的后端(endpoint)每个都分配一个dns域名。

3) 如何解析一个Pod的DNS

[root@node1 ~]# kubectl -n kubesphere-system get po | grep redis
redis-ha-haproxy-8668585566-2jwkv           1/1     Running   0          11d
redis-ha-haproxy-8668585566-k7m6k           1/1     Running   0          11d
redis-ha-haproxy-8668585566-n5fvn           1/1     Running   0          11d
redis-ha-server-0                           2/2     Running   0          13d
redis-ha-server-1                           2/2     Running   0          13d
redis-ha-server-2                           2/2     Running   0          13d
[root@node1 ~]# dig redis-ha-server-1.kubesphere-system.svc.cluster.local @10.233.0.3
...
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
...

answer为0,为什么找不到这个pod的dns?

关于coredns的踩坑记录

  1. 对于nodelocaldns 的cm中定义了解析的顺序,先是将cluster.loal的发送到coredns(169.254.25.10)来解析。不管是coredns还是nodelocaldns,如果不是集群内的域名,它最终会用主机的/etc/resolv.conf中的配置来解决。对于ubuntu18,在kubelet启动的时候会单独指定resolve文件。
 root@n1x2:~# cat /var/lib/kubelet/kubeadm-flags.env
KUBELET_KUBEADM_ARGS="--cgroup-driver=systemd --network-plugin=cni --pod-infra-container-image=kubesphere/pause:3.1 --resolv-conf=/run/systemd/resolve/resolv.conf"

读的是 /run/systemd/resolve/resolv.conf,而这里面有很dns地址,虽然第一个是私网内部dns能解析,但是第二个是1.2.4.8这个dns,一致卡着不解析,导致容器内的dns解析失败,把这个文件中的dns全部删除只留一个内网的dns地址,就正常了。