0%

【Kubernetes】CoreDNS

CoreDNS 其实就是一个 DNS 服务,而 DNS 作为一种常见的服务发现手段,所以很多开源项目以及工程师都会使用 CoreDNS 为集群提供服务发现的功能,Kubernetes 就在集群中使用 CoreDNS 解决服务发现的问题。本文将介绍 k8s 中的 DNS 服务 CoreDNS 的架构和实现原理。

实战

k8s Pod 中是如何实现 Service Name 解析的?查看集群中有一个默认的 kube-dns 的 Service。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@VM-1-5-ubuntu:~$ kubectl describe svc kube-dns -n kube-system
Name: kube-dns
Namespace: kube-system
Labels: addonmanager.kubernetes.io/mode=Reconcile
kubernetes.io/cluster-service=true
kubernetes.io/name=CoreDNS
Annotations: prometheus.io/port: 9153
prometheus.io/scrape: true
Selector: k8s-app=kube-dns
Type: ClusterIP
IP: 172.25.255.206
Port: dns-tcp 53/TCP
TargetPort: 53/TCP
Endpoints: 172.25.0.2:53,172.25.0.9:53
Port: dns 53/UDP
TargetPort: 53/UDP
Endpoints: 172.25.0.2:53,172.25.0.9:53
Session Affinity: None
Events: <none>

这个 Service 的 Endpoint 对应着集群中部署的 CoreDNS Pod:

1
2
3
4
5
6
7
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

1
2
3
4
5
ubuntu@VM-1-5-ubuntu:~$ kubectl exec -it centos bash
[root@centos /]# cat /etc/resolv.conf
nameserver 172.25.255.206
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

可以看到:

  • 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查找步骤:

1
2
3
4
5
6
7
// 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 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ubuntu@VM-1-5-ubuntu:~$ kubectl get cm coredns -n kube-system -o yaml
apiVersion: v1
data:
Corefile: |2-
.:53 {
errors
health
kubernetes cluster.local. in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
reload
loadbalance
}
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system

架构

整个 CoreDNS 服务都建立在一个使用 Go 编写的 HTTP/2 Web 服务器 Caddy · GitHub 上,CoreDNS 整个项目可以作为一个 Caddy 的教科书用法。

coredns-architecture

CoreDNS 的大多数功能都是由插件来实现的,插件和服务本身都使用了 Caddy 提供的一些功能,所以项目本身也不是特别的复杂。

插件

作为基于 Caddy 的 Web 服务器,CoreDNS 实现了一个插件链的架构,将很多 DNS 相关的逻辑都抽象成了一层一层的插件,包括 Kubernetes 等功能,每一个插件都是一个遵循如下协议的结构体:

1
2
3
4
5
6
7
8
type (
Plugin func(Handler) Handler

Handler interface {
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
Name() string
}
)

所以只需要为插件实现 ServeDNS 以及 Name 这两个接口并且写一些用于配置的代码就可以将插件集成到 CoreDNS 中。

Corefile

另一个 CoreDNS 的特点就是它能够通过简单易懂的 DSL 定义 DNS 服务,在 Corefile 中就可以组合多个插件对外提供服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
coredns.io:5300 {
file db.coredns.io
}

example.io:53 {
log
errors
file db.example.io
}

example.net:53 {
file db.example.net
}

.:53 {
kubernetes
proxy . 8.8.8.8
log
errors
cache
}

对于以上的配置文件,CoreDNS 会根据每一个代码块前面的区和端点对外暴露两个端点提供服务:

coredns-corefile-example

该配置文件对外暴露了两个 DNS 服务,其中一个监听在 5300 端口,另一个在 53 端口,请求这两个服务时会根据不同的域名选择不同区中的插件进行处理。

PluginChain

CoreDNS 可以通过四种方式对外直接提供 DNS 服务,分别是 UDP、gRPC、HTTPS 和 TLS:

coredns-servers

但是无论哪种类型的 DNS 服务,最终都会调用以下的 ServeDNS 方法,为服务的调用者提供 DNS 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
m, _ := edns.Version(r)

ctx, _ := incrementDepthAndCheck(ctx)

b := r.Question[0].Name
var off int
var end bool

var dshandler *Config

w = request.NewScrubWriter(r, w)

for {
if h, ok := s.zones[string(b[:l])]; ok {
ctx = context.WithValue(ctx, plugin.ServerCtx{}, s.Addr)
if r.Question[0].Qtype != dns.TypeDS {
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
dshandler = h
}
off, end = dns.NextLabel(q, off)
if end {
break
}
}

if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil {
rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r)
plugin.ClientWrite(rcode)
return
}

if h, ok := s.zones["."]; ok && h.pluginChain != nil {
ctx = context.WithValue(ctx, plugin.ServerCtx{}, s.Addr)

rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
plugin.ClientWrite(rcode)
return
}
}

在上述这个已经被简化的复杂函数中,最重要的就是调用了「插件链」的 ServeDNS 方法,将来源的请求交给一系列插件进行处理,如果我们使用以下的文件作为 Corefile:

1
2
3
4
5
6
example.org {
file /usr/local/etc/coredns/example.org
prometheus # enable metrics
errors # show errors
log # enable query logs
}

那么在 CoreDNS 服务启动时,对于当前的 example.org 这个组,它会依次加载 filelogerrorsprometheus 几个插件,这里的顺序是由 zdirectives.go 文件定义的,启动的顺序是从下到上:

1
2
3
4
5
6
7
8
9
10
11
var Directives = []string{
// ...
"prometheus",
"errors",
"log",
// ...
"file",
// ...
"whoami",
"on",
}

因为启动的时候会按照从下到上的顺序依次「包装」每一个插件,所以在真正调用时就是从上到下执行的,这就是因为 NewServer 方法中对插件进行了组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func NewServer(addr string, group []*Config) (*Server, error) {
s := &Server{
Addr: addr,
zones: make(map[string]*Config),
connTimeout: 5 * time.Second,
}

for _, site := range group {
s.zones[site.Zone] = site
if site.registry != nil {
for name := range enableChaos {
if _, ok := site.registry[name]; ok {
s.classChaos = true
break
}
}
}
var stack plugin.Handler
for i := len(site.Plugin) - 1; i >= 0; i-- {
stack = site.Plugin[i](stack)
site.registerHandler(stack)
}
site.pluginChain = stack
}

return s, nil
}

对于 Corefile 里面的每一个配置组,NewServer 都会讲配置组中提及的插件按照一定的顺序组合起来,原理跟 Rack Middleware 的机制非常相似,插件 Plugin 其实就是一个出入参数都是 Handler 的函数:

1
2
3
4
5
6
7
8
type (
Plugin func(Handler) Handler

Handler interface {
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
Name() string
}
)

所以我们可以将它们叠成堆栈的方式对它们进行操作,这样在最后就会形成一个插件的调用链,在每个插件执行方法时都可以通过 NextOrFailure 函数调用下一个插件的 ServerDNS 方法:

1
2
3
4
5
6
7
8
9
10
11
12
func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if next != nil {
if span := ot.SpanFromContext(ctx); span != nil {
child := span.Tracer().StartSpan(next.Name(), ot.ChildOf(span.Context()))
defer child.Finish()
ctx = ot.ContextWithSpan(ctx, child)
}
return next.ServeDNS(ctx, w, r)
}

return dns.RcodeServerFailure, Error(name, errors.New("no next plugin found"))
}

除了通过 ServeDNS 调用下一个插件之外,我们也可以调用 WriteMsg 方法并结束整个调用链。

coredns-plugin-chain

从插件的堆叠到顺序调用以及错误处理,我们对 CoreDNS 的工作原理已经非常清楚了,接下来我们可以简单介绍几个插件的作用。

loadbalance

loadbalance 这个插件的名字就告诉我们,使用这个插件能够提供基于 DNS 的负载均衡功能,在 setup 中初始化时传入了 RoundRobin 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
func setup(c *caddy.Controller) error {
err := parse(c)
if err != nil {
return plugin.Error("loadbalance", err)
}

dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return RoundRobin{Next: next}
})

return nil
}

当用户请求 CoreDNS 服务时,我们会根据插件链调用 loadbalance 这个包中的 ServeDNS 方法,在方法中会改变用于返回响应的 Writer

1
2
3
4
func (rr RoundRobin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
wrr := &RoundRobinResponseWriter{w}
return plugin.NextOrFailure(rr.Name(), rr.Next, ctx, wrr, r)
}

所以在最终服务返回响应时,会通过 RoundRobinResponseWriterWriteMsg 方法写入 DNS 消息:

1
2
3
4
5
6
7
8
9
10
11
func (r *RoundRobinResponseWriter) WriteMsg(res *dns.Msg) error {
if res.Rcode != dns.RcodeSuccess {
return r.ResponseWriter.WriteMsg(res)
}

res.Answer = roundRobin(res.Answer)
res.Ns = roundRobin(res.Ns)
res.Extra = roundRobin(res.Extra)

return r.ResponseWriter.WriteMsg(res)
}

上述方法会将响应中的 AnswerNs 以及 Extra 几个字段中数组的顺序打乱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func roundRobin(in []dns.RR) []dns.RR {
cname := []dns.RR{}
address := []dns.RR{}
mx := []dns.RR{}
rest := []dns.RR{}
for _, r := range in {
switch r.Header().Rrtype {
case dns.TypeCNAME:
cname = append(cname, r)
case dns.TypeA, dns.TypeAAAA:
address = append(address, r)
case dns.TypeMX:
mx = append(mx, r)
default:
rest = append(rest, r)
}
}

roundRobinShuffle(address)
roundRobinShuffle(mx)

out := append(cname, rest...)
out = append(out, address...)
out = append(out, mx...)
return out
}

打乱后的 DNS 记录会被原始的 ResponseWriter 结构写回到 DNS 响应中。

loop

loop 插件会检测 DNS 解析过程中出现的简单循环依赖,如果我们在 Corefile 中添加如下的内容并启动 CoreDNS 服务,CoreDNS 会向自己发送一个 DNS 查询,看最终是否会陷入循环:

1
2
3
4
. {
loop
forward . 127.0.0.1
}

在 CoreDNS 启动时,它会在 setup 方法中调用 Loop.exchange 方法向自己查询一个随机域名的 DNS 记录:

1
2
3
4
5
func (l *Loop) exchange(addr string) (*dns.Msg, error) {
m := new(dns.Msg)
m.SetQuestion(l.qname, dns.TypeHINFO)
return dns.Exchange(m, addr)
}

如果这个随机域名在 ServeDNS 方法中被查询了两次,那么就说明当前的 DNS 请求陷入了循环需要终止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (l *Loop) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if r.Question[0].Qtype != dns.TypeHINFO {
return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r)
}

// ...

if state.Name() == l.qname {
l.inc()
}

if l.seen() > 2 {
log.Fatalf("Forwarding loop detected in \"%s\" zone. Exiting. See https://coredns.io/plugins/loop#troubleshooting. Probe query: \"HINFO %s\".", l.zone, l.qname)
}

return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r)
}

就像 loop 插件的 README 中写的,这个插件只能够检测一些简单的由于配置造成的循环问题,复杂的循环问题并不能通过当前的插件解决。

总结

如果想要在分布式系统实现服务发现的功能,DNS 以及 CoreDNS 其实是一个非常好的选择,CoreDNS 作为一个已经进入 CNCF 并且在 Kubernetes 中作为 DNS 服务使用的应用,其本身的稳定性和可用性已经得到了证明,同时它基于插件实现的方式非常轻量并且易于使用,插件链的使用也使得第三方插件的定义变得非常的方便。

参考资料