概要
CoreDNS 的插件系统本质上使用了 Caddy 的插件系统。CoreDNS 在此基础上又定义了插件的接口和请求的格式,从而成为 CoreDNS 风格的插件。
CoreDNS 的代码量并不大,基本上 20% 的框架型代码 + 80% 的官方插件,所以还是很容易就能快速摸清楚大体的逻辑。相关的架构和使用文档还可以参考另外一篇博客:CoreDNS 使用与架构分析。
备注:下文中的代码分析流程取自 CoreDNS v1.2.6 版本。
插件加载的过程
编译过程
在讲 CoreDNS 插件加载的时候,必须先看看 CoreDNS 的编译过程 coredns/Makefile
。
这个 Makefile 相对比较简单,主 target coredns
没有太多依赖,所以逻辑还是很清晰,重点关注在编译 coredns
之前必须先完成 check
目标,而 check
目标则是:
1
2
|
.PHONY: check
check: presubmit core/zplugin.go core/dnsserver/zdirectives.go godeps
|
进行完 presubmit
(就是执行 coredns/.presubmit
下的 Bash 脚本),需要生成两个关键的目标:coredns/core/zplugin.go
和 coredns/core/dnsserver/zdirectives.go
,这两个目标都是用同一个方式生成:
1
2
|
core/zplugin.go core/dnsserver/zdirectives.go: plugin.cfg
go generate coredns.go
|
在 coredns/coredns.go
中,有这么一句 Go generate 的注释:
1
2
3
|
...
//go:generate go run directives_generate.go
...
|
当执行 go generate coredns.go
这句命令的时候,将触发以 go:generate
为标记的命令(所谓的 go generate 就是代码中内嵌一些特殊的命令注释,执行特定命令时将触发命令的执行),即:go run directives_generate.go
。该命令执行完之后将生成两个文件:coredns/core/zplugin.go
和 coredns/core/dnsserver/zdirectives.go
。自动代码的生成依赖于 plugin.cfg
配置文件,所以当配置文件更新时,对应的目标也需要被重新创建。
初始化注册插件
我们来分别看看 coredns/core/zplugin.go
的逻辑:
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
41
42
|
// generated by directives_generate.go; DO NOT EDIT
package plugin
import (
// Include all plugins.
_ "github.com/coredns/coredns/plugin/auto"
_ "github.com/coredns/coredns/plugin/autopath"
_ "github.com/coredns/coredns/plugin/bind"
_ "github.com/coredns/coredns/plugin/cache"
_ "github.com/coredns/coredns/plugin/chaos"
_ "github.com/coredns/coredns/plugin/debug"
_ "github.com/coredns/coredns/plugin/dnssec"
_ "github.com/coredns/coredns/plugin/dnstap"
_ "github.com/coredns/coredns/plugin/erratic"
_ "github.com/coredns/coredns/plugin/errors"
_ "github.com/coredns/coredns/plugin/etcd"
_ "github.com/coredns/coredns/plugin/federation"
_ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/health"
_ "github.com/coredns/coredns/plugin/hosts"
_ "github.com/coredns/coredns/plugin/kubernetes"
_ "github.com/coredns/coredns/plugin/loadbalance"
_ "github.com/coredns/coredns/plugin/log"
_ "github.com/coredns/coredns/plugin/loop"
_ "github.com/coredns/coredns/plugin/metadata"
_ "github.com/coredns/coredns/plugin/metrics"
_ "github.com/coredns/coredns/plugin/nsid"
_ "github.com/coredns/coredns/plugin/pprof"
_ "github.com/coredns/coredns/plugin/proxy"
_ "github.com/coredns/coredns/plugin/reload"
_ "github.com/coredns/coredns/plugin/rewrite"
_ "github.com/coredns/coredns/plugin/root"
_ "github.com/coredns/coredns/plugin/route53"
_ "github.com/coredns/coredns/plugin/secondary"
_ "github.com/coredns/coredns/plugin/template"
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/trace"
_ "github.com/coredns/coredns/plugin/whoami"
_ "github.com/mholt/caddy/onevent"
)
|
很简单,就是 import
语句,但是并不真正使用对应的 package,这又是为什么呢 ?其实就是为了执行每个 package 的 init
方法。
我们仔细观察 coredns/plugin/
下的每个插件,都有一个 setup.go
,每个 setup.go
都有类似的 init()
:
1
2
3
4
5
6
|
func init() {
caddy.RegisterPlugin("auto", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
|
即调用 Caddy 的逻辑注册插件,其中 Action
是一个函数 setup()
:
1
2
3
4
|
func setup(c *caddy.Controller) error {
// 注册插件的时候将会运行这个函数
// 主要用于配置解析等一些初始化工作
}
|
综上,当引用了 coredns/zplugin.go
时,将按照 import
中给出的顺序依次支持每个插件的 init()
来注册插件,当 Caddy 服务器启动时,将按序执行已注册插件的 setup()
以此来完成插件的初始化动作。
再来看看 coredns/core/dnsserver/zdirectives.go
:
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
41
42
43
44
45
46
47
48
|
// generated by directives_generate.go; DO NOT EDIT
package dnsserver
// Directives are registered in the order they should be
// executed.
//
// Ordering is VERY important. Every plugin will
// feel the effects of all other plugin below
// (after) them during a request, but they must not
// care what plugin above them are doing.
var Directives = []string{
"metadata",
"tls",
"reload",
"nsid",
"root",
"bind",
"debug",
"trace",
"health",
"pprof",
"prometheus",
"errors",
"log",
"dnstap",
"chaos",
"loadbalance",
"cache",
"rewrite",
"dnssec",
"autopath",
"template",
"hosts",
"route53",
"federation",
"kubernetes",
"file",
"auto",
"secondary",
"etcd",
"loop",
"forward",
"proxy",
"erratic",
"whoami",
"on",
}
|
同样也很简单,只是定义了一个 Directives
的字符串数组。这个变量将在 coredns/core/dnsserver/register.go
中用到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// Any flags defined here, need to be namespaced to the serverType other
// wise they potentially clash with other server types.
func init() {
flag.StringVar(&Port, serverType+".port", DefaultPort, "Default port")
caddy.RegisterServerType(serverType, caddy.ServerType{
Directives: func() []string { return Directives },
DefaultInput: func() caddy.Input {
return caddy.CaddyfileInput{
Filepath: "Corefile",
Contents: []byte(".:" + Port + " {\nwhoami\n}\n"),
ServerTypeName: serverType,
}
},
NewContext: newContext,
})
}
|
其实就是将 Directives
作为一个参数传递给 caddy.ServerType{}
,并最终在 caddy/caddy.go
中的 executeDirectives()
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func executeDirectives(inst *Instance, filename string,
directives []string, sblocks []caddyfile.ServerBlock, justValidate bool) error {
// ...
for _, dir := range directives {
for i, sb := range sblocks {
// ...
for j, key := range sb.Keys {
// Execute directive if it is in the server block
if tokens, ok := sb.Tokens[dir]; ok {
// ...
setup, err := DirectiveAction(inst.serverType, dir)
if err != nil {
return err
}
err = setup(controller)
// ...
}
}
}
// ...
}
// ...
}
|
这段代码将按 Directives
中顺序执行对应插件之前注册的 setup()
。
zplugin.go
和 zdirectives.go
都是由 coredns/directives_generate.go
生成,并依赖于 plugin.cfg
。
plugin.cfg
是一个很简单的配置文件:
1
2
3
4
5
6
|
metadata:metadata
tls:tls
reload:reload
nsid:nsid
root:root
...
|
每一行由冒号分割,第一部分是插件名,第二部分是插件的包名(可以是一个完整的外部地址,如 log:github.com/coredns/coredns/plugin/log
),且插件在 plugin.cfg
中的顺序就是最终生成文件中对应的顺序。
coredns/directives_generate.go
的逻辑也比较简单,基本上就是打开文件,按行解析文件并生成对应的 import
和 Directives
。
虽然 plugin.cfg
中定义了大量的默认插件,且编译的时候将其全部编译成一个二进制文件,但实际运行过程中并不会全部执行,CoreDNS 在处理请求过程中只会运行配置文件中所需要的插件。
入口逻辑
CoreDNS 的入口在 coredns/coredns.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
//go:generate go run directives_generate.go
import (
"github.com/coredns/coredns/coremain"
// Plug in CoreDNS
_ "github.com/coredns/coredns/core/plugin"
)
func main() {
coremain.Run()
}
|
非常简单,直接调用一个 coremain.Run()
并开始运行。此处我们注意到 import
有一个引用了但是并没真正使用的 package github.com/coredns/coredns/core/plugin
,而这个包下面只有一个文件,即 coredns/core/zplugin.go
。经过上文的分析,这样将在 Run()
开始执行之前执行各个插件的 init()
动作,即调用 Caddy 相关的函数注册插件。
coremain.Run()
逻辑同样也很简单:
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
|
package coremain
import (
// ...
"github.com/coredns/coredns/core/dnsserver"
// ...
)
func Run() {
caddy.TrapSignals()
// ...
// 获取 Caddy 的配置,生成对应的配置文件结构 corefile
corefile, err := caddy.LoadCaddyfile(serverType)
// ...
// 以 corefile 为配置启动 Caddy
instance, err := caddy.Start(corefile)
// ...
// Execute instantiation events
caddy.EmitEvent(caddy.InstanceStartupEvent, instance)
// Twiddle your thumbs
instance.Wait()
}
|
在 import
开头中将执行 dnsserver
package 的 init()
,即 register.go
中的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func init() {
flag.StringVar(&Port, serverType+".port", DefaultPort, "Default port")
caddy.RegisterServerType(serverType, caddy.ServerType{
Directives: func() []string { return Directives },
DefaultInput: func() caddy.Input {
return caddy.CaddyfileInput{
Filepath: "Corefile",
Contents: []byte(".:" + Port + " {\nwhoami\n}\n"),
ServerTypeName: serverType,
}
},
NewContext: newContext,
})
}
|
这是使用 Caddy 的接口注册一个服务器类型,即 DNS 服务器,其中 NewContext
字段是一个对应业务服务器的生成器:
1
2
3
|
func newContext(i *caddy.Instance) caddy.Context {
return &dnsContext{keysToConfigs: make(map[string]*Config)}
}
|
即将生成 dnsContext{}
结构,该结构满足 caddy/plugins.go
中 Context
的接口定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type Context interface {
// Called after the Caddyfile is parsed into server
// blocks but before the directives are executed,
// this method gives you an opportunity to inspect
// the server blocks and prepare for the execution
// of directives. Return the server blocks (which
// you may modify, if desired) and an error, if any.
// The first argument is the name or path to the
// configuration file (Caddyfile).
//
// This function can be a no-op and simply return its
// input if there is nothing to do here.
InspectServerBlocks(string, []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error)
// This is what Caddy calls to make server instances.
// By this time, all directives have been executed and,
// presumably, the context has enough state to produce
// server instances for Caddy to start.
MakeServers() ([]Server, error)
}
|
而在 dnsContext{}
中对应的 MakeServers()
方法将创建自定义的 Server 来处理服务器请求。
说白了,coremain.Run()
执行之后,我们将创建一个 Caddy 的服务器,服务器中接收到的请求将由我们自定义的 DNS 服务器来处理。
Plugin 的设计
CoreDNS 采用的是插件链(Plugin chain) 的方式来执行插件的逻辑,也就是可以把多个插件的执行简单理解为以下的伪代码:
1
2
3
|
for _, plugin := range plugins {
plugin()
}
|
其中插件链中的插件和顺序可从配置文件中配置。
一个请求在被插件处理时,大概有以下几种情况(可参考文章):
-
请求被当前插件处理,处理完返回对应的响应,至此插件的执行逻辑结束,不会运行插件链的下一个插件;
-
请求被当前插件处理之后跳至下一个插件,即每个插件将维护一个 next 指针,指向下一个插件,转至下一个插件通过 NextOrFailure()
实现;
-
请求被当前插件处理之后增加了新的信息,携带这些信息将请求交由下一个插件处理;
很明显,写一个插件必须符合一定的接口要求,CoreDNS 在 coredns/plugin/plugin.go
中定义了:
1
2
3
4
5
6
7
8
|
type (
Handler interface {
// 每个插件处理请求的逻辑
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
// 返回插件名
Name() string
}
)
|
每一个插件都会定义一个结构体(如果不需要对应数据结构就设置一个空的结构体),并为这个对象实现对应的接口,比如 whoami
插件的 whoami.go
是这样做的:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 定义一个空的 struct
type Whoami struct{}
// 给 Whoami 对象实现 ServeDNS 方法
// w 是用来写入响应
// r 是接收到的 DNS 请求
func (wh Whoami) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ...
}
// 给 Whoami 对象实现 Name 方法
// 只需要简单返回插件名字的字符串即可
func (wh Whoami) Name() string { return "whoami" }
|
对于每一个插件,其 setup.go
中的 setup()
中都有这么个函数:
1
2
3
4
5
6
7
8
9
10
11
12
|
func setup(c *caddy.Controller) error {
// 通常前面是做一些参数解析的逻辑
// dnsserver 层添加插件
// next 表示的是下一个插件
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
l.Next = next
return l
})
// ...
}
|
在 setup()
中 AddPlugin()
将一个函数对象添加到一个插件列表中:
1
2
3
|
func (c *Config) AddPlugin(m plugin.Plugin) {
c.Plugin = append(c.Plugin, m)
}
|
对于每一个配置块,都有一个 c.Plugin
的列表,如果在配置块中插件顺序是 ABCD
,那么对应到 c.Plugin
这个插件列表中位置也同样是 ABCD
。
AddPlugin()
添加的元素是 plugin.Plugin
类型:
1
2
3
|
func(next plugin.Handler) plugin.Handler {
...
}
|
其中 next
表示的是下一个插件对象。
当我们使用 NewServer()
创建对应的 Caddy 服务器之前,我们对每一个配置块会创建好一个插件列表(如上所示)c.Plugin
,而在执行 NewServer()
时将根据 c.Plugin
的内容创建 c.PluginChain
,即真正处理请求的插件链:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func NewServer(addr string, group []*Config) (*Server, error) {
// ...
var stack plugin.Handler
// 从插件列表的最后一个元素开始
for i := len(site.Plugin) - 1; i >= 0; i-- {
// stack 作为此时插件的 next 参数
// 如果配置文件中的插件顺序是 A,B,C,D,首次初始化时添加到列表就会变成 D,C,B,A
// 从最后一个元素 A,开始依次调用对应的 plugin.Handler,将有:
// A: next=nil
// B: next=A
// C: next=B
// D: next=C
// 最终插件从 D 开始,即原来配置顺序的最后一个
// 最终的执行顺序为配置文件插件顺序的逆序
stack = site.Plugin[i](stack)
// register the *handler* also
site.registerHandler(stack)
// ...
}
// 这时的插件是配置文件顺序中的最后一个
site.pluginChain = stack
// ...
}
|
当执行完之后,site.pluginChain
指向原始配置文件中插件顺序的最后一个插件,也就是说,插件链中的插件顺序与配置文件中的插件顺序是相反的,后面我们也将看到,插件的执行顺序是按照插件链的顺序进行,即是插件配置顺序的逆序。
CoreDNS 如何处理请求
在了解 CoreDNS 如何处理请求之前,我们需要重点看看 coredns/core/dnsserver
这个 package 的逻辑。
这个 package 定义一个 DNS 服务器,并将其注册到 Caddy 的运行逻辑中,从而接管请求的处理流程。
由上文可知,dnsContext{}
实现了 Caddy 的 Context
接口,其中比较关键的是 MakeServers()
的实现,即:
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
41
42
|
// coredns/core/dnsserver/register.go
func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
// ...
var servers []caddy.Server
// 由于我们可以定义多个 group 来对不同的域名做解析
// 每个 group 都将创建一个不同的 DNS server 的实例
for addr, group := range groups {
// switch on addr
switch tr, _ := parse.Transport(addr); tr {
case transport.DNS:
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
case transport.TLS:
s, err := NewServerTLS(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
case transport.GRPC:
s, err := NewServergRPC(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
case transport.HTTPS:
s, err := NewServerHTTPS(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
}
}
return servers, nil
}
|
从 MakeServers()
的逻辑可以看出,如果当前使用的是 DNS 协议,那么将会为每个 group 调用 NewServer()
创建一个 DNS Server。
不同协议的请求(DNS/TLS/gRPC/https)最终都会调用 ServeDNS()
来处理每一个请求,这也是最主要的核心逻辑:
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
|
// coredns/core/dnsserver/server.go
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
//...
// 如果请求落在对应的 zone,执行 zone 内的插件
if h, ok := s.zones[string(b[:l])]; ok {
//...
if r.Question[0].Qtype != dns.TypeDS {
// 如果没有过滤函数
if h.FilterFunc == nil {
// 执行插件链上上的插件
// 如果插件中有 NextOrFailure() 则将跳至下一个插件
// 否则则直接返回
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
if !plugin.ClientWrite(rcode) {
DefaultErrorFunc(ctx, w, r, rcode)
}
return
}
// FilterFunc is set, call it to see if we should use this handler.
// This is given to full query name.
if h.FilterFunc(q) {
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
if !plugin.ClientWrite(rcode) {
DefaultErrorFunc(ctx, w, r, rcode)
}
return
}
}
}
//...
}
|
至此,整个 CoreDNS 插件系统的基本逻辑告一段落。从整体来看,CoreDNS 不是一个复杂的系统,简简单单,但正是这种简洁的插件系统,才让 CoreDNS 渐渐演化出一个插件小生态,让 DNS 在原本的基础功能之上增加了更多扩展性功能,而最新的 CNCF 新闻,CoreDNS 已经从 CNCF 孵化项目中毕业,成为一个真正成熟的容器平台 DNS 方案的既定标准,可想而知,在不远的将来,CoreDNS 生态将枝繁叶茂。