容器编排Kubernetes之kube-dns源码解读

花了几天时间,研究了Kubernetes DNS插件的源代码,对其实现有了个简单的理解。这篇文章我简单梳理下代码流程。

注:阅读DNS源码前,可以阅读DNS原理入门增加对DNS的认识。

架构图

20170523213042

这是我简单画的架构图,希望能帮助大家理解。

代码结构

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() {
   config := options.NewKubeDNSConfig()
   config.AddFlags(pflag.CommandLine)

   flag.InitFlags()
   // Convinces goflags that we have called Parse() to avoid noisy logs.
   // OSS Issue: kubernetes/kubernetes#17162.
   goflag.CommandLine.Parse([]string{}) // 解析参数
   logs.InitLogs()
   defer logs.FlushLogs() // 初始化日志

   version.PrintAndExitIfRequested()

   glog.V(0).Infof("version: %+v", version.VERSION)

   // 实例化KubeDNSServer并运行
   server := app.NewKubeDNSServerDefault(config)
   server.Run()
}

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

type KubeDNSServer struct {
   // DNS domain name.
   domain         string
   healthzPort    int
   dnsBindAddress string
   dnsPort        int
   nameServers    string
   kd             *dns.KubeDNS
} // KubeDNSServer类里前几个变量都是main函数里传递过来的参数直接赋值,没啥可讲的。kd是pkg/dns包里的KubeDNS类的实例,我们后续再讲。
func NewKubeDNSServerDefault(config *options.KubeDNSConfig) *KubeDNSServer {} // 根据参数,填充KubeDNSServer对象
func newKubeClient(dnsConfig *options.KubeDNSConfig) (kubernetes.Interface, error) {} // 根据参数,实例化一个与apiserver通信的client,有两种模式的client可以使用。集群内client(通过serviceAccount认证)以及集群外client(当前Kubernetes集群配置的其他认证方式进行认证)
// main函数中的server.Run()调用的函数
func (server *KubeDNSServer) Run() {
   pflag.VisitAll(func(flag *pflag.Flag) {
      glog.V(0).Infof("FLAG: --%s=%q", flag.Name, flag.Value)
   })
   setupSignalHandlers() // 监听系统事件,主要作用是等待日志处理完成
   server.startSkyDNSServer() // 配置SkyDNS服务并启动服务,此处使用了SkyDNS的相关代码,这篇文章就不赘述了(我没看代码,囧)
   server.kd.Start() // 启动KubeDNS,后续再深入
   server.setupHandlers() // 添加两个http方法,/readiness用于健康检查,/cache返回当前dns中缓存的dns记录JSON

   glog.V(0).Infof("Status HTTP port %v", server.healthzPort)
   if server.nameServers != "" {
      glog.V(0).Infof("Upstream nameservers: %s", server.nameServers)
   }
   glog.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", server.healthzPort), nil)) // 监听http服务
}

app/options包中还有个options.go文件,这里主要包含app需要的配置信息,不再赘述。下面我们跳入pkg/dns包中探一探究竟,首先入场(也是唯一入场)的是pkg/dns/dns.go

type KubeDNS struct {
   // 与apiserver通信的client
   kubeClient clientset.Interface

   // 域名,默认为cluster.local.
   domain string
   // configMap的名称,默认为空,使用命令行参数
   configMap string

   // 存储集群中所有的endpoints
   endpointsStore kcache.Store
   // 存储集群中所有的services
   servicesStore kcache.Store
   // 存储集群中所有的nodes
   nodesStore kcache.Store

   // dns缓存
   cache treecache.TreeCache
   // PTR记录 ip --> skymsg.Service
   reverseRecordMap map[string]*skymsg.Service
   // 集群服务列表 ip --> v1.Service
   clusterIPServiceMap map[string]*v1.Service
   // 缓存锁,更新上述三者的数据时,需加锁
   cacheLock sync.RWMutex

   // 域名路径,是域名反向分割后的列表,比如默认的为[]string{"local", "cluster"}
   domainPath []string

   // endpointsController 管理endpoints的变更
   endpointsController *kcache.Controller
   // serviceController 管理services的变更
   serviceController *kcache.Controller

   // 配置对象
   config *config.Config
   // 配置更新锁
   configLock sync.RWMutex
   // 配置更新管理对象
   configSync config.Sync

   // 初始化同步endpoints和services的过期时间
   initialSyncTimeout time.Duration
}
func NewKubeDNS(client clientset.Interface, clusterDomain string, timeout time.Duration, configSync config.Sync) *KubeDNS {} // 根据参数配置KubeDNS对象,设置endpoint和service相关store和controller。
func (kd *KubeDNS) Start() {} // 启动KudeDNS,其实也就是启动在NewKubeDNS中设置的endpointsController和serviceController,并且监听配置文件的变化
func (kd *KubeDNS) GetCacheAsJSON() (string, error) {} // 获取dns缓存对象JSON,在cmd/app/server.go中被http方法/cache使用
func (kd *KubeDNS) setServicesStore() {} // 在NewKubeDNS中调用。监听service的变化,针对不同的操作(新增,删除,更新)执行不同的callback
func (kd *KubeDNS) setEndpointsStore() {} // 在NewKubeDNS中调用。监听endpoint的变化,针对不同的操作(新增,删除,更新)执行不同的callback
func (kd *KubeDNS) newService(obj interface{}) {} // 根据service的类型,生成不同类型的dns记录。简单的说,ExternalName类型的service是CNAME,只在dns缓存(KubeDNS.cache)中存储该记录;Headless(无ClusterIp)类型的service不在KubeDNS.clusterIPServiceMap中记录;而其他类型的service则在cache,reverseRecordMap,clusterIPServiceMap中一并存储
func (kd *KubeDNS) removeService(obj interface{}) {} // 删除service,也即删除在cache,reverseRecordMap,clusterIPServiceMap中的相关记录
func (kd *KubeDNS) updateService(oldObj, newObj interface{}) {} // 更新=删除+新增
func (kd *KubeDNS) handleEndpointAdd(obj interface{}) {} // 新增endpoints
func (kd *KubeDNS) handleEndpointUpdate(oldObj, newObj interface{}) {} // 更新endpoints,删除新endpoints子网里跟老endpoints子网里一样的PTR记录(KubeDNS.reverseRecordMap),即删除相同ip的endpoint,然后调用handleEndpointAdd新增endpoints
func (kd *KubeDNS) handleEndpointDelete(obj interface{}) {} // 删除相关的PTR记录(KubeDNS.reverseRecordMap)即可
func (kd *KubeDNS) addDNSUsingEndpoints(e *v1.Endpoints) error {} // 新增endpoints,如果endpoints对应的service是Headless service,则生成相关记录。如果不是,什么也不做。
func (kd *KubeDNS) getServiceFromEndpoints(e *v1.Endpoints) (*v1.Service, error) {} // 根据endpoints返回service
func (kd *KubeDNS) fqdn(service *v1.Service, subpaths ...string) string {} // 生成一个完整网域名称(Fully qualified domain name)
func (kd *KubeDNS) newPortalService(service *v1.Service) {} // 生成portalService,我的理解是一般类型的service,同时在cache,reverseRecordMap,clusterIPServiceMap中存储该service的记录
func (kd *KubeDNS) generateRecordsForHeadlessService(e *v1.Endpoints, svc *v1.Service) error {} // 生成headlessService,同时在cache,reverseRecordMap中存储该service的记录
func (kd *KubeDNS) newExternalNameService(service *v1.Service) {} // 生成ExternalNameService,只在cache中记录该条信息
func (kd *KubeDNS) Records(name string, exact bool) (retval []skymsg.Service, err error) {} // 查询DNS记录,参数中的exact标识是否精确匹配。其中federation相关的东西我还不是太明白
func (kd *KubeDNS) ReverseRecord(name string) (*skymsg.Service, error) {} // 查询PTR记录

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.
func RunNanny(sync config.Sync, opts RunNannyOpts) {
   defer glog.Flush()

   currentConfig, err := sync.Once()
   if err != nil {
      glog.Errorf("Error getting initial config, using default: %v", err)
      currentConfig = config.NewDefaultConfig()
   } // 解析配置

   nanny := &Nanny{Exec: opts.DnsmasqExec}
   nanny.Configure(opts.DnsmasqArgs, currentConfig)
   if err := nanny.Start(); err != nil {
      glog.Fatalf("Could not start dnsmasq with initial configuration: %v", err)
   } // 启动dnsmasq应用程序

   configChan := sync.Periodic()

   for {
      select {
      case status := <-nanny.ExitChannel:
         glog.Flush()
         glog.Fatalf("dnsmasq exited: %v", status)
         break
      case currentConfig = <-configChan:
         if opts.RestartOnChange {
            glog.V(0).Infof("Restarting dnsmasq with new configuration")
            nanny.Kill()
            nanny = &Nanny{Exec: opts.DnsmasqExec}
            nanny.Configure(opts.DnsmasqArgs, currentConfig)
            nanny.Start()
         } else {
            glog.V(2).Infof("Not restarting dnsmasq (--restartDnsmasq=false)")
         }
         break
      }
   } // 持续监听退出状态和配置更新
}
 
func (n *Nanny) Kill() error {} // 杀掉运行中的dnsmasq进程
func (n *Nanny) Start() error {} // 启动dnsmasq进程,并将日志信息输出到glog
func (n *Nanny) Configure(args []string, config *config.Config) {} // 解析配置,必须在Start方法前调用

dnsmasq的另一部分的作用是从dnsmasq应用程序读取监控信息,包括缓存命中数量,缓存未命中数量,缓存删除数量,缓存插入数量,缓存大小等信息。

sidecar

sidecar组件的主要作用是提供kube-dns和dnsmasq的健康检查和dns的监控。我们略过cmd/sidecar/main.go,他的主要作用也无非是解析参数。我们重点关注pkg/sidecar/server.go

func (s *server) Run(options *Options) {
   s.options = options
   glog.Infof("Starting server (options %+v)", *s.options)

   // 之前说sidecar监控kube-dns和dnsmasq,其实是通过参数传递进来的,具体的参数解析可以参考cmd/sidecar/main.go
   for _, probeOption := range options.Probes {
      probe := &dnsProbe{DNSProbeOption: probeOption}
      s.probes = append(s.probes, probe)
      probe.Start(options) // 启动组件健康检查
   }

   s.runMetrics(options) // dns监控信息
}