ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes 组件配置标准化指南:从命令行 Flag 迁移到版本化 ComponentConfig

Kubernetes 组件配置标准化指南:从命令行 Flag 迁移到版本化 ComponentConfig Kubernetes 组件配置标准化指南从命令行 Flag 迁移到版本化 ComponentConfig【免费下载链接】communityKubernetes Community Documentation项目地址: https://gitcode.com/GitHub_Trending/com/community导读本文基于 Kubernetes 社区归档的 Versioned Component Configuration Files 提案由 WG Component Standard 前组织者 Michael Taufen 撰写系统讲解核心集群组件kubelet、kube-proxy、scheduler 等如何从命令行 flag 迁移到 Kubernetes 风格的版本化配置文件componentconfig / ComponentConfig。通过本文你将掌握组件配置 API 的理想形态--configpath、迁移到版本化配置的完整五步方法论、利用 API machinery 完成反序列化/默认值/转换/校验的原理以及 Cobra/pflag 标志集控制的完整实战代码。该工作组已于 2021 年 9 月因缺乏活动而解散本提案作为社区沉淀继续在 archive/wg-component-standard 中提供参考。背景为什么核心组件需要版本化配置文件很长一段时间里Kubernetes 每个核心组件kubelet、kube-proxy、scheduler 等的部署与运维都依赖大量命令行 flag。社区将用 Kubernetes 风格的版本化配置文件取代 flag这一努力称为component configurationcomponentconfig。到 Kubernetes v1.10 时kubelet 已坚定走上从 flag 向版本化配置文件迁移的道路它可以消费一个 beta 版本的配置文件大量 flag 被标记废弃并待移除kube-proxy 也接近拥有自己的 beta 版本配置文件。从仓库中的配套文档 component-config-conventions.md 可以看到flag 化配置存在六大痛点flag 是扁平命名空间无法组织归类--help随着旋钮增多而失去参考价值无法区分有用旋钮与冗余旋钮无法按实例差异化要为 O(n) 个 controller 的 informer 调整不同的 resync period就需要全局命名空间里的 O(n) 个不同 flag变更需重启进程修改命令行意味着二进制重启直接影响可用性不适合传递机密配置宿主 pid 命名空间中的非特权进程也能读到进程命令行flag 是未版本化的公共 API无法独立于二进制进行版本管理全局变量的所有缺点都适用于 flag。版本化配置文件要解决的问题提案文档把 flag 的问题归结为三点核心矛盾Flag 是公共 API但无法与二进制分离版本化。核心组件的二进制版本与 Kubernetes 发布版本耦合采用语义化版本除非 Kubernetes 升主版本否则不能随意升级二进制主版本——这意味着既不能随便修复既有 flag 的坏默认值也不能随便移除 flag。现实中社区只能靠 flag 弃用策略提前警告后技术上破坏二进制语义化版本来绕过。参数被增量式地逐条弃用而不是保证一个 API 版本生命周期内参数集的一致性这既让用户困惑也让 API 更不稳定。flag 与二进制版本升级强耦合配置无法脱离二进制单独部署于是又催生出为 systemd 等进程管理代理做参数化、写配置的额外工具链。开发者被迫在字符串里内嵌结构化数据并发明一次性解析器埋下大量 bug。核心目标componentconfig 的两个核心目标标准化所有核心集群组件的配置方式支持动态配置部署机制。Kubernetes 恰好已具备实现这一切的基建API machinery 的 group/version 机制提供版本化能力与核心 API 相同的保证提供稳定的配置面API 对象以 Go struct 定义无需解析字符串处理结构化配置ConfigMap volume source 等部署机制适合向生产环境推送新版本配置文件且改文件无需重启进程与 flag 不同。结论所有核心 Kubernetes 组件最终都应通过版本化配置文件消费其配置而非命令行 flag。tl;dr每个组件的理想命令行 API提案给出的理想命令行 API只有一行$ component --configpath组件命令行只暴露一个稳定 flag——--config它指向一个版本化格式的配置文件路径其余所有配置信息都通过该文件引用。这是 componentconfig 推荐的理想 API一个稳定的 flag其余全部是版本化配置。如果从零创建新组件起点和终点都应是这个 API。一般意义上每个核心集群组件都应维护一个独立的 Kubernetes API group命名为{component}.config.k8s.io内含各版本的配置对象集合核心是每个版本里的{Component}Configurationstruct该 struct 经 API machinery 序列化到磁盘即为配置文件格式确保{component}.config.k8s.io遵循标准的 API 弃用策略 与 API 变更策略仓库中可找到对应文档暴露名为--config的 flag接受一个包含序列化{Component}Configurationstruct 的文件路径用 Kubernetes API machinery 反序列化配置文件数据、应用默认值并转换为内部版本供运行时使用校验内部版本后再使用校验失败则拒绝以该配置运行确保第三方库不泄漏 flag。深入如何将组件从 flag 迁移到版本化配置文件夺回命令行 API 的控制权迁移的首要目标是减少 flag 使用首先就要遏制组件 flag API 的增长。flag 增长有两个来源直接扩展 flag API 的 PR以及新增/更新第三方库。PR 把关负责某组件 componentconfig 工作的负责人应介入所有新增 flag 的 PR 评审对新 flag 坚决说不因为它会增加迁移工作量若确有必要新增也要确保其将来能兼容迁移。第三方库全局 flag 问题flag和pflag库都提供全局 flag set大多数组件默认直接解析全局 flag set于是不断累积来自这些库的 flag。库不礼貌组件太轻信。每个组件应当构造自己隔离的 flag set从第三方库显式注册必要 flag 到这个 flag set只解析这个 flag set 中的 flag。kubelet 夺回 flag set 控制的示例见 Explicit kubelet flags及其后续 PR #58095。许多组件通过pflag.CommandLine.AddGoFlagSet(flag.CommandLine)把全局 flag set 无差别并入主 flag set多数组件还把 flag 解析、帮助文本生成等委托给 Cobra而 Cobra 在多种情况下会隐式加入flag.CommandLine妨碍对 flag API 的显式控制。下面用完整的可运行 Go 程序演示这一陷阱及解法。示例一构造隔离 flag set但让 Cobra 解析失败package main import ( fmt os github.com/spf13/cobra github.com/spf13/pflag ) const use testcmd var ( globalFlagTarget string localFlagTarget string ) func init() { pflag.StringVar(globalFlagTarget, global-flag, globalFlagTarget, globally-registered flag) } func NewLocalFlagSet() *pflag.FlagSet { fs : pflag.NewFlagSet(use, pflag.ContinueOnError) fs.StringVar(localFlagTarget, local-flag, localFlagTarget, locally-registered flag) return fs } func NewTestCmd() *cobra.Command { cmd : cobra.Command{ Use: use, Run: func(cmd *cobra.Command, args []string) { fmt.Printf(globalFlagTarget: %q\n, globalFlagTarget) fmt.Printf(localFlagTarget: %q\n, localFlagTarget) }, } cmd.Flags().AddFlagSet(NewLocalFlagSet()) return cmd } func main() { cmd : NewTestCmd() if err : cmd.Execute(); err ! nil { fmt.Fprintf(os.Stderr, error: %v\n, err) os.Exit(1) } }运行结果出乎意料——全局 flag 被 Cobra 解析了$ testcmd --global-flag hello globalFlagTarget: hello localFlagTarget: 示例二禁用 Cobra flag 解析自己解析部分成功可以通过设置DisableFlagParsing: true绕过 Cobra 的 flag 解析但代价是要自己解析 flag、自己实现--help短路package main import ( fmt os github.com/spf13/cobra github.com/spf13/pflag ) const use testcmd var ( globalFlagTarget string localFlagTarget string ) func init() { pflag.StringVar(globalFlagTarget, global-flag, globalFlagTarget, globally-registered flag) } func NewLocalFlagSet() *pflag.FlagSet { fs : pflag.NewFlagSet(use, pflag.ContinueOnError) fs.StringVar(localFlagTarget, local-flag, localFlagTarget, locally-registered flag) return fs } func NewTestCmd() *cobra.Command { localFlagSet : NewLocalFlagSet() cmd : cobra.Command{ Use: use, DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { // parse our local flag set if err : localFlagSet.Parse(args); err ! nil { cmd.Usage() fatal(err) } // --help help, err : localFlagSet.GetBool(help) if err ! nil { fatal(fmt.Errorf(help flag is non-bool, programmer error, please correct)) } if help { cmd.Help() return } // print the flag values fmt.Printf(globalFlagTarget: %q\n, globalFlagTarget) fmt.Printf(localFlagTarget: %q\n, localFlagTarget) }, } localFlagSet.BoolP(help, h, false, fmt.Sprintf(help for %s, cmd.Name())) // Cobra still needs to be aware of our flag set to generate usage and help text cmd.Flags().AddFlagSet(localFlagSet) return cmd } func fatal(err error) { fmt.Fprintf(os.Stderr, error: %v\n, err) os.Exit(1) } func main() { cmd : NewTestCmd() if err : cmd.Execute(); err ! nil { fatal(err) } }全局 flag 现在被拒绝但它仍出现在 usage 文本里$ testcmd --global-flag hello Usage: testcmd [flags] Flags: --global-flag string globally-registered flag -h, --help help for testcmd --local-flag string locally-registered flag error: unknown flag: --global-flag--help输出同样如此因为 Cobra 生成 usage/help 文本时也使用了全局 flag。示例三连 usage/help 也自己生成完全可控package main import ( fmt os github.com/spf13/cobra github.com/spf13/pflag ) const use testcmd var ( globalFlagTarget string localFlagTarget string ) func init() { pflag.StringVar(globalFlagTarget, global-flag, globalFlagTarget, globally-registered flag) } func NewLocalFlagSet() *pflag.FlagSet { fs : pflag.NewFlagSet(use, pflag.ContinueOnError) fs.StringVar(localFlagTarget, local-flag, localFlagTarget, locally-registered flag) return fs } func NewTestCmd() *cobra.Command { localFlagSet : NewLocalFlagSet() cmd : cobra.Command{ Use: use, DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { // parse our local flag set if err : localFlagSet.Parse(args); err ! nil { cmd.Usage() fatal(err) } // --help help, err : localFlagSet.GetBool(help) if err ! nil { fatal(fmt.Errorf(help flag is non-bool, programmer error, please correct)) } if help { cmd.Help() return } // print the flag values fmt.Printf(globalFlagTarget: %q\n, globalFlagTarget) fmt.Printf(localFlagTarget: %q\n, localFlagTarget) }, } localFlagSet.BoolP(help, h, false, fmt.Sprintf(help for %s, cmd.Name())) // ugly, but necessary, because Cobras default UsageFunc and HelpFunc pollute the flagset with global flags const usageFmt Usage:\n %s\n\nFlags:\n%s cmd.SetUsageFunc(func(cmd *cobra.Command) error { fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine(), localFlagSet.FlagUsagesWrapped(2)) return nil }) cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { fmt.Fprintf(cmd.OutOrStdout(), usageFmt, cmd.UseLine(), localFlagSet.FlagUsagesWrapped(2)) }) return cmd } func fatal(err error) { fmt.Fprintf(os.Stderr, error: %v\n, err) os.Exit(1) } func main() { cmd : NewTestCmd() if err : cmd.Execute(); err ! nil { fatal(err) } }现在输出终于符合预期$ testcmd --global-flag hello Usage: testcmd [flags] Flags: -h, --help help for testcmd --local-flag string locally-registered flag error: unknown flag: --global-flag $ testcmd --help Usage: testcmd [flags] Flags: -h, --help help for testcmd --local-flag string locally-registered flag提案也坦承这种逐段 DIY 覆盖并不优雅欢迎更优方案——并指出所有核心组件都要解决此问题因此一个集中化的 Cobra 工具库至少是很有价值的。使用 flags 结构体flags struct迁移从 flag 到版本化配置文件会更轻松前提是先把目标 flag 的值、注册和弃用集中到一处。如果用一个结构体定义包含组件全部 flag 目标值就可以专注于把字段从这个结构体搬进版本化配置 API。kubelet 使用名为KubeletFlags的结构体配以func (f *KubeletFlags) AddFlags(fs *pflag.FlagSet)处理 flag 注册与弃用。注意AddFlags不注册第三方库的全局 flag只关心KubeletFlags结构体中的 flag。提案给出三点关键建议默认值行为与 flag 注册分离kubelet 在构造新KubeletFlags时初始化 flag 默认值注册 flag 时复用这些值——既容易看出应用了哪些默认值便于迁移到版本化配置也防止AddFlags覆盖结构体中的值当你需要在注册前修改值时。集中化校验kubelet 提供ValidateKubeletFlags函数校验 flags 结构体。把校验集中在此处既便于迁移到版本化配置也能把配置错误提前到组件生命周期更早的阶段。把 flag 校验埋进业务逻辑是反模式应尽可能重构集中化kubelet 自身也落入了到处校验的陷阱未来需要重构。OS 专属 flag 处理KubeletFlags中有些 flag 只在特定操作系统注册如 Windows字段以 OS 名做前缀如Windows*注册由addOSFlags方法的 OS 专属实现完成借助 Go build tags 管理。kubelet flag 代码的总体结构如下cmd/kubelet/app/options/options.gotype KubeletFlags struct { KubeletConfigFile string … } func NewKubeletFlags() *KubeletFlags { return KubeletFlags{ // apply defaults here } } func (f *KubeletFlags) AddFlags(fs *pflag.FlagSet) { f.addOSFlags(fs) fs.StringVar(f.KubeletConfigFile, config, f.KubeletConfigFile, …) … } func ValidateKubeletFlags(f *KubeletFlags) error { // validate here, return error if validation fails, nil otherwise … }OS 专属实现cmd/kubelet/app/options/osflags_windows.go// build windows … func (f *KubeletFlags) addOSFlags(fs *pflag.FlagSet) { // add windows flags here }非 Windows 实现cmd/kubelet/app/options/osflags_other.go// build !windows … func (f *KubeletFlags) addOSFlags(fs *pflag.FlagSet) { // noop }创建组件的配置 API group组件应暴露版本化的 Kubernetes 风格配置 API。如 API 约定 所述Kubernetes API 对象由一个规范内部版本和多个外部版本组成。在某个发布版本中任意外部版本之间都可以转换先转到内部版本再从内部版本转到目标版本。所有版本共存于同一个 API group 中典型源码树是顶层目录实现内部版本包子目录对应各外部版本包另有若干工具文件以及文件树中省略的用于转换、深拷贝和默认值注册的生成文件。kubelet 的kubelet.config.k8s.ioAPI group 文件层次如下- pkg/kubelet/apis/kubeletconfig | - fuzzer // utility for fuzzing kubelet.config.k8s.io objects | | - fuzzer.go | | - scheme // utility for scheme and codecs (serializations and conversions) | | - scheme.go | | - scheme_test.go // round trip tests that use the fuzzer | | - v1beta1 // implementation of v1beta1 external type | | - defaults.go // v1beta1 defaults | | - doc.go // various build tags that trigger code generation | | - register.go // functions for registering API with a scheme | | - types.go // v1beta1 versions of kubelet.config.k8s.io objects | | - validation // utility for validating kubelet.config.k8s.io objects | | - validation.go | | - doc.go // various build tags that trigger code generation | - helpers.go // utility functions | - helpers_test.go // tests for utility functions | - register.go // | - types.go创建组件 API group 时应参考上游 Kubernetes 最新的示例并注意以下要点API group 命名为{component}.config.k8s.io{component}为组件名惯例上目录也命名为{component}config如kubeletconfig先只创建v1alpha1外部配置版本把字段从 flags 结构体迁移到该版本对象中在确信 v1alpha1 足够成熟前从文件加载配置都应视为 alpha 特性若配置包含相对文件路径加载时应相对于配置文件所在位置解析kubelet 在helpers.go中有工具函数KubeletConfigurationPathRefs枚举这些字段创建 API group或每次更新配置 struct后运行make clean_generated; make generated_files生成转换、深拷贝与默认值注册代码初期大多数 componentconfig API 只加载单个对象但该对象可以是子对象的组合kubelet 只加载KubeletConfiguration它是types.go中定义的子对象组合内部与外部版本中必须存在同构对象才能生成转换代码只需校验内部规范类型外部版本化配置文件必须先转换到内部表示再校验之后组件才能使用该配置不要将 nil 与空容器类型map/slice视为语义不同过去这给序列化器带来过问题实践中所有 map/slice 字段都应optional且omitempty参考 Optional vs. Required。一般配置版本应携带可用默认值因此很少存在必填配置字段暂时无需支持从 Proto 加载配置但也不应排除这种可能保持内部/外部类型、默认值器与 flag 注册之间的字段顺序一致便于阅读与维护让组件配置模块化、可组合不同组件共享同一功能的 struct类比 PodSpec 在众多核心资源中被共享。让 flag 可以解析进内部配置对象为维持命令行兼容性在字段进入配置 API 后的一段时间内仍必须能用 flag 解析它。当字段从 flags 结构体迁入配置结构体时应把对应 flag 注册改指向内部配置结构体并让所有 flag 注册彼此靠近。kubelet 在 options 包中提供了针对KubeletConfiguration值的额外工具一个类似NewKubeletFlags的构造函数返回默认KubeletConfiguration以及一个注册指向该配置的 flag 的函数cmd/kubelet/app/options/options.gofunc NewKubeletConfiguration() (*kubeletconfig.KubeletConfiguration, error) { scheme, _, err : kubeletscheme.NewSchemeAndCodecs() if err ! nil { return nil, err } versioned : v1beta1.KubeletConfiguration{} scheme.Default(versioned) config : kubeletconfig.KubeletConfiguration{} if err : scheme.Convert(versioned, config, nil); err ! nil { return nil, err } applyLegacyDefaults(config) return config, nil } func AddKubeletConfigFlags(fs *pflag.FlagSet, c *kubeletconfig.KubeletConfiguration) { // register flags here, in the same style as in KubeletFlags.AddFlags … }修复/改进 flag 与版本化配置之间的默认值注意上例中调用了applyLegacyDefaults。转向版本化配置文件后每个 API 版本可拥有自己的默认值集flag 隐式构成自己的版本因此从文件加载配置与从 flag 加载配置可以有不同的默认值。NewKubeletConfiguration专门构造被 flag 指向的配置对象所以它把值修改为 flag 关联的默认值。按版本分离默认值的能力让我们能在不破坏兼容性的前提下让v1beta1使用比 kubelet flag API 更好的默认值见 Secure Kubelets componentconfig defaults while maintaining CLI compatibility。如果你组件 flag API 中有想改的默认值这正是改它的机会。增量迁移 flag 到配置 API对多数 flag迁移只需五步从 flags 结构体剪切字段把字段粘贴进内部与版本化配置结构体确保版本化配置结构体上的字段 tag 正确json、omitempty 等移动默认值若旧默认值应保留移入版本化默认值器若版本化默认值应与旧值不同则新默认值加入版本化默认值器、旧默认值放进applyLegacyDefaults把 flag 注册移到内部配置结构体的AddFlags函数中。有些 flag 需要更多工作内嵌结构 flag如果 flag 以字符串格式内嵌结构如列表或 map应在配置 API 中用对应语言结构Go 的 slice 或 map表示方便编写 JSON/YAML 文件为向后兼容可写 shim 把 flag 解析进结构化字段。相关 PRMake feature gates loadable from a map[string]bool#53025、Lift embedded structure out of ManifestURLHeader field#54643、Lift embedded structure out of eviction-related KubeletConfiguration fields#54823、ColonSeparatedMultimapStringString: allow multiple Set invocations with default override#55254。alpha/实验特性若 flag 启用/配置的 alpha 或实验特性没有关联 feature gate即kube_features.go中列出的 gate迁移前必须添加 gate 或把特性升到 beta。API 变更策略 允许在 beta 或 GA 版本配置中加入 alpha 字段前提是该字段配置的行为受 feature gate 保护且默认关闭见 Adding Unstable Features to Stable Versions。注意若把 alpha 字段加入 beta/GA 版本配置该字段名就被永久占用若在字段脱离 alpha 前改名必须 tombstone 旧名且永不复用。非零默认值若字段需要非零默认值、但零值仍是合法选项则版本化配置结构体中该字段应使用指针类型让默认值器能区分未设置字段与显式配置为零值。对许多字段尤其路径直接用零值作默认值即可。尽量在内部类型上避免指针字段以减少 nil 检查让默认值器处理外部类型上的 nil可能需要添加转换函数以生成指针到非指针的转换。可空容器字段若字段是可空容器类型slice 或 map且需要非空默认值用户必须显式设置字段才能得到非默认值若此前空容器类型合法如用于禁用需要提供显式替代方式。因为某些序列化器如 proto不区分 nil 与空容器类型我们也不能区分。示例见 Add none option to EnforceNodeAllocatable。两类暂不迁移的 flag仅特定操作系统注册的 flag如 kubelet 的--windows-service尚未确定如何在版本化配置文件中表示暂缓迁移实例特定值的 flag如 kubelet 的--hostname-override无法在组件多实例间共享同一值若存在或怀疑存在多实例共享同一配置源的用例如通过 ConfigMap 下发配置暂缓迁移。重要提醒不要在版本化配置中使用非版本化类型坚持使用语言原语与来自版本化 API 的类型即便如此使用其他版本化 API 的类型也有风险——该 API 版本被弃用时你仍需响应。组件启动引导Component Bootstrap本节描述组件引导到确认自己持有有效内部版本配置的一般步骤从 Cobra 命令的Run方法开始到完全解析的内部配置对象校验结束。第一步初始 flag 解析。组件第一件事是把命令行解析进 flags 结构体实例与内部版本配置结构体实例解析 flag 前先应用默认值见夺回命令行 API 的控制权示例。若有 flags 结构体校验此处是执行它的合适位置。第二步加载配置文件、转换内部版本、解析相对路径。初始解析完成后检查用户是否提供了--config路径若提供加载该路径的文件若为相对路径--config应相对于 kubelet 工作目录解析并用 API machinery 的UniversalDecoder反序列化——它会执行默认值与内部类型转换pkg/kubelet/kubeletconfig/util/codec/codec.go// DecodeKubeletConfiguration decodes a serialized KubeletConfiguration to the internal type func DecodeKubeletConfiguration(kubeletCodecs *serializer.CodecFactory, data []byte) (*kubeletconfig.KubeletConfiguration, error) { // the UniversalDecoder runs defaulting and returns the internal type by default obj, gvk, err : kubeletCodecs.UniversalDecoder().Decode(data, nil, nil) if err ! nil { return nil, fmt.Errorf(failed to decode, error: %v, err) } internalKC, ok : obj.(*kubeletconfig.KubeletConfiguration) if !ok { return nil, fmt.Errorf(failed to cast object to KubeletConfiguration, unexpected type: %v, gvk) } return internalKC, nil }接下来配置中所有文件路径字段都应相对于配置文件位置解析。kubelet 的辅助函数返回给定配置的路径字段指针pkg/kubelet/apis/kubeletconfig/helpers.go// KubeletConfigurationPathRefs returns pointers to all of the KubeletConfiguration fields that contain filepaths. // You might use this, for example, to resolve all relative paths against some common root before // passing the configuration to the application. This method must be kept up to date as new fields are added. func KubeletConfigurationPathRefs(kc *KubeletConfiguration) []*string { paths : []*string{} paths append(paths, kc.StaticPodPath) paths append(paths, kc.Authentication.X509.ClientCAFile) paths append(paths, kc.TLSCertFile) paths append(paths, kc.TLSPrivateKeyFile) paths append(paths, kc.ResolverConfig) return paths }这些指针用于加载配置时解析相对路径pkg/kubelet/kubeletconfig/configfiles/configfiles.go// resolveRelativePaths makes relative paths absolute by resolving them against root func resolveRelativePaths(paths []*string, root string) { for _, path : range paths { // leave empty paths alone, no path is a valid input // do not attempt to resolve paths that are already absolute if len(*path) 0 !filepath.IsAbs(*path) { *path filepath.Join(root, *path) } } }第三步实施 flag 优先级。若你成功把所有命令行 flag 迁入版本化配置即没有 OS 专属、实例专属或 alpha-未-feature-gated 类别的 flag可能不需要此步。但只要还有遗留 flag就需要在不破坏向后兼容的前提下把它们增量迁入 API——因为字段移入配置结构体会隐式带来默认值只要命令行设置了该 flag就必须用 flag 值覆盖默认值否则升级到迁移了该 flag 的版本就可能破坏命令行 API。kubelet 的做法是构造一个能解析整个命令行、但只向配置结构体填充值的 flag set——所有非配置 flag 的注册指向 mock 值配置 flag 指向真实值。全局 flag 的 mock 值由下面的辅助函数生成它用无操作 Set 实现替代值cmd/kubelet/app/server.goNoOp由k8s.io/apiserver/pkg/util/flag/noop.go提供// newFakeFlagSet constructs a pflag.FlagSet with the same flags as fs, but where // all values have noop Set implementations func newFakeFlagSet(fs *pflag.FlagSet) *pflag.FlagSet { ret : pflag.NewFlagSet(, pflag.ExitOnError) ret.SetNormalizeFunc(fs.GetNormalizeFunc()) fs.VisitAll(func(f *pflag.Flag) { ret.VarP(flag.NoOp{}, f.Name, f.Shorthand, f.Usage) }) return ret }组件 flag 则通过指向一次性废弃 flags 结构体来 mock配置 flag 直接经AddKubeletConfigFlags注册。通常 flag 粒度就足够但某些 map 字段可能需要键值对粒度的优先级。FeatureGates就是 kubelet 选择分段优先级的例子命令行与配置文件中的键值对合并命令行优先。这是为在命令行设置了部分 feature gate 时仍能通过 alpha 的 Dynamic Kubelet Config 特性做功能灰度cmd/kubelet/app/server.go// Remember original feature gates, so we can merge with flag gates later original : kc.FeatureGates // re-parse flags if err : fs.Parse(args); err ! nil { return err } // Add back feature gates that were set in the original kc, but not in flags for k, v : range original { if _, ok : kc.FeatureGates[k]; !ok { kc.FeatureGates[k] v } }第四步校验配置。此时你应该有一份待校验的配置配置文件已加载若指定配置文件中的相对路径已相对文件位置解析注意flag 的相对路径隐式相对于 kubelet 工作目录——这正是 Go 文件系统工具的行为flag 优先级已实施含 feature gate 合并。下一步用 API group 自带的校验函数校验它。若校验逻辑引用 feature gates记得在校验前先从配置设置 feature gates可用FeatureGate.SetFromMap直接从配置对象字段设置 gate。 恭喜你的组件现在拥有版本化配置文件 API 了剩余工作与未解问题第三方库 flag 值的版本化配置文档未详述如何把第三方库如 glog的 flag 迁移进版本化配置 API。最佳实践当然是不暴露这些 flag但某些场景glog 即一例不可行。此时可以在配置 API 中为每个第三方值提供字段并手动把字段值通过注册了对应 flag 的 flag set 打通。小心若第三方库在更新中移除某个 flag其弃用策略可能与我们的不一致你仍有责任维护配置 API 中这些字段的行为。未解问题一按实例配置Per-instance configuration实例特定参数之所以保留为 flag是因为单个配置源可能需要在多个实例间共享。但不应把这些参数抛弃在命令行。最显而易见的方案是引入--instance-configflag它接受一个文件内含由组件配置 API group 定义格式的实例特定参数——这样实例特定参数也能纳入版本化配置 API。未解问题二OS 专属配置OS 专属参数保留为 flag是因为尚未讨论清楚如何在配置 API 中表达。与按实例配置类似也不应把它们抛弃在命令行。一个简单灵活的方案是给 OS 专属字段加 OS 名前缀这避免了为每个支持的操作系统维护顶层子结构或 OS 专属子结构嵌套在通用子结构里的复杂度这些字段应始终optional且omitempty以便在不需要的环境中省略若出现公共字段添加一个跨多操作系统工作的非前缀字段相对容易很可能需要确保默认值与校验只处理当前运行 OS 的专属字段若 API machinery 能标记字段在哪些 OS 上受支持那会非常理想。演进脉络与相关实践提案末尾记录了推动该方向的主要工作可作为追踪演进脉络的线索核心里程碑Graduate kubeletconfig API group to beta#53833Deprecate KubeletConfiguration flags#60148配置对象打磨移除 KubeletConfiguration 中已弃用与实验字段#53088相对路径本地配置文件#55648kubeletconfig round trip 测试#55961flag 优先级与兼容性Kubelet flags take precedence over config from files/ConfigMaps#56097flag precedence redo#56995PodCIDR flag 默认值修正#57621--init-config-dir替换为--config#57624feature gate 收敛把未受 feature gate 保护的 alpha KubeletConfiguration 字段与自注册字段移到 KubeletFlags#55562seccomp 为未 feature-gated 的 alpha 特性#55983KubeletConfigFile feature gate 分三步移除#58760/#6490/#58978默认值安全化Secure Kubelets componentconfig defaults while maintaining CLI compatibility#59666Ignore 0% and 100% eviction thresholds#59681。此外仓库中的 component-config-conventions.md 对配置 API 的长期形态给出了补充约定配置 API group 按component.config.k8s.io命名以区别于被服务的 API静态配置主要通过文件反序列化获取除 kubelet 外大多源自 ConfigMap API 并由 kubelet 管理配置应分组相关选项为独立对象与子对象例如把ipTablesSyncPeriod等扁平字段组织为ipTables.syncPeriod、ipTables.conntrack.hashSize的层级结构进程内表示应分离flags 结构体、可序列化配置结构体、运行时结构体三类flag → serializable config → runtime 逐级转换并通过新配置选项只通过组件配置 API 提供来激励运维者迁移。总结componentconfig 提案为 Kubernetes 核心组件确立了一个稳定的--configflag 版本化配置文件的理想配置形态并给出了一条可复制的迁移路径先夺回命令行 flag 集的控制权隔离 flag set、显式注册、自管 usage/help再用 flags 结构体集中默认值与校验创建{component}.config.k8s.ioAPI group让 flag 解析进内部配置对象按版本分离默认值最后增量迁移字段并实现文件加载 → 内部转换 → 相对路径解析 → flag 优先级 → 校验的引导流程。这套方法论不仅沉淀于本文档也直接反映在 kubelet、kube-proxy 等组件的真实演进历史中——如果你正在为自己的 Kubernetes 组件设计配置接口或想理解 kubelet--config配置文件背后的工程决策本文档与仓库中的 component-config-conventions.md、api-conventions.md、api_changes.md、feature-gates.md 是互为补充的完整参考资料。【免费下载链接】communityKubernetes Community Documentation项目地址: https://gitcode.com/GitHub_Trending/com/community创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表