k8s实践(11) –服务发现CoreDNS详解

参考:

一.Kubernetes DNS服务发展史

从Kubernetes 1.11开始,可使用CoreDNS作为Kubernetes的DNS插件进入GA状态,Kubernetes推荐使用CoreDNS作为集群内的DNS服务。 我们先看一下Kubernetes DNS服务的发展历程。

1.1 Kubernetes 1.3之前的版本 – skyDNS

Kubernetes 1.3之前的版本使用skyDNS作为DNS服务,这个有点久远了。Kubernetes的DNS服务由kube2sky、skyDNS、etcd组成。 kube2sky通过kube-apiserver监听集群中Service的变化,将生成的DNS记录信息更新到etcd中,而skyDNS将从etcd中获取数据对外提供DNS的查询服务。

1.2 Kubernetes 1.3版本开始 – kubeDNS

Kubernetes 1.3开始使用kubeDNS和dnsmasq替换了原来的kube2sky和skyDNS,不再使用etcd,而是将DNS记录直接存放在内存中,通过dnsmasq的缓存功能提高DNS的查询效率。下图是描述了Kubernetes使用kubeDNS实现服务发现的整体架构:

1.3 Kubernetes 1.11版本开始 – CoreDNS进入GA

从Kubernetes 1.11开始,可使用CoreDNS作为Kubernetes的DNS插件进入GA状态,Kubernetes推荐使用CoreDNS作为集群内的DNS服务。 CoreDNS从2017年初就成为了CNCF的的孵化项目,CoreDNS的特点就是十分灵活和可扩展的插件机制,

各种插件实现:

不同的功能,如重定向、定制DNS记录、记录日志等等。下图描述了CoreDNS的整体架构:

二、CoreDNS简介

Kubernetes包括用于服务发现的DNS服务器Kube-DNS。 该DNS服务器利用SkyDNS的库来为Kubernetes pod和服务提供DNS请求。SkyDNS2的作者,Miek Gieben,创建了一个新的DNS服务器,CoreDNS,它采用更模块化,可扩展的框架构建。 Infoblox已经与Miek合作,将此DNS服务器作为Kube-DNS的替代品。

CoreDNS利用作为Web服务器Caddy的一部分而开发的服务器框架。该框架具有非常灵活,可扩展的模型,用于通过各种中间件组件传递请求。这些中间件组件根据请求提供不同的操作,例如记录,重定向,修改或维护 。虽然它一开始作为Web服务器,但是Caddy并不是专门针对HTTP协议的,而是构建了一个基于CoreDNS的理想框架。

在这种灵活的模型中添加对Kubernetes的支持,相当于创建了一个Kubernetes中间件。该中间件使用Kubernetes API来满足针对特定Kubernetes pod或服务的DNS请求。而且由于Kube-DNS作为Kubernetes的另一项服务,kubelet和Kube-DNS之间没有紧密的绑定。您只需要将DNS服务的IP地址和域名传递给kubelet,而Kubernetes并不关心谁在实际处理该IP请求。

1、CoreDNS支持行为

1.0.0版本主要遵循Kube-DNS的当前行为。 CoreDNS的005及更高版本实现了完整的规范和更多功能。

  • A记录(正常的Service分配了一个名为my-svc.my-namespace.svc.cluster.local的DNS A记录。 这解决了服务的集群IP)
  • “headless”(没有集群IP)的Service也分配了一个名为my-svc.my-namespace.svc.cluster.local的DNS A记录。 与普通服务不同,这解决了Service选择了pods的一组IP。 客户预计将从这ip集合中消耗集合或使用标准循环选择。
  • 针对名为正常或无头服务的端口创建的SRV记录,对于每个命名的端口,SRV记录的格式为_my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local。对于常规服务,这将解析为端口号和CNAME:my-svc.my-namespace.svc.cluster.local;对于无头服务,这解决了多个答案,一个用于支持服务的每个pod,并包含端口号还有格式为auto-generated-name.my-svc.my-namespace.svc.cluster.local 的pod的CNAME 。SRV记录包含它们中的“svc”段,对于省略“svc”段的旧式CNAME不支持。
  • 作为Service一部分的endpoints的A记录(比如“pets”的记录)
  • pod的Spec中描述的A记录
  • 还有就是用来发现正在使用的DNS模式版本的TXT记录

所有群集中不需要pod A记录支持,默认情况下禁用。 此外,CoreDNS对此用例的支持超出了在Kube-DNS中找到的标准行为。

在Kube-DNS中,这些记录不反映集群的状态,例如,对w-x-y-z.namespace.pod.cluster.local的任何查询将返回带有w.x.y.z(ip)的A记录,即使该IP不属于指定的命名空间,甚至不属于集群地址空间。最初的想法是启用对* .namespace.pod.cluster.local这样的域使用通配符SSL证书。

CoreDNS集成了提供pod验证的选项,验证返回的IP地址w.x.y.z实际上是指定命名空间中的pod的IP。他防止在命名空间中欺骗DNS名称。 然而,它确实会大大增加CoreDNS实例的内存占用,因为现在它需要观察所有的pod,而不仅仅是服务端点。

2、架构

整个 CoreDNS 服务都建立在一个使用 Go 编写的 HTTP/2 Web 服务器 Caddy · GitHub 上,CoreDNS 整个项目可以作为一个 Caddy 的教科书用法。

CoreDNS 的大多数功能都是由插件来实现的,插件和服务本身都使用了 Caddy 提供的一些功能,所以项目本身也不是特别的复杂。

3、插件

作为基于 Caddy 的 Web 服务器,CoreDNS 实现了一个插件链的架构,将很多 DNS 相关的逻辑都抽象成了一层一层的插件,包括 Kubernetes 等功能,每一个插件都是一个遵循如下协议的结构体:

type (
    Plugin func(Handler) Handler

    Handler interface {
        ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
        Name() string
    }
)

所以只需要为插件实现 ServeDNS 以及  Name 这两个接口并且写一些用于配置的代码就可以将插件集成到 CoreDNS 中。

4、Corefile

另一个 CoreDNS 的特点就是它能够通过简单易懂的 DSL 定义 DNS 服务,在 Corefile 中就可以组合多个插件对外提供服务:

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 端口,请求这两个服务时会根据不同的域名选择不同区中的插件进行处理。

原理

CoreDNS 可以通过四种方式对外直接提供 DNS 服务,分别是 UDP、gRPC、HTTPS 和 TLS:

但是无论哪种类型的 DNS 服务,最终队会调用以下的 ServeDNS 方法,为服务的调用者提供 DNS 服务:

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:

example.org {
    file /usr/local/etc/coredns/example.org
    prometheus     # enable metrics
    errors         # show errors
    log            # enable query logs
}

那么在 CoreDNS 服务启动时,对于当前的 example.org 这个组,它会依次加载  filelogerrors 和  prometheus 几个插件,这里的顺序是由 zdirectives.go 文件定义的,启动的顺序是从下到上:

var Directives = []string{
  // ...
    "prometheus",
    "errors",
    "log",
  // ...
    "file",
  // ...
    "whoami",
    "on",
}

因为启动的时候会按照从下到上的顺序依次『包装』每一个插件,所以在真正调用时就是从上到下执行的,这就是因为 NewServer 方法中对插件进行了组合:

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 的函数:

type (
    Plugin func(Handler) Handler

    Handler interface {
        ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
        Name() string
    }
)

所以我们可以将它们叠成堆栈的方式对它们进行操作,这样在最后就会形成一个插件的调用链,在每个插件执行方法时都可以通过 NextOrFailure 函数调用下一个插件的  ServerDNS 方法:

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 的工作原理已经非常清楚了,接下来我们可以简单介绍几个插件的作用。

三、在kubernetes中部署coredns

1、下载coredns部署包并说明

https://github.com/coredns/deployment/tree/master/kubernetes

主要有几个文件:

deploy.sh是一个便捷的脚本,用于生成用于在当前运行标准kube-dns的集群上运行CoreDNS的清单。使用coredns.yaml.sed文件作为模板,它创建一个ConfigMap和一个CoreDNS  deployment,然后更新 Kube-DNS service selector以使用CoreDNS deployment。 通过重新使用现有服务,服务请求不会中断。

脚本不会删除kube-dns的deployment或replication controller – 您必须手动执行:

kubectl delete --namespace=kube-system deployment kube-dns

要使用它,只需将它们放在同一目录中,然后运行deploy.sh脚本 ,将其传递给您的服务CIDR(10.3.0.0/24) 。 这将生成具有必要Corefile的ConfigMap。 它还将查找现有的kube-dns服务的集群IP。 

[root@k8s-master conf.d]# etcdctl ls /k8s/network/subnets  /k8s/network/subnets/10.0.24.0-24  /k8s/network/subnets/10.0.86.0-24  /k8s/network/subnets/10.0.35.0-24

(注意:以上原始脚本只适用于当前kubernetes集群含有kube-dns的情况,如果没有需要修改下脚本

#!/bin/bash

# Deploys CoreDNS to a cluster currently running Kube-DNS.

SERVICE_CIDR=$1
CLUSTER_DOMAIN=${2:-cluster.local}
YAML_TEMPLATE=${3:-`pwd`/coredns.yaml.sed}
YAML=${4:-`pwd`/coredns.yaml}

if [[ -z $SERVICE_CIDR ]]; then
echo "Usage: $0 SERVICE-CIDR [ CLUSTER-DOMAIN ] [ YAML-TEMPLATE ] [ YAML ]"
exit 1
fi

#CLUSTER_DNS_IP=$(kubectl get service --namespace kube-system kube-dns -o jsonpath="{.spec.clusterIP}")
CLUSTER_DNS_IP=
10.0.24.200

默认情况下CLUSTER_DNS_IP是自动获取kube-dns的集群ip的,但是由于没有部署kube-dns所以只能手动指定一个集群ip了。

执行: ./deploy.sh 10.0.0.0/24 cluster.local 

以上脚本执行后可以看到预览的效果。

仔细观察上面的Corefile部分,这是一个在端口53上运行CoreDNS并为Kubernetes提供cluster.local域的示例

.:53 {
        errors
        log stdout
        health
        kubernetes cluster.local 10.3.0.0/24
        proxy . /etc/resolv.conf
        cache 30
    }

1)errors官方没有明确解释,后面研究

2)log stdout:日志中间件配置为将日志写入STDOUT

3)health:健康检查,提供了指定端口(默认为8080)上的HTTP端点,如果实例是健康的,则返回“OK”。

4)cluster.local:CoreDNS为kubernetes提供的域,10.3.0.0/24这告诉Kubernetes中间件它负责为反向区域提供PTR请求0.0.3.10.in-addr.arpa ..换句话说,这是允许反向DNS解析服务(我们经常使用到得DNS服务器里面有两个区域,即“正向查找区域”和“反向查找区域”,正向查找区域就是我们通常所说的域名解析,反向查找区域即是这里所说的IP反向解析,它的作用就是通过查询IP地址的PTR记录来得到该IP地址指向的域名,当然,要成功得到域名就必需要有该IP地址的PTR记录。PTR记录是邮件交换记录的一种,邮件交换记录中有A记录和PTR记录,A记录解析名字到地址,而PTR记录解析地址到名字。地址是指一个客户端的IP地址,名字是指一个客户的完全合格域名。通过对PTR记录的查询,达到反查的目的。)

5)proxy:这可以配置多个upstream 域名服务器,也可以用于延迟查找 /etc/resolv.conf 中定义的域名服务器

6)cache:这允许缓存两个响应结果,一个是肯定结果(即,查询返回一个结果)和否定结果(查询返回“没有这样的域”),具有单独的高速缓存大小和TTLs。

2、现在安装coredns到kubernetes中

#    ./deploy.sh -r 10.0.0.0/16 -i 192.168.10.90  -d cluster.local -t coredns.yaml.sed -s  > coredns.yaml

#   .kubectl apply -f coredns.yaml

或者直接执行:

# ./deploy.sh -r 10.0.0.0/16 -i 192.168.10.90  -d cluster.local -t coredns.yaml.sed -s  | kubectl apply -f –

查看service

[root@k8s-master coredns]# kubectl get service -l k8s-app=kube-dns --namespace=kube-system  NAME       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE  kube-dns   ClusterIP   192.168.10.90           53/UDP,53/TCP,9153/TCP   15d

查看coredns的Pod,确认所有Pod都处于Running状态:

kubectl get pods -n kube-system -l k8s-app=kube-dns
查看日志:# kubectl logs -f coredns-6cc7bf59f4-4rfq8 --namespace=kube-system

3、修改cluster-dns

修改master节点和所有node节点–cluster-dns配置,修改内容如红色所注,与上面的Corefile中的值对应。

4、测试CoreDNS

现在我们来创建一个wepapp的pod和service,测试一下coredns是否起作用

[root@k8s-master conf.d]# kubectl get pods  -o wide

NAME           READY     STATUS    RESTARTS   AGE       IP          NODE

webapp-nrz4t   1/1       Running   0          1d        10.0.35.3   192.168.10.39

webapp-zp69q   1/1       Running   0          1d        10.0.24.4   192.168.10.50

[root@k8s-master ~]# kubectl get svc

NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE

kubernetes   ClusterIP   192.168.0.1              443/TCP    28d

webapp       ClusterIP   192.168.14.242           9081/TCP   17d

webapp2      ClusterIP   192.168.22.2             9082/TCP   17d

[root@k8s-master ~]# curl 192.168.22.2:9082

Hello world

1、检查集群的pod /etc/resolv.conf是否生效:

首先进入这个集群内的另一个pod

kubectl exec -it webapp-nrz4t /bin/sh 或者 docker exec -it 0d0874df9e15 /bin/sh

$ cat /etc/resolv.conf

sh-4.2#  cat /etc/resolv.conf

nameserver 192.168.10.90

search default.svc.cluster.local svc.cluster.local cluster.local

options ndots:5

2、curl测试服务:

curl webapp.default.svc.cluster.local:9081

3、在master的主机上修改 /etc/resolv.conf,增加一行:nameserver 192.168.10.90后执行

curl webapp.default.svc.cluster.local:9081

通过域名访问成功!

问题排查技巧

如果执行 nslookup 命令失败,检查如下内容:

1、先检查本地 DNS 配置

查看配置文件 resolv.conf。

按照如下方法(注意搜索路径可能会因为云提供商不同而变化)验证搜索路径和 Name Server 的建立:

nameserver 192.168.10.90

search default.svc.cluster.local svc.cluster.local cluster.local

options ndots:5

2、快速诊断

出现类似如下指示的错误,说明 kube-dns 插件或相关 Service 存在问题:

检查service是否正常运行:

kubectl get svc –namespace=kube-system

或者检查是否 DNS Pod 正在运行

使用 kubectl get pods 命令验证 DNS Pod 正在运行:

kubectl get pods –namespace=kube-system -l k8s-app=kube-dns

应该能够看到类似如下信息:

如果看到没有 Pod 运行,或 Pod 失败/结束,DNS 插件不能默认部署到当前的环境,必须手动部署。

或者查看service的Endpoint是否正常:

kubectl get ep kube-dns –namespace=kube-system

如果没有看到 Endpoint,查看 调试 Service 文档 中的 Endpoint 段内容。

3、检查 DNS Pod 中的错误信息

使用 kubectl logs  命令查看 DNS 后台进程的日志:

kubectl logs coredns-6cc7bf59f4-vj7cc -n kube-system

看到错误信息:

eflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Get https://192.168.0.1:443/api/v1/namespace

2.168.0.1:443/api/v1/endpoints?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:58.986103       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Get https://192.168.0.1:443/api/v1/namespaces?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:58.989633       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Get https://192.168.0.1:443/api/v1/services?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:58.991655       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Get https://192.168.0.1:443/api/v1/endpoints?limit=500&resourceVersion=0: x509: certificate is valid for 192.168.10.50, 10.0.0.1, not 192.168.0.1
E0710 16:00:59.990732       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+inco

原因:192.168.0.1不在证书里面

解决:需要重新设置:apiserver证书

masterssl.cnf文件的示例如下:IP.3 = 192.168.0.1

[req]  req_extensions = v3_req  distinguished_name = req_distinguished_name  [req_distinguished_name]  [ v3_req ]  basicConstraints = CA:FALSE  keyUsage = nonRepudiation, digitalSignature, keyEncipherment  subjectAltName = @alt_names  [alt_names]  DNS.1 = kubernetes  DNS.2 = kubernetes.default  DNS.3 = kubernetes.default.svc  DNS.4 = kubernetes.default.svc.cluster.local  IP.1 = ${K8S_SERVICE_IP}  IP.2 = ${MASTER_IPV4}
IP.3 = 192.168.0.1

错误信息:

E0712 02:25:45.326123       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Unauthorized

E0712 02:25:45.327038       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Unauthorized

E0712 02:25:45.328029       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Unauthorized

E0712 02:25:46.327153       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Unauthorized

E0712 02:25:46.328210       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Unauthorized

E0712 02:25:46.329160       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Unauthorized

E0712 02:25:47.328243       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Service: Unauthorized

E0712 02:25:47.329143       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Endpoints: Unauthorized

E0712 02:25:47.330086       1 reflector.go:134] pkg/mod/k8s.io/client-go@v10.0.0+incompatible/tools/cache/reflector.go:95: Failed to list *v1.Namespace: Unauthorized

原因:service account 的token认知不通过,是因为重新生成证书后,需要删除旧的token。

解决:

kubectl get secret -n kube-system

NAME                  TYPE                                  DATA      AGE

coredns-token-cdn9x   kubernetes.io/service-account-token   3         3d

default-token-lht2v   kubernetes.io/service-account-token   3         3d

kubectl delete secret coredns-token-cdn9x  -n kube-system

secret “coredns-token-cdn9x” deleted