CoreDNS 原理浅析

2023-08-21 15:14:05 浏览数 (1)

在学习CoreDNS之前,我们先回顾一下DNS的相关知识。

域名系统(英语: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》

0 人点赞