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 -owideNAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES kube-system coredns-65 d9c796fc-n56d8 1 /1 Running 1 46 h 172.25 .0 .9 10.0 .1 .5 <none> <none> kube-system coredns-65 d9c796fc-qj769 1 /1 Running 1 46 h 172.25 .0 .2 10.0 .1 .2 <none> <none> kube-system kube-proxy-6 sq9z 1 /1 Running 0 46 h 10.0 .1 .2 10.0 .1 .2 <none> <none> kube-system kube-proxy-bk8xl 1 /1 Running 0 46 h 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 /] 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 的大多数功能都是由插件来实现的,插件和服务本身都使用了 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 会根据每一个代码块前面的区和端点对外暴露两个端点提供服务:
该配置文件对外暴露了两个 DNS 服务,其中一个监听在 5300 端口,另一个在 53 端口,请求这两个服务时会根据不同的域名选择不同区中的插件进行处理。
PluginChain CoreDNS 可以通过四种方式对外直接提供 DNS 服务,分别是 UDP、gRPC、HTTPS 和 TLS:
但是无论哪种类型的 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
这个组,它会依次加载 file
、log
、errors
和 prometheus
几个插件,这里的顺序是由 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 的工作原理已经非常清楚了,接下来我们可以简单介绍几个插件的作用。
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) }
所以在最终服务返回响应时,会通过 RoundRobinResponseWriter
的 WriteMsg
方法写入 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) }
上述方法会将响应中的 Answer
、Ns
以及 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 服务使用的应用,其本身的稳定性和可用性已经得到了证明,同时它基于插件实现的方式非常轻量并且易于使用,插件链的使用也使得第三方插件的定义变得非常的方便。
参考资料