域名系统(英语:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。(维基百科)
在DNS分布式数据库中的索引被称为域名,DNS数据库中的名称形成一个分层树状结构称为域命名空间。域名包含单个标签分隔点,例如:www.baidu.com
DNS中的数据是以资源记录为单位进行存储的。资源记录有不同的类别和类型。实际上,应用在互联网上的类别只有一个:IN,IN类别中资源记录的类型制定了数据存储的格式和使用。下面列举了一些最常见的资源记录类型:
A(IPv4地址):
将一个域名映射到单个IPv4地址。
AAAA(IPv6地址):
将一个域名映射到单个IPv6地址。
CNAME(别名):
将一个域名(别名)映射到另一个域名(规范名称)。
MX(邮件交换器):
为邮件目标命名一个邮件交换器(邮件服务器)。
NS(名称服务器):
为区域命名一个名称服务器(或DNS服务器)。
PTR(指针):
将一个IP地址映射回一个域名。
SOA(起始授权机构):
为区域提供参数。
作者:张永曦,中国移动云能力中心软件研发工程师,专注于云原生、微服务等领域。
01
Kubernetes中的DNS服务器—CoreDNS
DNS被认为是kubernetes中“附加”的组件,集群没有它也能正常的工作,然而很少有集群不使用DNS,而且Kubernetes DNS规范被认为是标准一致性套件的一部分。该规范是一种形式固定的DNS模式,它定义了一组特定的名称,这些名称必须基于ApiServer的内容存在,Kubernetes中的Service资源是用户指定服务发现模式的主要方式。规范中的所有记录都属于集群域(cluster domain)。通常,集群域会被设置为cluster.local。例如,CoreDNS服务的全域名为kube-dns.kube-system.svc.cluster.local。
用过Kubernetes的小伙伴或许会问,为何使用部署应用的时候,没有感知到CoreDNS的存在?这是由于Kubernetes已经自动将Pod的默认域名服务器指定成了CoreDNS的地址。例如,当创建了一个Pod之后,Kubernetes会自动将这个Pod的dnsPolicy设置为ClusterFirst,即使用CoreDNS的ClusterIP作为Pod的NameServer。若是想手动指定Pod的默认域名服务器,则按照如下配置:
代码语言:javascript复制dnsPolicy: None
dnsConfig:
nameservers:
- 10.233.10.10
searches:
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
- localdomain
options:
- name: ndots
value: "5"
02
CoreDNS的配置解析
在Kubernetes中,CoreDNS的配置Corefile存在ConfigMap资源中,是位于kube-system命名空间下的coredns。在默认情况下,Corefile的内容如下:
代码语言:javascript复制.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
ttl 30
}
hosts /etc/add-hosts/customer-hosts . {
fallthrough cluster.local in-addr.arpa ip6.arpa
}
prometheus :9153
cache 30
reload
loadbalance
}
Corefile是由一个或者多个条目组成,这些条目本身则由标签和定义组成,条目的定义包含在花括号“{}”中,花括号之间的文本被称为一个块。在Corefile中,只存在一个条目,.:53表示在端口53上运行的服务器开启了一个新的服务器块,并指示该服务器解析根域及其下的所有查询。
下一行是errors,它启用了错误插件,这个插件的作用是当错误发生时,会被记录下来,故障排除会更容易。
health插件为Kubelet暴露了一个运行健康状况检查的端点,以监控CoreDNS是否正常运行,它在端口8080上运行一个HTTP服务器,该服务器将相应访问路径“/health”的HTTP请求。
kubernetes CLUSTER_DOMAIN REVERSE_CIDRS的作用是启用kubernetes插件,并授予它对CLUSTER_DOMAIN的权限,示例是cluster.local。同时kubernetes插件还需要响应在PTR请求,REVERSE_CIDRS应该用Kubernetes服务的CIDR填充。或者可以指定为in-addr.arpa ip6.arpa,插件会考虑所有的Ipv4和Ipv6的反向DNS请求。示例中,pods insecure表示支持pod的域名查询,但是不关心pod此时是否还存在。
hosts 插件的配置在kubernetes 的下方,CoreDNS在/etc/add-hosts/customer-hosts中先查找是否有对应的域名配置,没有的话,通过fallthrough 插件,将满足cluster.local in-addr.arpa ip6.arpa规则的查询传递给下一个插件,即kubernetes 插件。
prometheus :9153这一行,顾名思义,就是启动prometheus 插件,并指定9153端口供prometheus 采集监控指标。
cache 30这一行启动了缓存插件,挺设置超时时间为30秒,其实kubernetes插件本身已经在内存中保存并索引了所有的Kubernetes信息,所以这一行是没必要的。
reload表示CoreDNS可以重新加载它的corefile,无需重新启动进程。
loadbalance会打乱A记录或者AAAA记录,当使用headless服务时,客户端将以不同的顺序接收查询结果,这样客户端只取第一个IP,就可以达到负载均衡的效果。
03
CoreDNS代码解析
CoreDNS的代码,相对来说有点不好理解,因为它是基于Caddy的框架来实现的,而CoreDNS本身,可以理解为实现了很多不同的插件,当然其中最重要的插件就是kubernetes插件。这些插件,实现了Caddy规定的接口,由此便嵌入到了Caddy的框架中,插件的整个生命周期归Caddy框架统一管理。接口很简单,如下所示:
代码语言:javascript复制 Handler interface {
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
Name() string
}
其中Name()方法很简单,只需要返回插件的名字即可。而ServeDNS方法,则是重中之重,它是DNS域名查询的底层实现。
在CoreDNS,kubernetes插件作为最重要,也是最重量级的插件,它还需要实现另一个接口:
代码语言:javascript复制type ServiceBackend interface {
Services(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error)
Reverse(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error)
Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error)
Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error)
IsNameError(err error) bool
Serial(state request.Request) uint32
MinTTL(state request.Request) uint32
}
看名字ServiceBackend便可得知,它实现了CoreDNS内部的后端dns查询服务。复杂插件(例如kubernetes插件)调用ServeDNS时,由于内部查询逻辑比较复杂,它还会基于CoreDNS本身提供的一套查询工具方法,使用ServiceBackend接口来实现dns查询。当然了,除了kubernetes插件,其他复杂插件诸如Etcd插件,也使用了这一套查询逻辑,这里就不多讲了。
接下来分析CoreDNS的启动流程代码:
既然CoreDNS是在Caddy框架上实现的,每个插件是怎么注册到Caddy当中的呢?对于CoreDNS的每个插件,都有一个setup.go文件,该文件有一个init()方法,方法中实现了插件本身在Caddy中的注册:
代码语言:javascript复制 caddy.RegisterPlugin(name, caddy.Plugin{
ServerType: "dns",
Action: action,
})
在启动文件coredns.go中,通过import github.com/coredns/coredns/core/plugin的方式,隐式执行了所有的插件的init()函数,即完成了插件本身的注册。
CoreDNS的启动,是coremain.Run()函数。函数主要干了两件事:1. 加载corefile,2.将corefile为参数,调用caddy.Start方法,由此,整个启动流程交由Caddy框架处理。接下俩介绍Start方法的具体流程:
1.初始化instance,这个数据结构为同serverType公用,其中包含了两个重要的组成,分别为context Context和servers []ServerListener,此时context和servers还未初始化。
2.接下来调用startWithListenerFds方法,该方法中重要的逻辑在ValidateAndExecuteDirectives方法是实现。
3.在ValidateAndExecuteDirectives中,首先调用loadServerBlocks,完成对了corefile的解析,解析完成的结果是[]caddyfile.ServerBlock。接下来执行InspectServerBlocks方法,该方法是CoreDNS自身实现的,基于[]caddyfile.ServerBlock对corefile进行了分析,并填充dnsContext,并填充其中的configs []*Config字段。其中,每个Config对应一个Server,可以理解为是Server的配置。执行完loadServerBlocks,接下来执行executeDirectives方法,该方法的逻辑主要是执行各个插件的setup方法,完成各插件的准备逻辑。
4.ValidateAndExecuteDirectives后,执行MakeServers方法,该方法和loadServerBlocks方法同属于Context接口的两个方法,都是CoreDNS自身实现的代码。该方法的作用基于前边的解析,创建[]caddy.Server,将每个插件以上一章提到的Handler 接口方式,注入到caddy.Server中提供服务。
5.最后,调用startServers方法,启动[]caddy.Server对外提供服务,其中Serve方法,是CoreDNS本身实现的。
参考文献:
[1]https://zh.wikipedia.org/wiki/域名系统
[2]https://github.com/coredns/coredns
[3]https://www.kawabangga.com/wp-content/uploads/2022/07/main-run.png
[4]《Learning CoreDNS: Configuring DNS for Cloud Native Environments》