香港服务器 Kubernetes 集群 DNS 解析问题排查
18 Dec 20 16:48 +0000

我在两台香港服务器上部署好 Kubernetes 集群后解析 Telegram, Google 等墙外服务 DNS 出现异常,有很大概率受到 DNS 污染,但是在宿主机上没有任何受到影响,那么问题到底出在哪里呢?

问题现象

首先确定问题现象及复现方式:在 Kubernetes 集群的服务 Pod 当中,解析 Telegram, Google 等墙外服务,有很大概率受到 DNS 污染,解析出的地址有的时候指向不可访问 IP (这时会请求超时),有的时候指向其它网站的服务器 IP (比如经常出现 Facebook 服务器 IP,这时可以 ping 通,但 HTTPS 请求证书非法),当然也有一定概率返回正确地址。在多个容器中出现了这种情况,而在宿主机上不存在此问题。

在宿主机:

➜  ~ ping -c 5 google.com

输出:

PING google.com (172.217.26.142) 56(84) bytes of data.
64 bytes from kul08s06-in-f14.1e100.net (172.217.26.142): icmp_seq=1 ttl=115 time=3.70 ms
64 bytes from kul08s06-in-f14.1e100.net (172.217.26.142): icmp_seq=2 ttl=115 time=3.73 ms
64 bytes from kul08s06-in-f14.1e100.net (172.217.26.142): icmp_seq=3 ttl=115 time=3.54 ms
64 bytes from kul08s06-in-f14.1e100.net (172.217.26.142): icmp_seq=4 ttl=115 time=3.91 ms
64 bytes from kul08s06-in-f14.1e100.net (172.217.26.142): icmp_seq=5 ttl=115 time=3.71 ms

--- google.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4005ms
rtt min/avg/max/mdev = 3.540/3.720/3.911/0.118 ms

在 Pod 容器内:

➜  ~ kubectl run tmp-shell --rm -i --tty --image nicolaka/netshoot -- ping -c 5 google.com

输出:

PING google.com (46.82.174.69) 56(84) bytes of data.

--- google.com ping statistics ---
5 packets transmitted, 0 received, 100% packet loss, time 4094ms

有的时候容器内也可以解析到正确地址,以上是解析错误的情况,下同。

调试

使用 nicolaka/netshoot 镜像调试网络,以下 "在容器中" 的命令都是在以下 Pod 中执行:

kubectl run -it --rm --image nicolaka/netshoot tmp-shell -- /bin/bash

Ping

Ping 的运行结果如上所示,在容器中,google.com 被解析到了不正确的 IP 导致 ping 不通。

nslookup

在宿主机:

➜  ~ nslookup google.com

输出:

Server:		8.8.8.8
Address:	8.8.8.8#53

Non-authoritative answer:
Name:	google.com
Address: 172.217.26.142
Name:	google.com
Address: 2404:6800:4005:805::200e

在容器内输出:

Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	google.com
Address: 93.46.8.90
Name:	google.com
Address: 93.46.8.90

经过检查,确定 10.96.0.10 是 Kubernetes 集群中 CoreDNS 的 service IP:

➜  ~ kubectl get svc -n kube-system kube-dns
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   36d

DNS 服务器通过 /etc/resolv.conf 文件确定,宿主机的文件内容:

nameserver 8.8.8.8

容器内的文件内容:

nameserver 10.96.0.10

也就是说 CoreDNS 给 DNS 返回了错误的 IP。

CoreDNS

检查 CoreDNS 的部署,得知 CoreDNS 的配置来自 ConfigMap kube-system/coredns,查看这个配置的数据段:

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
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }

因为 google.com 是外部地址,所以我们只关注一个配置:forward . /etc/resolv.conf,即 CoreDNS 对外部地址的解析方式,该插件的配置格式为 forward FROM TO,以上配置即:将所有域名转发到 /etc/resolv.conf 中配置的服务器进行解析。

在尝试查看 CoreDNS Pod 中的 /etc/resolv.conf 文件内容时会遇到一些麻烦,就是 CoreDNS 镜像似乎没有提供 shell,连 cat 也没有:

➜  ~ kubectl exec -it -n kube-system coredns-79569d5586-74xpr -- /bin/bash
OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown
command terminated with exit code 126
➜  ~ kubectl exec -it -n kube-system coredns-79569d5586-74xpr -- /bin/sh
OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown
command terminated with exit code 126
➜  ~ kubectl exec -it -n kube-system coredns-79569d5586-74xpr -- 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
command terminated with exit code 126

CoreDNS 的 Docker 镜像是直接基于 scratch 构建的,所以什么命令都没有,可以使用 docker history 查看镜像分层 (layers)。

既然不能进到容器,我们只能把文件拷贝出来,或者到容器的 rootfs 中找,先找到 CoreDNS 容器 ID, 在 Pod 所在节点执行:

➜  ~ docker ps | grep coredns

输出:

ac3921579e79        bfe3a36ebd25           "/coredns -conf /etc…"   2 weeks ago         Up 2 weeks                              k8s_coredns_coredns-79569d5586-74xpr_kube-system_e52be710-2d09-4b83-b650-bef85f57d02e_0
212785d1ee6c        k8s.gcr.io/pause:3.2   "/pause"                 2 weeks ago         Up 2 weeks                              k8s_POD_coredns-79569d5586-74xpr_kube-system_e52be710-2d09-4b83-b650-bef85f57d02e_0

可知 CoreDNS 进程所在的容器 ID 是 ac3921579e79 (另一个容器 212785d1ee6cpause 容器)。

使用 docker cp 命令导出容器中的 /etc/resolv.conf 文件:

docker cp ac3921579e79:/etc/resolv.conf ./t.conf

这样才能看到 CoreDNS 容器中 /etc/resolv.conf 文件的内容:

nameserver 114.114.114.114
nameserver 8.8.8.8

这里就看到差别了,CoreDNS 容器里的 /etc/resolv.conf 出现了额外的 DNS 服务器 114.114.114.114,而这个服务器是中国大陆的 DNS 提供商,到这里基本可以确定了 114 对 google.com 等墙外域名解析存在 DNS 污染问题。

解决

因为这个问题影响了服务运行,所以在寻找问题原因前,先解决掉它,解决思路两个:1. 修复 CoreDNS 容器中的 /etc/resolv.conf 文件,2. 修改 CoreDNS 转发配置。后者实现起来比较简单,所以优先采用这个方法,修改 CoreDNS 配置为:

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
        }
        prometheus :9153
        forward . 8.8.8.8
        cache 30
        loop
        reload
        loadbalance
    }

修改完成后 CoreDNS 会自动重载配置。

修改完成后,可以发现墙外网站域名解析恢复正常。

溯源

CoreDNS 镜像中的 /etc/resolv.conf 文件不是挂载进去的,但也不是打在镜像中的,那么它是哪里来的呢?而且为什么会有 114 的配置,很令人疑惑的一点是,宿主机上的 /etc/resolv.conf 文件却没有 114 的配置,那容器中这个 114 到底从哪里来的?

经过翻阅很多文档资料,我最后发现这个问题的根源其实在 kubelet,114 配置的来源则是宿主机的配置。

kubelet

Kubelet 在 Kubernetes 中负责维护一个节点上的 Pod 调度和正常运行,包括与向上与 api server 的状态同步,和向下与 CRI/CNI/CSI 等接口进行的容器管理。

在 Pod 管理过程中,kubelet 会对容器做一些额外的操作,比如就有我们关心的 DNS 相关配置,kubelet 需要保证集群中的 Pod 都配置了集群内部 DNS 服务作为 DNS 解析服务器,所以其实 Pod 中的 /etc/resolv.conf 文件是 kubelet 挂载进去的。

挂载过程取决于 kubelet 和 Pod 的配置组合,kubelet 相关的配置有:clusterDNS, resolvConf, Pod 相关的配置有:dnsPolicy, dnsConfig。

clusterDNS 用于指定集群内部 DNS 服务器地址,这个地址在集群启动时就固定了,在我的集群中即 10.96.0.10, resolvConf 则是外部域名的解析配置,是一个宿主机上的文件(如 /run/systemd/resolve/resolv.conf),具体挂载到 Pod 中的配置取决于 Pod 自身配置 dnsPolicy 和 dnsConfig,参考 DNS for Services and Pods | Kubernetes,当 dnsPolicy 为 Default 时,resolvConf 将被挂载到 Pod 中的 /etc/resolv.conf 文件,当 dnsPolicy 为 ClusterFirst 时,/etc/resolv.conf 文件将被配置为集群内部 DNS 地址。

通过检查宿主机的 kubelet 发现,resolvConf 被配置为了 /run/systemd/resolve/resolv.conf (而不是 /etc/resolv.conf),问题就出在这个文件,它的内容是:

# This file is managed by man:systemd-resolved(8). Do not edit.
#
# This is a dynamic resolv.conf file for connecting local clients directly to
# all known uplink DNS servers. This file lists all configured search domains.
#
# Third party programs must not access this file directly, but only through the
# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
# replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.

nameserver 114.114.114.114
nameserver 8.8.8.8

而 /etc/resolv.conf 文件的内容是:

nameserver 8.8.8.8

从而造成了集群内外对部分域名的解析行为不一致。

阅读 /run/systemd/resolve/resolv.conf 的注释信息,可以知道这个文件是由 systemd-resolved 服务管理的,systemd-resolved 的功能是 DNS 服务器管理,参考 systemd-resolved.service 中文手册 [金步国],systemd-resolved 默认从 /etc/systemd/resolved.conf 文件读取 DNS 配置,查看内容:

cat /etc/systemd/resolved.conf

输出:

#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
#
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See resolved.conf(5) for details

[Resolve]
DNS=114.114.114.114 8.8.8.8
#DNS=8.8.8.8
#DNS=
#FallbackDNS=
#Domains=
#LLMNR=no
#MulticastDNS=no
#DNSSEC=no
#Cache=yes
#DNSStubListener=yes

调整文件内容,删除 114.114.114.114,重启 systemd-resolved 服务:

sudo systemctl restart systemd-resolved

再打印 /run/systemd/resolve/resolv.conf 文件内容:

# This file is managed by man:systemd-resolved(8). Do not edit.
#
# This is a dynamic resolv.conf file for connecting local clients directly to
# all known uplink DNS servers. This file lists all configured search domains.
#
# Third party programs must not access this file directly, but only through the
# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
# replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.

nameserver 8.8.8.8

这样即使把 CoreDNS 配置再改回来也可以正常使用了。


Loading comments...