容器編排Kubernetes之kube-dns源碼解讀

花了幾天時間,研究了Kubernetes DNS插件的源代碼,對其實現有了個簡單的理解。這篇文章我簡單梳理下代碼流程。

註:閱讀DNS源碼前,可以閱讀DNS原理入門增加對DNS的認識。

架構圖

這是我簡單畫的架構圖,希望能幫助大家理解。

代碼結構

k8s.io

| dns

| cmd // 三大組件的入口

| dnsmasq-nanny // DNS緩存

| kube-dns // dns主項目

| sidecar // 附加組件

| pkg 組件代碼庫,主要實現代碼在該目錄下

| dns // kube-dns代碼庫, 監聽service、pod等資源,動態更新DNS記錄

| dnsmasq // 內部封裝dnsmasq程序用於緩存,並可從dns伺服器獲取dns監控指標

| sidecar 用於監控和健康檢查

主要的代碼都集中在上述樹形結構中,下面依次講解。

kube-dns

kube-dns是提供DNS功能的組件,我們重點關注。

首先看main方法:

func main() {n config := options.NewKubeDNSConfig()n config.AddFlags(pflag.CommandLine)nn flag.InitFlags()n // Convinces goflags that we have called Parse() to avoid noisy logs.n // OSS Issue: kubernetes/kubernetes#17162.n goflag.CommandLine.Parse([]string{}) // 解析參數n logs.InitLogs()n defer logs.FlushLogs() // 初始化日誌nn version.PrintAndExitIfRequested()nn glog.V(0).Infof("version: %+v", version.VERSION)nn // 實例化KubeDNSServer並運行n server := app.NewKubeDNSServerDefault(config)n server.Run()n}n

下面我們分析app包里的server.go文件

type KubeDNSServer struct {n // DNS domain name.n domain stringn healthzPort intn dnsBindAddress stringn dnsPort intn nameServers stringn kd *dns.KubeDNSn} // KubeDNSServer類里前幾個變數都是main函數里傳遞過來的參數直接賦值,沒啥可講的。kd是pkg/dns包里的KubeDNS類的實例,我們後續再講。nfunc NewKubeDNSServerDefault(config *options.KubeDNSConfig) *KubeDNSServer {} // 根據參數,填充KubeDNSServer對象nfunc newKubeClient(dnsConfig *options.KubeDNSConfig) (kubernetes.Interface, error) {} // 根據參數,實例化一個與apiserver通信的client,有兩種模式的client可以使用。集群內client(通過serviceAccount認證)以及集群外client(當前Kubernetes集群配置的其他認證方式進行認證)n// main函數中的server.Run()調用的函數nfunc (server *KubeDNSServer) Run() {n pflag.VisitAll(func(flag *pflag.Flag) {n glog.V(0).Infof("FLAG: --%s=%q", flag.Name, flag.Value)n })n setupSignalHandlers() // 監聽系統事件,主要作用是等待日誌處理完成n server.startSkyDNSServer() // 配置SkyDNS服務並啟動服務,此處使用了SkyDNS的相關代碼,這篇文章就不贅述了(我沒看代碼,囧)n server.kd.Start() // 啟動KubeDNS,後續再深入n server.setupHandlers() // 添加兩個http方法,/readiness用於健康檢查,/cache返回當前dns中緩存的dns記錄JSONnn glog.V(0).Infof("Status HTTP port %v", server.healthzPort)n if server.nameServers != "" {n glog.V(0).Infof("Upstream nameservers: %s", server.nameServers)n }n glog.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", server.healthzPort), nil)) // 監聽http服務n}n

app/options包中還有個options.go文件,這裡主要包含app需要的配置信息,不再贅述。下面我們跳入pkg/dns包中探一探究竟,首先入場(也是唯一入場)的是pkg/dns/dns.go

type KubeDNS struct {n // 與apiserver通信的clientn kubeClient clientset.Interfacenn // 域名,默認為cluster.local.n domain stringn // configMap的名稱,默認為空,使用命令行參數n configMap stringnn // 存儲集群中所有的endpointsn endpointsStore kcache.Storen // 存儲集群中所有的servicesn servicesStore kcache.Storen // 存儲集群中所有的nodesn nodesStore kcache.Storenn // dns緩存n cache treecache.TreeCachen // PTR記錄 ip --> skymsg.Servicen reverseRecordMap map[string]*skymsg.Servicen // 集群服務列表 ip --> v1.Servicen clusterIPServiceMap map[string]*v1.Servicen // 緩存鎖,更新上述三者的數據時,需加鎖n cacheLock sync.RWMutexnn // 域名路徑,是域名反向分割後的列表,比如默認的為[]string{"local", "cluster"}n domainPath []stringnn // endpointsController 管理endpoints的變更n endpointsController *kcache.Controllern // serviceController 管理services的變更n serviceController *kcache.Controllernn // 配置對象n config *config.Confign // 配置更新鎖n configLock sync.RWMutexn // 配置更新管理對象n configSync config.Syncnn // 初始化同步endpoints和services的過期時間n initialSyncTimeout time.Durationn}nfunc NewKubeDNS(client clientset.Interface, clusterDomain string, timeout time.Duration, configSync config.Sync) *KubeDNS {} // 根據參數配置KubeDNS對象,設置endpoint和service相關store和controller。nfunc (kd *KubeDNS) Start() {} // 啟動KudeDNS,其實也就是啟動在NewKubeDNS中設置的endpointsController和serviceController,並且監聽配置文件的變化nfunc (kd *KubeDNS) GetCacheAsJSON() (string, error) {} // 獲取dns緩存對象JSON,在cmd/app/server.go中被http方法/cache使用nfunc (kd *KubeDNS) setServicesStore() {} // 在NewKubeDNS中調用。監聽service的變化,針對不同的操作(新增,刪除,更新)執行不同的callbacknfunc (kd *KubeDNS) setEndpointsStore() {} // 在NewKubeDNS中調用。監聽endpoint的變化,針對不同的操作(新增,刪除,更新)執行不同的callbacknfunc (kd *KubeDNS) newService(obj interface{}) {} // 根據service的類型,生成不同類型的dns記錄。簡單的說,ExternalName類型的service是CNAME,只在dns緩存(KubeDNS.cache)中存儲該記錄;Headless(無ClusterIp)類型的service不在KubeDNS.clusterIPServiceMap中記錄;而其他類型的service則在cache,reverseRecordMap,clusterIPServiceMap中一併存儲nfunc (kd *KubeDNS) removeService(obj interface{}) {} // 刪除service,也即刪除在cache,reverseRecordMap,clusterIPServiceMap中的相關記錄nfunc (kd *KubeDNS) updateService(oldObj, newObj interface{}) {} // 更新=刪除+新增nfunc (kd *KubeDNS) handleEndpointAdd(obj interface{}) {} // 新增endpointsnfunc (kd *KubeDNS) handleEndpointUpdate(oldObj, newObj interface{}) {} // 更新endpoints,刪除新endpoints子網裡跟老endpoints子網裡一樣的PTR記錄(KubeDNS.reverseRecordMap),即刪除相同ip的endpoint,然後調用handleEndpointAdd新增endpointsnfunc (kd *KubeDNS) handleEndpointDelete(obj interface{}) {} // 刪除相關的PTR記錄(KubeDNS.reverseRecordMap)即可nfunc (kd *KubeDNS) addDNSUsingEndpoints(e *v1.Endpoints) error {} // 新增endpoints,如果endpoints對應的service是Headless service,則生成相關記錄。如果不是,什麼也不做。nfunc (kd *KubeDNS) getServiceFromEndpoints(e *v1.Endpoints) (*v1.Service, error) {} // 根據endpoints返回servicenfunc (kd *KubeDNS) fqdn(service *v1.Service, subpaths ...string) string {} // 生成一個完整網域名稱(Fully qualified domain name)nfunc (kd *KubeDNS) newPortalService(service *v1.Service) {} // 生成portalService,我的理解是一般類型的service,同時在cache,reverseRecordMap,clusterIPServiceMap中存儲該service的記錄nfunc (kd *KubeDNS) generateRecordsForHeadlessService(e *v1.Endpoints, svc *v1.Service) error {} // 生成headlessService,同時在cache,reverseRecordMap中存儲該service的記錄nfunc (kd *KubeDNS) newExternalNameService(service *v1.Service) {} // 生成ExternalNameService,只在cache中記錄該條信息nfunc (kd *KubeDNS) Records(name string, exact bool) (retval []skymsg.Service, err error) {} // 查詢DNS記錄,參數中的exact標識是否精確匹配。其中federation相關的東西我還不是太明白nfunc (kd *KubeDNS) ReverseRecord(name string) (*skymsg.Service, error) {} // 查詢PTR記錄n

Records 和 ReverseRecord 兩個方法是實現了skydns的Backend介面,這樣的話,KubeDNS就可以作為skydns的後端存儲提供dns查詢服務了,二者怎麼關聯起來的呢?回看cmd/kube-dns/app/server.go文件里的startSkyDNSServer方法,你會找到匯合點的,試試看吧。

至此,我們對kubeDNS組件有了初步大概的認識。接下來,我們再接著看dnsmasq組件。

dnsmasq

cmd/dnsmasq-nanny沒有什麼可以講的,也就是解析命令行參數然後調用pkg/dnsmasq/nanny.go的RunNanny方法。或者可以這麼說,整個dnsmasq就沒什麼事,就是在其中內嵌了dnsmasql應用程序,通過代碼啟動並管理該程序的生命進程

// RunNanny runs the nanny and handles configuration updates.nfunc RunNanny(sync config.Sync, opts RunNannyOpts) {n defer glog.Flush()nn currentConfig, err := sync.Once()n if err != nil {n glog.Errorf("Error getting initial config, using default: %v", err)n currentConfig = config.NewDefaultConfig()n } // 解析配置nn nanny := &Nanny{Exec: opts.DnsmasqExec}n nanny.Configure(opts.DnsmasqArgs, currentConfig)n if err := nanny.Start(); err != nil {n glog.Fatalf("Could not start dnsmasq with initial configuration: %v", err)n } // 啟動dnsmasq應用程序nn configChan := sync.Periodic()nn for {n select {n case status := <-nanny.ExitChannel:n glog.Flush()n glog.Fatalf("dnsmasq exited: %v", status)n breakn case currentConfig = <-configChan:n if opts.RestartOnChange {n glog.V(0).Infof("Restarting dnsmasq with new configuration")n nanny.Kill()n nanny = &Nanny{Exec: opts.DnsmasqExec}n nanny.Configure(opts.DnsmasqArgs, currentConfig)n nanny.Start()n } else {n glog.V(2).Infof("Not restarting dnsmasq (--restartDnsmasq=false)")n }n breakn }n } // 持續監聽退出狀態和配置更新n}n nfunc (n *Nanny) Kill() error {} // 殺掉運行中的dnsmasq進程nfunc (n *Nanny) Start() error {} // 啟動dnsmasq進程,並將日誌信息輸出到glognfunc (n *Nanny) Configure(args []string, config *config.Config) {} // 解析配置,必須在Start方法前調用n

dnsmasq的另一部分的作用是從dnsmasq應用程序讀取監控信息,包括緩存命中數量,緩存未命中數量,緩存刪除數量,緩存插入數量,緩存大小等信息。

sidecar

sidecar組件的主要作用是提供kube-dns和dnsmasq的健康檢查和dns的監控。我們略過cmd/sidecar/main.go,他的主要作用也無非是解析參數。我們重點關注pkg/sidecar/server.go

func (s *server) Run(options *Options) {n s.options = optionsn glog.Infof("Starting server (options %+v)", *s.options)nn // 之前說sidecar監控kube-dns和dnsmasq,其實是通過參數傳遞進來的,具體的參數解析可以參考cmd/sidecar/main.gon for _, probeOption := range options.Probes {n probe := &dnsProbe{DNSProbeOption: probeOption}n s.probes = append(s.probes, probe)n probe.Start(options) // 啟動組件健康檢查n }nn s.runMetrics(options) // dns監控信息n}n

推薦閱讀:

互聯網雙出口是如何做到動態切換的(包括DNS)?
鳳凰視頻,包括鏘鏘三人行,笑逐顏開等節目不能看了,時間是2017年6月?
從Chrome源碼看DNS解析過程
Google是如何拿到8.8.8.8和8.8.4.4這兩個IP地址的?

TAG:容器云 | Kubernetes | DNS |