前言
对于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了。
如:
- 使用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/
这个方法要下载镜像,还要执行个命令进入容器,麻烦!
-
使用busybox镜像 镜像busybox封装了大量这种nslookup/dig这些命令,缺陷是没有curl,跟上面的类似
-
使用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的踩坑记录
- 对于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地址,就正常了。