【k8s源码分析系列】kubelet源码解读(一)

[TOC]

1. 目录结构

以kubernetes 1.22版本代码为例,kubelet的核心源码在cmd/kubelet、pkg/kubelet下,其中cmd/kubelet为main函数入口,主要负责参数加载、运行框架构建等前期准备工作逻辑,pkg/kubelet为kubelet主要功能细节运行逻辑代码,如下图分别是代码目录结构:


cmd/kubelet代码目录.png

pkg/kubelet代码目录.png

2. 程序入口

kubelet的main函数文件路径:kubernetes/cmd/kubelet/kubelet.go,cmd下是各类组件(如:kubectl、kubelet、kube-apiserver等)的程序入口,都是通过cobra工具生成的脚手架,其中kubernetes/cmd/kubelet/下app目录则是通过cobra生成的,后续我们将简单聊一聊cobra工具


image.png

3. cobra工具

cobra是基于go的开源代码脚手架工具,网上资料很多,k8s的组件代码基本都是通过该工具生成,所以我们这里就拣一些基本使用流程说说,不深入细节了,主要从以下几个点来聊:

  • cobra使用
  • 生成的代码框架
  • 程序入口及调用逻辑
  • Run函数解析

3.1 cobra使用

cobra工具需要下载安装,安装后通过命令生成代码框架,通过cobra -h我们能看到,cobra有init和add两个子命令,init即从0生成自己的程序主代码结构,add则是添加程序子命令或分支:

$ cobra -h
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

Use "cobra [command] --help" for more information about a command.

3.2 生成的代码框架

通过init生成代码,以下为主要生成的代码架构

#通过init生成包名为myapp的go程序框架
$ cobra init --pkg-name=myapp
Your Cobra applicaton is ready at /root/go/src/cobratest
#查看其目录结构,主要包含main.go及次级目录cmd
$ ls /root/go/src/cobratest 
-rw-r--r--  1 root  root  11358 12 11 11:09 LICENSE
drwxr-x--x  3 root  root  96 12 11 11:09 cmd
-rw-r--r--  1 root  root  641 12 11 11:09 main.go
#cmd下生成了root.go文件
$ ls -l /root/go/src/cobratest/cmd/
-rw-r--r--  1 root  root  2818 12 11 11:09 root.go

通过add添加子程序,即会在cmd下生成play.go文件

$ cobra add play
play created at /root/go/src/cobratest

$ ls -l cmd/
-rw-r--r--  1 root  root  1588 12 11 11:14 play.go
-rw-r--r--  1 root  root  2818 12 11 11:09 root.go

3.3 程序入口及调用逻辑

首先,我们看看main.go

package main

import "myapp/cmd"

#生成了main函数,并调用了cmd.Execute(),即cmd目录下root.go里的Execute()
func main() {
  cmd.Execute()
}

接下来我们看看cmd/root.go,可以看到root.go就做了几件事:

  • 定义cobra.command结构体,关键是被注释掉的Run函数变量,该函数将承载整个程序的核心逻辑,否则该Run函数变量默认为空,该程序将只是cobra生成的架构,仅为空壳程序
  • 定义Execute()函数,作为程序点火按钮,通过cobra框架内部流程,最终触发我们我们定义Run函数
  • 定义init()和initConfig()用于做一些变量、配置的初始化
#root.go文件
package cmd

import (
  "fmt"
  "os"
  "github.com/spf13/cobra"

  homedir "github.com/mitchellh/go-homedir"
  "github.com/spf13/viper"

)

var cfgFile string

// rootCmd represents the base command when called without any subcommands
#构建cobra.command,Use即程序命令,Short、Long即程序功能说明,最后被注释的Run函数变量是核心关键,即程序主逻辑执行函数,需要我们自定义,否则为空
var rootCmd = &cobra.Command{
  Use:   "myapp",
  Short: "A brief description of your application",
  Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
  // Uncomment the following line if your bare application
  // has an action associated with it:
  #Run生成时,默认为空,可以把注释去掉,即为空函数,一般我们会把业务程序主逻辑包装进该函数来执行,该函数定义好后,将会被后续Execute()触发执行
  //    Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
#Execute()即整个程序启动的点火按钮,如果将该程序比作一台车,前面的流程是将车做好,把准备工作都做到位,此处为将车开动起来的启动按钮
func Execute() {
  #通过执行cobra.Command内置的方法Execute(),然后一步步引导到执行上面定义好的Run函数,如果Run为空,则退出,此处我们就不做深入分析,如果有兴趣深挖的同学,可以网上查资料自行学习
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

#初始化流程,主要为参数加载,参数来源一般命令行flag、配置文件、环境变量
func init() {
  cobra.OnInitialize(initConfig)

  // Here you will define your flags and configuration settings.
  // Cobra supports persistent flags, which, if defined here,
  // will be global for your application.

  rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.myapp.yaml)")


  // Cobra also supports local flags, which will only run
  // when this action is called directly.
  rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}


// initConfig reads in config file and ENV variables if set.
#通过配置文件、环境变量等方式解析、加载参数
func initConfig() {
  if cfgFile != "" {
    // Use config file from the flag.
    viper.SetConfigFile(cfgFile)
  } else {
    // Find home directory.
    home, err := homedir.Dir()
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }

    // Search config in home directory with name ".myapp" (without extension).
    viper.AddConfigPath(home)
    viper.SetConfigName(".myapp")
  }

  viper.AutomaticEnv() // read in environment variables that match

  // If a config file is found, read it in.
  if err := viper.ReadInConfig(); err == nil {
    fmt.Println("Using config file:", viper.ConfigFileUsed())
  }
}

下面我们再来看看play.go文件,play.go一般为旁路逻辑,即次级命令或分支程序,代码流程和上面root.go类似

#play.go文件
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

// playCmd represents the play command
#同样构建一个cobra.Command,同样生成Run函数
var playCmd = &cobra.Command{
    Use:   "play",
    Short: "A brief description of your command",
    Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
    #Run函数默认为空逻辑,仅有个打印
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("play called")
    },
}

#此处init()逻辑核心为rootCmd.AddCommand(playCmd),主要把当前命令,添加进rootCmd,即上面root.go定义的,成为其二次命令使用,后续我们将该代码编译运行后,即可直观看到其效果
func init() {
    rootCmd.AddCommand(playCmd)

    // Here you will define your flags and configuration settings.

    // Cobra supports Persistent Flags which will work for this command
    // and all subcommands, e.g.:
    // playCmd.PersistentFlags().String("foo", "", "A help for foo")

    // Cobra supports local flags which will only run when this command
    // is called directly, e.g.:
    // playCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

编译执行该程序,看其使用方法即效果

$ go build .
$ go build .

3.4 Run函数解析

我们先看看run函数如何启动的,然后通过kubelet的Run函数,剖析一下run函数的功能
首先,我们先看看Run函数定义

    #从注释看,该函数为实际工作的函数,绝大多数命令都只执行它
    // Run: Typically the actual work function. Most commands will only implement this.
    Run func(cmd *Command, args []string)

然后,进入Execute()里的rootCmd.Execute(),

#Execute()入口函数
func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

#进入rootCmd.Execute(),实际执行rootCmd.ExecuteC()
func (c *Command) Execute() error {
    _, err := c.ExecuteC()
    return err
}

#我们再进入ExecuteC(),此处主要对主命令做校验及解析,然后根据解析的子命令或参数,进入下一轮execute()
func (c *Command) ExecuteC() (cmd *Command, err error) {
    ... #此处缩减了部分代码

    var flags []string
    if c.TraverseChildren {
           # 如果有子命令,根据传入参数遍历到对应子命令,并解析出对应参数
        cmd, flags, err = c.Traverse(args)
    } else {
        cmd, flags, err = c.Find(args)
    }
    ... #此处缩减了部分代码

    #我们可以看到,还有一层execute函数,以小写开头的内部函数,根据上面遍历的子命令与flag进行执行
    err = cmd.execute(flags)
    if err != nil {
        // Always show help if requested, even if SilenceErrors is in
        // effect
        if err == flag.ErrHelp {
            cmd.HelpFunc()(cmd, args)
            return cmd, nil
        }

        // If root command has SilenceErrors flagged,
        // all subcommands should respect it
        if !cmd.SilenceErrors && !c.SilenceErrors {
            c.PrintErrln("Error:", err.Error())
        }

        // If root command has SilenceUsage flagged,
        // all subcommands should respect it
        if !cmd.SilenceUsage && !c.SilenceUsage {
            c.Println(cmd.UsageString())
        }
    }
    return cmd, err
}

我们再进入下一轮execute()

func (c *Command) execute(a []string) (err error) {
    ... #此处缩减了部分代码

    # 校验命令、参数是否可执行,若不可,则返回错误
    if !c.Runnable() {
        return flag.ErrHelp
    }
    # cobra.Command结构体围绕Run共定义了10类执行体,分别是PersistentPreRun、PersistentPreRunE、PreRun、PreRunE、Run、RunE、PostRun、PostRunE、PersistentPostRun、PersistentPostRunE,这类函数Run和RunE的差别为是否返回错误信息,另外,他们的执行先后顺序依次是PersistentPreRun() -> PreRun() -> Run() -> PostRun() -> PersistentPostRun(),此处不对每个函数做详解,若有兴趣,可自行查资料或看分析cobra源码
     # 执行前置任务
    c.preRun()

    argWoFlags := c.Flags().Args()
    if c.DisableFlagParsing {
        argWoFlags = a
    }

    if err := c.ValidateArgs(argWoFlags); err != nil {
        return err
    }

     # 执行前置任务
    for p := c; p != nil; p = p.Parent() {
        if p.PersistentPreRunE != nil {
            if err := p.PersistentPreRunE(c, argWoFlags); err != nil {
                return err
            }
            break
        } else if p.PersistentPreRun != nil {
            p.PersistentPreRun(c, argWoFlags)
            break
        }
    }
    if c.PreRunE != nil {
        if err := c.PreRunE(c, argWoFlags); err != nil {
            return err
        }
    } else if c.PreRun != nil {
        c.PreRun(c, argWoFlags)
    }

    if err := c.validateRequiredFlags(); err != nil {
        return err
    }

     # 此处为核心,判断用户使用的是RunE还是Run,两者功能类似RunE会返回err信息,此处即进入到用户定义的核心Run函数体中了
    if c.RunE != nil {
        if err := c.RunE(c, argWoFlags); err != nil {
            return err
        }
    } else {
        c.Run(c, argWoFlags)
    }

     # 执行垫后任务
    if c.PostRunE != nil {
        if err := c.PostRunE(c, argWoFlags); err != nil {
            return err
        }
    } else if c.PostRun != nil {
        c.PostRun(c, argWoFlags)
    }
    for p := c; p != nil; p = p.Parent() {
        if p.PersistentPostRunE != nil {
            if err := p.PersistentPostRunE(c, argWoFlags); err != nil {
                return err
            }
            break
        } else if p.PersistentPostRun != nil {
            p.PersistentPostRun(c, argWoFlags)
            break
        }
    }

    return nil
}

4. kubelet Run函数解析

kubelet Run函数定义的具体代码,如下:

        #Run函数主体
        Run: func(cmd *cobra.Command, args []string) {
            // initial flag parse, since we disable cobra's flag parsing
            #解析参数
            if err := cleanFlagSet.Parse(args); err != nil {
                cmd.Usage()
                klog.Fatal(err)
            }

            // check if there are non-flag arguments in the command line
            #检查是否有命令未传入参数,若是,则返回错误和使用指引
            cmds := cleanFlagSet.Args()
            if len(cmds) > 0 {
                cmd.Usage()
                klog.Fatalf("unknown command: %s", cmds[0])
            }

            // short-circuit on help
            #检查是否设置了帮助指引
            help, err := cleanFlagSet.GetBool("help")
            if err != nil {
                klog.Fatal(`"help" flag is non-bool, programmer error, please correct`)
            }
            if help {
                cmd.Help()
                return
            }

            // short-circuit on verflag
            #若是查看版本信息,则返回版本信息及退出
            verflag.PrintAndExitIfRequested()
            #若是查看参数使用,则输出所有参数说明
            cliflag.PrintFlags(cleanFlagSet)

            // set feature gates from initial flags-based config
            #通过flag参数列表设置启动的feature gates
            if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
                klog.Fatal(err)
            }

            // validate the initial KubeletFlags
            #校验kubelet flag相关参数传入是否合法,如果合法则初始化到对应变量
            if err := options.ValidateKubeletFlags(kubeletFlags); err != nil {
                klog.Fatal(err)
            }

            #检测ContainerRuntime 使用,如果设置成remote,则--pod-infra-container-image将被忽略
            if kubeletFlags.ContainerRuntime == "remote" && cleanFlagSet.Changed("pod-infra-container-image") {
                klog.Warning("Warning: For remote container runtime, --pod-infra-container-image is ignored in kubelet, which should be set in that remote runtime instead")
            }

            // load kubelet config file, if provided
            #检测并解析kubelet config文件
            if configFile := kubeletFlags.KubeletConfigFile; len(configFile) > 0 {
                kubeletConfig, err = loadConfigFile(configFile)
                if err != nil {
                    klog.Fatal(err)
                }
                // We must enforce flag precedence by re-parsing the command line into the new object.
                // This is necessary to preserve backwards-compatibility across binary upgrades.
                // See issue #56171 for more details.
                if err := kubeletConfigFlagPrecedence(kubeletConfig, args); err != nil {
                    klog.Fatal(err)
                }
                // update feature gates based on new config
                if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
                    klog.Fatal(err)
                }
            }

            // We always validate the local configuration (command line + config file).
            // This is the default "last-known-good" config for dynamic config, and must always remain valid.
            #校验kubelet config文件中的配置内容是否合法,遇错误则退出
            if err := kubeletconfigvalidation.ValidateKubeletConfiguration(kubeletConfig); err != nil {
                klog.Fatal(err)
            }

            #检测kubelet congfig中cgroup相关配置内容是否合法
            if (kubeletConfig.KubeletCgroups != "" && kubeletConfig.KubeReservedCgroup != "") && (0 != strings.Index(kubeletConfig.KubeletCgroups, kubeletConfig.KubeReservedCgroup)) {
                klog.Warning("unsupported configuration:KubeletCgroups is not within KubeReservedCgroup")
            }

            // use dynamic kubelet config, if enabled
            #检测是否启用了kubelet热加载配置模式,及动态kubelet config,如果启动了,则按动态kubelet config模式启动kubelet
            var kubeletConfigController *dynamickubeletconfig.Controller
            if dynamicConfigDir := kubeletFlags.DynamicConfigDir.Value(); len(dynamicConfigDir) > 0 {
                var dynamicKubeletConfig *kubeletconfiginternal.KubeletConfiguration
                #获取kubelet动态配置信息
                dynamicKubeletConfig, kubeletConfigController, err = BootstrapKubeletConfigController(dynamicConfigDir,
                    func(kc *kubeletconfiginternal.KubeletConfiguration) error {
                        // Here, we enforce flag precedence inside the controller, prior to the controller's validation sequence,
                        // so that we get a complete validation at the same point where we can decide to reject dynamic config.
                        // This fixes the flag-precedence component of issue #63305.
                        // See issue #56171 for general details on flag precedence.
                        return kubeletConfigFlagPrecedence(kc, args)
                    })
                #获取失败,报错退出
                if err != nil {
                    klog.Fatal(err)
                }
                // If we should just use our existing, local config, the controller will return a nil config
                #获取到动态配置,则加载进来,否则报错退出
                if dynamicKubeletConfig != nil {
                    kubeletConfig = dynamicKubeletConfig
                    // Note: flag precedence was already enforced in the controller, prior to validation,
                    // by our above transform function. Now we simply update feature gates from the new config.
                    if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
                        klog.Fatal(err)
                    }
                }
            }

            #通过kubelet flag和config构建kubelet server,用以支持对kubelet的请求
            // construct a KubeletServer from kubeletFlags and kubeletConfig
            kubeletServer := &options.KubeletServer{
                KubeletFlags:         *kubeletFlags,
                KubeletConfiguration: *kubeletConfig,
            }

            #通过kubelet server和DefaultFeatureGate,构建kubeletDeps,kubeletDeps主要集成了kubelet的运行依赖如:认证信息、第三方云厂商信息、事件记录、挂载、网络/卷插件、OOM管理等依赖组件调用链操作句柄
            // use kubeletServer to construct the default KubeletDeps
            kubeletDeps, err := UnsecuredDependencies(kubeletServer, utilfeature.DefaultFeatureGate)
            if err != nil {
                klog.Fatal(err)
            }

            #把kubelet config controller注入kubeletDeps
            // add the kubelet config controller to kubeletDeps
            kubeletDeps.KubeletConfigController = kubeletConfigController

            #设置信号上下文,将被kubelet和docker shim重复使用,做全局控制
            // set up signal context here in order to be reused by kubelet and docker shim
            ctx := genericapiserver.SetupSignalContext()

            #启动kubelet,把ctx、kubeletServer、kubeletDeps、utilfeature.DefaultFeatureGate这些准备好的参数传入,执行内层核心Run函数,此处暂不做深入剖析,后续我们将在kubelet源码解析第3章展开聊!
            // run the kubelet
            klog.V(5).Infof("KubeletConfiguration: %#v", kubeletServer.KubeletConfiguration)
            if err := Run(ctx, kubeletServer, kubeletDeps, utilfeature.DefaultFeatureGate); err != nil {
                klog.Fatal(err)
            }
        },

从上面流程,我们可以看到,kubelet的第一层Run函数,主要工作就是做些kubelet运行的前期准备,如:参数解析、校验、构建依赖、构建 server等,最后通过这些备好的参数运行kubelet,进入下一轮Run函数,然后真正开启kubelet的工作之旅

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容