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   <none>        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      <none>        443/TCP    28d
webapp       ClusterIP   192.168.14.242   <none>        9081/TCP   17d
webapp2      ClusterIP   192.168.22.2     <none>        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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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/[email protected]+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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章