本篇文章主要围绕两种 DNS Server 的实现方式展开,会比较两种 Server 的优缺点。
在说两个 DNS 服务之前,我们先来了解一下在 k8s 中域名是如何被解析的。
在 k8s 中,一个 Pod 如果要访问相同同 Namespace 下的 Service(比如 user-svc),那么只需要 curl user-svc
。如果 Pod 和 Service 不在同一域名下,那么就需要在 Service Name 之后添加上 Service 所在的 Namespace(比如 beta),curl user-svc.beta
。那么 k8s 是如何知道这些域名是内部域名并为他们做解析的呢?
无论是在 宿主机 或者是在 k8s 集群中,DNS 解析会依赖 /etc/host.conf
、 /etc/hosts
和 /etc/resolv.conf
这个三个文件,这里主要会讲下 /etc/resolv.conf
的配置和作用。
resolv.conf 是 DNS 域名解析的配置文件。每行都会以一个关键字开头,然后跟配置参数。在集群中主要使用到的关键词有 3 个。
.
的数量少于 options.ndots
的值时,会依次匹配列表中的每个值那么我们进入一个 Pod 查看它的 resolv.conf
- nameserver 10.250.0.10
- search default.svc.cluster.local svc.cluster.local cluster.local
- options ndots:5
这里的 nameserver、search 和 options 都是可以通过 dnsConfig 字段进行配置的, 官方文档中已有详细的讲述
上述配置文件 resolv.conf 是 Pod 在 dnsPolicy: ClusterFirst
的情况下,k8s 为其自动生成的,这里的 nameserver 所对应的地址正是 DNS Service 的 Cluster IP(该值在启动 kubelet 的时候,通过 clusterDNS 指定)。所以,在该 Pod 内请求的所有的域名解析都需要经过 DNS Service 进行解析,不管是集群内部域名还是外部域名。
可以看到这里的 search 域默认包含了 namespace.svc.cluster.local、svc.cluster.local 和 cluster.local 三种。当我们在 Pod 中访问 a
Service时( curl a
),会选择nameserver 10.250.0.10 进行解析,然后依次带入 search 域进行 DNS 查找,直到找到为止。
- # curl a
- a.default.svc.cluster.local
显然因为 Pod 和 a
Service 在同一 Namespace 下,所以第一次 lookup 就能找到。
如果 Pod 要访问不同 Namespace(例如: beta
)下的 Service b
( curl b.beta
),会经过两次 DNS 查找,分别是
- # curl b.beta
- b.beta.default.svc.cluster.local # Not Found
- b.beta.svc.cluster.local # Found
正是因为 search 的顺序性,所以访问同一 Namespace 下的 Service, curl a
是要比 curl a.default
的效率更高的,因为后者多经过了一次 DNS 解析。
- # curl a
- a.default.svc.cluster.local # Found
- # curl a.default
- b.default.default.svc.cluster.local # Not Found
- b.default.svc.cluster.local # Found
这个答案,不能说肯定也不能说否定,看情况,可以说,大部分情况要走 search 域。
以 iftech.io
为例,通过抓包,查看在某一个 Pod 中访问外部域名时,DNS 的查找过程中都产生了什么样的数据包。首先先进入 DNS 容器的网络。
ps: 由于 DNS 容器往往不具备 bash,所以不能通过 docker exec 或者 kubectl exec 的方式进入容器抓包,需要采用其他方法
- docker inspect --format "{{.State.Pid}}" dns_container_id
- # 进入 container 的 network namespace
- nsenter -n -t pid
- # 对 53 端口进行抓包
- tcpdump -i eth0 -N udp dst port 53
在其他 Pod 中进行 http://iftech.io 域名查找
nslookup iftech.io dns_container_ip
指定 dns_container_ip,是为了避免有多个 DNS 容器的情况,DNS请求会分到各个容器。这样可以让 DNS 请求只发往这个地址,这样抓包的数据才会完整。
可以看到如下的结果:
- 15:33:20.744907 IP 10.55138 > worker-1.domain: 33420+ A? iftech.io.default.svc.cluster.local. (53)
- 15:33:20.746039 IP 10.55433 > worker-1.domain: 15376+ A? iftech.io.svc.cluster.local. (45)
- 15:33:20.746459 IP 10.39096 > worker-1.domain: 4260+ A? iftech.io.cluster.local. (41)
- 15:33:20.746780 IP 10.37841 > worker-1.domain: 25956+ A? iftech.io. (27)
可以看到在真正解析 http://iftech.io 之前,经历了 iftech.io.default.svc.cluster.local. -> iftech.io.svc.cluster.local. -> iftech.io.com.cluster.local. -> iftech.io.
这样也就意味着前 3 次 DNS 请求是浪费的,没有意义的。
在研究如何避免之前可以思考一下造成这种情况的原因。在 /etc/resolv.conf 文件中,我们可以看到 options
中有个配置项 ndots:5 。
ndots:5,表示:如果需要 lookup 的 Domain 中包含少于5个 .
,那么将会被当做非绝对域名,如果需要查询的 Domain 中包含大于或等于5个 .
,那么就会被当做绝对域名。如果是绝对域名则不会走 search 域,如果是非绝对域名,就会按照 search 域中进行逐一匹配查询,如果 search 走完了都没有找到,那么就会使用原域名进行查找。
综上可以找到两种优化的方法
.
如:iftech.io.
,这样就可以避免走 search 域进行匹配。dnsConfig
字段进行配置。这就允许你根据你自己的需求配置域名解析的规则。.
或以上时,就能被当做绝对域名直接进行域名解析。yaml apiVersion: v1 kind: Pod metadata: namespace: default name: dns-example spec: containers: - name: test image: nginx dnsConfig: options: - name: ndots value: 1
bash nslookup iftech.io 10.244.0.53 15:41:44.709664 IP 10.45636 > worker-1.domain: 29277+ A? iftech.io. (27) # 也只进行了一次 lookup
在k8s中,有4种DNS策略,分别是 ClusterFirstWithHostNet
、ClusterFirst
、Default
、和 None
,这些策略可以通过 dnsPolicy
这个字段来定义
如果在初始化 Pod、Deployment 或者 RC 等资源时没有定义,则会默认使用 ClusterFirst
策略
hostNetwork: true
)启动时,这个 POD 中的所有容器都会使用宿主机的 /etc/resolv.conf 配置进行 DNS 查询,但是如果在 Pod 中仍然还想继续使用 k8s 集群 的 DNS 服务时,就需要将 dnsPolicy 设置为 ClusterFirstWithHostNet。kubeDNS由3个部分组成。
client-go
中的 informer
机制监视 k8s 中的 Service
和 Endpoint
的变化,并将这些结构维护进内存来服务内部 DNS 解析请求。以下是结构图
在 kubedns 包含两个部分, kubedns 和 skydns。
其中 kubedns 是负责监听 k8s 集群中的 Service
和 Endpoint
的变化,并将这些变化通过 treecache
的数据结构缓存下来,作为 Backend
给 skydns 提供 Record。 而真正负责dns解析的其实是 skydns
(skydns 目前有两个版本 skydns1 和 skydns2,下面所说的是 skydns2,也是当前 kubedns 所使用的版本)。
我们可以先看下 treecache,以下是 treecache 的数据结构
- // /dns/pkg/dns/treecache/treecache.go#54
- type treeCache struct {
- ChildNodes map[string]*treeCache
- Entries map[string]interface{}
- }
treeCache 的结构类似于目录树。从根节点到叶子节点的每个路径与一个域名是相对应的,顺序是颠倒的。它的叶子节点只包含 Entries,非叶子节点只包含 ChildNodes。叶子节点中保存的就是 SkyDNS 定义的 msg.Service 结构,可以理解为 DNS 记录。
在 Records 接口方法实现中,只需根据域名查找到对应的叶子节点,并返回叶子节点中保存的所有msg.Service 数据。K8S 就是通过这样的一个数据结构来保存 DNS 记录的,并替换了 etcd( skydns2 默认使用 etcd 作为存储),来提供基于内存的高效存储。
我们可以直接阅读代码来了解 kubedns 的启动流程。
首先看它的结构体
- // dns/cmd/kube-dns/app/server.go#43
- type KubeDNSServer struct {
- // DNS domain name. = cluster.local.
- domain string
- healthzPort int
- // skydns启动的地址和端口
- dnsBindAddress string
- dnsPort int
- // 配置上游查询的地址,虽然 skydns 也支持上游域名解析,
- // 但是在 kubedns 一般情况下并不会由它来做,因为上游域名会被 dnsmasq 提前处理
- nameServers string
- kd *dns.KubeDNS
- }
接下来可以看到一个叫 NewKubeDNSServerDefault
的函数,它初始化了 KubeDNSServer。并执行 server.Run()
启动了服务。那么我们来看下 NewKubeDNSServerDefault
这个方法做了什么。
- // dns/cmd/kube-dns/app/server.go#53
- func NewKubeDNSServerDefault(config *options.KubeDNSConfig) *KubeDNSServer {
- // 初始化 kubeclient
- kubeClient, err := newKubeClient(config)
-
- // 同步配置文件,如果观察到配置信息改变,就会重启skydns
- var configSync dnsconfig.Sync
- switch {
- // 同时配置了 configMap 和 configDir 会报错
- case config.ConfigMap != "" && config.ConfigDir != "":
- glog.Fatal("Cannot use both ConfigMap and ConfigDir")
- case config.ConfigMap != "":
- configSync = dnsconfig.NewConfigMapSync(kubeClient, config.ConfigMapNs, config.ConfigMap)
- case config.ConfigDir != "":
- configSync = dnsconfig.NewFileSync(config.ConfigDir, config.ConfigPeriod)
- default:
- conf := dnsconfig.Config{Federations: config.Federations}
- if len(config.NameServers) > 0 {
- conf.UpstreamNameservers = strings.Split(config.NameServers, ",")
- }
- configSync = dnsconfig.NewNopSync(&conf)
- }
-
- return &KubeDNSServer{
- domain: config.ClusterDomain,
- healthzPort: config.HealthzPort,
- dnsBindAddress: config.DNSBindAddress,
- dnsPort: config.DNSPort,
- nameServers: config.NameServers,
- kd: dns.NewKubeDNS(kubeClient, config.ClusterDomain, config.InitialSyncTimeout, configSync),
- }
- }
可以看到这里 dnsconfig
会返回一个 configSync
的 interface 用来实时同步配置,也就是 kube-dns
这个 configmap,或者是本地的 dir(但一般来说这个 dir 也是由 configmap 挂载进去的)。在方法的最后 dns.NewKubeDNS
返回一个 KubeDNS 的结构体。那么我们看下这个函数初始化了哪些东西。
- // dns/pkg/dns/dns.go#124
- func NewKubeDNS(client clientset.Interface, clusterDomain string, timeout time.Duration, configSync config.Sync) *KubeDNS {
- kd := &KubeDNS{
- kubeClient: client,
- domain: clusterDomain,
- // 初始化目录树
- cache: treecache.NewTreeCache(),
- cacheLock: sync.RWMutex{},
- nodesStore: kcache.NewStore(kcache.MetaNamespaceKeyFunc),
- reverseRecordMap: make(map[string]*skymsg.Service),
- clusterIPServiceMap: make(map[string]*v1.Service),
- domainPath: util.ReverseArray(strings.Split(strings.TrimRight(clusterDomain, "."), ".")),
- initialSyncTimeout: timeout,
-
- configLock: sync.RWMutex{},
- configSync: configSync,
- }
-
- kd.setEndpointsStore()
- kd.setServicesStore()
-
- return kd
- }
可以看到kd.setEndpointsStore()
和 kd.setServicesStore()
这两个方法会在 informer
中注册 Service
和 Endpoint
的回调,用来观测这些资源的变动并作出相应的调整。
下面我们看下当集群中新增一个 Service,kubedns 会以怎样的方式处理。
- // dns/pkg/dns/dns.go#499
- func (kd *KubeDNS) newPortalService(service *v1.Service) {
- // 构建了一个空的叶子节点, recordLabel是clusterIP经过 FNV-1a hash运算后得到的32位数字
- // recordValue 的结构
- // &msg.Service{
- // Host: service.Spec.ClusterIP,
- // Port: 0,
- // Priority: defaultPriority,
- // Weight: defaultWeight,
- // Ttl: defaultTTL,
- // }
- subCache := treecache.NewTreeCache()
- recordValue, recordLabel := util.GetSkyMsg(service.Spec.ClusterIP, 0)
- subCache.SetEntry(recordLabel, recordValue, kd.fqdn(service, recordLabel))
-
- // 查看service的ports列表,将每个port信息转换成skydns.Service并加入上面构建的叶子节点
- for i := range service.Spec.Ports {
- port := &service.Spec.Ports[i]
- if port.Name != "" && port.Protocol != "" {
- srvValue := kd.generateSRVRecordValue(service, int(port.Port))
-
- l := []string{"_" + strings.ToLower(string(port.Protocol)), "_" + port.Name}
-
- subCache.SetEntry(recordLabel, srvValue, kd.fqdn(service, append(l, recordLabel)...), l...)
- }
- }
- subCachePath := append(kd.domainPath, serviceSubdomain, service.Namespace)
- host := getServiceFQDN(kd.domain, service)
- reverseRecord, _ := util.GetSkyMsg(host, 0)
-
- kd.cacheLock.Lock()
- defer kd.cacheLock.Unlock()
- // 将构建好的叶子节点加入treecache
- kd.cache.SetSubCache(service.Name, subCache, subCachePath...)
- kd.reverseRecordMap[service.Spec.ClusterIP] = reverseRecord
- kd.clusterIPServiceMap[service.Spec.ClusterIP] = service
- }
再看一下当 Endpoint 添加到集群时,kubedns 会如何处理
- // dns/pkg/dns/dns.go#460
- func (kd *KubeDNS) addDNSUsingEndpoints(e *v1.Endpoints) error {
- // 获取ep所属的svc
- svc, err := kd.getServiceFromEndpoints(e)
- if err != nil {
- return err
- }
- // 判断这个svc,如果这个svc不是 headless,就不会处理此次添加,因为 svc 有 clusterIP 的情况,在处理
- // svc 的增删改时已经都被处理了。所以当 ep 属于 headless svc 时,需要将这个 ep 加入到 cache
- if svc == nil || v1.IsServiceIPSet(svc) || svc.Spec.Type == v1.ServiceTypeExternalName {
- // No headless service found corresponding to endpoints object.
- return nil
- }
- return kd.generateRecordsForHeadlessService(e, svc)
- }
-
- // 把 endpoint 添加到它所属的 headless service 的缓存下
- func (kd *KubeDNS) generateRecordsForHeadlessService(e *v1.Endpoints, svc *v1.Service) error {
- subCache := treecache.NewTreeCache()
- generatedRecords := map[string]*skymsg.Service{}
- // 遍历这个 ep 下所有的 ip+port,并将它们添加到 treecache 中
- for idx := range e.Subsets {
- for subIdx := range e.Subsets[idx].Addresses {
- address := &e.Subsets[idx].Addresses[subIdx]
- endpointIP := address.IP
- recordValue, endpointName := util.GetSkyMsg(endpointIP, 0)
- if hostLabel, exists := getHostname(address); exists {
- endpointName = hostLabel
- }
- subCache.SetEntry(endpointName, recordValue, kd.fqdn(svc, endpointName))
- for portIdx := range e.Subsets[idx].Ports {
- endpointPort := &e.Subsets[idx].Ports[portIdx]
- if endpointPort.Name != "" && endpointPort.Protocol != "" {
- srvValue := kd.generateSRVRecordValue(svc, int(endpointPort.Port), endpointName)
- l := []string{"_" + strings.ToLower(string(endpointPort.Protocol)), "_" + endpointPort.Name}
- subCache.SetEntry(endpointName, srvValue, kd.fqdn(svc, append(l, endpointName)...), l...)
- }
- }
-
- // Generate PTR records only for Named Headless service.
- if _, has := getHostname(address); has {
- reverseRecord, _ := util.GetSkyMsg(kd.fqdn(svc, endpointName), 0)
- generatedRecords[endpointIP] = reverseRecord
- }
- }
- }
- subCachePath := append(kd.domainPath, serviceSubdomain, svc.Namespace)
- kd.cacheLock.Lock()
- defer kd.cacheLock.Unlock()
- for endpointIP, reverseRecord := range generatedRecords {
- kd.reverseRecordMap[endpointIP] = reverseRecord
- }
- kd.cache.SetSubCache(svc.Name, subCache, subCachePath...)
- return nil
- }
整体流程其实和 Service 差不多,只不过在添加 cache 之前会先去查找Endpoint所属的 Service,然后不同的是 Endpoint 的叶子节点中的host存储的是 EndpointIP,而 Service 的叶子节点的 host 中存储的是 fqdn。
最后再看一下 SkyDNS 的启动过程。
- // 启动skydns server
- func (d *KubeDNSServer) startSkyDNSServer() {
- skydnsConfig := &server.Config{
- Domain: d.domain,
- DnsAddr: fmt.Sprintf("%s:%d", d.dnsBindAddress, d.dnsPort),
- }
- if err := server.SetDefaults(skydnsConfig); err != nil {
- glog.Fatalf("Failed to set defaults for Skydns server: %s", err)
- }
- // 使用d.kd作为存储的后端,因为kubedns实现了skydns.Backend的接口
- // type Backend interface {
- // HasSynced() bool
- // Records(name string, exact bool) ([]msg.Service, error)
- // ReverseRecord(name string) (*msg.Service, error)
- // }
- s := server.New(d.kd, skydnsConfig)
- // ...
-
- d.kd.SkyDNSConfig = skydnsConfig
- go s.Run()
- }
Service
和Endpoint
并将它们转换为 skydns 能够理解的格式,以目录树的形式存在内存中。dnsmasq 也由两个部分组成
1.dnsmasq-nanny,容器里的1号进程,不负责处理 DNS LookUp 请求,只负责管理 dnsmasq。 2.dnsmasq,负责处理 DNS LookUp 请求,并缓存结果。
dnsmasq-nanny 负责监控 config 文件(/etc/k8s/dns/dnsmasq-nanny,也就是kube-dns-config这个 configmap 所挂载的位置)的变化(每 10s 查看一次),如果 config 变化了就会Kill掉 dnsmasq,并重新启动它。
- // dns/pkg/dnsmasq/nanny.go#198
- // RunNanny 启动 nanny 服务并处理配置变化
- func RunNanny(sync config.Sync, opts RunNannyOpts, kubednsServer string) {
- // ...
- configChan := sync.Periodic()
- for {
- select {
- // ...
- // 观察到config变化
- case currentConfig = <-configChan:
- if opts.RestartOnChange {
- // 直接杀掉dnsmasq进程
- nanny.Kill()
- nanny = &Nanny{Exec: opts.DnsmasqExec}
- // 重新加载配置
- nanny.Configure(opts.DnsmasqArgs, currentConfig, kubednsServer)
- // 重新启动dnsmasq进程
- nanny.Start()
- } else {
- glog.V(2).Infof("Not restarting dnsmasq (--restartDnsmasq=false)")
- }
- break
- }
- }
- }
让我们看下 sync.Periodic() 这个函数做了些什么
- // dns/pkg/dns/config/sync.go#81
- func (sync *kubeSync) Periodic() <-chan *Config {
- go func() {
- // Periodic函数中设置了一个Tick,每10s会去load一下configDir下
- // 所有的文件,并对每个文件进行sha256的摘要计算
- // 并将这个结果返回。
- resultChan := sync.syncSource.Periodic()
- for {
- syncResult := <-resultChan
- // processUpdate函数会比较新的文件的版本和旧的
- // 文件的版本,如果不一致会返回changed。
- // 值得注意的是有三个文件是需要单独处理的
- // federations
- // stubDomains
- // upstreamNameservers
- // 当这三个文件变化是会触发单独的函数(打印日志)
- config, changed, err := sync.processUpdate(syncResult, false)
- if err != nil {
- continue
- }
- if !changed {
- continue
- }
- sync.channel <- config
- }
- }()
- return sync.channel
- }
dnsmasq 中是如何加载配置的呢?
- // dns/pkg/dnsmasq/nanny.go#58
- // Configure the nanny. This must be called before Start().
- // 这个函数会配置 dnsmasq,Nanny 每次 Kill 掉 dnsmasq 后,调用 Start() 之前都会调用这个函数
- // 重新加载配置。
- func (n *Nanny) Configure(args []string, config *config.Config, kubednsServer string) {
- // ...
- for domain, serverList := range config.StubDomains {
- resolver := &net.Resolver{
- PreferGo: true,
- Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
- d := net.Dialer{}
- return d.DialContext(ctx, "udp", kubednsServer)
- },
- }
- // 因为 stubDomain 中可以是以 host:port 的形式存在,所以这里还要做一次 上游的 dns 解析
- for _, server := range serverList {
- if isIP := (net.ParseIP(server) != nil); !isIP {
- switch {
- // 如果 server 是以 cluster.local(不知道为什么这里是 hardCode 的)结尾的,就会发往 kubednsServer 进行 DNS 解析
- // 因为上面已经配置了 d.DialContext(ctx, "udp", kubednsServer)
- case strings.HasSuffix(server, "cluster.local"):
- // ...
- resolver.LookupIPAddr(context.Background(), server)
- default:
- // 如果没有以 cluster.local 结尾,就会走外部解析 DNS
- // ...
- net.LookupIP(server)
- }
- }
- }
- }
- // ...
- }
sidecar 启动后会在内部开启一个协程,并在循环中每默认 5s 向 kubedns 发送一次 dns 解析。并记录解析结果。
sidecar 提供了两个http的接口 /healthcheck/kubedns
和 /healthcheck/dnsmasq
给 k8s 用作 livenessProbe
的健康检查。每次请求,sidecar 会将上述记录的 DNS 解析结果返回。
CoreDNS 是一个高速并且十分灵活的DNS服务。CoreDNS 允许你通过编写插件的形式去自行处理DNS数据。
CoreDNS 使用Caddy作为底层的 Web Server,Caddy 是一个轻量、易用的Web Server,它支持 HTTP、HTTPS、HTTP/2、GRPC 等多种连接方式。所有 coreDNS 可以通过四种方式对外直接提供 DNS 服务,分别是 UDP、gRPC、HTTPS 和 TLS
CoreDNS 的大多数功能都是由插件来实现的,插件和服务本身都使用了 Caddy 提供的一些功能,所以项目本身也不是特别的复杂。
CoreDNS 定义了一套插件的接口,只要实现 Handler 接口就能将插件注册到插件链中。
- type (
- // 只需要为插件实现 ServeDNS 以及 Name 这两个接口并且写一些用于配置的代码就可以将插件集成到 CoreDNS 中
- Handler interface {
- ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
- Name() string
- }
- )
现在在 CoreDNS 中已经支持40种左右的插件。
该插件可以让 coreDNS 读取到k8s集群内的 endpoint 以及 service 等信息,从而替代 kubeDNS 作为 k8s 集群内的 DNS 解析服务。不仅如此,该插件还支持多种配置如:
- kubernetes [ZONES...] {
- ; 使用该配置可以连接到远程的k8s集群
- kubeconfig KUBECONFIG CONTEXT
- endpoint URL
- tls CERT KEY CACERT
- ; 可以设置需要暴露Service的namespace列表
- namespaces NAMESPACE...
- ; 可以暴露带有特定label的namespace
- labels EXPRESSION
- ; 是否可以解析10-0-0-1.ns.pod.cluster.local这种domain(为了兼容kube-dns)
- pods POD-MODE
- endpoint_pod_names
- ttl TTL
- noendpoints
- transfer to ADDRESS...
- fallthrough [ZONES...]
- ignore empty_service
- }
提供上游解析功能
- forward FROM TO... {
- except IGNORED_NAMES...
- ; 强制使用tcp进行域名解析
- force_tcp
- ; 当请求是tcp时,先尝试一次udp解析,失败了再使用tcp
- prefer_udp
- expire DURATION
- ; upstream的healthcheck失败的最多次数,默认2,超过的话upstream就会被下掉
- max_fails INTEGER
- tls CERT KEY CA
- tls_servername NAME
- ; 选择nameserver的策略,random、round_robin、sequential
- policy random|round_robin|sequential
- health_check DURATION
- }
更多的插件可以到 CoreDNS 的插件市场查看
CoreDNS 提供了一种简单易懂的 DSL 语言,它允许你通过 Corefile 来自定义 DNS 服务。
- 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 会开启两个端口 5300 和 53 ,提供 DNS 解析服务。对于 http://coredns.io 相关的域名会通过 5300 端口进行解析,其他域名会被解析到 53 端口,不同的域名会应用不同的插件。
在 CoreDNS 中 Plugin
其实就是一个出入参数都是 Handler
的函数
- // 所谓的插件链其实是一个Middle layer,通过传递链中的下一个Handler,将一个Handler链接到下一个Handler。
- type Plugin func(Handler) Handler
同时 CoreDNS 提供了 NextOrFailure
方法,供每个插件在执行完自身的逻辑之后执行下一个插件
- // NextOrFailure calls next.ServeDNS when next is not nil, otherwise it will return, a ServerFailure and a nil error.
- func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // nolint: golint
- if next != nil {
- // tracing
- // ...
- return next.ServeDNS(ctx, w, r)
- }
-
- return dns.RcodeServerFailure, Error(name, errors.New("no next plugin found"))
- }
如果 next 为 nil,说明插件链已经调用结束,直接返回 no next plugin found
的 error 即可。
每个 Plugin
也可以调用 (dns.ResponseWriter).WriteMsg(*dns.Msg)
方法来结束整个调用链。
CoreDNS 正是通过 kubernetes 插件实现了解析 k8s 集群内域名的功能。那么我们看下这个插件做了些什么事情。
- // coredns/plugin/kubernetes/setup.go#44
- func setup(c *caddy.Controller) error {
- // 检查了 corefile 中 kubernetes 配置的定义,并配置了一些缺省值
- k, err := kubernetesParse(c)
- if err != nil {
- return plugin.Error("kubernetes", err)
- }
- // 启动了对 pod, service, endpoint 三种资源增、删、改的 watch,并注册了一些回调
- // 注意:pod 是否启动 watch 是根据配置文件中 pod 的值来决定的,如果值不是 verified 就不会启动 pod 的 watch
- // 这里的 watch 方法观测到变化后,仅仅只改变 dns.modified 这个值,它会将该值设置为当前时间戳
- err = k.InitKubeCache()
- if err != nil {
- return plugin.Error("kubernetes", err)
- }
-
- // 将插件注册到 Caddy,让 Caddy 启动时能够同时启动该插件
- k.RegisterKubeCache(c)
-
- // 注册插件到调用链
- dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
- k.Next = next
- return k
- })
-
- return nil
- }
- // coredns/plugin/kubernetes/controller.go#408
- // 这三个方法就是 watch 资源时的回调
- func (dns *dnsControl) Add(obj interface{}) { dns.detectChanges(nil, obj) }
- func (dns *dnsControl) Delete(obj interface{}) { dns.detectChanges(obj, nil) }
- func (dns *dnsControl) Update(oldObj, newObj interface{}) { dns.detectChanges(oldObj, newObj) }
-
- // detectChanges detects changes in objects, and updates the modified timestamp
- func (dns *dnsControl) detectChanges(oldObj, newObj interface{}) {
- // 判断新老对象的版本
- if newObj != nil && oldObj != nil && (oldObj.(meta.Object).GetResourceVersion() == newObj.(meta.Object).GetResourceVersion()) {
- return
- }
- obj := newObj
- if obj == nil {
- obj = oldObj
- }
- switch ob := obj.(type) {
- case *object.Service:
- dns.updateModifed()
- case *object.Endpoints:
- if newObj == nil || oldObj == nil {
- dns.updateModifed()
- return
- }
- p := oldObj.(*object.Endpoints)
- // endpoint updates can come frequently, make sure it's a change we care about
- if endpointsEquivalent(p, ob) {
- return
- }
- dns.updateModifed()
- case *object.Pod:
- dns.updateModifed()
- default:
- log.Warningf("Updates for %T not supported.", ob)
- }
- }
-
- func (dns *dnsControl) Modified() int64 {
- unix := atomic.LoadInt64(&dns.modified)
- return unix
- }
-
- // updateModified set dns.modified to the current time.
- func (dns *dnsControl) updateModifed() {
- unix := time.Now().Unix()
- atomic.StoreInt64(&dns.modified, unix)
- }
上面展示的就是 kubernetes 这个 Plugin Watch 各个资源变化后的回调。可以看到它仅仅只改变 dns.modified 一个值,那么当 Service 发生变化后,kubernetes 插件如何感知,并将它们更新到内存呢。其实并没有或者说并不需要。。。因为这里使用了 client-go
中的 informer
机制,kubernetes 在解析 Service DNS 时会根据直接列出所有 Service(这里其实这么说并不准确,如果查找的是泛域名,那么才会列出所有 Service,如果是正常的 servicename.namespace,那么插件会使用 client-go
的 Indexer
机制,根据索引查找符合的 ServiceList),再进行匹配,直到找到匹配的 Service 再根据它的不同类型,决定返回结果。如果是 ClusterIP 类型,则返回 svc 的 ClusterIP,如果是 Headless 类型,则返回它所有的 Endpoint 的IP,如果是 ExternalName 类型,且 external_name 的值为 CNAME 类型,则返回 external_name 的值。整个操作仍然是在内存中进行的,效率并不会很低。
- // findServices returns the services matching r from the cache.
- func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg.Service, err error) {
- // 如果 namespace 为 * 或者 为 any,或者该 namespace 在配置文件中没有被 namespace: 这个配置项中配置
- // 则返回 NXDOMAIN
- if !wildcard(r.namespace) && !k.namespaceExposed(r.namespace) {
- return nil, errNoItems
- }
-
- // 如果 lookup 的 service 为空
- if r.service == "" {
- // 如果 namepace 存在 或者 namespace 是通配符就返回空的 Service 列表
- if k.namespaceExposed(r.namespace) || wildcard(r.namespace) {
- // NODATA
- return nil, nil
- }
- // 否则返回 NXDOMAIN
- return nil, errNoItems
- }
-
- err = errNoItems
- if wildcard(r.service) && !wildcard(r.namespace) {
- // If namespace exists, err should be nil, so that we return NODATA instead of NXDOMAIN
- if k.namespaceExposed(r.namespace) {
- err = nil
- }
- }
-
- var (
- endpointsListFunc func() []*object.Endpoints
- endpointsList []*object.Endpoints
- serviceList []*object.Service
- )
-
- if wildcard(r.service) || wildcard(r.namespace) {
- // 如果 service 或者 namespace 为 * 或者 any,列出当前所有的 Service
- serviceList = k.APIConn.ServiceList()
- endpointsListFunc = func() []*object.Endpoints { return k.APIConn.EndpointsList() }
- } else {
- // 根据 service.namespace 获取 index
- idx := object.ServiceKey(r.service, r.namespace)
- // 通过 client-go 的 indexer 返回 serviceList
- serviceList = k.APIConn.SvcIndex(idx)
- endpointsListFunc = func() []*object.Endpoints { return k.APIConn.EpIndex(idx) }
- }
-
- // 将 zone 转化为 etcd key 的格式
- // /c/local/cluster
- zonePath := msg.Path(zone, coredns)
- for _, svc := range serviceList {
- if !(match(r.namespace, svc.Namespace) && match(r.service, svc.Name)) {
- continue
- }
-
- // If request namespace is a wildcard, filter results against Corefile namespace list.
- // (Namespaces without a wildcard were filtered before the call to this function.)
- if wildcard(r.namespace) && !k.namespaceExposed(svc.Namespace) {
- continue
- }
-
- // 如果查找的 Service 没有 Endpoint,就返回 NXDOMAIN,除非这个 Service 是 Headless Service 或者 External name
- if k.opts.ignoreEmptyService && svc.ClusterIP != api.ClusterIPNone && svc.Type != api.ServiceTypeExternalName {
- // serve NXDOMAIN if no endpoint is able to answer
- podsCount := 0
- for _, ep := range endpointsListFunc() {
- for _, eps := range ep.Subsets {
- podsCount = podsCount + len(eps.Addresses)
- }
- }
-
- // No Endpoints
- if podsCount == 0 {
- continue
- }
- }
-
- // lookup 的 Service 是 headless Service 或者是使用 Endpoint lookup
- if svc.ClusterIP == api.ClusterIPNone || r.endpoint != "" {
- if endpointsList == nil {
- endpointsList = endpointsListFunc()
- }
- for _, ep := range endpointsList {
- if ep.Name != svc.Name || ep.Namespace != svc.Namespace {
- continue
- }
-
- for _, eps := range ep.Subsets {
- for _, addr := range eps.Addresses {
-
- // See comments in parse.go parseRequest about the endpoint handling.
- if r.endpoint != "" {
- if !match(r.endpoint, endpointHostname(addr, k.endpointNameMode)) {
- continue
- }
- }
-
- for _, p := range eps.Ports {
- if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) {
- continue
- }
- s := msg.Service{Host: addr.IP, Port: int(p.Port), TTL: k.ttl}
- s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name, endpointHostname(addr, k.endpointNameMode)}, "/")
-
- err = nil
- // 遍历 Endpoints 并将结果添加到返回列表
- services = append(services, s)
- }
- }
- }
- }
- continue
- }
-
- // External service
- // 如果 svc 是 External Service
- if svc.Type == api.ServiceTypeExternalName {
- s := msg.Service{Key: strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/"), Host: svc.ExternalName, TTL: k.ttl}
- // 只有当 External Name 是 CNAME 时,才会添加该 Service 到结果
- if t, _ := s.HostType(); t == dns.TypeCNAME {
- s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/")
- services = append(services, s)
-
- err = nil
- }
- continue
- }
-
- // ClusterIP service
- // 正常情况,返回的 msg.Service 的 Host 为 ClusterIP
- for _, p := range svc.Ports {
- if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) {
- continue
- }
-
- err = nil
-
- s := msg.Service{Host: svc.ClusterIP, Port: int(p.Port), TTL: k.ttl}
- s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/")
-
- services = append(services, s)
- }
- }
- return services, err
- }
在 CoreDNS 的官网中已有详细的性能测试报告,