DNS
CoreDNS 其实就是一个 DNS 服务,而 DNS 作为一种常见的服务发现手段,所以很多开源项目以及工程师都会使用 CoreDNS 为集群提供服务发现的功能,Kubernetes 就在集群中使用 CoreDNS 解决服务发现的问题。本文将介绍 k8s 中的 DNS 服务 CoreDNS 的架构和实现原理。
实战
k8s Pod 中是如何实现 Service Name 解析的?查看集群中有一个默认的 kube-dns 的 Service。
|
|
这个 Service 的 Endpoint 对应着集群中部署的 CoreDNS Pod:
ubuntu@VM-1-5-ubuntu:~$ kubectl get po -A -owide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system coredns-65d9c796fc-n56d8 1/1 Running 1 46h 172.25.0.9 10.0.1.5 <none> <none>
kube-system coredns-65d9c796fc-qj769 1/1 Running 1 46h 172.25.0.2 10.0.1.2 <none> <none>
kube-system kube-proxy-6sq9z 1/1 Running 0 46h 10.0.1.2 10.0.1.2 <none> <none>
kube-system kube-proxy-bk8xl 1/1 Running 0 46h 10.0.1.5 10.0.1.5 <none> <none>
...
进入已经运行的 Pod,查看 Pod 中的 /etc/resolve.conf
|
|
可以看到:
- Pod 中的 nameserver 指向
172.25.255.206,这即是kube-dns这个 service search字段主要是会指示 Pod 依次进行不同的 DNS 查找步骤,比如有一个 service b,则会依次发送b.default.svc.cluster.local -> b.svc.cluster.local -> b.cluster.local给 CoreDNS 进行解析,直到找到为止。
所以,我们执行 curl b,或者执行 curl b.default,都可以完成 DNS 请求,这 2 个不同的操作,会分别进行不同的 DNS 查找步骤:
// curl b,可以一次性找到(b +default.svc.cluster.local)
b.default.svc.cluster.local
// curl b.default,第一次找不到( b.default + default.svc.cluster.local)
b.default.default.svc.cluster.local
// 第二次查找( b.default + svc.cluster.local),可以找到
b.default.svc.cluster.local
在集群中有 ConfigMap 作为 CoreDNS 的配置:
|
|
架构
整个 CoreDNS 服务都建立在一个使用 Go 编写的 HTTP/2 Web 服务器 Caddy · GitHub 上,CoreDNS 整个项目可以作为一个 Caddy 的教科书用法。
CoreDNS 的大多数功能都是由插件来实现的,插件和服务本身都使用了 Caddy 提供的一些功能,所以项目本身也不是特别的复杂。
插件
作为基于 Caddy 的 Web 服务器,CoreDNS 实现了一个插件链的架构,将很多 DNS 相关的逻辑都抽象成了一层一层的插件,包括 Kubernetes 等功能,每一个插件都是一个遵循如下协议的结构体:
|
|
所以只需要为插件实现 ServeDNS 以及 Name 这两个接口并且写一些用于配置的代码就可以将插件集成到 CoreDNS 中。
Corefile
另一个 CoreDNS 的特点就是它能够通过简单易懂的 DSL 定义 DNS 服务,在 Corefile 中就可以组合多个插件对外提供服务:
|
|
对于以上的配置文件,CoreDNS 会根据每一个代码块前面的区和端点对外暴露两个端点提供服务:
该配置文件对外暴露了两个 DNS 服务,其中一个监听在 5300 端口,另一个在 53 端口,请求这两个服务时会根据不同的域名选择不同区中的插件进行处理。
PluginChain
CoreDNS 可以通过四种方式对外直接提供 DNS 服务,分别是 UDP、gRPC、HTTPS 和 TLS:
但是无论哪种类型的 DNS 服务,最终都会调用以下的 ServeDNS 方法,为服务的调用者提供 DNS 服务:
|
|
在上述这个已经被简化的复杂函数中,最重要的就是调用了「插件链」的 ServeDNS 方法,将来源的请求交给一系列插件进行处理,如果我们使用以下的文件作为 Corefile:
|
|
那么在 CoreDNS 服务启动时,对于当前的 example.org 这个组,它会依次加载 file、log、errors 和 prometheus 几个插件,这里的顺序是由 zdirectives.go 文件定义的,启动的顺序是从下到上:
|
|
因为启动的时候会按照从下到上的顺序依次「包装」每一个插件,所以在真正调用时就是从上到下执行的,这就是因为 NewServer 方法中对插件进行了组合:
|
|
对于 Corefile 里面的每一个配置组,NewServer 都会讲配置组中提及的插件按照一定的顺序组合起来,原理跟 Rack Middleware 的机制非常相似,插件 Plugin 其实就是一个出入参数都是 Handler 的函数:
|
|
所以我们可以将它们叠成堆栈的方式对它们进行操作,这样在最后就会形成一个插件的调用链,在每个插件执行方法时都可以通过 NextOrFailure 函数调用下一个插件的 ServerDNS 方法:
|
|
除了通过 ServeDNS 调用下一个插件之外,我们也可以调用 WriteMsg 方法并结束整个调用链。
从插件的堆叠到顺序调用以及错误处理,我们对 CoreDNS 的工作原理已经非常清楚了,接下来我们可以简单介绍几个插件的作用。
loadbalance
loadbalance 这个插件的名字就告诉我们,使用这个插件能够提供基于 DNS 的负载均衡功能,在 setup 中初始化时传入了 RoundRobin 结构体:
|
|
当用户请求 CoreDNS 服务时,我们会根据插件链调用 loadbalance 这个包中的 ServeDNS 方法,在方法中会改变用于返回响应的 Writer:
|
|
所以在最终服务返回响应时,会通过 RoundRobinResponseWriter 的 WriteMsg 方法写入 DNS 消息:
|
|
上述方法会将响应中的 Answer、Ns 以及 Extra 几个字段中数组的顺序打乱:
|
|
打乱后的 DNS 记录会被原始的 ResponseWriter 结构写回到 DNS 响应中。
loop
loop 插件会检测 DNS 解析过程中出现的简单循环依赖,如果我们在 Corefile 中添加如下的内容并启动 CoreDNS 服务,CoreDNS 会向自己发送一个 DNS 查询,看最终是否会陷入循环:
|
|
在 CoreDNS 启动时,它会在 setup 方法中调用 Loop.exchange 方法向自己查询一个随机域名的 DNS 记录:
|
|
如果这个随机域名在 ServeDNS 方法中被查询了两次,那么就说明当前的 DNS 请求陷入了循环需要终止:
|
|
就像 loop 插件的 README 中写的,这个插件只能够检测一些简单的由于配置造成的循环问题,复杂的循环问题并不能通过当前的插件解决。
总结
如果想要在分布式系统实现服务发现的功能,DNS 以及 CoreDNS 其实是一个非常好的选择,CoreDNS 作为一个已经进入 CNCF 并且在 Kubernetes 中作为 DNS 服务使用的应用,其本身的稳定性和可用性已经得到了证明,同时它基于插件实现的方式非常轻量并且易于使用,插件链的使用也使得第三方插件的定义变得非常的方便。
参考资料
-
No backlinks found.