深入理解 Linux 和 Kubernetes 中的 DNS 解析
~jpetazzo/深入理解 Linux 和 Kubernetes 中的 DNS 解析
我最近研究了 Kubernetes 上的一个警告消息:DNSConfigForming ... Nameserver limits were exceeded, some nameservers have been omitted
。 从技术上讲,这是一个 type: Warning
的 Kubernetes event,通常表明存在问题,因此我想对其进行调查。
这引导我深入研究了 Linux 通用以及 Kubernetes 特有的 DNS 解析。 我认为解释这一切的工作原理可能会对其他人有所帮助,以防万一有一天你必须排查 DNS 问题(正如我们现在所知,在 Linux 或 Kubernetes 上,总是 DNS 的问题)。
Kubernetes DNS 理论基础
Kubernetes 提供了基于 DNS 的服务发现。 当我们在 bar
命名空间中创建一个名为 foo
的 service 时,Kubernetes 会创建一个 DNS 条目 foo.bar.svc.cluster.local
,该条目解析为该 service 的 ClusterIP
。
集群中的任何 pod 都可以解析 foo.bar
或 foo.bar.svc
并获得该 service 的 ClusterIP
。 同一个 bar
命名空间中的任何 pod 甚至可以直接解析 foo
以获得该 ClusterIP
。
这意味着当我们编写将在 Kubernetes 上运行的代码时,如果我们需要连接到数据库,我们可以将 db
作为数据库名称(而不是硬编码 IP 地址),或者例如 db.prod
如果我们要连接到 prod
命名空间中的 service db
。
这很方便,因为类似的机制存在于例如 Docker Compose 中;这意味着我们可以编写代码,使用 Docker Compose 进行测试,然后在 Kubernetes 中运行它,而无需更改任何一行代码。 酷。
现在,这在幕后是如何运作的?
Linux 上的 DNS 解析(第一层:resolv.conf)
乍一看,Linux 上的 DNS 解析是通过 /etc/resolv.conf
配置的。 一个典型的 resolv.conf
文件可能如下所示:
nameserver 192.168.1.1
nameserver 192.168.1.2
search example.com example.net
这定义了两个 DNS 服务器(用于冗余目的;但在许多情况下,你只会有一个)以及一个“搜索列表”。 这意味着如果我们尝试解析名称 hello
,会发生以下情况:
- 首先,我们查找
hello.example.com
- 如果该名称不存在,我们查找
hello.example.net
- 如果该名称不存在,我们查找
hello
- 如果该名称不存在,我们报告错误(如
Name or service not known
)
默认情况下,仅使用第一个域名服务器。 只有当第一个超时时,才会查询其他域名服务器。 这意味着所有域名服务器必须提供完全相同的记录。 例如,你不能有一个用于内部域的域名服务器,另一个用于外部域! 如果你向第一个服务器发送查询,并且该服务器回复“未找到”(从技术上讲,是 NXDOMAIN 回复),则不会查询第二个服务器 - “未找到”错误会立即报告。
(注意:可以使用 rotate
选项,在这种情况下,域名服务器会以轮询顺序进行查询,以分散查询负载。 但是,域名服务器仍然需要具有相同的记录;否则,DNS 回复可能不一致,这将导致一些 非常奇怪的 错误。)
有一些限制和“细则”:
- 我们可以指定最多 3 个域名服务器(其他域名服务器将被忽略);
- 我们可以指定最多 6 个搜索域;
ndots
选项可用于更改是否(以及何时)首先尝试搜索列表,或“初始绝对查询”(即没有搜索域的hello
)。
你可以在 resolv.conf(5)
的 man page 中查看更多详细信息,其中解释了如何更改超时值、重试次数以及类似内容。
Linux 上的 DNS 解析(第二层:nsswitch.conf)
也许你遇到过 .local
名称。 例如,在我家的 LAN 上,我可以使用 ping zagreb.local
来 ping 机器 zagreb
:
$ ping -4 zagreb.local
PING zagreb.local (10.0.0.30) 56(84) bytes of data.
64 bytes from 10.0.0.30: icmp_seq=1 ttl=64 time=2.16 ms
...
这有时被称为 Zeroconf、Bonjour、Avahi 或 mDNS。 在不深入研究这些各自的协议和实现的情况下,这如何适应我们上面解释的系统?
这是因为实际上,在使用 /etc/resolv.conf
之前,系统会检查 /etc/nsswitch.conf
。 NSS 是“名称服务开关”,用于配置许多不同的名称查找服务,包括:
- hosts(将名称映射到 IP 地址)
- passwd(将用户名映射到其 UID,反之亦然)
- services(将服务名称(如
http
、ftp
、ssh
)映射到端口号,反之亦然)
在该文件中,可能有一行如下所示:
hosts: files mymachines myhostname mdns_minimal [NOTFOUND=return] dns
这里有很多内容需要解释。 我不会深入研究所有小细节,因为这与 Kubernetes 无关,但这本质上意味着在尝试解析主机名时,系统将查找:
files
,表示/etc/hosts
(这就是为什么我们可以在该文件中硬编码一些名称和 IP 地址!);mymachines
,用于systemd-machined
容器;myhostname
,自动将localhost
之类的名称映射到127.0.0.1
,或将我们的本地主机名映射到本地 IP 地址;mdns_minimal
,解析.local
名称;dns
,这是由resolv.conf
配置的传统 DNS 解析器,如上所述。
你可能还会遇到 resolve
,它使用 systemd-resolved
进行名称解析。 这是一个完全不同的系统,具有自己的配置和设置,并且与 Kubernetes 几乎无关,因此我们在此不讨论它。
Linux 上的 DNS 解析(第三层:musl 和 systemd-resolved)
我们在前两节中解释的所有内容仅适用于使用 GNU libc 或 “glibc” 的程序。 这是 几乎 所有 Linux 发行版上使用的系统库,但 Alpine Linux 除外,后者使用 musl 代替 glibc。
musl 名称解析器要简单得多:没有 NSS(名称服务开关),并且 DNS 解析完全通过 /etc/resolv.conf
配置。 它的行为也有所不同(它并行向所有服务器发送查询,而不是一次一个)。 你可以在此页面中查看更多详细信息,该页面解释了 musl 和 glibc 之间的差异。
这很重要,因为 Alpine 用于许多容器镜像中,尤其是在优化容器镜像大小时。 一些基于 Alpine 的镜像可能比非 Alpine 镜像小 10 倍。 当然,确切的收益将很大程度上取决于程序、其依赖项等,但这解释了为什么 Alpine 在容器生态系统中非常常见。
此外,如果你的系统使用 systemd-resolved
(systemd
的一个可选组件),则 DNS 配置看起来会完全不同。
使用 systemd-resolved
时:
systemd-resolved.service
单元将正在运行;- 在
/etc/nsswitch.conf
中,在hosts:
行上,将提到模块resolve
,表明主机名解析将使用通过 DBUS 的 systemd-resolved,而不是通过 UDP 或 TCP 的“传统” DNS 查询; /etc/resolv.conf
将是指向/run/systemd/resolve/stub-resolv.conf
的符号链接,并包含行nameserver 127.0.0.53
;systemd-resolved
将在127.0.0.53
上公开一个遗留解析器,适用于不使用名称服务开关的应用程序(例如,与 Alpine 链接或使用 Go 原生网络库的应用程序);- DNS 配置将通过
systemd
配置文件和/或使用resolvectl
工具完成,而不是编辑/etc/resolv.conf
; /run/systemd/resolve/resolv.conf
将包含一个兼容性配置文件,列出上游 DNS 服务器,供需要“经典”resolv.conf
文件的应用程序使用。
最后一项与 Kubernetes 相关,我们将在后面看到,因为 kubelet
有时需要该 resolv.conf
文件。
Kubernetes 上的 DNS 解析(第一层:kube-dns)
掌握了所有这些 DNS 配置知识后,让我们看一下 Kubernetes pod 中的 /etc/resolv.conf
文件。 该特定 pod 位于 default
命名空间中,其 resolv.conf
文件如下所示:
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.10.0.10
options ndots:5
nameserver
行指示使用 Kubernetes 内部 DNS 服务器。 此处的 IP 地址对应于 kube-system
命名空间中 kube-dns
service 的 ClusterIP
地址:
$ kubectl get service -n kube-system kube-dns
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.10.0.10 <none> 53/UDP,53/TCP,9153/TCP 9d
确切的 IP 地址在你的集群中可能有所不同。 它通常是集群 IP 服务子网中的第 10 个主机地址,但这不是硬性规定。
该 kube-dns
service 通常会与 coredns
deployment 一起配置; 并且该 coredns
deployment 将被配置为提供 Kubernetes DNS 记录(例如前面提到的 foo.bar.svc.cluster.local
)并将外部名称的查询传递给上游服务器。
注意:也可以使用 CoreDNS 以外的东西。 实际上,早期版本的 Kubernetes(最高 1.10)使用了一个名为 kube-dns
的自定义服务器,这就是为什么该 service 仍然具有该名称。 并且有些人会替换 CoreDNS,或添加主机本地缓存,以提高性能并解决高流量场景中的一些问题。 你可以查看此 KubeCon 演示文稿 作为一个例子。 (即使它已经存在几年了,该演示文稿仍然在解释 DNS 机制方面做得很好,并且它解释的想法和技术在今天仍然具有高度相关性!)
现在,让我们看一下 search
行。 它基本上意味着当我们尝试解析 foo
时,我们将按以下顺序尝试:
foo.default.svc.cluster.local
,对应于“与 pod 位于同一命名空间中的foo
service”;foo.svc.cluster.local
,在这种情况下不对应于任何内容,但如果我们尝试解析foo.bar
,它将很有用,因为它将对应于“bar
命名空间中的foo
service”;foo.cluster.local
,在这种情况下也不对应于任何内容,但如果我们尝试解析foo.bar.svc
,它将很有用;foo
本身,它也无法解析为任何内容。
这意味着在我们的代码中,我们可以连接到,例如:
foo
,它将解析为当前命名空间中的foo
service,foo.bar
,它将解析为bar
命名空间中的foo
service,foo.bar.svc
,它将解析为相同的内容,foo.bar.svc.cluster.local
,它也将解析为相同的内容,foo.example.com
,它将使用外部 DNS 解析。
唷! 当然,有一些小细节需要注意。
Kubernetes 上的 DNS 解析(第二层:自定义)
让我们从更容易的事情开始:可以更改 cluster.local
后缀。 它通常在设置集群时配置,类似于例如 Cluster IP 子网。 它需要更新 kubelet
配置以及 CoreDNS 配置。 很少需要更改该后缀,除非我们想将多个集群连接在一起,并使一个集群能够解析在另一个集群上运行的 service 的名称。 这是非常不寻常的,除非运行巨大的应用程序 - 巨大是指它们不适合单个集群;或者我们出于各种原因不想将它们放在单个集群上。
然后,svc
组件又如何呢? 它存在是因为还有 pod
,换句话说,pod.cluster.local
。 Kubernetes DNS 将 A-B-C-D.N.pod.cluster.local
解析为 A.B.C.D
,只要 A.B.C.D
是有效的 IP 地址并且 N
是现有的命名空间。 老实说:我不知道这有什么用,但如果你知道,请告诉我!
最后,还有 options ndots:5
。 这表明“名称中应该有多少个点才能将该名称视为外部名称”。 换句话说,如果我们尝试解析 a
、a.b
、a.b.c
、a.b.c.d
或 a.b.c.d.e
,则将使用搜索列表 - 因此解析 api.example.com
将导致 5 个 DNS 查询(对于搜索列表的 4 个元素 + 上游查询)。 但是如果我们尝试解析 a.b.c.d.e.f
,由于至少有 5 个点,它将直接尝试上游查询。 (ndots
的默认值为 1。)
这引发了一个问题:如果我们想解析 api.example.com
,我们如何避免不必要的 DNS 查询? 还有另一个问题:如果我们想解析外部名称 purple.dev
,同时在命名空间 dev
中也有一个名为 purple
的 service,我们应该怎么做? 这两个问题的答案是在域的末尾添加一个点。 解析 purple.dev.
将跳过 search
列表,这意味着它不会产生不必要的 DNS 查询,并且它将解析外部名称,而不是内部 Kubernetes 名称。
这就是我们需要了解的关于 Kubernetes DNS 的全部内容吗? 不完全是。
Kubernetes 上的 DNS 解析(第三层:dnsPolicy)
“正常” pod 将具有类似于先前显示的 DNS 配置 - 为方便起见,此处重新生成:
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.10.0.10
options ndots:5
但是可以通过设置 pod 清单中的 dnsPolicy
字段来更改此设置。
可以告诉 Kubernetes 使用主机的 DNS 配置。 例如,CoreDNS pod 中使用了此选项,因为它们需要知道要查询哪些解析器以获取外部名称。 一些基础设施、必要的 pod 也使用它,这些 pod 需要解析外部名称,但不希望依赖 Kubernetes 内部 DNS 来正常运行。 (我在一些需要连接到云提供商 API 端点的 CNI 或 CSI pod 中看到了这种情况。)
也可以告诉 Kubernetes 为 pod 使用完全任意的 DNS 配置,指定 DNS 服务器和搜索列表。
你可以在此 Kubernetes 文档页面中找到有关 dnsPolicy
字段及其可能值的所有详细信息。
当使用主机的 DNS 配置时,Kubernetes(从技术上讲,kubelet
)将使用主机上的 /etc/resolv.conf
- 或者,如果它检测到主机正在使用 systemd-resolved
,它将使用 /run/systemd/resolve/resolv.conf
代替。 还有一个 kubelet
选项 --resolv-conf
,用于指示它使用不同的文件。
回到 Nameserver limits were exceeded
让我们回到我们的错误消息。
当我们告诉 Kubernetes 使用主机的 DNS 配置时(通过显式 dnsPolicy: Default
或隐式配置,因为 pod 具有 hostNetwork: true
,这是 kube-proxy
和许多 CNI pod 的情况),它将从主机上的 /etc/resolv.conf
获取该配置。 我们在上面提到,DNS 解析器(glibc 和 musl 中)最多支持 3 个域名服务器; 其他域名服务器将被忽略。 如果在该文件中配置了 3 个以上的解析器,Kubernetes 将发出该警告,因为它“知道”不会使用额外的服务器。
在传统的 IPv4 环境中,列出 2 个以上的服务器的情况非常少见。 但是,在双栈 IPv4/IPv6 环境中,很可能最终会有更多。
例如,Hetzner 上的这台服务器有两个 IPv4 服务器和两个 IPv6 服务器:
nameserver 2a01:4ff:ff00::add:1
nameserver 2a01:4ff:ff00::add:2
nameserver 185.12.64.1
nameserver 185.12.64.2
这台机器有一个 IPv4 服务器,每个接口有一个 IPv6 服务器,并且有 3 个接口:
nameserver 10.0.0.1
nameserver fe80::1%eno2
nameserver fe80::1%wlo1
nameserver fe80::1%enp0s20f0u3u4
在这种情况下,当创建 pod 时,kubelet
将发出警告 - 我们在本文开头提到的 DNSConfigForming
。
该警告是完全无害的(它不表示配置问题或我们的 pod 的潜在问题),并且我们的 pod 的 DNS 行为根本不会改变。 请记住:使用 glibc 解析器,我们无论如何都会按顺序尝试每个解析器。 有第二个作为备份是很好的,但是 3 或 4 个通常有点过分。
不过,我们如何消除该警告?
这就是事情变得有点复杂的地方。
如果你手动配置了 DNS 解析(通过编辑 /etc/resolv.conf
),你需要做的就是修剪域名服务器列表,使其只有 3 个或更少。
但是很可能此配置是自动提供的,通常由 DHCP 客户端提供(如果这是你的本地计算机,则在你的 LAN 上,如果这是某个地方的服务器,则由你的托管提供商提供)。 如果你可以更新 DHCP 服务器的配置,以便它提供 3 个或更少的域名服务器,那就太好了! 如果你不能,你将必须在客户端修剪该列表。
手动编辑 /etc/resolv.conf
(或者,当使用 systemd-resolved
时,手动编辑 /run/systemd/resolve/resolv.conf
)很诱人,但它可能不会持久。 首先,几乎可以肯定的是,当机器重新启动时,将重新生成此文件。 接下来,也很可能此文件会在某个时候被重新生成,无论是被 DHCP 客户端还是被 systemd-resolved
重新生成。
一种可能性是修剪 DHCP 客户端收到的 DNS 服务器列表。 确切的方法将取决于 DHCP 客户端。 在 Ubuntu 上,默认的 DHCP 客户端是 dhclient
,DNS 是使用 dhclient-script
配置的,dhclient-script
本身具有一个 hook 系统。 例如,在使用 systemd-resolved
的系统上,/etc/dhcp/dhclient-exit-hooks.d/resolved
中有一个脚本,用于将 DNS 解析器提供给 systemd-resolved
。 DNS 解析器通过环境变量 $new_domain_name_servers
传递。 应该可以在该目录中放置一个脚本,例如 reduce-nameservers
,以更改该变量。 (由于 reduce-nameservers
在 resolved
之前,因此应该在其之前调用;但是 dhclient-script
文档未指定调用脚本的顺序。)
不幸的是,在某些情况下,这将更加复杂,因为某些域名服务器将在启动时传递(并由 cloud-init、netplan 等系统收集),并且 DHCP 客户端稍后会添加更多域名服务器。 这也可能发生在具有多个网络接口的系统上,例如连接到多个虚拟网络的系统。 这些系统可能会在每个接口上收到几个 DNS 服务器,并且看起来 systemd-resolved
只会愉快地聚合所有这些服务器,导致 kubelet
向我们显示该警告。
另一种方法(如果控制你的 kubelet
配置对你来说是可行的)是将 kubelet
指向自定义 resolv.conf
文件,并从现有的 resolv.conf
文件生成该文件,仅保留前 3 个域名服务器。
当然,虽然这些方法相对简单(尤其是在具有一组固定节点的本地运行时),但当你想要将它们捆绑到节点部署过程中时,它们会立即需要大量额外的工作 - 例如,当使用托管节点和/或集群自动缩放时。
结论
坏消息:没有万无一失的方法可以消除该 DNSConfigForming
警告。
好消息:它完全无害。
虽然这篇文章没有为我们提供一种轻松可靠地消除该错误消息的方法,但我们希望它为你提供了许多关于 DNS 如何工作的有见地的详细信息 - 在 Kubernetes 上,以及在现代 Linux 系统上!
Jérôme Petazzoni Tinkerer Extraordinaire github.com/jpetazzo @jpetazzo@hachyderm.io twitter.com/jpetazzo
Jérôme Petazzoni 的这项作品已获得 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License 的许可。