You are on page 1of 468

目 录

致谢
Introduction
kubernetes
kube-apiserver 的设计与实现
kube-apiserver 中 apiserver service 的实现
node controller 源码分析
job controller 源码分析
garbage collector controller 源码分析
daemonset controller 源码分析
statefulset controller 源码分析
deployment controller 源码分析
replicaset controller 源码分析
kube-scheduler 源码分析
kube-scheduler predicates 与 priorities 调度算法源码分析
kube-scheduler 优先级与抢占机制源码分析
kubernetes service 原理解析
kube-proxy 源码分析
kube-proxy iptables 模式源码分析
kube-proxy ipvs 模式源码分析
kubelet 架构浅析
kubelet 启动流程分析
kubelet 创建 pod 的流程
kubelet 状态上报的方式
kubelet 中事件处理机制
kubelet statusManager 源码分析
kubernetes 中 Qos 的设计与实现
kubelet 中垃圾回收机制的设计与实现

本文档使用 书栈网 · BookStack.CN 构建 -2-


致谢

致谢

当前文档 《kubernetes 源码分析》 由 进击的皇虫 使用 书栈网(BookStack.CN) 进行构


建,生成于 2020-03-08。

书栈网仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工具。

文档内容由网友们编写和整理,书栈网难以确认文档内容知识点是否错漏。如果您在阅读文档获取
知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手,将知识准确、高效且有
效地传递给每一个人。

同时,如果您在日常工作、生活和学习中遇到有价值有营养的知识文档,欢迎分享到书栈网,为知
识的传承献上您的一份力量!

如果当前文档生成时间太久,请到书栈网获取最新的文档,以跟上知识更新换代的步伐。

内容来源:田飞雨 https://github.com/gosoon/source-code-reading-notes

文档地址:http://www.bookstack.cn/books/source-code-reading-notes

书栈官网:https://www.bookstack.cn

书栈开源:https://github.com/TruthHun

分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的
读者,因为我们都将成为知识的传承者。

本文档使用 书栈网 · BookStack.CN 构建 -3-


Introduction

1、关于本书
本书主要记录工作过程中阅读过的一些开源项目的源码,并加以自己的分析与注解,目前专注于 k8s
云原生实践,包括但不限于 docker、kubernetes、etcd、promethus、istio、knative、
serverless 等相关云原生项目。

2、内容更新
本书会不定期更新,目前专注于 k8s 源码的阅读,输出的阅读笔记会同步至多个平台,主要有以下几
个:

本书 github 地址:https://github.com/gosoon/source-code-reading-notes

在线阅读:https://blog.tianfeiyu.com/source-code-reading-notes/

个人博客:https://blog.tianfeiyu.com

简书:田飞雨

知乎专栏:kubernetes 源码分析

腾讯云—云+社区:田飞雨的专栏

微信公众号:田飞雨

欢迎关注公众号或者通过 RSS 订阅博客。

3、贡献
如果你对云原生领域的开源项目感兴趣,欢迎参与本书编写!

本文档使用 书栈网 · BookStack.CN 构建 -4-


kubernetes

本章记录 kubernetes 源码分析相关的文章,文章主要基于 kubernetes v1.16 版本,文中如有不


当之处望指正。

kube-apiserver 的设计与实现
kube-apiserver 中 apiserver service 的实现
node controller 源码分析
job controller 源码分析
garbage collector controller 源码分析
daemonset controller 源码分析
statefulset controller 源码分析
deployment controller 源码分析
replicaset controller 源码分析
kube-scheduler 源码分析
kube-scheduler predicates 与 priorities 调度算法源码分析
kube-scheduler 优先级与抢占机制源码分析
kubernetes service 原理解析
kube-proxy 源码分析
kube-proxy iptables 模式源码分析
kube-proxy ipvs 模式源码分析
kubelet 架构浅析
kubelet 启动流程分析
kubelet 创建 pod 的流程
kubelet 状态上报的方式
kubelet 中事件处理机制
kubelet statusManager 源码分析
kubernetes 中 Qos 的设计与实现
kubelet 中垃圾回收机制的设计与实现

本文档使用 书栈网 · BookStack.CN 构建 -5-


kube-apiserver 的设计与实现

kube-apiserver 是 kubernetes 中与 etcd 直接交互的一个组件,其控制着 kubernetes 中


核心资源的变化。它主要提供了以下几个功能:

提供 Kubernetes API,包括认证授权、数据校验以及集群状态变更等,供客户端及其他组件
调用;
代理集群中的一些附加组件组件,如 Kubernetes UI、metrics-server、npd 等;
创建 kubernetes 服务,即提供 apiserver 的 Service,kubernetes Service;
资源在不同版本之间的转换;

kube-apiserver 处理流程
kube-apiserver 主要通过对外提供 API 的方式与其他组件进行交互,可以调用 kube-
apiserver 的接口 $ curl -k https://<masterIP>:6443 或者通过其提供的 swagger-ui 获
取到,其主要有以下三种 API:

core group:主要在 /api/v1 下;


named groups:其 path 为 /apis/$NAME/$VERSION ;
暴露系统状态的一些 API:如 /metrics 、 /healthz 等;

API 的 URL 大致以 /apis/group/version/namespaces/my-ns/myresource 组成,其中 API


的结构大致如下图所示:

了解了 kube-apiserver 的 API 后,下面会介绍 kube-apiserver 如何处理一个 API 请求,


一个请求完整的流程如下图所示:

本文档使用 书栈网 · BookStack.CN 构建 -6-


kube-apiserver 的设计与实现

此处以一次 POST 请求示例说明,当请求到达 kube-apiserver 时,kube-apiserver 首先会执


行在 http filter chain 中注册的过滤器链,该过滤器对其执行一系列过滤操作,主要有认证、鉴
权等检查操作。当 filter chain 处理完成后,请求会通过 route 进入到对应的 handler 中,
handler 中的操作主要是与 etcd 的交互,在 handler 中的主要的操作如下所示:

本文档使用 书栈网 · BookStack.CN 构建 -7-


kube-apiserver 的设计与实现

本文档使用 书栈网 · BookStack.CN 构建 -8-


kube-apiserver 的设计与实现

Decoder

kubernetes 中的多数 resource 都会有一个 internal version ,因为在整个开发过程中一个


resource 可能会对应多个 version,比如 deployment 会有
extensions/v1beta1 , apps/v1 。 为了避免出现问题,kube-apiserver 必须要知道如何在
每一对版本之间进行转换(例如,v1⇔v1alpha1,v1⇔v1beta1,v1beta1⇔v1alpha1),因此其
使用了一个特殊的 internal version , internal version 作为一个通用的 version 会包含
所有 version 的字段,它具有所有 version 的功能。 Decoder 会首先把 creater object
转换到 internal version ,然后将其转换为 storage version , storage version 是在
etcd 中存储时的另一个 version。

在解码时,首先从 HTTP path 中获取期待的 version,然后使用 scheme 以正确的 version


创建一个与之匹配的空对象,并使用 JSON 或 protobuf 解码器进行转换,在转换的第一步中,如
果用户省略了某些字段,Decoder 会把其设置为默认值。

Admission

在解码完成后,需要通过验证集群的全局约束来检查是否可以创建或更新对象,并根据集群配置设置默
认值。在 k8s.io/kubernetes/plugin/pkg/admission 目录下可以看到 kube-apiserver 可
以使用的所有全局约束插件,kube-apiserver 在启动时通过设置 --enable-admission-
plugins 参数来开启需要使用的插件,通过 ValidatingAdmissionWebhook 或
MutatingAdmissionWebhook 添加的插件也都会在此处进行工作。

Validation

主要检查 object 中字段的合法性。

在 handler 中执行完以上操作后最后会执行与 etcd 相关的操作,POST 操作会将数据写入到


etcd 中,以上在 handler 中的主要处理流程如下所示:

1. v1beta1 ⇒ internal ⇒ | ⇒ | ⇒ v1 ⇒ json/yaml ⇒ etcd


2. admission validation

kube-apiserver 中的组件
kube-apiserver 共由 3 个组件构成(Aggregator、KubeAPIServer、
APIExtensionServer),这些组件依次通过 Delegation 处理请求:

Aggregator:暴露的功能类似于一个七层负载均衡,将来自用户的请求拦截转发给其他服务
器,并且负责整个 APIServer 的 Discovery 功能;
KubeAPIServer :负责对请求的一些通用处理,认证、鉴权等,以及处理各个内建资源的
REST 服务;

本文档使用 书栈网 · BookStack.CN 构建 -9-


kube-apiserver 的设计与实现

APIExtensionServer:主要处理 CustomResourceDefinition(CRD)和
CustomResource(CR)的 REST 请求,也是 Delegation 的最后一环,如果对应 CR 不能
被处理的话则会返回 404。

Aggregator 和 APIExtensionsServer 对应两种主要扩展 APIServer 资源的方式,即分别是


AA 和 CRD。

Aggregator

Aggregator 通过 APIServices 对象关联到某个 Service 来进行请求的转发,其关联的


Service 类型进一步决定了请求转发形式。Aggregator 包括一个 GenericAPIServer 和维护
自身状态的 Controller。其中 GenericAPIServer 主要处理 apiregistration.k8s.io
组下的 APIService 资源请求。

Aggregator 除了处理资源请求外还包含几个 controller:

1、 apiserviceRegistrationController :负责 APIServices 中资源的注册与删除;


2、 availableConditionController :维护 APIServices 的可用状态,包括其引用
Service 是否可用等;
3、 autoRegistrationController :用于保持 API 中存在的一组特定的 APIServices;
4、 crdRegistrationController :负责将 CRD GroupVersions 自动注册到
APIServices 中;
5、 openAPIAggregationController :将 APIServices 资源的变化同步至提供的
OpenAPI 文档;

kubernetes 中的一些附加组件,比如 metrics-server 就是通过 Aggregator 的方式进行扩展


的,实际环境中可以通过使用 apiserver-builder 工具轻松以 Aggregator 的扩展方式创建自
定义资源。

启用 API Aggregation

在 kube-apiserver 中需要增加以下配置来开启 API Aggregation:

1. --proxy-client-cert-file=/etc/kubernetes/certs/proxy.crt
2. --proxy-client-key-file=/etc/kubernetes/certs/proxy.key
3. --requestheader-client-ca-file=/etc/kubernetes/certs/proxy-ca.crt
4. --requestheader-allowed-names=aggregator
5. --requestheader-extra-headers-prefix=X-Remote-Extra-
6. --requestheader-group-headers=X-Remote-Group
7. --requestheader-username-headers=X-Remote-User

KubeAPIServer

本文档使用 书栈网 · BookStack.CN 构建 - 10 -


kube-apiserver 的设计与实现

KubeAPIServer 主要是提供对 API Resource 的操作请求,为 kubernetes 中众多 API 注册


路由信息,暴露 RESTful API 并且对外提供 kubernetes service,使集群中以及集群外的服务
都可以通过 RESTful API 操作 kubernetes 中的资源。

APIExtensionServer

APIExtensionServer 作为 Delegation 链的最后一层,是处理所有用户通过 Custom


Resource Definition 定义的资源服务器。

其中包含的 controller 以及功能如下所示:

1、 openapiController :将 crd 资源的变化同步至提供的 OpenAPI 文档,可通过访问


/openapi/v2 进行查看;
2、 crdController :负责将 crd 信息注册到 apiVersions 和 apiResources 中,两者
的信息可通过 $ kubectl api-versions 和 $ kubectl api-resources 查看;
3、 namingController :检查 crd obj 中是否有命名冲突,可在 crd
.status.conditions 中查看;
4、 establishingController :检查 crd 是否处于正常状态,可在 crd
.status.conditions 中查看;
5、 nonStructuralSchemaController :检查 crd obj 结构是否正常,可在 crd
.status.conditions 中查看;
6、 apiApprovalController :检查 crd 是否遵循 kubernetes API 声明策略,可在 crd
.status.conditions 中查看;
7、 finalizingController :类似于 finalizes 的功能,与 CRs 的删除有关;

kube-apiserver 启动流程分析

kubernetes 版本:v1.16

首先分析 kube-apiserver 的启动方式,kube-apiserver 也是通过其 Run 方法启动主逻辑


的,在 Run 方法调用之前会进行解析命令行参数、设置默认值等。

Run

Run 方法的主要逻辑为:

1、调用 CreateServerChain 构建服务调用链并判断是否启动非安全的 http server,


http server 链中包含 apiserver 要启动的三个 server,以及为每个 server 注册对应
资源的路由;
2、调用 server.PrepareRun 进行服务运行前的准备,该方法主要完成了健康检查、存活检查
和 OpenAPI 路由的注册工作;
3、调用 prepared.Run 启动 https server;

本文档使用 书栈网 · BookStack.CN 构建 - 11 -


kube-apiserver 的设计与实现

server 的初始化使用委托模式,通过 DelegationTarget 接口,把基本的 API Server、


CustomResource、Aggregator 这三种服务采用链式结构串联起来,对外提供服务。

k8s.io/kubernetes/cmd/kube-apiserver/app/server.go:147

func Run(completeOptions completedServerRunOptions, stopCh <-chan struct{})


1. error {
2. server, err := CreateServerChain(completeOptions, stopCh)
3. if err != nil {
4. return err
5. }
6.
7. prepared, err := server.PrepareRun()
8. if err != nil {
9. return err
10. }
11.
12. return prepared.Run(stopCh)
13. }

CreateServerChain

CreateServerChain 是完成 server 初始化的方法,里面包含


APIExtensionsServer 、 KubeAPIServer 、 AggregatorServer 初始化的所有流程,最终返
回 aggregatorapiserver.APIAggregator 实例,初始化流程主要有:http filter chain 的
配置、API Group 的注册、http path 与 handler 的关联以及 handler 后端存储 etcd 的配
置。其主要逻辑为:

1、调用 CreateKubeAPIServerConfig 创建 KubeAPIServer 所需要的配置,主要是创建


master.Config ,其中会调用 buildGenericConfig 生成 genericConfig,
genericConfig 中包含 apiserver 的核心配置;
2、判断是否启用了扩展的 API server 并调用 createAPIExtensionsConfig 为其创建配
置,apiExtensions server 是一个代理服务,用于代理 kubeapiserver 中的其他
server,比如 metric-server;
3、调用 createAPIExtensionsServer 创建 apiExtensionsServer 实例;
4、调用 CreateKubeAPIServer 初始化 kubeAPIServer;
5、调用 createAggregatorConfig 为 aggregatorServer 创建配置并调用
createAggregatorServer 初始化 aggregatorServer;
6、配置并判断是否启动非安全的 http server;

k8s.io/kubernetes/cmd/kube-apiserver/app/server.go:165

本文档使用 书栈网 · BookStack.CN 构建 - 12 -


kube-apiserver 的设计与实现

func CreateServerChain(completedOptions completedServerRunOptions, stopCh <-


1. chan struct{}) (*aggregatorapiserver.APIAggregator, error) {
2. nodeTunneler, proxyTransport, err := CreateNodeDialer(completedOptions)
3. if err != nil {
4. return nil, err
5. }
6. // 1、为 kubeAPIServer 创建配置
kubeAPIServerConfig, insecureServingInfo, serviceResolver,
pluginInitializer, admissionPostStartHook, err :=
7. CreateKubeAPIServerConfig(completedOptions, nodeTunneler, proxyTransport)
8. if err != nil {
9. return nil, err
10. }
11.
12. // 2、判断是否配置了 APIExtensionsServer,创建 apiExtensionsConfig
apiExtensionsConfig, err :=
createAPIExtensionsConfig(*kubeAPIServerConfig.GenericConfig,
kubeAPIServerConfig.ExtraConfig.VersionedInformers, pluginInitializer,
13. completedOptions.ServerRunOptions, completedOptions.MasterCount,
serviceResolver,
webhook.NewDefaultAuthenticationInfoResolverWrapper(proxyTransport,
14. kubeAPIServerConfig.GenericConfig.LoopbackClientConfig))
15. if err != nil {
16. return nil, err
17. }
18.
19. // 3、初始化 APIExtensionsServer
apiExtensionsServer, err := createAPIExtensionsServer(apiExtensionsConfig,
20. genericapiserver.NewEmptyDelegate())
21. if err != nil {
22. return nil, err
23. }
24.
25. // 4、初始化 KubeAPIServer
kubeAPIServer, err := CreateKubeAPIServer(kubeAPIServerConfig,
26. apiExtensionsServer.GenericAPIServer, admissionPostStartHook)
27. if err != nil {
28. return nil, err
29. }
30.
31. // 5、创建 AggregatorConfig

本文档使用 书栈网 · BookStack.CN 构建 - 13 -


kube-apiserver 的设计与实现

aggregatorConfig, err :=
createAggregatorConfig(*kubeAPIServerConfig.GenericConfig,
completedOptions.ServerRunOptions, kubeAPIServerConfig.
ExtraConfig.VersionedInformers, serviceResolver, proxyTransport,
32. pluginInitializer)
33. if err != nil {
34. return nil, err
35. }
36.
37. // 6、初始化 AggregatorServer
aggregatorServer, err := createAggregatorServer(aggregatorConfig,
38. kubeAPIServer.GenericAPIServer, apiExtensionsServer.Informers)
39. if err != nil {
40. return nil, err
41. }
42.
43. // 7、判断是否启动非安全端口的 http server
44. if insecureServingInfo != nil {
insecureHandlerChain :=
kubeserver.BuildInsecureHandlerChain(aggregatorServer.GenericAPIServer.UnprotectedHandl
45. kubeAPIServerConfig.GenericConfig)
if err := insecureServingInfo.Serve(insecureHandlerChain,
46. kubeAPIServerConfig.GenericConfig.RequestTimeout, stopCh); err != nil {
47. return nil, err
48. }
49. }
50. return aggregatorServer, nil
51. }

CreateKubeAPIServerConfig

在 CreateKubeAPIServerConfig 中主要是调用 buildGenericConfig 创建


genericConfig 以及构建 master.Config 对象。

k8s.io/kubernetes/cmd/kube-apiserver/app/server.go:271

1. func CreateKubeAPIServerConfig(
2. s completedServerRunOptions,
3. nodeTunneler tunneler.Tunneler,
4. proxyTransport *http.Transport,
5. ) (......) {
6.
7. // 1、构建 genericConfig

本文档使用 书栈网 · BookStack.CN 构建 - 14 -


kube-apiserver 的设计与实现

genericConfig, versionedInformers, insecureServingInfo, serviceResolver,


pluginInitializers, admissionPostStartHook, storageFactory, lastErr =
8. buildGenericConfig(s.ServerRunOptions, proxyTransport)
9. if lastErr != nil {
10. return
11. }
12.
13. ......
14.
15. // 2、初始化所支持的 capabilities
16. capabilities.Initialize(capabilities.Capabilities{
17. AllowPrivileged: s.AllowPrivileged,
18. PrivilegedSources: capabilities.PrivilegedSources{
19. HostNetworkSources: []string{},
20. HostPIDSources: []string{},
21. HostIPCSources: []string{},
22. },
23. PerConnectionBandwidthLimitBytesPerSec: s.MaxConnectionBytesPerSec,
24. })
25.
26. // 3、获取 service ip range 以及 api server service IP
serviceIPRange, apiServerServiceIP, lastErr :=
27. master.DefaultServiceIPRange(s.PrimaryServiceClusterIPRange)
28. if lastErr != nil {
29. return
30. }
31.
32. ......
33.
34. // 4、构建 master.Config 对象
35. config = &master.Config{......}
36.
37. if nodeTunneler != nil {
38. config.ExtraConfig.KubeletClientConfig.Dial = nodeTunneler.Dial
39. }
40. if config.GenericConfig.EgressSelector != nil {
config.ExtraConfig.KubeletClientConfig.Lookup =
41. config.GenericConfig.EgressSelector.Lookup
42. }
43.
44. return
45. }

本文档使用 书栈网 · BookStack.CN 构建 - 15 -


kube-apiserver 的设计与实现

buildGenericConfig

主要逻辑为:

1、调用 genericapiserver.NewConfig 生成默认的 genericConfig,genericConfig


中主要配置了 DefaultBuildHandlerChain , DefaultBuildHandlerChain 中包含了认证、
鉴权等一系列 http filter chain;
2、调用 master.DefaultAPIResourceConfigSource 加载需要启用的 API Resource,集
群中所有的 API Resource 可以在代码的 k8s.io/api 目录中看到,随着版本的迭代也会
不断变化;
3、为 genericConfig 中的部分字段设置默认值;
4、调用 completedStorageFactoryConfig.New 创建 storageFactory,后面会使用
storageFactory 为每种API Resource 创建对应的 RESTStorage;

k8s.io/kubernetes/cmd/kube-apiserver/app/server.go:386

1. func buildGenericConfig(
2. s *options.ServerRunOptions,
3. proxyTransport *http.Transport,
4. ) (......) {
5. // 1、为 genericConfig 设置默认值
6. genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)
genericConfig.MergedResourceConfig =
7. master.DefaultAPIResourceConfigSource()
8.
if lastErr = s.GenericServerRunOptions.ApplyTo(genericConfig); lastErr !=
9. nil {
10. return
11. }
12. ......
13.
14. genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(......)
15. genericConfig.OpenAPIConfig.Info.Title = "Kubernetes"
16. genericConfig.LongRunningFunc = filters.BasicLongRunningRequestCheck(
17. sets.NewString("watch", "proxy"),
18. sets.NewString("attach", "exec", "proxy", "log", "portforward"),
19. )
20.
21. kubeVersion := version.Get()
22. genericConfig.Version = &kubeVersion
23.
24. storageFactoryConfig := kubeapiserver.NewStorageFactoryConfig()
25. storageFactoryConfig.ApiResourceConfig = genericConfig.MergedResourceConfig

本文档使用 书栈网 · BookStack.CN 构建 - 16 -


kube-apiserver 的设计与实现

26. completedStorageFactoryConfig, err := storageFactoryConfig.Complete(s.Etcd)


27. if err != nil {
28. lastErr = err
29. return
30. }
31. // 初始化 storageFactory
32. storageFactory, lastErr = completedStorageFactoryConfig.New()
33. if lastErr != nil {
34. return
35. }
36. if genericConfig.EgressSelector != nil {
storageFactory.StorageConfig.Transport.EgressLookup =
37. genericConfig.EgressSelector.Lookup
38. }
39.
// 2、初始化 RESTOptionsGetter,后期根据其获取操作 Etcd 的句柄,同时添加 etcd 的健
40. 康检查方法
if lastErr = s.Etcd.ApplyWithStorageFactoryTo(storageFactory,
41. genericConfig); lastErr != nil {
42. return
43. }
44.
45. // 3、设置使用 protobufs 用来内部交互,并且禁用压缩功能
genericConfig.LoopbackClientConfig.ContentConfig.ContentType =
46. "application/vnd.kubernetes.protobuf"
47.
48. genericConfig.LoopbackClientConfig.DisableCompression = true
49.
50. // 4、创建 clientset
51. kubeClientConfig := genericConfig.LoopbackClientConfig
clientgoExternalClient, err :=
52. clientgoclientset.NewForConfig(kubeClientConfig)
53. if err != nil {
lastErr = fmt.Errorf("failed to create real external clientset: %v",
54. err)
55. return
56. }
versionedInformers =
clientgoinformers.NewSharedInformerFactory(clientgoExternalClient,
57. 10*time.Minute)
58.
// 5、创建认证实例,支持多种认证方式:请求 Header 认证、Auth 文件认证、CA 证书认证、
59. Bearer token 认证、

本文档使用 书栈网 · BookStack.CN 构建 - 17 -


kube-apiserver 的设计与实现

60. // ServiceAccount 认证、BootstrapToken 认证、WebhookToken 认证等


genericConfig.Authentication.Authenticator,
genericConfig.OpenAPIConfig.SecurityDefinitions, err = BuildAuthenticator(s,
61. clientgoExternalClient, versionedInformers)
62. if err != nil {
63. lastErr = fmt.Errorf("invalid authentication config: %v", err)
64. return
65. }
66.
67. // 6、创建鉴权实例,包含:Node、RBAC、Webhook、ABAC、AlwaysAllow、AlwaysDeny
genericConfig.Authorization.Authorizer, genericConfig.RuleResolver, err =
68. BuildAuthorizer(s, versionedInformers)
69. ......
70.
serviceResolver = buildServiceResolver(s.EnableAggregatorRouting,
71. genericConfig.LoopbackClientConfig.Host, versionedInformers)
72.
authInfoResolverWrapper :=
webhook.NewDefaultAuthenticationInfoResolverWrapper(proxyTransport,
73. genericConfig.LoopbackClientConfig)
74.
75. // 7、审计插件的初始化
76. lastErr = s.Audit.ApplyTo(......)
77. if lastErr != nil {
78. return
79. }
80.
81. // 8、准入插件的初始化
pluginInitializers, admissionPostStartHook, err =
82. admissionConfig.New(proxyTransport, serviceResolver)
83. if err != nil {
lastErr = fmt.Errorf("failed to create admission plugin initializer:
84. %v", err)
85. return
86. }
87. err = s.Admission.ApplyTo(......)
88. if err != nil {
89. lastErr = fmt.Errorf("failed to initialize admission: %v", err)
90. }
91.
92. return
93. }

本文档使用 书栈网 · BookStack.CN 构建 - 18 -


kube-apiserver 的设计与实现

以上主要分析 KubeAPIServerConfig 的初始化,其他两个 server config 的初始化暂且不详


细分析,下面接着继续分析 server 的初始化。

createAPIExtensionsServer

APIExtensionsServer 是最先被初始化的,在 createAPIExtensionsServer 中调用


apiextensionsConfig.Complete().New 来完成 server 的初始化,其主要逻辑为:

1、首先调用 c.GenericConfig.New 按照 go-restful 的模式初始化 Container,在


c.GenericConfig.New 中会调用 NewAPIServerHandler 初始化 handler,
APIServerHandler 包含了 API Server 使用的多种http.Handler 类型,包括 go-
restful 以及 non-go-restful ,以及在以上两者之间选择的 Director 对象, go-
restful 用于处理已经注册的 handler, non-go-restful 用来处理不存在的 handler,
API URI 处理的选择过程为: FullHandlerChain-> Director ->{GoRestfulContainer,
NonGoRestfulMux} 。在 c.GenericConfig.New 中还会调用 installAPI 来添加包括
/ 、 /debug/* 、 /metrics 、 /version 等路由信息。三种 server 在初始化时首先都
会调用 c.GenericConfig.New 来初始化一个 genericServer,然后进行 API 的注册;
2、调用 s.GenericAPIServer.InstallAPIGroup 在路由中注册 API Resources,此方法
的调用链非常深,主要是为了将需要暴露的 API Resource 注册到 server 中,以便能通过
http 接口进行 resource 的 REST 操作,其他几种 server 在初始化时也都会执行对应的
InstallAPI ;
3、初始化 server 中需要使用的 controller,主要有
openapiController 、 crdController 、 namingController 、 establishingController
、 nonStructuralSchemaController 、 apiApprovalController 、 finalizingControlle
r;
4、将需要启动的 controller 以及 informer 添加到 PostStartHook 中;

k8s.io/kubernetes/cmd/kube-apiserver/app/apiextensions.go:94

func createAPIExtensionsServer(apiextensionsConfig
*apiextensionsapiserver.Config, delegateAPIServer
genericapiserver.DelegationTarget) (*
1. apiextensionsapiserver.CustomResourceDefinitions, error) {
2. return apiextensionsConfig.Complete().New(delegateAPIServer)
3. }

k8s.io/kubernetes/staging/src/k8s.io/apiextensions-
apiserver/pkg/apiserver/apiserver.go:132

func (c completedConfig) New(delegationTarget


1. genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
2. // 1、初始化 genericServer

本文档使用 书栈网 · BookStack.CN 构建 - 19 -


kube-apiserver 的设计与实现

genericServer, err := c.GenericConfig.New("apiextensions-apiserver",


3. delegationTarget)
4. if err != nil {
5. return nil, err
6. }
7.
8. s := &CustomResourceDefinitions{
9. GenericAPIServer: genericServer,
10. }
11.
12. // 2、初始化 APIGroup Info,APIGroup 指该 server 需要暴露的 API
13. apiResourceConfig := c.GenericConfig.MergedResourceConfig
apiGroupInfo :=
genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme,
14. metav1.ParameterCodec, Codecs)
15. if apiResourceConfig.VersionEnabled(v1beta1.SchemeGroupVersion) {
16. storage := map[string]rest.Storage{}
customResourceDefintionStorage :=
17. customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
18. storage["customresourcedefinitions"] = customResourceDefintionStorage
storage["customresourcedefinitions/status"] =
19. customresourcedefinition.NewStatusREST(Scheme, customResourceDefintionStorage)
20.

apiGroupInfo.VersionedResourcesStorageMap[v1beta1.SchemeGroupVersion.Version] =
21. storage
22. }
23. if apiResourceConfig.VersionEnabled(v1.SchemeGroupVersion) {
24. ......
25. }
26.
27. // 3、注册 APIGroup
28. if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
29. return nil, err
30. }
31.
32. // 4、初始化需要使用的 controller
crdClient, err :=
33. internalclientset.NewForConfig(s.GenericAPIServer.LoopbackClientConfig)
34. if err != nil {
35. return nil, fmt.Errorf("failed to create clientset: %v", err)
36. }

本文档使用 书栈网 · BookStack.CN 构建 - 20 -


kube-apiserver 的设计与实现

s.Informers = internalinformers.NewSharedInformerFactory(crdClient,
37. 5*time.Minute)
38.
39. ......
establishingController :=
establish.NewEstablishingController(s.Informers.Apiextensions().InternalVersion().
40. CustomResourceDefinitions(), crdClient.Apiextensions())
41. crdHandler, err := NewCustomResourceDefinitionHandler(......)
42. if err != nil {
43. return nil, err
44. }
45. s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/",
46. crdHandler)
47.
crdController :=
NewDiscoveryController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefi
48. versionDiscoveryHandler, groupDiscoveryHandler)
namingController :=
status.NewNamingConditionController(s.Informers.Apiextensions().InternalVersion().
49. crdClient.Apiextensions())
nonStructuralSchemaController :=
nonstructuralschema.NewConditionController(s.Informers.Apiextensions().InternalVersion
50. CustomResourceDefinitions(), crdClient.Apiextensions())
apiApprovalController :=
apiapproval.NewKubernetesAPIApprovalPolicyConformantConditionController(s.Informers
51. InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions())
52. finalizingController := finalizer.NewCRDFinalizer(

53. s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(),
54. crdClient.Apiextensions(),
55. crdHandler,
56. )
57. var openapiController *openapicontroller.Controller
if
utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourcePublishOpenA
58. {
openapiController =
59. openapicontroller.NewController(s.Informers.Apiextensions().InternalVersion().CustomRes
60. }
61.
62. // 5、将 informer 以及 controller 添加到 PostStartHook 中
s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-informers",
63. func(context genericapiserver.PostStartHookContext) error {

本文档使用 书栈网 · BookStack.CN 构建 - 21 -


kube-apiserver 的设计与实现

64. s.Informers.Start(context.StopCh)
65. return nil
66. })
s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-controllers",
67. func(context genericapiserver.PostStartHookContext) error {
68. ......
69. go crdController.Run(context.StopCh)
70. go namingController.Run(context.StopCh)
71. go establishingController.Run(context.StopCh)
72. go nonStructuralSchemaController.Run(5, context.StopCh)
73. go apiApprovalController.Run(5, context.StopCh)
74. go finalizingController.Run(5, context.StopCh)
75. return nil
76. })
77.
s.GenericAPIServer.AddPostStartHookOrDie("crd-informer-synced",
78. func(context genericapiserver.PostStartHookContext) error {
return wait.PollImmediateUntil(100*time.Millisecond, func() (bool,
79. error) {
return
s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions().Informer
80. nil
81. }, context.StopCh)
82. })
83.
84. return s, nil
85. }

以上是 APIExtensionsServer 的初始化流程,其中最核心方法是


s.GenericAPIServer.InstallAPIGroup ,也就是 API 的注册过程,三种 server 中 API 的注
册过程都是其核心。

CreateKubeAPIServer

本节继续分析 KubeAPIServer 的初始化,在 CreateKubeAPIServer 中调用了


kubeAPIServerConfig.Complete().New 来完成相关的初始化操作。

kubeAPIServerConfig.Complete().New

主要逻辑为:

1、调用 c.GenericConfig.New 初始化 GenericAPIServer,其主要实现在上文已经分析


过;
2、判断是否支持 logs 相关的路由,如果支持,则添加 /logs 路由;

本文档使用 书栈网 · BookStack.CN 构建 - 22 -


kube-apiserver 的设计与实现

3、调用 m.InstallLegacyAPI 将核心 API Resource 添加到路由中,对应到 apiserver


就是以 /api 开头的 resource;
4、调用 m.InstallAPIs 将扩展的 API Resource 添加到路由中,在 apiserver 中即是
以 /apis 开头的 resource;

k8s.io/kubernetes/cmd/kube-apiserver/app/server.go:214

1. func CreateKubeAPIServer(......) (*master.Master, error) {


2. kubeAPIServer, err := kubeAPIServerConfig.Complete().New(delegateAPIServer)
3. if err != nil {
4. return nil, err
5. }
6.
kubeAPIServer.GenericAPIServer.AddPostStartHookOrDie("start-kube-apiserver-
7. admission-initializer", admissionPostStartHook)
8.
9. return kubeAPIServer, nil
10. }

k8s.io/kubernetes/pkg/master/master.go:325

func (c completedConfig) New(delegationTarget


1. genericapiserver.DelegationTarget) (*Master, error) {
2. ......
3. // 1、初始化 GenericAPIServer
4. s, err := c.GenericConfig.New("kube-apiserver", delegationTarget)
5. if err != nil {
6. return nil, err
7. }
8.
9. // 2、注册 logs 相关的路由
10. if c.ExtraConfig.EnableLogsSupport {
11. routes.Logs{}.Install(s.Handler.GoRestfulContainer)
12. }
13.
14. m := &Master{
15. GenericAPIServer: s,
16. }
17.
18. // 3、安装 LegacyAPI
if
c.ExtraConfig.APIResourceConfigSource.VersionEnabled(apiv1.SchemeGroupVersion)
19. {

本文档使用 书栈网 · BookStack.CN 构建 - 23 -


kube-apiserver 的设计与实现

20. legacyRESTStorageProvider := corerest.LegacyRESTStorageProvider{


21. StorageFactory: c.ExtraConfig.StorageFactory,
22. ProxyTransport: c.ExtraConfig.ProxyTransport,
23. ......
24. }
if err := m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter,
25. legacyRESTStorageProvider); err != nil {
26. return nil, err
27. }
28. }
29. restStorageProviders := []RESTStorageProvider{
30. auditregistrationrest.RESTStorageProvider{},
authenticationrest.RESTStorageProvider{Authenticator:
c.GenericConfig.Authentication.Authenticator, APIAudiences: c.GenericConfig.
31. Authentication.APIAudiences},
32. ......
33. }
34. // 4、安装 APIs
if err := m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource,
35. c.GenericConfig.RESTOptionsGetter, restStorageProviders...); err != nil {
36. return nil, err
37. }
38.
39. if c.ExtraConfig.Tunneler != nil {
m.installTunneler(c.ExtraConfig.Tunneler,
40. corev1client.NewForConfigOrDie(c.GenericConfig.LoopbackClientConfig).Nodes())
41. }
42.
m.GenericAPIServer.AddPostStartHookOrDie("ca-registration",
43. c.ExtraConfig.ClientCARegistrationHook.PostStartHook)
44.
45. return m, nil
46. }

本文档使用 书栈网 · BookStack.CN 构建 - 24 -


kube-apiserver 的设计与实现

m.InstallLegacyAPI

此方法的主要功能是将 core API 注册到路由中,是 apiserver 初始化流程中最核心的方法之


一,不过其调用链非常深,下面会进行深入分析。将 API 注册到路由其最终的目的就是对外提供
RESTful API 来操作对应 resource,注册 API 主要分为两步,第一步是为 API 中的每个
resource 初始化 RESTStorage 以此操作后端存储中数据的变更,第二步是为每个 resource 根
据其 verbs 构建对应的路由。 m.InstallLegacyAPI 的主要逻辑为:

1、调用 legacyRESTStorageProvider.NewLegacyRESTStorage 为 LegacyAPI 中各个资源


创建 RESTStorage,RESTStorage 的目的是将每种资源的访问路径及其后端存储的操作对应
起来;
2、初始化 bootstrap-controller ,并将其加入到 PostStartHook 中, bootstrap-
controller 是 apiserver 中的一个 controller,主要功能是创建系统所需要的一些
namespace 以及创建 kubernetes service 并定期触发对应的 sync 操作,apiserver
在启动后会通过调用 PostStartHook 来启动 bootstrap-controller ;
3、在为资源创建完 RESTStorage 后,调用
m.GenericAPIServer.InstallLegacyAPIGroup 为 APIGroup 注册路由信
息, InstallLegacyAPIGroup 方法的调用链非常深,主要为 InstallLegacyAPIGroup-->
installAPIResources --> InstallREST --> Install --> registerResourceHandlers ,最
终核心的路由构造在 registerResourceHandlers 方法内,该方法比较复杂,其主要功能是通过
上一步骤构造的 REST Storage 判断该资源可以执行哪些操作(如 create、update等),
将其对应的操作存入到 action 中,每一个 action 对应一个标准的 REST 操作,如
create 对应的 action 操作为 POST、update 对应的 action 操作为PUT。最终根据
actions 数组依次遍历,对每一个操作添加一个 handler 方法,注册到 route 中去,再将
route 注册到 webservice 中去,webservice 最终会注册到 container 中,遵循 go-
restful 的设计模式;

关于 legacyRESTStorageProvider.NewLegacyRESTStorage 以及
m.GenericAPIServer.InstallLegacyAPIGroup 方法的详细说明在后文中会继续进行讲解。

k8s.io/kubernetes/pkg/master/master.go:406

1. func (m *Master) InstallLegacyAPI(......) error {


legacyRESTStorage, apiGroupInfo, err :=
2. legacyRESTStorageProvider.NewLegacyRESTStorage(restOptionsGetter)
3. if err != nil {
4. return fmt.Errorf("Error building core storage: %v", err)
5. }
6.
7. controllerName := "bootstrap-controller"
coreClient :=
8. corev1client.NewForConfigOrDie(c.GenericConfig.LoopbackClientConfig)

本文档使用 书栈网 · BookStack.CN 构建 - 25 -


kube-apiserver 的设计与实现

bootstrapController := c.NewBootstrapController(legacyRESTStorage,
9. coreClient, coreClient, coreClient, coreClient.RESTClient())
m.GenericAPIServer.AddPostStartHookOrDie(controllerName,
10. bootstrapController.PostStartHook)
m.GenericAPIServer.AddPreShutdownHookOrDie(controllerName,
11. bootstrapController.PreShutdownHook)
12.
if err :=
m.GenericAPIServer.InstallLegacyAPIGroup(genericapiserver.DefaultLegacyAPIPrefix,
13. &apiGroupInfo); err != nil {
14. return fmt.Errorf("Error in registering group versions: %v", err)
15. }
16. return nil
17. }

InstallAPIs 与 InstallLegacyAPI 的主要流程是类似的,限于篇幅此处不再深入分析。

createAggregatorServer

AggregatorServer 主要用于自定义的聚合控制器的,使 CRD 能够自动注册到集群中。

主要逻辑为:

1、调用 aggregatorConfig.Complete().NewWithDelegate 创建 aggregatorServer;


2、初始化 crdRegistrationController 和
autoRegistrationController , crdRegistrationController 负责注册
CRD, autoRegistrationController 负责将 CRD 对应的 APIServices 自动注册到
apiserver 中,CRD 创建后可通过 $ kubectl get apiservices 查看是否注册到
apiservices 中;
3、将 autoRegistrationController 和 crdRegistrationController 加入到
PostStartHook 中;

k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go:124

func createAggregatorServer(......) (*aggregatorapiserver.APIAggregator, error)


1. {
2. // 1、初始化 aggregatorServer
aggregatorServer, err :=
3. aggregatorConfig.Complete().NewWithDelegate(delegateAPIServer)
4. if err != nil {
5. return nil, err
6. }
7.

本文档使用 书栈网 · BookStack.CN 构建 - 26 -


kube-apiserver 的设计与实现

8. // 2、初始化 auto-registration controller


apiRegistrationClient, err :=
9. apiregistrationclient.NewForConfig(aggregatorConfig.GenericConfig.LoopbackClientConfig
10. if err != nil {
11. return nil, err
12. }
autoRegistrationController :=
13. autoregister.NewAutoRegisterController(......)
apiServices := apiServicesToRegister(delegateAPIServer,
14. autoRegistrationController)
crdRegistrationController :=
15. crdregistration.NewCRDRegistrationController(......)
err = aggregatorServer.GenericAPIServer.AddPostStartHook("kube-apiserver-
16. autoregistration", func(context genericapiserver.PostStartHookContext) error {
17. go crdRegistrationController.Run(5, context.StopCh)
18. go func() {
if
aggregatorConfig.GenericConfig.MergedResourceConfig.AnyVersionForGroupEnabled("apiexten
19. {
20. crdRegistrationController.WaitForInitialSync()
21. }
22. autoRegistrationController.Run(5, context.StopCh)
23. }()
24. return nil
25. })
26. if err != nil {
27. return nil, err
28. }
29.
30. err = aggregatorServer.GenericAPIServer.AddBootSequenceHealthChecks(
31. makeAPIServiceAvailableHealthCheck(
32. "autoregister-completion",
33. apiServices,

34. aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(),
35. ),
36. )
37. if err != nil {
38. return nil, err
39. }
40.
41. return aggregatorServer, nil
42. }

本文档使用 书栈网 · BookStack.CN 构建 - 27 -


kube-apiserver 的设计与实现

aggregatorConfig.Complete().NewWithDelegate

aggregatorConfig.Complete().NewWithDelegate 是初始化 aggregatorServer 的方法,主


要逻辑为:

1、调用 c.GenericConfig.New 初始化 GenericAPIServer,其内部的主要功能在上文已


经分析过;
2、调用 apiservicerest.NewRESTStorage 为 APIServices 资源创建 RESTStorage,
RESTStorage 的目的是将每种资源的访问路径及其后端存储的操作对应起来;
3、调用 s.GenericAPIServer.InstallAPIGroup 为 APIGroup 注册路由信息;

k8s.io/kubernetes/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go:158

func (c completedConfig) NewWithDelegate(delegationTarget


1. genericapiserver.DelegationTarget) (*APIAggregator, error) {
2. openAPIConfig := c.GenericConfig.OpenAPIConfig
3. c.GenericConfig.OpenAPIConfig = nil
4. // 1、初始化 genericServer
genericServer, err := c.GenericConfig.New("kube-aggregator",
5. delegationTarget)
6. if err != nil {
7. return nil, err
8. }
9.
apiregistrationClient, err :=
10. clientset.NewForConfig(c.GenericConfig.LoopbackClientConfig)
11. if err != nil {
12. return nil, err
13. }
14. informerFactory := informers.NewSharedInformerFactory(
15. apiregistrationClient,
16. 5*time.Minute,
17. )
18. s := &APIAggregator{
19. GenericAPIServer: genericServer,
20. delegateHandler: delegationTarget.UnprotectedHandler(),
21. ......
22. }
23.
24. // 2、为 API 注册路由
apiGroupInfo :=
apiservicerest.NewRESTStorage(c.GenericConfig.MergedResourceConfig,
25. c.GenericConfig.RESTOptionsGetter)

本文档使用 书栈网 · BookStack.CN 构建 - 28 -


kube-apiserver 的设计与实现

26. if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {


27. return nil, err
28. }
29.
30. // 3、初始化 apiserviceRegistrationController、availableController
31. apisHandler := &apisHandler{
32. codecs: aggregatorscheme.Codecs,
33. lister: s.lister,
34. }
35. s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", apisHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandle("/apis/",
36. apisHandler)
apiserviceRegistrationController :=
NewAPIServiceRegistrationController(informerFactory.Apiregistration().V1().APIServices
37. s)
availableController, err :=
38. statuscontrollers.NewAvailableConditionController(
39. ......
40. )
41. if err != nil {
42. return nil, err
43. }
44.
45. // 4、添加 PostStartHook
s.GenericAPIServer.AddPostStartHookOrDie("start-kube-aggregator-informers",
46. func(context genericapiserver.PostStartHookContext) error {
47. informerFactory.Start(context.StopCh)
48. c.GenericConfig.SharedInformerFactory.Start(context.StopCh)
49. return nil
50. })
s.GenericAPIServer.AddPostStartHookOrDie("apiservice-registration-
51. controller", func(context genericapiserver.PostStartHookContext) error {
52. go apiserviceRegistrationController.Run(context.StopCh)
53. return nil
54. })
s.GenericAPIServer.AddPostStartHookOrDie("apiservice-status-available-
55. controller", func(context genericapiserver.PostStartHookContext) error {
56. go availableController.Run(5, context.StopCh)
57. return nil
58. })
59.
60. return s, nil
61. }

本文档使用 书栈网 · BookStack.CN 构建 - 29 -


kube-apiserver 的设计与实现

以上是对 AggregatorServer 初始化流程的分析,可以看出,在创建 APIExtensionsServer、


KubeAPIServer 以及 AggregatorServer 时,其模式都是类似的,首先调用
c.GenericConfig.New 按照 go-restful 的模式初始化 Container,然后为 server 中需要
注册的资源创建 RESTStorage,最后将 resource 的 APIGroup 信息注册到路由中。

至此,CreateServerChain 中流程已经分析完,其中的调用链如下所示:

1. |--> CreateNodeDialer
2. |
3. |--> CreateKubeAPIServerConfig
4. |
5. CreateServerChain --|--> createAPIExtensionsConfig
6. |
|
7. |--> c.GenericConfig.New
|--> createAPIExtensionsServer -->
8. apiextensionsConfig.Complete().New --|
|
9. |--> s.GenericAPIServer.InstallAPIGroup
10. |
|
11. |--> c.GenericConfig.New --> legacyRESTStorageProvider.NewLegacyRESTStorage
|
12. |
|--> CreateKubeAPIServer -->
13. kubeAPIServerConfig.Complete().New --|--> m.InstallLegacyAPI
|
14. |
|
15. |--> m.InstallAPIs
16. |
17. |
18. |--> createAggregatorConfig
19. |
|
20. |--> c.GenericConfig.New
|
21. |
|--> createAggregatorServer -->
aggregatorConfig.Complete().NewWithDelegate --|-->
22. apiservicerest.NewRESTStorage

23. |

本文档使用 书栈网 · BookStack.CN 构建 - 30 -


kube-apiserver 的设计与实现

24. |--> s.GenericAPIServer.InstallAPIGroup

prepared.Run

在 Run 方法中首先调用 CreateServerChain 完成各 server 的初始化,然后调用


server.PrepareRun 完成服务启动前的准备工作,最后调用 prepared.Run 方法来启动安全
的 http server。 server.PrepareRun 主要完成了健康检查、存活检查和 OpenAPI 路由的注
册工作,下面继续分析 prepared.Run 的流程,在 prepared.Run 中主要调用
s.NonBlockingRun 来完成启动工作。

k8s.io/kubernetes/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go:269

1. func (s preparedAPIAggregator) Run(stopCh <-chan struct{}) error {


2. return s.runnable.Run(stopCh)
3. }

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go:316

1. func (s preparedGenericAPIServer) Run(stopCh <-chan struct{}) error {


2. delayedStopCh := make(chan struct{})
3.
4. go func() {
5. defer close(delayedStopCh)
6. <-stopCh
7.
8. time.Sleep(s.ShutdownDelayDuration)
9. }()
10.
11. // 调用 s.NonBlockingRun 完成启动流程
12. err := s.NonBlockingRun(delayedStopCh)
13. if err != nil {
14. return err
15. }
16.
17. // 当收到退出信号后完成一些收尾工作
18. <-stopCh
19. err = s.RunPreShutdownHooks()
20. if err != nil {
21. return err
22. }
23.

本文档使用 书栈网 · BookStack.CN 构建 - 31 -


kube-apiserver 的设计与实现

24. <-delayedStopCh
25. s.HandlerChainWaitGroup.Wait()
26. return nil
27. }

s.NonBlockingRun

s.NonBlockingRun 的主要逻辑为:

1、判断是否要启动审计日志服务;
2、调用 s.SecureServingInfo.Serve 配置并启动 https server;
3、执行 postStartHooks;
4、向 systemd 发送 ready 信号;

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go:351

func (s preparedGenericAPIServer) NonBlockingRun(stopCh <-chan struct{}) error


1. {
2. auditStopCh := make(chan struct{})
3.
4. // 1、判断是否要启动审计日志
5. if s.AuditBackend != nil {
6. if err := s.AuditBackend.Run(auditStopCh); err != nil {
7. return fmt.Errorf("failed to run the audit backend: %v", err)
8. }
9. }
10.
11. // 2、启动 https server
12. internalStopCh := make(chan struct{})
13. var stoppedCh <-chan struct{}
14. if s.SecureServingInfo != nil && s.Handler != nil {
15. var err error
stoppedCh, err = s.SecureServingInfo.Serve(s.Handler,
16. s.ShutdownTimeout, internalStopCh)
17. if err != nil {
18. close(internalStopCh)
19. close(auditStopCh)
20. return err
21. }
22. }
23.
24. go func() {
25. <-stopCh

本文档使用 书栈网 · BookStack.CN 构建 - 32 -


kube-apiserver 的设计与实现

26. close(s.readinessStopCh)
27. close(internalStopCh)
28. if stoppedCh != nil {
29. <-stoppedCh
30. }
31. s.HandlerChainWaitGroup.Wait()
32. close(auditStopCh)
33. }()
34.
35. // 3、执行 postStartHooks
36. s.RunPostStartHooks(stopCh)
37.
38. // 4、向 systemd 发送 ready 信号
39. if _, err := systemd.SdNotify(true, "READY=1\n"); err != nil {
klog.Errorf("Unable to send systemd daemon successful start message:
40. %v\n", err)
41. }
42.
43. return nil
44. }

以上就是 server 的初始化以及启动流程过程的分析,上文已经提到各 server 初始化过程中最重


要的就是 API Resource RESTStorage 的初始化以及路由的注册,由于该过程比较复杂,下文会
单独进行讲述。

storageFactory 的构建
上文已经提到过,apiserver 最终实现的 handler 对应的后端数据是以 Store 的结构保存的,
这里以 /api 开头的路由举例,通过 NewLegacyRESTStorage 方法创建各个资源的
RESTStorage。RESTStorage 是一个结构体,具体的定义
在 k8s.io/apiserver/pkg/registry/generic/registry/store.go 下,结构体内主要包
含 NewFunc 返回特定资源信息、 NewListFunc 返回特定资源列表、 CreateStrategy 特定资源
创建时的策略、 UpdateStrategy 更新时的策略以及 DeleteStrategy 删除时的策略等重要方法。
在 NewLegacyRESTStorage 内部,可以看到创建了多种资源的 RESTStorage。

NewLegacyRESTStorage 的调用链为 CreateKubeAPIServer -->


kubeAPIServerConfig.Complete().New --> m.InstallLegacyAPI -->
legacyRESTStorageProvider.NewLegacyRESTStorage 。

NewLegacyRESTStorage

一个 API Group 下的资源都有其 REST 实现, k8s.io/kubernetes/pkg/registry 下所有的

本文档使用 书栈网 · BookStack.CN 构建 - 33 -


kube-apiserver 的设计与实现

Group 都有一个rest目录,存储的就是对应资源的 RESTStorage。


在 NewLegacyRESTStorage 方法中,通过 NewREST 或者 NewStorage 会生成各种资源对应的
Storage,此处以 pod 为例进行说明。

k8s.io/kubernetes/pkg/registry/core/rest/storage_core.go:102

func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter


generic.RESTOptionsGetter) (LegacyRESTStorage, genericapiserver. APIGroupInfo,
1. error) {
2. apiGroupInfo := genericapiserver.APIGroupInfo{
PrioritizedVersions:
3. legacyscheme.Scheme.PrioritizedVersionsForGroup(""),
4. VersionedResourcesStorageMap: map[string]map[string]rest.Storage{},
5. Scheme: legacyscheme.Scheme,
6. ParameterCodec: legacyscheme.ParameterCodec,
7. NegotiatedSerializer: legacyscheme.Codecs,
8. }
9.
10. var podDisruptionClient policyclient.PodDisruptionBudgetsGetter
if policyGroupVersion := (schema.GroupVersion{Group: "policy", Version:
"v1beta1"}); legacyscheme.Scheme.
11. IsVersionRegistered(policyGroupVersion) {
12. var err error
podDisruptionClient, err =
13. policyclient.NewForConfig(c.LoopbackClientConfig)
14. if err != nil {
15. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
16. }
17. }
18. // 1、LegacyAPI 下的 resource RESTStorage 的初始化
19. restStorage := LegacyRESTStorage{}
20.
21. podTemplateStorage, err := podtemplatestore.NewREST(restOptionsGetter)
22. if err != nil {
23. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
24. }
eventStorage, err := eventstore.NewREST(restOptionsGetter,
25. uint64(c.EventTTL.Seconds()))
26. if err != nil {
27. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
28. }
29. limitRangeStorage, err := limitrangestore.NewREST(restOptionsGetter)
30. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 34 -


kube-apiserver 的设计与实现

31. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err


32. }
33.
34. ......
35.
36. endpointsStorage, err := endpointsstore.NewREST(restOptionsGetter)
37. if err != nil {
38. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
39. }
40.
nodeStorage, err := nodestore.NewStorage(restOptionsGetter,
41. c.KubeletClientConfig, c.ProxyTransport)
42. if err != nil {
43. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
44. }
45.
46. // 2、pod RESTStorage 的初始化
47. podStorage, err := podstore.NewStorage(......)
48. if err != nil {
49. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
50. }
51. ......
52.
serviceClusterIPAllocator, err :=
ipallocator.NewAllocatorCIDRRange(&serviceClusterIPRange, func(max int,
53. rangeSpec string) (allocator. Interface, error) {
54. ......
55. })
56. if err != nil {
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{},
57. fmt.Errorf("cannot create cluster IP allocator: %v", err)
58. }
59. restStorage.ServiceClusterIPAllocator = serviceClusterIPRegistry
60.
61. var secondaryServiceClusterIPAllocator ipallocator.Interface
if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) &&
62. c.SecondaryServiceIPRange.IP != nil {
63. ......
64. }
65.
66. var serviceNodePortRegistry rangeallocation.RangeRegistry

本文档使用 书栈网 · BookStack.CN 构建 - 35 -


kube-apiserver 的设计与实现

serviceNodePortAllocator, err :=
portallocator.NewPortAllocatorCustom(c.ServiceNodePortRange, func(max int,
67. rangeSpec string) (allocator.Interface, error) {
68. ......
69. })
70. if err != nil {
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{},
71. fmt.Errorf("cannot create cluster port allocator: %v", err)
72. }
73. restStorage.ServiceNodePortAllocator = serviceNodePortRegistry
74.
75. controllerStorage, err := controllerstore.NewStorage(restOptionsGetter)
76. if err != nil {
77. return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
78. }
79.
80. serviceRest, serviceRestProxy := servicestore.NewREST(......)
81.
82. // 3、restStorageMap 保存 resource http path 与 RESTStorage 对应关系
83. restStorageMap := map[string]rest.Storage{
84. "pods": podStorage.Pod,
85. "pods/attach": podStorage.Attach,
86. "pods/status": podStorage.Status,
87. "pods/log": podStorage.Log,
88. "pods/exec": podStorage.Exec,
89. "pods/portforward": podStorage.PortForward,
90. "pods/proxy": podStorage.Proxy,
91. ......
"componentStatuses":
92. componentstatus.NewStorage(componentStatusStorage{c.StorageFactory}.serversToValidate
93. }
94. ......
95. }

podstore.NewStorage

podstore.NewStorage 是为 pod 生成 storage 的方法,该方法主要功能是为 pod 创建后端


存储最终返回一个 RESTStorage 对象,其中调用 store.CompleteWithOptions 来创建后端存
储的。

k8s.io/kubernetes/pkg/registry/core/pod/storage/storage.go:71

1. func NewStorage(......) (PodStorage, error) {

本文档使用 书栈网 · BookStack.CN 构建 - 36 -


kube-apiserver 的设计与实现

2. store := &genericregistry.Store{
3. NewFunc: func() runtime.Object { return &api.Pod{} },
NewListFunc: func() runtime.Object { return &api.PodList{}
4. },
5. ......
6. }
7. options := &generic.StoreOptions{
8. RESTOptions: optsGetter,
9. AttrFunc: pod.GetAttrs,
TriggerFunc: map[string]storage.IndexerFunc{"spec.nodeName":
10. pod.NodeNameTriggerFunc},
11. }
12.
13. // 调用 store.CompleteWithOptions
14. if err := store.CompleteWithOptions(options); err != nil {
15. return PodStorage{}, err
16. }
17. statusStore := *store
18. statusStore.UpdateStrategy = pod.StatusStrategy
19. ephemeralContainersStore := *store
20. ephemeralContainersStore.UpdateStrategy = pod.EphemeralContainersStrategy
21.
22. bindingREST := &BindingREST{store: store}
23.
24. // PodStorage 对象
25. return PodStorage{
26. Pod: &REST{store, proxyTransport},
27. Binding: &BindingREST{store: store},
28. LegacyBinding: &LegacyBindingREST{bindingREST},
Eviction: newEvictionStorage(store,
29. podDisruptionBudgetClient),
30. Status: &StatusREST{store: &statusStore},
EphemeralContainers: &EphemeralContainersREST{store:
31. &ephemeralContainersStore},
32. Log: &podrest.LogREST{Store: store, KubeletConn: k},
Proxy: &podrest.ProxyREST{Store: store, ProxyTransport:
33. proxyTransport},
34. Exec: &podrest.ExecREST{Store: store, KubeletConn: k},
35. Attach: &podrest.AttachREST{Store: store, KubeletConn: k},
PortForward: &podrest.PortForwardREST{Store: store,
36. KubeletConn: k},
37. }, nil
38. }

本文档使用 书栈网 · BookStack.CN 构建 - 37 -


kube-apiserver 的设计与实现

可以看到最终返回的对象里对 pod 的不同操作都是一个 REST 对象,REST 中自动集成了


genericregistry.Store 对象,而 store.CompleteWithOptions 方法就是对
genericregistry.Store 对象中存储实例就行初始化的。

1. type REST struct {


2. *genericregistry.Store
3. proxyTransport http.RoundTripper
4. }
5.
6. type BindingREST struct {
7. store *genericregistry.Store
8. }
9. ......

store.CompleteWithOptions

store.CompleteWithOptions 主要功能是为 store 中的配置设置一些默认的值以及根据提供的


options 更新 store,其中最主要的就是初始化 store 的后端存储实例。

在 CompleteWithOptions 方法内,调用了 options.RESTOptions.GetRESTOptions 方法,其最


终返回 generic.RESTOptions 对象, generic.RESTOptions 对象中包含对 etcd 初始化的一
些配置、数据序列化方法以及对 etcd 操作的 storage.Interface 对象。其会依次调
用 StorageWithCacher-->NewRawStorage-->Create 方法创建最终依赖的后端存储。

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.g
o:1192

1. func (e *Store) CompleteWithOptions(options *generic.StoreOptions) error {


2. ......
3.
4. var isNamespaced bool
5. switch {
6. case e.CreateStrategy != nil:
7. isNamespaced = e.CreateStrategy.NamespaceScoped()
8. case e.UpdateStrategy != nil:
9. isNamespaced = e.UpdateStrategy.NamespaceScoped()
10. default:
return fmt.Errorf("store for %s must have CreateStrategy or
11. UpdateStrategy set", e.DefaultQualifiedResource.String())
12. }
13. ......
14.
15. // 1、调用 options.RESTOptions.GetRESTOptions

本文档使用 书栈网 · BookStack.CN 构建 - 38 -


kube-apiserver 的设计与实现

16. opts, err := options.RESTOptions.GetRESTOptions(e.DefaultQualifiedResource)


17. if err != nil {
18. return err
19. }
20.
21. // 2、设置 ResourcePrefix
22. prefix := opts.ResourcePrefix
23. if !strings.HasPrefix(prefix, "/") {
24. prefix = "/" + prefix
25. }
26.
27. if prefix == "/" {
return fmt.Errorf("store for %s has an invalid prefix %q",
28. e.DefaultQualifiedResource.String(), opts.ResourcePrefix)
29. }
30.
31. if e.KeyRootFunc == nil && e.KeyFunc == nil {
32. ......
33. }
34.
35. keyFunc := func(obj runtime.Object) (string, error) {
36. ......
37. }
38.
39. // 3、以下操作主要是将 opts 对象中的值赋值到 store 对象中
40. if e.DeleteCollectionWorkers == 0 {
41. e.DeleteCollectionWorkers = opts.DeleteCollectionWorkers
42. }
43.
44. e.EnableGarbageCollection = opts.EnableGarbageCollection
45. if e.ObjectNameFunc == nil {
46. ......
47. }
48.
49. if e.Storage.Storage == nil {
50. e.Storage.Codec = opts.StorageConfig.Codec
51. var err error
52. e.Storage.Storage, e.DestroyFunc, err = opts.Decorator(
53. opts.StorageConfig,
54. prefix,
55. keyFunc,
56. e.NewFunc,

本文档使用 书栈网 · BookStack.CN 构建 - 39 -


kube-apiserver 的设计与实现

57. e.NewListFunc,
58. attrFunc,
59. options.TriggerFunc,
60. )
61. if err != nil {
62. return err
63. }
64. e.StorageVersioner = opts.StorageConfig.EncodeVersioner
65.
66. if opts.CountMetricPollPeriod > 0 {
67. stopFunc := e.startObservingCount(opts.CountMetricPollPeriod)
68. previousDestroy := e.DestroyFunc
69. e.DestroyFunc = func() {
70. stopFunc()
71. if previousDestroy != nil {
72. previousDestroy()
73. }
74. }
75. }
76. }
77.
78. return nil
79. }

options.RESTOptions 是一个 interface,想要找到其 GetRESTOptions 方法的实现必须


知道 options.RESTOptions 初始化时对应的实例,其初始化是在
CreateKubeAPIServerConfig --> buildGenericConfig -->
s.Etcd.ApplyWithStorageFactoryTo 方法中进行初始化的, RESTOptions 对应的实例为
StorageFactoryRestOptionsFactory ,所以 PodStorage 初始时构建的 store 对象
中 genericserver.Config.RESTOptionsGetter 实际的对象类型为
StorageFactoryRestOptionsFactory ,其 GetRESTOptions 方法如下所示:

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/server/options/etcd.go:253

func (f *StorageFactoryRestOptionsFactory) GetRESTOptions(resource


1. schema.GroupResource) (generic.RESTOptions, error) {
2. storageConfig, err := f.StorageFactory.NewConfig(resource)
3. if err != nil {
return generic.RESTOptions{}, fmt.Errorf("unable to find storage
4. destination for %v, due to %v", resource, err.Error())
5. }
6.

本文档使用 书栈网 · BookStack.CN 构建 - 40 -


kube-apiserver 的设计与实现

7. ret := generic.RESTOptions{
8. StorageConfig: storageConfig,
9. Decorator: generic.UndecoratedStorage,
10. DeleteCollectionWorkers: f.Options.DeleteCollectionWorkers,
11. EnableGarbageCollection: f.Options.EnableGarbageCollection,
12. ResourcePrefix: f.StorageFactory.ResourcePrefix(resource),
13. CountMetricPollPeriod: f.Options.StorageConfig.CountMetricPollPeriod,
14. }
15. if f.Options.EnableWatchCache {
16. sizes, err := ParseWatchCacheSizes(f.Options.WatchCacheSizes)
17. if err != nil {
18. return generic.RESTOptions{}, err
19. }
20. cacheSize, ok := sizes[resource]
21. if !ok {
22. cacheSize = f.Options.DefaultWatchCacheSize
23. }
24. // 调用 generic.StorageDecorator
25. ret.Decorator = genericregistry.StorageWithCacher(cacheSize)
26. }
27.
28. return ret, nil
29. }

在 genericregistry.StorageWithCacher 中又调用了不同的方法最终会调用
factory.Create 来初始化存储实例,其调用链为: genericregistry.StorageWithCacher -->
generic.NewRawStorage --> factory.Create 。

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory/fa
ctory.go:30

1. func Create(c storagebackend.Config) (storage.Interface, DestroyFunc, error) {


2. switch c.Type {
3. case "etcd2":
return nil, nil, fmt.Errorf("%v is no longer a supported storage
4. backend", c.Type)
5. // 目前 k8s 只支持使用 etcd v3
6. case storagebackend.StorageTypeUnset, storagebackend.StorageTypeETCD3:
7. return newETCD3Storage(c)
8. default:
9. return nil, nil, fmt.Errorf("unknown storage type: %s", c.Type)
10. }
11. }

本文档使用 书栈网 · BookStack.CN 构建 - 41 -


kube-apiserver 的设计与实现

newETCD3Storage

在 newETCD3Storage 中,首先通过调用 newETCD3Client 创建 etcd 的 client,


client 的创建最终是通过 etcd 官方提供的客户端工具 clientv3 进行创建的。

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory/et
cd3.go:209

func newETCD3Storage(c storagebackend.Config) (storage.Interface, DestroyFunc,


1. error) {
2. stopCompactor, err := startCompactorOnce(c.Transport, c.CompactionInterval)
3. if err != nil {
4. return nil, nil, err
5. }
6.
7. client, err := newETCD3Client(c.Transport)
8. if err != nil {
9. stopCompactor()
10. return nil, nil, err
11. }
12.
13. var once sync.Once
14. destroyFunc := func() {
15. once.Do(func() {
16. stopCompactor()
17. client.Close()
18. })
19. }
20. transformer := c.Transformer
21. if transformer == nil {
22. transformer = value.IdentityTransformer
23. }
return etcd3.New(client, c.Codec, c.Prefix, transformer, c.Paging),
24. destroyFunc, nil
25. }

至此对于 pod resource 中 store 的构建基本分析完成,不同 resource 对应一个 REST 对


象,其中又引用了 genericregistry.Store 对象,最终是对 genericregistry.Store 的初
始化。在分析完 store 的初始化后还有一个重要的步骤就是路由的注册,路由注册主要的流程是为
resource 根据不同 verbs 构建 http path 以及将 path 与对应 handler 进行绑定。

路由注册

本文档使用 书栈网 · BookStack.CN 构建 - 42 -


kube-apiserver 的设计与实现

上文 RESTStorage 的构建对应的是 InstallLegacyAPI 中的


legacyRESTStorageProvider.NewLegacyRESTStorage 方法,下面继续分析
InstallLegacyAPI 中的 m.GenericAPIServer.InstallLegacyAPIGroup 方法的实现。

k8s.io/kubernetes/pkg/master/master.go:406

1. func (m *Master) InstallLegacyAPI(......) error {


legacyRESTStorage, apiGroupInfo, err :=
2. legacyRESTStorageProvider.NewLegacyRESTStorage(restOptionsGetter)
3. if err != nil {
4. return fmt.Errorf("Error building core storage: %v", err)
5. }
6. ......
7.
if err :=
m.GenericAPIServer.InstallLegacyAPIGroup(genericapiserver.DefaultLegacyAPIPrefix,
8. &apiGroupInfo); err != nil {
9. return fmt.Errorf("Error in registering group versions: %v", err)
10. }
11. return nil
12. }

m.GenericAPIServer.InstallLegacyAPIGroup 的调用链非常深,最终是为 Group 下每一个


API resources 注册 handler 及路由信息,其调用链
为: m.GenericAPIServer.InstallLegacyAPIGroup --> s.installAPIResources -->
apiGroupVersion.InstallREST --> installer.Install --> a.registerResourceHandlers 。其
中几个方法的作用如下所示:

s.installAPIResources :为每一个 API resource 调用


apiGroupVersion.InstallREST 添加路由;
apiGroupVersion.InstallREST :将 restful.WebServic 对象添加到 container 中;
installer.Install :返回最终的 restful.WebService 对象

a.registerResourceHandlers

该方法实现了 rest.Storage 到 restful.Route 的转换,其首先会判断 API Resource 所


支持的 REST 接口,然后为 REST 接口添加对应的 handler,最后将其注册到路由中。

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go:181

func (a *APIInstaller) registerResourceHandlers(path string, storage


1. rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
2. admit := a.group.Admit

本文档使用 书栈网 · BookStack.CN 构建 - 43 -


kube-apiserver 的设计与实现

3.
4. ......
5.
// 1、判断该 resource 实现了哪些 REST 操作接口,以此来判断其支持的 verbs 以便为其添
6. 加路由
7. creater, isCreater := storage.(rest.Creater)
8. namedCreater, isNamedCreater := storage.(rest.NamedCreater)
9. lister, isLister := storage.(rest.Lister)
10. getter, isGetter := storage.(rest.Getter)
11. getterWithOptions, isGetterWithOptions := storage.(rest.GetterWithOptions)
12. gracefulDeleter, isGracefulDeleter := storage.(rest.GracefulDeleter)
13. collectionDeleter, isCollectionDeleter := storage.(rest.CollectionDeleter)
14. updater, isUpdater := storage.(rest.Updater)
15. patcher, isPatcher := storage.(rest.Patcher)
16. watcher, isWatcher := storage.(rest.Watcher)
17. connecter, isConnecter := storage.(rest.Connecter)
18. storageMeta, isMetadata := storage.(rest.StorageMetadata)
storageVersionProvider, isStorageVersionProvider := storage.
19. (rest.StorageVersionProvider)
20. if !isMetadata {
21. storageMeta = defaultStorageMetadata{}
22. }
23. exporter, isExporter := storage.(rest.Exporter)
24. if !isExporter {
25. exporter = nil
26. }
27.
28. ......
29.
30. // 2、为 resource 添加对应的 actions 并根据是否支持 namespace
31. switch {
32. case !namespaceScoped:
33. ......
34.
actions = appendIf(actions, action{"LIST", resourcePath,
35. resourceParams, namer, false}, isLister)
actions = appendIf(actions, action{"POST", resourcePath,
36. resourceParams, namer, false}, isCreater)
actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath,
37. resourceParams, namer, false}, isCollectionDeleter)
actions = appendIf(actions, action{"WATCHLIST", "watch/" +
38. resourcePath, resourceParams, namer, false}, allowWatchList)
39.

本文档使用 书栈网 · BookStack.CN 构建 - 44 -


kube-apiserver 的设计与实现

actions = appendIf(actions, action{"GET", itemPath, nameParams, namer,


40. false}, isGetter)
41. if getSubpath {
actions = appendIf(actions, action{"GET", itemPath + "/{path:*}",
42. proxyParams, namer, false}, isGetter)
43. }
actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer,
44. false}, isUpdater)
actions = appendIf(actions, action{"PATCH", itemPath, nameParams,
45. namer, false}, isPatcher)
actions = appendIf(actions, action{"DELETE", itemPath, nameParams,
46. namer, false}, isGracefulDeleter)
actions = appendIf(actions, action{"WATCH", "watch/" + itemPath,
47. nameParams, namer, false}, isWatcher)
actions = appendIf(actions, action{"CONNECT", itemPath, nameParams,
48. namer, false}, isConnecter)
actions = appendIf(actions, action{"CONNECT", itemPath + "/{path:*}",
49. proxyParams, namer, false}, isConnecter && connectSubpath)
50. default:
51. ......
actions = appendIf(actions, action{"LIST", resourcePath,
52. resourceParams, namer, false}, isLister)
actions = appendIf(actions, action{"POST", resourcePath,
53. resourceParams, namer, false}, isCreater)
actions = appendIf(actions, action{"DELETECOLLECTION", resourcePath,
54. resourceParams, namer, false}, isCollectionDeleter)
actions = appendIf(actions, action{"WATCHLIST", "watch/" +
55. resourcePath, resourceParams, namer, false}, allowWatchList)
56.
actions = appendIf(actions, action{"GET", itemPath, nameParams, namer,
57. false}, isGetter)
58. ......
59. }
60.
61. // 3、根据 action 创建对应的 route
62. kubeVerbs := map[string]struct{}{}
63. reqScope := handlers.RequestScope{
64. Serializer: a.group.Serializer,
65. ParameterCodec: a.group.ParameterCodec,
66. Creater: a.group.Creater,
67. Convertor: a.group.Convertor,
68. ......
69. }
70. ......

本文档使用 书栈网 · BookStack.CN 构建 - 45 -


kube-apiserver 的设计与实现

71. // 4、从 rest.Storage 到 restful.Route 映射


72. // 为每个操作添加对应的 handler
73. for _, action := range actions {
74. ......
75. verbOverrider, needOverride := storage.(StorageMetricsOverride)
76. switch action.Verb {
77. case "GET": ......
78. case "LIST":
79. case "PUT":
80. case "PATCH":
81. // 此处以 POST 操作进行说明
82. case "POST":
83. var handler restful.RouteFunction
84. // 5、初始化 handler
85. if isNamedCreater {
handler = restfulCreateNamedResource(namedCreater, reqScope,
86. admit)
87. } else {
88. handler = restfulCreateResource(creater, reqScope, admit)
89. }
handler = metrics.InstrumentRouteFunc(action.Verb, group, version,
90. resource, subresource, requestScope, metrics.APIServerComponent, handler)
91. article := GetArticleForNoun(kind, " ")
92. doc := "create" + article + kind
93. if isSubresource {
94. doc = "create " + subresource + " of" + article + kind
95. }
96. // 6、route 与 handler 进行绑定
97. route := ws.POST(action.Path).To(handler).
98. Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output
99. is pretty printed.")).

100. Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb),
101. mediaTypes...)...).
102. Returns(http.StatusOK, "OK", producedObject).
103. Returns(http.StatusCreated, "Created", producedObject).
104. Returns(http.StatusAccepted, "Accepted", producedObject).
105. Reads(defaultVersionedObject).
106. Writes(producedObject)
if err := AddObjectParams(ws, route, versionedCreateOptions); err
107. != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 46 -


kube-apiserver 的设计与实现

108. return nil, err


109. }
110. addParams(route, action.Params)
111. // 7、添加到路由中
112. routes = append(routes, route)
113. case "DELETE":
114. case "DELETECOLLECTION":
115. case "WATCH":
116. case "WATCHLIST":
117. case "CONNECT":
118. default:
119. }
120. ......
121. return &apiResource, nil
122. }

restfulCreateNamedResource

restfulCreateNamedResource 是 POST 操作对应的 handler,最终会调用


createHandler 方法完成。

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go:1087

func restfulCreateNamedResource(r rest.NamedCreater, scope


1. handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
2. return func(req *restful.Request, res *restful.Response) {
handlers.CreateNamedResource(r, &scope, admit)(res.ResponseWriter,
3. req.Request)
4. }
5. }
6.
func CreateNamedResource(r rest.NamedCreater, scope *RequestScope, admission
7. admission.Interface) http.HandlerFunc {
8. return createHandler(r, scope, admission, true)
9. }

createHandler

createHandler 是将数据写入到后端存储的方法,对于资源的操作都有相关的权限控制,在
createHandler 中首先会执行 decoder 和 admission 操作,然后调用 create 方
法完成 resource 的创建,在 create 方法中会进行 validate 以及最终将数据保存到后端
存储中。 admit 操作即执行 kube-apiserver 中的 admission-plugins,admission-
plugins 在 CreateKubeAPIServerConfig 中被初始化为了 admissionChain,其初始化的调

本文档使用 书栈网 · BookStack.CN 构建 - 47 -


kube-apiserver 的设计与实现

用链为 CreateKubeAPIServerConfig --> buildGenericConfig --> s.Admission.ApplyTo -->


a.GenericAdmission.ApplyTo --> a.Plugins.NewFromPlugins ,最终在
a.Plugins.NewFromPlugins 中将所有已启用的 plugins 封装为 admissionChain,此处要执
行的 admit 操作即执行 admission-plugins 中的 admit 操作。

createHandler 中调用的 create 方法是 genericregistry.Store 对象的方法,在每个


resource 初始化 RESTStorage 都会引入 genericregistry.Store 对象。

createHandler 中所有的操作就是本文开头提到的请求流程,如下所示:

1. v1beta1 ⇒ internal ⇒ | ⇒ | ⇒ v1 ⇒ json/yaml ⇒ etcd


2. admission validation

k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go:46

func createHandler(r rest.NamedCreater, scope *RequestScope, admit


1. admission.Interface, includeName bool) http.HandlerFunc {
2. return func(w http.ResponseWriter, req *http.Request) {
3. trace := utiltrace.New("Create", utiltrace.Field{"url", req.URL.Path})
4. defer trace.LogIfLong(500 * time.Millisecond)
5. ......
6.
7. gv := scope.Kind.GroupVersion()
8. // 1、得到合适的SerializerInfo
s, err := negotiation.NegotiateInputSerializer(req, false,
9. scope.Serializer)
10. if err != nil {
11. scope.err(err, w, req)
12. return
13. }
14. // 2、找到合适的 decoder
decoder := scope.Serializer.DecoderToVersion(s.Serializer,
15. scope.HubGroupVersion)
16.
17. body, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
18. if err != nil {
19. scope.err(err, w, req)
20. return
21. }
22.
23. ......
24.
25. defaultGVK := scope.Kind

本文档使用 书栈网 · BookStack.CN 构建 - 48 -


kube-apiserver 的设计与实现

26. original := r.New()


27. trace.Step("About to convert to expected version")
28. // 3、decoder 解码
29. obj, gvk, err := decoder.Decode(body, &defaultGVK, original)
30. ......
31.
32. ae := request.AuditEventFrom(ctx)
33. admit = admission.WithAudit(admit, ae)
audit.LogRequestObject(ae, obj, scope.Resource, scope.Subresource,
34. scope.Serializer)
35.
36. userInfo, _ := request.UserFrom(ctx)
37.
38.
39. if len(name) == 0 {
40. _, name, _ = scope.Namer.ObjectName(obj)
41. }
// 4、执行 admit 操作,即执行 kube-apiserver 启动时加载的 admission-
42. plugins,
43. admissionAttributes := admission.NewAttributesRecord(......)
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok &&
44. mutatingAdmission.Handles(admission.Create) {
45. err = mutatingAdmission.Admit(ctx, admissionAttributes, scope)
46. if err != nil {
47. scope.err(err, w, req)
48. return
49. }
50. }
51.
52. ......
53. // 5、执行 create 操作
54. result, err := finishRequest(timeout, func() (runtime.Object, error) {
55. return r.Create(
56. ctx,
57. name,
58. obj,
rest.AdmissionToValidateObjectFunc(admit, admissionAttributes,
59. scope),
60. options,
61. )
62. })
63. ......
64. }

本文档使用 书栈网 · BookStack.CN 构建 - 49 -


kube-apiserver 的设计与实现

65. }

总结
本文主要分析 kube-apiserver 的启动流程,kube-apiserver 中包含三个 server,分别为
KubeAPIServer、APIExtensionsServer 以及 AggregatorServer,三个 server 是通过委
托模式连接在一起的,初始化过程都是类似的,首先为每个 server 创建对应的 config,然后初始
化 http server,http server 的初始化过程为首先初始化 GoRestfulContainer ,然后安装
server 所包含的 API,安装 API 时首先为每个 API Resource 创建对应的后端存储
RESTStorage,再为每个 API Resource 支持的 verbs 添加对应的 handler,并将 handler
注册到 route 中,最后将 route 注册到 webservice 中,启动流程中 RESTFul API 的实现流
程是其核心,至于 kube-apiserver 中认证鉴权等 filter 的实现、多版本资源转换、
kubernetes service 的实现等一些细节会在后面的文章中继续进行分析。

参考:

https://mp.weixin.qq.com/s/hTEWatYLhTnC5X0FBM2RWQ

https://bbbmj.github.io/2019/04/13/Kubernetes/code-analytics/kube-
apiserver/

https://mp.weixin.qq.com/s/TQuqAAzBjeWHwKPJZ3iJhA

https://blog.openshift.com/kubernetes-deep-dive-api-server-part-1/

https://www.jianshu.com/p/daa4ff387a78

本文档使用 书栈网 · BookStack.CN 构建 - 50 -


kube-apiserver 中 apiserver service 的实现

在 kubernetes,可以从集群外部和内部两种方式访问 kubernetes API,在集群外直接访问


apiserver 提供的 API,在集群内即 pod 中可以通过访问 service 为 kubernetes 的
ClusterIP。kubernetes 集群在初始化完成后就会创建一个 kubernetes service,该
service 是 kube-apiserver 创建并进行维护的,如下所示:

1. $ kubectl get service


2. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
3. kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4d22h
4.
5. $ kubectl get endpoints kubernetes
6. NAME ENDPOINTS AGE
7. kubernetes 192.168.99.113:6443 4d22h

内置的 kubernetes service 无法删除,其 ClusterIP 为通过 --service-cluster-ip-


range 参数指定的 ip 段中的首个 ip,kubernetes endpoints 中的 ip 以及 port 可以通
过 --advertise-address 和 --secure-port 启动参数来指定。

kubernetes service 是由 kube-apiserver 中的 bootstrap controller 进行控制的,其


主要以下几个功能:

创建 kubernetes service;
创建 default、kube-system 和 kube-public 命名空间,如果启用了 NodeLease 特性
还会创建 kube-node-lease 命名空间;
提供基于 Service ClusterIP 的修复及检查功能;
提供基于 Service NodePort 的修复及检查功能;

kubernetes service 默认使用 ClusterIP 对外暴露服务,若要使用 nodePort 的方式可在


kube-apiserver 启动时通过 --kubernetes-service-node-port 参数指定对应的端口。

bootstrap controller 源码分析

kubernetes 版本:v1.16

bootstrap controller 的初始化以及启动是在 CreateKubeAPIServer 调用链的


InstallLegacyAPI 方法中完成的,bootstrap controller 的启停是由 apiserver 的
PostStartHook 和 ShutdownHook 进行控制的。

k8s.io/kubernetes/pkg/master/master.go:406

1. func (m *Master) InstallLegacyAPI(......) error {


legacyRESTStorage, apiGroupInfo, err :=
2. legacyRESTStorageProvider.NewLegacyRESTStorage(restOptionsGetter)

本文档使用 书栈网 · BookStack.CN 构建 - 51 -


kube-apiserver 中 apiserver service 的实现

3. if err != nil {
4. return fmt.Errorf("Error building core storage: %v", err)
5. }
6.
7. // 初始化 bootstrap-controller
8. controllerName := "bootstrap-controller"
coreClient :=
9. corev1client.NewForConfigOrDie(c.GenericConfig.LoopbackClientConfig)
10. bootstrapController := c.NewBootstrapController(......)
m.GenericAPIServer.AddPostStartHookOrDie(controllerName,
11. bootstrapController.PostStartHook)
m.GenericAPIServer.AddPreShutdownHookOrDie(controllerName,
12. bootstrapController.PreShutdownHook)
13.
if err :=
m.GenericAPIServer.InstallLegacyAPIGroup(genericapiserver.DefaultLegacyAPIPrefix,
14. &apiGroupInfo); err != nil {
15. return fmt.Errorf("Error in registering group versions: %v", err)
16. }
17. return nil
18. }

postStartHooks 会在 kube-apiserver 的启动方法 prepared.Run 中调用


RunPostStartHooks 启动所有 Hook。

NewBootstrapController

bootstrap controller 在初始化时需要设定多个参数,主要有 PublicIP、ServiceCIDR、


PublicServicePort 等。PublicIP 是通过命令行参数 --advertise-address 指定的,如果
没有指定,系统会自动选出一个 global IP。PublicServicePort 通过 --secure-port 启
动参数来指定(默认为 6443),ServiceCIDR 通过 --service-cluster-ip-range 参数指定
(默认为 10.0.0.0/24)。

k8s.io/kubernetes/pkg/master/controller.go:89

1. func (c *completedConfig) NewBootstrapController(......) *Controller {


2. // 1、获取 PublicServicePort
3. _, publicServicePort, err := c.GenericConfig.SecureServing.HostPort()
4. if err != nil {
5. klog.Fatalf("failed to get listener address: %v", err)
6. }
7.

本文档使用 书栈网 · BookStack.CN 构建 - 52 -


kube-apiserver 中 apiserver service 的实现

8. // 2、指定需要创建的 kube-system 和 kube-public


systemNamespaces := []string{metav1.NamespaceSystem,
9. metav1.NamespacePublic}
10. if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
11. systemNamespaces = append(systemNamespaces, corev1.NamespaceNodeLease)
12. }
13.
14. return &Controller{
15. ......
// ServiceClusterIPRegistry 是在 CreateKubeAPIServer 初始化 RESTStorage
16. 时初始化的,是一个 etcd 实例
ServiceClusterIPRegistry:
17. legacyRESTStorage.ServiceClusterIPAllocator,
18. ServiceClusterIPRange: c.ExtraConfig.ServiceIPRange,
SecondaryServiceClusterIPRegistry:
19. legacyRESTStorage.SecondaryServiceClusterIPAllocator,
20.
21. // SecondaryServiceClusterIPRange 需要在启用 IPv6DualStack 后才能使用
SecondaryServiceClusterIPRange:
22. c.ExtraConfig.SecondaryServiceIPRange,
23.
24. ServiceClusterIPInterval: 3 * time.Minute,
25.
26. ServiceNodePortRegistry: legacyRESTStorage.ServiceNodePortAllocator,
27. ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange,
28. ServiceNodePortInterval: 3 * time.Minute,
29.
30. // API Server 绑定的IP,这个IP会作为kubernetes service的Endpoint的IP
31. PublicIP: c.GenericConfig.PublicAddress,
32. // 取 clusterIP range 中的第一个 IP
33. ServiceIP: c.ExtraConfig.APIServerServiceIP,
34. // 默认为 6443
35. ServicePort: c.ExtraConfig.APIServerServicePort,
36. ExtraServicePorts: c.ExtraConfig.ExtraServicePorts,
37. ExtraEndpointPorts: c.ExtraConfig.ExtraEndpointPorts,
38. // 这里为 6443
39. PublicServicePort: publicServicePort,
40.
41. // 缺省是基于 ClusterIP 启动模式,这里为0
42. KubernetesServiceNodePort: c.ExtraConfig.KubernetesServiceNodePort,
43. }
44. }

本文档使用 书栈网 · BookStack.CN 构建 - 53 -


kube-apiserver 中 apiserver service 的实现

自动选出 global IP 的代码如下所示:

k8s.io/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/net/interface.go:323

1. func ChooseHostInterface() (net.IP, error) {


2. var nw networkInterfacer = networkInterface{}
3. if _, err := os.Stat(ipv4RouteFile); os.IsNotExist(err) {
4. return chooseIPFromHostInterfaces(nw)
5. }
6. routes, err := getAllDefaultRoutes()
7. if err != nil {
8. return nil, err
9. }
10. return chooseHostInterfaceFromRoute(routes, nw)
11. }

bootstrapController.Start

上文已经提到了 bootstrap controller 主要的四个功能:修复 ClusterIP、修复


NodePort、更新 kubernetes service、创建系统所需要的名字空间(default、kube-
system、kube-public)。bootstrap controller 在启动后首先会完成一次 ClusterIP、
NodePort 和 Kubernets 服务的处理,然后异步循环运行上面的4个工作。以下是其 start 方
法:

k8s.io/kubernetes/pkg/master/controller.go:146

1. func (c *Controller) Start() {


2. if c.runner != nil {
3. return
4. }
5.
6. // 1、首次启动时首先从 kubernetes endpoints 中移除自身的配置,
7. // 此时 kube-apiserver 可能处于非 ready 状态
endpointPorts := createEndpointPortSpec(c.PublicServicePort, "https",
8. c.ExtraEndpointPorts)
if err := c.EndpointReconciler.RemoveEndpoints(kubernetesServiceName,
9. c.PublicIP, endpointPorts); err != nil {
klog.Errorf("Unable to remove old endpoints from kubernetes service:
10. %v", err)
11. }
12.
13. // 2、初始化 repairClusterIPs 和 repairNodePorts 对象
14. repairClusterIPs := servicecontroller.NewRepair(......)

本文档使用 书栈网 · BookStack.CN 构建 - 54 -


kube-apiserver 中 apiserver service 的实现

15. repairNodePorts := portallocatorcontroller.NewRepair(......)


16.
17. // 3、首先运行一次 epairClusterIPs 和 repairNodePorts,即进行初始化
18. if err := repairClusterIPs.RunOnce(); err != nil {
19. klog.Fatalf("Unable to perform initial IP allocation check: %v", err)
20. }
21. if err := repairNodePorts.RunOnce(); err != nil {
klog.Fatalf("Unable to perform initial service nodePort check: %v",
22. err)
23. }
24. // 4、定期执行 bootstrap controller 主要的四个功能
c.runner = async.NewRunner(c.RunKubernetesNamespaces,
25. c.RunKubernetesService, repairClusterIPs.RunUntil, repairNodePorts.RunUntil)
26. c.runner.Start()
27. }

c.RunKubernetesNamespaces

c.RunKubernetesNamespaces 主要功能是创建 kube-system 和 kube-public 命名空间,如


果启用了 NodeLease 特性功能还会创建 kube-node-lease 命名空间,之后每隔一分钟检查一
次。

k8s.io/kubernetes/pkg/master/controller.go:199

1. func (c *Controller) RunKubernetesNamespaces(ch chan struct{}) {


2. wait.Until(func() {
3. for _, ns := range c.SystemNamespaces {
if err := createNamespaceIfNeeded(c.NamespaceClient, ns); err !=
4. nil {
runtime.HandleError(fmt.Errorf("unable to create required
5. kubernetes system namespace %s: %v", ns, err))
6. }
7. }
8. }, c.SystemNamespacesInterval, ch)
9. }

c.RunKubernetesService

c.RunKubernetesService 主要是检查 kubernetes service 是否处于正常状态,并定期执行


同步操作。首先调用 /healthz 接口检查 apiserver 当前是否处于 ready 状态,若处于
ready 状态然后调用 c.UpdateKubernetesService 服务更新 kubernetes service 状态。

k8s.io/kubernetes/pkg/master/controller.go:210

本文档使用 书栈网 · BookStack.CN 构建 - 55 -


kube-apiserver 中 apiserver service 的实现

1. func (c *Controller) RunKubernetesService(ch chan struct{}) {


2. wait.PollImmediateUntil(100*time.Millisecond, func() (bool, error) {
3. var code int
4. c.healthClient.Get().AbsPath("/healthz").Do().StatusCode(&code)
5. return code == http.StatusOK, nil
6. }, ch)
7.
8. wait.NonSlidingUntil(func() {
9. if err := c.UpdateKubernetesService(false); err != nil {
runtime.HandleError(fmt.Errorf("unable to sync kubernetes service:
10. %v", err))
11. }
12. }, c.EndpointInterval, ch)
13. }

c.UpdateKubernetesService

c.UpdateKubernetesService 的主要逻辑为:

1、调用 createNamespaceIfNeeded 创建 default namespace;


2、调用 c.CreateOrUpdateMasterServiceIfNeeded 为 master 创建 kubernetes
service;
3、调用 c.EndpointReconciler.ReconcileEndpoints 更新 master 的 endpoint;

k8s.io/kubernetes/pkg/master/controller.go:230

1. func (c *Controller) UpdateKubernetesService(reconcile bool) error {


if err := createNamespaceIfNeeded(c.NamespaceClient,
2. metav1.NamespaceDefault); err != nil {
3. return err
4. }
5.
servicePorts, serviceType := createPortAndServiceSpec(c.ServicePort,
6. c.PublicServicePort, c.KubernetesServiceNodePort, "https", c.ExtraServicePorts)
if err := c.CreateOrUpdateMasterServiceIfNeeded(kubernetesServiceName,
7. c.ServiceIP, servicePorts, serviceType, reconcile); err != nil {
8. return err
9. }
endpointPorts := createEndpointPortSpec(c.PublicServicePort, "https",
10. c.ExtraEndpointPorts)
if err := c.EndpointReconciler.ReconcileEndpoints(kubernetesServiceName,
11. c.PublicIP, endpointPorts, reconcile); err != nil {
12. return err

本文档使用 书栈网 · BookStack.CN 构建 - 56 -


kube-apiserver 中 apiserver service 的实现

13. }
14. return nil
15. }

c.EndpointReconciler.ReconcileEndpoints

EndpointReconciler 的具体实现由 EndpointReconcilerType 决


定, EndpointReconcilerType 是 --endpoint-reconciler-type 参数指定的,可选的参数
有 master-count, lease, none ,每种类型对应不同的 EndpointReconciler 实例,在
v1.16 中默认为 lease,此处仅分析 lease 对应的 EndpointReconciler 的实现。

一个集群中可能会有多个 apiserver 实例,因此需要统一管理 apiserver service 的


endpoints, c.EndpointReconciler.ReconcileEndpoints 就是用来管理 apiserver
endpoints 的。一个集群中 apiserver 的所有实例会在 etcd 中的对应目录下创建 key,并定
期更新这个 key 来上报自己的心跳信息,ReconcileEndpoints 会从 etcd 中获取 apiserver
的实例信息并更新 endpoint。

k8s.io/kubernetes/pkg/master/reconcilers/lease.go:144

1. func (r *leaseEndpointReconciler) ReconcileEndpoints(......) error {


2. r.reconcilingLock.Lock()
3. defer r.reconcilingLock.Unlock()
4.
5. if r.stopReconcilingCalled {
6. return nil
7. }
8.
9. // 更新 lease 信息
10. if err := r.masterLeases.UpdateLease(ip.String()); err != nil {
11. return err
12. }
13.
14. return r.doReconcile(serviceName, endpointPorts, reconcilePorts)
15. }
16. func (r *leaseEndpointReconciler) doReconcile(......) error {
17. // 1、获取 master 的 endpoint
e, err := r.epAdapter.Get(corev1.NamespaceDefault, serviceName,
18. metav1.GetOptions{})
19. shouldCreate := false
20. if err != nil {
21. if !errors.IsNotFound(err) {
22. return err
23. }

本文档使用 书栈网 · BookStack.CN 构建 - 57 -


kube-apiserver 中 apiserver service 的实现

24.
25. shouldCreate = true
26. e = &corev1.Endpoints{
27. ObjectMeta: metav1.ObjectMeta{
28. Name: serviceName,
29. Namespace: corev1.NamespaceDefault,
30. },
31. }
32. }
33.
34. // 2、从 etcd 中获取所有的 master
35. masterIPs, err := r.masterLeases.ListLeases()
36. if err != nil {
37. return err
38. }
39.
40. if len(masterIPs) == 0 {
return fmt.Errorf("no master IPs were listed in storage, refusing to
41. erase all endpoints for the kubernetes service")
42. }
43.
44. // 3、检查 endpoint 中 master 信息,如果与 etcd 中的不一致则进行更新
formatCorrect, ipCorrect, portsCorrect :=
45. checkEndpointSubsetFormatWithLease(e, masterIPs, endpointPorts, reconcilePorts)
46. if formatCorrect && ipCorrect && portsCorrect {
47. return nil
48. }
49.
50. if !formatCorrect {
51. e.Subsets = []corev1.EndpointSubset{{
52. Addresses: []corev1.EndpointAddress{},
53. Ports: endpointPorts,
54. }}
55. }
56. if !formatCorrect || !ipCorrect {
57. e.Subsets[0].Addresses = make([]corev1.EndpointAddress, len(masterIPs))
58. for ind, ip := range masterIPs {
59. e.Subsets[0].Addresses[ind] = corev1.EndpointAddress{IP: ip}
60. }
61.
62. e.Subsets = endpointsv1.RepackSubsets(e.Subsets)
63. }
64.

本文档使用 书栈网 · BookStack.CN 构建 - 58 -


kube-apiserver 中 apiserver service 的实现

65. if !portsCorrect {
66. e.Subsets[0].Ports = endpointPorts
67. }
68.
69. if shouldCreate {
if _, err = r.epAdapter.Create(corev1.NamespaceDefault, e);
70. errors.IsAlreadyExists(err) {
71. err = nil
72. }
73. } else {
74. _, err = r.epAdapter.Update(corev1.NamespaceDefault, e)
75. }
76. return err
77. }

repairClusterIPs.RunUntil

repairClusterIP 主要解决的问题有:

保证集群中所有的 ClusterIP 都是唯一分配的;


保证分配的 ClusterIP 不会超出指定范围;
确保已经分配给 service 但是因为 crash 等其他原因没有正确创建 ClusterIP;
自动将旧版本的 Kubernetes services 迁移到 ipallocator 原子性模型;

repairClusterIPs.RunUntil 其实是调用 repairClusterIPs.runOnce 来处理的,其代码中


的主要逻辑如下所示:

k8s.io/kubernetes/pkg/registry/core/service/ipallocator/controller/repair.go:134

1. func (c *Repair) runOnce() error {


2. ......
3.
4. // 1、首先从 etcd 中获取已经使用 ClusterIP 的快照
err = wait.PollImmediate(time.Second, 10*time.Second, func() (bool, error)
5. {
6. var err error
7. snapshot, err = c.alloc.Get()
8. if err != nil {
9. return false, err
10. }
11.
12. if c.shouldWorkOnSecondary() {
13. secondarySnapshot, err = c.secondaryAlloc.Get()

本文档使用 书栈网 · BookStack.CN 构建 - 59 -


kube-apiserver 中 apiserver service 的实现

14. if err != nil {


15. return false, err
16. }
17. }
18. return true, nil
19. })
20. if err != nil {
21. return fmt.Errorf("unable to refresh the service IP block: %v", err)
22. }
23. // 2、判断 snapshot 是否已经初始化
24. if snapshot.Range == "" {
25. snapshot.Range = c.network.String()
26. }
27.
28. if c.shouldWorkOnSecondary() && secondarySnapshot.Range == "" {
29. secondarySnapshot.Range = c.secondaryNetwork.String()
30. }
31.
32. stored, err = ipallocator.NewFromSnapshot(snapshot)
33. if c.shouldWorkOnSecondary() {
secondaryStored, secondaryErr =
34. ipallocator.NewFromSnapshot(secondarySnapshot)
35. }
36.
37. if err != nil || secondaryErr != nil {
return fmt.Errorf("unable to rebuild allocator from snapshots: %v",
38. err)
39. }
40. // 3、获取 service list
list, err :=
41. c.serviceClient.Services(metav1.NamespaceAll).List(metav1.ListOptions{})
42. if err != nil {
43. return fmt.Errorf("unable to refresh the service IP block: %v", err)
44. }
45.
46. // 4、将 CIDR 转换为对应的 IP range 格式
47. var rebuilt, secondaryRebuilt *ipallocator.Range
48. rebuilt, err = ipallocator.NewCIDRRange(c.network)
49.
50. ......
51.
52. // 5、检查每个 Service 的 ClusterIP,保证其处于正常状态
53. for _, svc := range list.Items {

本文档使用 书栈网 · BookStack.CN 构建 - 60 -


kube-apiserver 中 apiserver service 的实现

54. if !helper.IsServiceIPSet(&svc) {
55. continue
56. }
57. ip := net.ParseIP(svc.Spec.ClusterIP)
58. ......
59.
60. actualAlloc := c.selectAllocForIP(ip, rebuilt, secondaryRebuilt)
61. switch err := actualAlloc.Allocate(ip); err {
62. // 6、检查 ip 是否泄漏
63. case nil:
64. actualStored := c.selectAllocForIP(ip, stored, secondaryStored)
65. if actualStored.Has(ip) {
66. actualStored.Release(ip)
67. } else {
68. ......
69. }
70. delete(c.leaks, ip.String())
71. // 7、ip 重复分配
72. case ipallocator.ErrAllocated:
73. ......
74. // 8、ip 超出范围
75. case err.(*ipallocator.ErrNotInRange):
76. ......
77. // 9、ip 已经分配完
78. case ipallocator.ErrFull:
79. ......
80. default:
81. ......
82. }
83. }
84. // 10、对比是否有泄漏 ip
85. c.checkLeaked(stored, rebuilt)
86. if c.shouldWorkOnSecondary() {
87. c.checkLeaked(secondaryStored, secondaryRebuilt)
88. }
89.
90. // 11、更新快照
91. err = c.saveSnapShot(rebuilt, c.alloc, snapshot)
92. if err != nil {
93. return err
94. }
95.

本文档使用 书栈网 · BookStack.CN 构建 - 61 -


kube-apiserver 中 apiserver service 的实现

96. if c.shouldWorkOnSecondary() {
err := c.saveSnapShot(secondaryRebuilt, c.secondaryAlloc,
97. secondarySnapshot)
98. if err != nil {
99. return nil
100. }
101. }
102. return nil
103. }

repairNodePorts.RunUnti

repairNodePorts 主要是用来纠正 service 中 nodePort 的信息,保证所有的 ports 都基于


cluster 创建的,当没有与 cluster 同步时会触发告警,其最终是调用
repairNodePorts.runOnce 进行处理的,主要逻辑与 ClusterIP 的处理逻辑类似。

k8s.io/kubernetes/pkg/registry/core/service/portallocator/controller/repair.go:84

1. func (c *Repair) runOnce() error {


2. // 1、首先从 etcd 中获取已使用 nodeport 的快照
err := wait.PollImmediate(time.Second, 10*time.Second, func() (bool, error)
3. {
4. var err error
5. snapshot, err = c.alloc.Get()
6. return err == nil, err
7. })
8. if err != nil {
9. return fmt.Errorf("unable to refresh the port allocations: %v", err)
10. }
11. // 2、检查 snapshot 是否初始化
12. if snapshot.Range == "" {
13. snapshot.Range = c.portRange.String()
14. }
15. // 3、获取已分配 nodePort 信息
16. stored, err := portallocator.NewFromSnapshot(snapshot)
17. if err != nil {
18. return fmt.Errorf("unable to rebuild allocator from snapshot: %v", err)
19. }
20. // 4、获取 service list
list, err :=
21. c.serviceClient.Services(metav1.NamespaceAll).List(metav1.ListOptions{})
22. if err != nil {
23. return fmt.Errorf("unable to refresh the port block: %v", err)

本文档使用 书栈网 · BookStack.CN 构建 - 62 -


kube-apiserver 中 apiserver service 的实现

24. }
25.
26. rebuilt, err := portallocator.NewPortAllocator(c.portRange)
27. if err != nil {
28. return fmt.Errorf("unable to create port allocator: %v", err)
29. }
30.
31. // 5、检查每个 Service ClusterIP 的 port,保证其处于正常状态
32. for i := range list.Items {
33. svc := &list.Items[i]
34. ports := collectServiceNodePorts(svc)
35. if len(ports) == 0 {
36. continue
37. }
38. for _, port := range ports {
39. switch err := rebuilt.Allocate(port); err {
40. // 6、检查 port 是否泄漏
41. case nil:
42. if stored.Has(port) {
43. stored.Release(port)
44. } else {
45. ......
46. }
47. delete(c.leaks, port)
48. // 7、port 重复分配
49. case portallocator.ErrAllocated:
50. ......
51. // 8、port 超出分配范围
52. case err.(*portallocator.ErrNotInRange):
53. ......
54. // 9、port 已经分配完
55. case portallocator.ErrFull:
56. ......
57. default:
58. ......
59. }
60. }
61. }
62. // 10、检查 port 是否泄漏
63. stored.ForEach(func(port int) {
64. count, found := c.leaks[port]
65. switch {

本文档使用 书栈网 · BookStack.CN 构建 - 63 -


kube-apiserver 中 apiserver service 的实现

66. case !found:


67. ......
68. count = numRepairsBeforeLeakCleanup - 1
69. fallthrough
70. case count > 0:
71. c.leaks[port] = count - 1
72. if err := rebuilt.Allocate(port); err != nil {
runtime.HandleError(fmt.Errorf("the node port %d may have
73. leaked, but can not be allocated: %v", port, err))
74. }
75. default:
76. ......
77. }
78. })
79.
80. // 11、更新 snapshot
81. if err := rebuilt.Snapshot(snapshot); err != nil {
return fmt.Errorf("unable to snapshot the updated port allocations:
82. %v", err)
83. }
84. ......
85. return nil
86. }

以上就是 bootstrap controller 的主要实现。

总结
本文主要分析了 kube-apiserver 中 apiserver service 的实现,apiserver service 是
通过 bootstrap controller 控制的,bootstrap controller 会保证 apiserver
service 以及其 endpoint 处于正常状态,需要注意的是,apiserver service 的 endpoint
根据启动时指定的参数分为三种控制方式,本文仅分析了 lease 的实现方式,如果使用 master-
count 方式,需要将每个 master 实例的 port、apiserver-count 等配置参数改为一致。

本文档使用 书栈网 · BookStack.CN 构建 - 64 -


node controller 源码分析

在早期的版本中 NodeController 只有一种,v1.16 版本中 NodeController 已经分为了


NodeIpamController 与 NodeLifecycleController,本文主要介绍
NodeLifecycleController。

NodeLifecycleController 的功能
NodeLifecycleController 主要功能是定期监控 node 的状态并根据 node 的 condition 添
加对应的 taint 标签或者直接驱逐 node 上的 pod。

taint 的作用

在介绍 NodeLifecycleController 的源码前有必要先介绍一下 taint 的作用,因为


NodeLifecycleController 功能最终的结果有很大一部分都体现在 node taint 上。

taint 使用效果(Effect):

PreferNoSchedule :调度器尽量避免把 pod 调度到具有该污点的节点上,如果不能避免(如


其他节点资源不足),pod 也能调度到这个污点节点上,已存在于此节点上的 pod 不会被驱
逐;
NoSchedule :不容忍该污点的 pod 不会被调度到该节点上,通过 kubelet 管理的
pod(static pod)不受限制,之前没有设置污点的 pod 如果已运行在此节点(有污点的节点)
上,可以继续运行;
NoExecute :不容忍该污点的 pod 不会被调度到该节点上,同时会将已调度到该节点上但不容
忍 node 污点的 pod 驱逐掉;

NodeLifecycleController 中的 feature-gates

在 NodeLifecycleController 用到了多个 feature-gates,此处先进行解释下:

NodeDisruptionExclusion :该特性在 v1.16 引入,Alpha 版本,默认为 false,其功能


是当 node 存在 node.kubernetes.io/exclude-disruption 标签时,当 node 网络中断
时其节点上的 pod 不会被驱逐掉;
LegacyNodeRoleBehavior :该特性在 v1.16 中引入,Alpha 版本且默认为 true,在创建
load balancers 以及中断处理时不会忽略具有 node-role.kubernetes.io/master
label 的 node,该功能在 v1.19 中将被移除;
TaintBasedEvictions :该特性从 v1.13 开始为 Beta 版本,默认为 true。其功能是当
node 处于 NodeNotReady 、 NodeUnreachable 状态时为 node 添加对应的
taint, TaintBasedEvictions 添加的 taint effect 为 NoExecute ,即会驱逐 node
上对应的 pod;
TaintNodesByCondition :该特性从 v1.12 开始为 Beta 版本,默认为 true,v1.17 为
GA 版本。其功能是基于节点状态添加 taint,当节点处于
NetworkUnavailable 、 MemoryPressure 、 PIDPressure 、 DiskPressure 状态时会添

本文档使用 书栈网 · BookStack.CN 构建 - 65 -


node controller 源码分析

加对应的 taint, TaintNodesByCondition 添加的 taint effect 仅为 NoSchedule ,


即仅仅不会让新创建的 pod 调度到该 node 上;
NodeLease :该特性在 v1.12 引入,v 1.14 为 Beta 版本且默认启用,v 1.17 GA,主
要功能是减少 node 的心跳请求以减轻 apiserver 的负担;

NodeLifecycleController 源码分析

kubernetes 版本:v1.16

startNodeLifecycleController

首先还是看 NodeLifecycleController 的启动方法 startNodeLifecycleController ,在


startNodeLifecycleController 中主要调用了
lifecyclecontroller.NewNodeLifecycleController 对 lifecycleController 进行初始
化,在该方法中传入了组件的多个参数以及 TaintBasedEvictions 和
TaintNodesByCondition 两个 feature-gates,然后调用了 lifecycleController.Run
启动 lifecycleController,可以看到 NodeLifecycleController 主要监听 lease、
pods、nodes、daemonSets 四种对象。

其中在启动时指定的几个参数默认值分别为:

NodeMonitorPeriod :通过 --node-monitor-period 设置,默认为 5s,表示在


NodeController 中同步NodeStatus 的周期;
NodeStartupGracePeriod : --node-startup-grace-period 默认 60s,在 node 启动完
成前标记节点为unhealthy 的允许无响应时间;
NodeMonitorGracePeriod :通过 --node-monitor-grace-period 设置,默认 40s,表示
在标记某个 node为 unhealthy 前,允许 40s 内该 node 无响应;
PodEvictionTimeout :通过 --pod-eviction-timeout 设置,默认 5 分钟,表示在强制删
除 node 上的 pod 时,容忍 pod 时间;
NodeEvictionRate :通过 --node-eviction-rate 设置, 默认 0.1,表示当集群下某个
zone 为 unhealthy 时,每秒应该剔除的 node 数量,默认即每 10s 剔除1个 node;
SecondaryNodeEvictionRate :通过 --secondary-node-eviction-rate 设置,默认为
0.01,表示如果某个 zone 下的 unhealthy 节点的百分比超过 --unhealthy-zone-
threshold (默认为 0.55)时,驱逐速率将会减小,如果集群较小(小于等于 --large-
cluster-size-threshold 个 节点 - 默认为 50),驱逐操作将会停止,否则驱逐速率将降为
每秒 --secondary-node-eviction-rate 个(默认为 0.01);
LargeClusterSizeThreshold :通过 --large-cluster-size-threshold 设置,默认为
50,当该 zone 的节点超过该阈值时,则认为该 zone 是一个大集群;
UnhealthyZoneThreshold :通过 --unhealthy-zone-threshold 设置,默认为 0.55,不
健康 zone 阈值,会影响什么时候开启二级驱赶速率,即当该 zone 中节点宕机数目超过
55%,认为该 zone 不健康;

本文档使用 书栈网 · BookStack.CN 构建 - 66 -


node controller 源码分析

EnableTaintManager : --enable-taint-manager 默认为 true,Beta feature,如果


为 true,则表示NodeController 将会启动 TaintManager,当已经调度到该 node 上的
pod 不能容忍 node 的 taint 时,由 TaintManager 负责驱逐此类 pod,若不开启该特
性则已调度到该 node 上的 pod 会继续存在;
TaintBasedEvictions :默认为 true;
TaintNodesByCondition :默认为 true;

k8s.io/kubernetes/cmd/kube-controller-manager/app/core.go:163

func startNodeLifecycleController(ctx ControllerContext) (http.Handler, bool,


1. error) {
2. lifecycleController, err := lifecyclecontroller.NewNodeLifecycleController(
3. ctx.InformerFactory.Coordination().V1beta1().Leases(),
4. ctx.InformerFactory.Core().V1().Pods(),
5. ctx.InformerFactory.Core().V1().Nodes(),
6. ctx.InformerFactory.Apps().V1().DaemonSets(),
7. ctx.ClientBuilder.ClientOrDie("node-controller"),
8. ctx.ComponentConfig.KubeCloudShared.NodeMonitorPeriod.Duration,

9. ctx.ComponentConfig.NodeLifecycleController.NodeStartupGracePeriod.Duration,

10. ctx.ComponentConfig.NodeLifecycleController.NodeMonitorGracePeriod.Duration,

11. ctx.ComponentConfig.NodeLifecycleController.PodEvictionTimeout.Duration,
12. ctx.ComponentConfig.NodeLifecycleController.NodeEvictionRate,
13. ctx.ComponentConfig.NodeLifecycleController.SecondaryNodeEvictionRate,
14. ctx.ComponentConfig.NodeLifecycleController.LargeClusterSizeThreshold,
15. ctx.ComponentConfig.NodeLifecycleController.UnhealthyZoneThreshold,
16. ctx.ComponentConfig.NodeLifecycleController.EnableTaintManager,
17. utilfeature.DefaultFeatureGate.Enabled(features.TaintBasedEvictions),
18. utilfeature.DefaultFeatureGate.Enabled(features.TaintNodesByCondition),
19. )
20. if err != nil {
21. return nil, true, err
22. }
23. go lifecycleController.Run(ctx.Stop)
24. return nil, true, nil
25. }

NewNodeLifecycleController

首先有必要说明一下 NodeLifecycleController 对象中部分字段的意义,其结构体如下所示:

本文档使用 书栈网 · BookStack.CN 构建 - 67 -


node controller 源码分析

1. type Controller struct {


2. taintManager *scheduler.NoExecuteTaintManager
3.
4. podInformerSynced cache.InformerSynced
5. kubeClient clientset.Interface
6.
7. now func() metav1.Time
8.
9. // 计算 zone 下 node 驱逐速率
10. enterPartialDisruptionFunc func(nodeNum int) float32
11. enterFullDisruptionFunc func(nodeNum int) float32
12.
13. // 计算 zone 状态
computeZoneStateFunc func(nodeConditions []*v1.NodeCondition) (int,
14. ZoneState)
15.
16. // 用来记录NodeController observed节点的集合
17. knownNodeSet map[string]*v1.Node
18. // 记录 node 最近一次状态的集合
19. nodeHealthMap map[string]*nodeHealthData
20.
21. evictorLock sync.Mutex
22.
23. // 需要驱逐节点上 pod 的 node 队列
24. zonePodEvictor map[string]*scheduler.RateLimitedTimedQueue
25.
26. // 需要打 taint 标签的 node 队列
27. zoneNoExecuteTainter map[string]*scheduler.RateLimitedTimedQueue
28.
29. // 将 node 划分为不同的 zone
30. zoneStates map[string]ZoneState
31.
32. daemonSetStore appsv1listers.DaemonSetLister
33. daemonSetInformerSynced cache.InformerSynced
34. leaseLister coordlisters.LeaseLister
35. leaseInformerSynced cache.InformerSynced
36. nodeLister corelisters.NodeLister
37. nodeInformerSynced cache.InformerSynced
38.
39. getPodsAssignedToNode func(nodeName string) ([]v1.Pod, error)
40.
41. recorder record.EventRecorder

本文档使用 书栈网 · BookStack.CN 构建 - 68 -


node controller 源码分析

42.
43. // kube-controller-manager 启动时指定的几个参数
44. nodeMonitorPeriod time.Duration
45. nodeStartupGracePeriod time.Duration
46. nodeMonitorGracePeriod time.Duration
47. podEvictionTimeout time.Duration
48. evictionLimiterQPS float32
49. secondaryEvictionLimiterQPS float32
50. largeClusterThreshold int32
51. unhealthyZoneThreshold float32
52.
53. // 启动时默认开启的几个 feature-gates
54. runTaintManager bool
55. useTaintBasedEvictions bool
56. taintNodeByCondition bool
57.
58. nodeUpdateQueue workqueue.Interface
59. }

NewNodeLifecycleController 的主要逻辑为:

1、初始化 controller 对象;


2、为 podInformer 注册与 taintManager 相关的 EventHandler;
3、若启用 TaintManager 则为 nodeInformer 注册与 taintManager 相关的
EventHandler;
4、为 NodeLifecycleController 注册 nodeInformer;
5、检查是否启用了 NodeLease feature-gates;
6、daemonSet 默认不会注册对应的 EventHandler,此处仅仅是同步该对象;

由以上逻辑可以看出, taintManager 以及 NodeLifecycleController 都会 watch node


的变化并进行不同的处理。

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:268

1. func NewNodeLifecycleController(......) (*Controller, error) {


2. ......
3.
4. // 1、初始化 controller 对象
5. nc := &Controller{
6. ......
7. }
8.
9. ......

本文档使用 书栈网 · BookStack.CN 构建 - 69 -


node controller 源码分析

10.
11. // 2、注册计算 node 驱逐速率以及 zone 状态的方法
12. nc.enterPartialDisruptionFunc = nc.ReducedQPSFunc
13. nc.enterFullDisruptionFunc = nc.HealthyQPSFunc
14. nc.computeZoneStateFunc = nc.ComputeZoneState
15.
// 3、为 podInformer 注册 EventHandler,监听到的对象会被放到
16. nc.taintManager.PodUpdated 中
17. podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
18. AddFunc: func(obj interface{}) {
19. pod := obj.(*v1.Pod)
20. if nc.taintManager != nil {
21. nc.taintManager.PodUpdated(nil, pod)
22. }
23. },
24. UpdateFunc: func(prev, obj interface{}) {
25. prevPod := prev.(*v1.Pod)
26. newPod := obj.(*v1.Pod)
27. if nc.taintManager != nil {
28. nc.taintManager.PodUpdated(prevPod, newPod)
29. }
30. },
31. DeleteFunc: func(obj interface{}) {
32. pod, isPod := obj.(*v1.Pod)
33. if !isPod {
34. deletedState, ok := obj.(cache.DeletedFinalStateUnknown)
35. if !ok {
36. return
37. }
38. pod, ok = deletedState.Obj.(*v1.Pod)
39. if !ok {
40. return
41. }
42. }
43. if nc.taintManager != nil {
44. nc.taintManager.PodUpdated(pod, nil)
45. }
46. },
47. })
48. nc.podInformerSynced = podInformer.Informer().HasSynced
49. podInformer.Informer().AddIndexers(cache.Indexers{
50. nodeNameKeyIndex: func(obj interface{}) ([]string, error) {

本文档使用 书栈网 · BookStack.CN 构建 - 70 -


node controller 源码分析

51. pod, ok := obj.(*v1.Pod)


52. if !ok {
53. return []string{}, nil
54. }
55. if len(pod.Spec.NodeName) == 0 {
56. return []string{}, nil
57. }
58. return []string{pod.Spec.NodeName}, nil
59. },
60. })
61.
62. podIndexer := podInformer.Informer().GetIndexer()
63. nc.getPodsAssignedToNode = func(nodeName string) ([]v1.Pod, error) {
64. objs, err := podIndexer.ByIndex(nodeNameKeyIndex, nodeName)
65. if err != nil {
66. return nil, err
67. }
68. pods := make([]v1.Pod, 0, len(objs))
69. for _, obj := range objs {
70. pod, ok := obj.(*v1.Pod)
71. if !ok {
72. continue
73. }
74. pods = append(pods, *pod)
75. }
76. return pods, nil
77. }
78.
79. // 4、初始化 TaintManager,为 nodeInformer 注册 EventHandler
80. // 监听到的对象会被放到 nc.taintManager.NodeUpdated 中
81. if nc.runTaintManager {
82. podLister := podInformer.Lister()
podGetter := func(name, namespace string) (*v1.Pod, error) { return
83. podLister.Pods(namespace).Get(name) }
84. nodeLister := nodeInformer.Lister()
nodeGetter := func(name string) (*v1.Node, error) { return
85. nodeLister.Get(name) }
86.
87. // 5、初始化 taintManager
nc.taintManager = scheduler.NewNoExecuteTaintManager(kubeClient,
88. podGetter, nodeGetter, nc.getPodsAssignedToNode)

89. nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{

本文档使用 书栈网 · BookStack.CN 构建 - 71 -


node controller 源码分析

90. AddFunc: nodeutil.CreateAddNodeHandler(func(node *v1.Node) error {


91. nc.taintManager.NodeUpdated(nil, node)
92. return nil
93. }),
UpdateFunc: nodeutil.CreateUpdateNodeHandler(func(oldNode, newNode
94. *v1.Node) error {
95. nc.taintManager.NodeUpdated(oldNode, newNode)
96. return nil
97. }),
DeleteFunc: nodeutil.CreateDeleteNodeHandler(func(node *v1.Node)
98. error {
99. nc.taintManager.NodeUpdated(node, nil)
100. return nil
101. }),
102. })
103. }
104.
105. // 6、为 NodeLifecycleController 注册 nodeInformer
106. nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
107. AddFunc: nodeutil.CreateAddNodeHandler(func(node *v1.Node) error {
108. nc.nodeUpdateQueue.Add(node.Name)
109. return nil
110. }),
UpdateFunc: nodeutil.CreateUpdateNodeHandler(func(_, newNode *v1.Node)
111. error {
112. nc.nodeUpdateQueue.Add(newNode.Name)
113. return nil
114. }),
115. })
116.
117. ......
118.
119. // 7、检查是否启用了 NodeLease feature-gates
120. nc.leaseLister = leaseInformer.Lister()
121. if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
122. nc.leaseInformerSynced = leaseInformer.Informer().HasSynced
123. } else {
124. nc.leaseInformerSynced = func() bool { return true }
125. }
126.
127. nc.nodeLister = nodeInformer.Lister()
128. nc.nodeInformerSynced = nodeInformer.Informer().HasSynced
129.

本文档使用 书栈网 · BookStack.CN 构建 - 72 -


node controller 源码分析

130. nc.daemonSetStore = daemonSetInformer.Lister()


131. nc.daemonSetInformerSynced = daemonSetInformer.Informer().HasSynced
132.
133. return nc, nil
134. }

Run

Run 方法是 NodeLifecycleController 的启动方法,其中会启动多个 goroutine 完成


controller 的功能,主要逻辑为:

1、等待四种对象 Informer 中的 cache 同步完成;


2、若指定要运行 taintManager 则调用 nc.taintManager.Run 启动 taintManager;
3、启动多个 goroutine 调用 nc.doNodeProcessingPassWorker 处理
nc.nodeUpdateQueue 队列中的 node;
4、若启用了 TaintBasedEvictions 特性则启动一个 goroutine 调用
nc.doNoExecuteTaintingPass 处理 nc.zoneNoExecuteTainter 队列中的 node,否则调
用 nc.doEvictionPass 处理 nc.zonePodEvictor 队列中的 node;
5、启动一个 goroutine 调用 nc.monitorNodeHealth 定期监控 node 的状态;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:455

1. func (nc *Controller) Run(stopCh <-chan struct{}) {


2. defer utilruntime.HandleCrash()
3.
4. defer klog.Infof("Shutting down node controller")
5.
if !cache.WaitForNamedCacheSync("taint", stopCh, nc.leaseInformerSynced,
nc.nodeInformerSynced, nc.podInformerSynced, nc.daemonSetInformerSynced) {
6. return
7. }
8.
9. // 1、启动 taintManager
10. if nc.runTaintManager {
11. go nc.taintManager.Run(stopCh)
12. }
13.
14. defer nc.nodeUpdateQueue.ShutDown()
15.
16. // 2、执行 nc.doNodeProcessingPassWorker
17. for i := 0; i < scheduler.UpdateWorkerSize; i++ {
18. go wait.Until(nc.doNodeProcessingPassWorker, time.Second, stopCh)

本文档使用 书栈网 · BookStack.CN 构建 - 73 -


node controller 源码分析

19. }
20.
21. // 3、根据是否启用 TaintBasedEvictions 执行不同的处理逻辑
22. if nc.useTaintBasedEvictions {
go wait.Until(nc.doNoExecuteTaintingPass, scheduler.NodeEvictionPeriod,
23. stopCh)
24. } else {
25. go wait.Until(nc.doEvictionPass, scheduler.NodeEvictionPeriod, stopCh)
26. }
27.
28. // 4、执行 nc.monitorNodeHealth
29. go wait.Until(func() {
30. if err := nc.monitorNodeHealth(); err != nil {
31. klog.Errorf("Error monitoring node health: %v", err)
32. }
33. }, nc.nodeMonitorPeriod, stopCh)
34.
35. <-stopCh
36. }

Run 方法中主要调用了 5 个方法来完成其核心功能:

nc.taintManager.Run :处理 taintManager 中 nodeUpdateQueue 和


podUpdateQueue 中的 pod 以及 node,若 pod 不能容忍 node 上的 taint 则将其加入
到 taintEvictionQueue 中并最终会删除;
nc.doNodeProcessingPassWorker :从 NodeLifecycleController 的
nodeUpdateQueue 取出 node,(1)若启用 taintNodeByCondition 特性时根据 node
condition 以及 node 是否调度为 node 添加对应的 NoSchedule taint 标签;(2)
调用 nc.reconcileNodeLabels 为 node 添加默认的 label;
nc.doNoExecuteTaintingPass :处理 nc.zoneNoExecuteTainter 队列中的数据,根据
node 的 NodeReadyCondition 添加或移除对应的 taint;
nc.doEvictionPass :处理 nc.zonePodEvictor 队列中的 node,将 node 上的 pod
进行驱逐;
nc.monitorNodeHealth :持续监控 node 的状态,当 node 处于异常状态时更新 node 的
taint 以及 node 上 pod 的状态或者直接驱逐 node 上的 pod,此外还会为集群下的所有
node 划分 zoneStates 并为每个 zoneStates 设置对应的驱逐速率;

下文会详细分析以上 5 种方法的具体实现。

nc.taintManager.Run

当组件启动时设置 --enable-taint-manager 参数为 true 时(默认为 true),该功能会启用,

本文档使用 书栈网 · BookStack.CN 构建 - 74 -


node controller 源码分析

其主要作用是当该 node 上的 pod 不容忍 node taint 时将 pod 进行驱逐,若不开启该功能则


已调度到该 node 上的 pod 会继续存在,新创建的 pod 需要容忍 node 的 taint 才会调度至该
node 上。

主要逻辑为:

1、处理 nodeUpdateQueue 中的 node 并将其发送到 nodeUpdateChannels 中;


2、处理 podUpdateQueue 中的 pod 并将其发送到 podUpdateChannels 中;
3、调用 tc.worker 处理 nodeUpdateChannels 和 podUpdateChannels 中的数据;

k8s.io/kubernetes/pkg/controller/nodelifecycle/scheduler/taint_manager.go:185

1. func (tc *NoExecuteTaintManager) Run(stopCh <-chan struct{}) {


2. for i := 0; i < UpdateWorkerSize; i++ {
tc.nodeUpdateChannels = append(tc.nodeUpdateChannels, make(chan
3. nodeUpdateItem, NodeUpdateChannelSize))
tc.podUpdateChannels = append(tc.podUpdateChannels, make(chan
4. podUpdateItem, podUpdateChannelSize))
5. }
6.
7. go func(stopCh <-chan struct{}) {
8. for {
9. item, shutdown := tc.nodeUpdateQueue.Get()
10. if shutdown {
11. break
12. }
13. nodeUpdate := item.(nodeUpdateItem)
14. hash := hash(nodeUpdate.nodeName, UpdateWorkerSize)
15. select {
16. case <-stopCh:
17. tc.nodeUpdateQueue.Done(item)
18. return
19. case tc.nodeUpdateChannels[hash] <- nodeUpdate:
20. }
21. }
22. }(stopCh)
23.
24. go func(stopCh <-chan struct{}) {
25. for {
26. item, shutdown := tc.podUpdateQueue.Get()
27. if shutdown {
28. break
29. }

本文档使用 书栈网 · BookStack.CN 构建 - 75 -


node controller 源码分析

30. podUpdate := item.(podUpdateItem)


31. hash := hash(podUpdate.nodeName, UpdateWorkerSize)
32. select {
33. case <-stopCh:
34. tc.podUpdateQueue.Done(item)
35. return
36. case tc.podUpdateChannels[hash] <- podUpdate:
37. }
38. }
39. }(stopCh)
40.
41. wg := sync.WaitGroup{}
42. wg.Add(UpdateWorkerSize)
43. for i := 0; i < UpdateWorkerSize; i++ {
44. go tc.worker(i, wg.Done, stopCh)
45. }
46. wg.Wait()
47. }

tc.worker

tc.worker 主要功能是调用 tc.handleNodeUpdate 和 tc.handlePodUpdate 处理


tc.nodeUpdateChannels 和 tc.podUpdateChannels 两个 channel 中的数据,但会优先处
理 nodeUpdateChannels 中的数据。

k8s.io/kubernetes/pkg/controller/nodelifecycle/scheduler/taint_manager.go:243

func (tc *NoExecuteTaintManager) worker(worker int, done func(), stopCh <-chan


1. struct{}) {
2. defer done()
3.
4. for {
5. select {
6. case <-stopCh:
7. return
8. case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
9. tc.handleNodeUpdate(nodeUpdate)
10. tc.nodeUpdateQueue.Done(nodeUpdate)
11. case podUpdate := <-tc.podUpdateChannels[worker]:
12.
13. // 优先处理 nodeUpdateChannels
14. priority:
15. for {

本文档使用 书栈网 · BookStack.CN 构建 - 76 -


node controller 源码分析

16. select {
17. case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
18. tc.handleNodeUpdate(nodeUpdate)
19. tc.nodeUpdateQueue.Done(nodeUpdate)
20. default:
21. break priority
22. }
23. }
24. tc.handlePodUpdate(podUpdate)
25. tc.podUpdateQueue.Done(podUpdate)
26. }
27. }
28. }

tc.handleNodeUpdate

tc.handleNodeUpdate 的主要逻辑为:

1、首先通过 nodeLister 获取 node 对象;


2、获取 node 上 effect 为 NoExecute 的 taints;
3、调用 tc.getPodsAssignedToNode 获取该 node 上的所有 pods;
4、若 node 上的 taints 为空直接返回,否则遍历每一个 pod 调用
tc.processPodOnNode 检查 pod 是否要被驱逐;

k8s.io/kubernetes/pkg/controller/nodelifecycle/scheduler/taint_manager.go:417

1. func (tc *NoExecuteTaintManager) handleNodeUpdate(nodeUpdate nodeUpdateItem) {


2. node, err := tc.getNode(nodeUpdate.nodeName)
3. if err != nil {
4. ......
5. }
6. // 1、获取 node 的 taints
7. taints := getNoExecuteTaints(node.Spec.Taints)
8. func() {
9. tc.taintedNodesLock.Lock()
10. defer tc.taintedNodesLock.Unlock()
11. if len(taints) == 0 {
12. delete(tc.taintedNodes, node.Name)
13. } else {
14. tc.taintedNodes[node.Name] = taints
15. }
16. }()
17.

本文档使用 书栈网 · BookStack.CN 构建 - 77 -


node controller 源码分析

18. // 2、获取 node 上的所有 pod


19. pods, err := tc.getPodsAssignedToNode(node.Name)
20. if err != nil {
21. klog.Errorf(err.Error())
22. return
23. }
24. if len(pods) == 0 {
25. return
26. }
27.
28. // 3、若不存在 taints,则取消所有的驱逐操作
29. if len(taints) == 0 {
30. for i := range pods {
tc.cancelWorkWithEvent(types.NamespacedName{Namespace:
31. pods[i].Namespace, Name: pods[i].Name})
32. }
33. return
34. }
35.
36. now := time.Now()
37. for i := range pods {
38. pod := &pods[i]
podNamespacedName := types.NamespacedName{Namespace: pod.Namespace,
39. Name: pod.Name}
40. // 4、调用 tc.processPodOnNode 进行处理
tc.processPodOnNode(podNamespacedName, node.Name, pod.Spec.Tolerations,
41. taints, now)
42. }
43. }

tc.handlePodUpdate

主要逻辑为:

1、通过 podLister 获取 pod 对象;


2、获取 pod 所在 node 的 taints;
3、调用 tc.processPodOnNode 进行处理;

k8s.io/kubernetes/pkg/controller/nodelifecycle/scheduler/taint_manager.go:377

1. func (tc *NoExecuteTaintManager) handlePodUpdate(podUpdate podUpdateItem) {


2. pod, err := tc.getPod(podUpdate.podName, podUpdate.podNamespace)
3. if err != nil {
4. ......

本文档使用 书栈网 · BookStack.CN 构建 - 78 -


node controller 源码分析

5. }
6.
7. if pod.Spec.NodeName != podUpdate.nodeName {
8. return
9. }
10.
podNamespacedName := types.NamespacedName{Namespace: pod.Namespace, Name:
11. pod.Name}
12.
13. nodeName := pod.Spec.NodeName
14. if nodeName == "" {
15. return
16. }
17. taints, ok := func() ([]v1.Taint, bool) {
18. tc.taintedNodesLock.Lock()
19. defer tc.taintedNodesLock.Unlock()
20. taints, ok := tc.taintedNodes[nodeName]
21. return taints, ok
22. }()
23.
24. if !ok {
25. return
26. }
27. // 调用 tc.processPodOnNode 进行处理
tc.processPodOnNode(podNamespacedName, nodeName, pod.Spec.Tolerations,
28. taints, time.Now())
29. }

本文档使用 书栈网 · BookStack.CN 构建 - 79 -


node controller 源码分析

tc.processPodOnNode

tc.handlePodUpdate 和 tc.handleNodeUpdate 最终都是调用 tc.processPodOnNode


检查 pod 是否容忍 node 的 taints, tc.processPodOnNode 首先检查 pod 的
tolerations 是否能匹配 node 上所有的 taints,若无法完全匹配则将 pod 加入到
taintEvictionQueue 然后被删除,若能匹配首先获取 pod tolerations 中的最小容忍时间,
如果 tolerations 未设置容忍时间说明会一直容忍则直接返回,否则加入到
taintEvictionQueue 的延迟队列中,当达到最小容忍时间时 pod 会被加入到
taintEvictionQueue 中并驱逐。

通常情况下,如果给一个节点添加了一个 effect 值为 NoExecute 的 taint,则任何不能忍受


这个 taint 的 pod 都会马上被驱逐,任何可以忍受这个 taint 的 pod 都不会被驱逐。但是,如
果 pod 存在一个 effect 值为 NoExecute 的 toleration 指定了可选属性
tolerationSeconds 的值,则表示在给节点添加了上述 taint 之后,pod 还能继续在节点上运
行的时间。例如,

1. tolerations:
2. - key: "key1"
3. operator: "Equal"
4. value: "value1"
5. effect: "NoExecute"
6. tolerationSeconds: 3600

这表示如果这个 pod 正在运行,然后一个匹配的 taint 被添加到其所在的节点,那么 pod 还将继


续在节点上运行 3600 秒,然后被驱逐。如果在此之前上述 taint 被删除了,则 pod 不会被驱
逐。

k8s.io/kubernetes/pkg/controller/nodelifecycle/scheduler/taint_manager.go:339

1. func (tc *NoExecuteTaintManager) processPodOnNode(......) {


2. if len(taints) == 0 {
3. tc.cancelWorkWithEvent(podNamespacedName)
4. }
5. // 1、检查 pod 的 tolerations 是否匹配所有 taints
allTolerated, usedTolerations := v1helper.GetMatchingTolerations(taints,
6. tolerations)
7. if !allTolerated {
8. tc.cancelWorkWithEvent(podNamespacedName)
tc.taintEvictionQueue.AddWork(NewWorkArgs(podNamespacedName.Name,
9. podNamespacedName.Namespace), time.Now(), time.Now())
10. return
11. }

本文档使用 书栈网 · BookStack.CN 构建 - 80 -


node controller 源码分析

12.
13. // 2、获取最小容忍时间
14. minTolerationTime := getMinTolerationTime(usedTolerations)
15. if minTolerationTime < 0 {
16. return
17. }
18.
19. // 3、若存在最小容忍时间则将其加入到延时队列中
20. startTime := now
21. triggerTime := startTime.Add(minTolerationTime)
scheduledEviction :=
22. tc.taintEvictionQueue.GetWorkerUnsafe(podNamespacedName.String())
23. if scheduledEviction != nil {
24. startTime = scheduledEviction.CreatedAt
25. if startTime.Add(minTolerationTime).Before(triggerTime) {
26. return
27. }
28. tc.cancelWorkWithEvent(podNamespacedName)
29. }
tc.taintEvictionQueue.AddWork(NewWorkArgs(podNamespacedName.Name,
30. podNamespacedName.Namespace), startTime, triggerTime)
31. }

nc.doNodeProcessingPassWorker

NodeLifecycleController 中 nodeInformer 监听到 node 变化时会将其添加到


nodeUpdateQueue 中, nc.doNodeProcessingPassWorker 主要是处理 nodeUpdateQueue 中
的 node,为其添加合适的 NoSchedule taint 以及 label,其主要逻辑为:

1、从 nc.nodeUpdateQueue 中取出 node;


2、若启用了 TaintNodeByCondition feature-gates,调
用 nc.doNoScheduleTaintingPass 检查该 node 是否需要添加对应的 NoSchedule
taint; nc.doNoScheduleTaintingPass 中的主要逻辑为:
1、从 nodeLister 中获取该 node 对象;
2、判断该 node 是否存在以下几种 Condition:(1) False 或 Unknown 状态的
NodeReady Condition;(2) MemoryPressureCondition;(3)
DiskPressureCondition;(4) NetworkUnavailableCondition;(5)
PIDPressureCondition;若任一一种存在会添加对应的 NoSchedule taint;
3、判断 node 是否处于 Unschedulable 状态,若为 Unschedulable 也添加对应
的 NoSchedule taint;
4、对比 node 已有的 taints 以及需要添加的 taints,以需要添加的 taints 为
准,调用 nodeutil.SwapNodeControllerTaint 为 node 添加不存在的 taints 并

本文档使用 书栈网 · BookStack.CN 构建 - 81 -


node controller 源码分析

删除不需要的 taints;

3、调用 nc.reconcileNodeLabels 检查 node 是否存在以下 label,若不存在则为其添


加;

1. labels:
2. beta.kubernetes.io/arch: amd64
3. beta.kubernetes.io/os: linux
4. kubernetes.io/arch: amd64
5. kubernetes.io/os: linux

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:502

1. func (nc *Controller) doNodeProcessingPassWorker() {


2. for {
3. obj, shutdown := nc.nodeUpdateQueue.Get()
4. if shutdown {
5. return
6. }
7. nodeName := obj.(string)
8. if nc.taintNodeByCondition {
9. if err := nc.doNoScheduleTaintingPass(nodeName); err != nil {
10. ......
11. }
12. }
13.
14. if err := nc.reconcileNodeLabels(nodeName); err != nil {
15. ......
16. }
17. nc.nodeUpdateQueue.Done(nodeName)
18. }
19. }
20.
21. func (nc *Controller) doNoScheduleTaintingPass(nodeName string) error {
22. // 1、获取 node 对象
23. node, err := nc.nodeLister.Get(nodeName)
24. if err != nil {
25. ......
26. }
27.
28. // 2、若 node 存在对应的 condition 则为其添加对应的 taint
29. var taints []v1.Taint

本文档使用 书栈网 · BookStack.CN 构建 - 82 -


node controller 源码分析

30. for _, condition := range node.Status.Conditions {


if taintMap, found := nodeConditionToTaintKeyStatusMap[condition.Type];
31. found {
32. if taintKey, found := taintMap[condition.Status]; found {
33. taints = append(taints, v1.Taint{
34. Key: taintKey,
35. Effect: v1.TaintEffectNoSchedule,
36. })
37. }
38. }
39. }
40.
41. // 3、判断是否为 Unschedulable
42. if node.Spec.Unschedulable {
43. taints = append(taints, v1.Taint{
44. Key: schedulerapi.TaintNodeUnschedulable,
45. Effect: v1.TaintEffectNoSchedule,
46. })
47. }
48.
nodeTaints := taintutils.TaintSetFilter(node.Spec.Taints, func(t *v1.Taint)
49. bool {
50. if t.Effect != v1.TaintEffectNoSchedule {
51. return false
52. }
53. if t.Key == schedulerapi.TaintNodeUnschedulable {
54. return true
55. }
56. _, found := taintKeyToNodeConditionMap[t.Key]
57. return found
58. })
59.
60. // 4、对比 node 已有 taints 和需要添加的 taints 得到 taintsToAdd, taintsToDel
61. taintsToAdd, taintsToDel := taintutils.TaintSetDiff(taints, nodeTaints)
62. if len(taintsToAdd) == 0 && len(taintsToDel) == 0 {
63. return nil
64. }
65.
66. // 5、更新 node 的 taints
if !nodeutil.SwapNodeControllerTaint(nc.kubeClient, taintsToAdd,
67. taintsToDel, node) {
68. return fmt.Errorf("failed to swap taints of node %+v", node)
69. }

本文档使用 书栈网 · BookStack.CN 构建 - 83 -


node controller 源码分析

70. return nil


71. }

nc.doNoExecuteTaintingPass

当启用了 TaintBasedEvictions 特性时,通过 nc.monitorNodeHealth 检测到 node 异常


时会将其加入到 nc.zoneNoExecuteTainter 队列中, nc.doNoExecuteTaintingPass 会处理
nc.zoneNoExecuteTainter 队列中的 node,并且会按一定的速率进行,此时会根据 node 实际
的 NodeCondition 为 node 添加对应的 taint,当 node 存在 taint 时,taintManager
会驱逐 node 上的 pod。此过程中为 node 添加 taint 时进行了限速避免一次性驱逐过多 pod,
在驱逐 node 上的 pod 时不会限速。

nc.doNoExecuteTaintingPass 的主要逻辑为:

1、遍历 zoneNoExecuteTainter 中的 node 列表,从 nodeLister 中获取 node 对


象;
2、获取该 node 的 NodeReadyCondition;
3、判断 NodeReadyCondition 的状态,若为 false,则为 node 添加
node.kubernetes.io/not-ready:NoExecute 的 taint 且保证 node 不存在
node.kubernetes.io/unreachable:NoExecute 的 taint;
4、若 NodeReadyCondition 为 unknown,则为 node 添加
node.kubernetes.io/unreachable:NoExecute 的 taint 且保证 node 不存在
node.kubernetes.io/not-ready:NoExecute 的 taint; “unreachable” 和 “not
ready” 两个 taint 是互斥的,只能存在一个;
5、若 NodeReadyCondition 为 true,此时说明该 node 处于正常状态直接返回;
6、调用 nodeutil.SwapNodeControllerTaint 更新 node 的 taint;
7、若整个过程中有失败的操作会进行重试;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:582

1. func (nc *Controller) doNoExecuteTaintingPass() {


2. nc.evictorLock.Lock()
3. defer nc.evictorLock.Unlock()
4. for k := range nc.zoneNoExecuteTainter {
nc.zoneNoExecuteTainter[k].Try(func(value scheduler.TimedValue) (bool,
5. time.Duration) {
6. // 1、获取 node 对象
7. node, err := nc.nodeLister.Get(value.Value)
8. if apierrors.IsNotFound(err) {
9. return true, 0
10. } else if err != nil {
11. return false, 50 * time.Millisecond

本文档使用 书栈网 · BookStack.CN 构建 - 84 -


node controller 源码分析

12. }
13.
14. // 2、获取 node 的 NodeReadyCondition
_, condition := nodeutil.GetNodeCondition(&node.Status,
15. v1.NodeReady)
16. taintToAdd := v1.Taint{}
17. oppositeTaint := v1.Taint{}
18.
19. // 3、判断 Condition 状态,并为其添加对应的 taint
20. switch condition.Status {
21. case v1.ConditionFalse:
22. taintToAdd = *NotReadyTaintTemplate
23. oppositeTaint = *UnreachableTaintTemplate
24. case v1.ConditionUnknown:
25. taintToAdd = *UnreachableTaintTemplate
26. oppositeTaint = *NotReadyTaintTemplate
27. default:
28. return true, 0
29. }
30.
31. // 4、更新 node 的 taint
result := nodeutil.SwapNodeControllerTaint(nc.kubeClient,
32. []*v1.Taint{&taintToAdd}, []*v1.Taint{&oppositeTaint}, node)
33. if result {
34. zone := utilnode.GetZoneKey(node)
35. evictionsNumber.WithLabelValues(zone).Inc()
36. }
37.
38. return result, 0
39. })
40. }
41. }

nc.doEvictionPass

若未启用 TaintBasedEvictions 特性,此时通过 nc.monitorNodeHealth 检测到 node 异


常时会将其加入到 nc.zonePodEvictor 队列中, nc.doEvictionPass 会将
nc.zonePodEvictor 队列中 node 上的 pod 驱逐掉。

nc.doEvictionPass 的主要逻辑为:

1、遍历 zonePodEvictor 的 node 列表,从 nodeLister 中获取 node 对象;


2、调用 nodeutil.DeletePods 删除该 node 上的所有 pod,在 nodeutil.DeletePods

本文档使用 书栈网 · BookStack.CN 构建 - 85 -


node controller 源码分析

中首先通过从 apiserver 获取该 node 上所有的 pod,逐个删除 pod,若该 pod 为


daemonset 所管理的 pod 则忽略;
3、若整个过程中有失败的操作会进行重试;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:626

1. func (nc *Controller) doEvictionPass() {


2. nc.evictorLock.Lock()
3. defer nc.evictorLock.Unlock()
4. for k := range nc.zonePodEvictor {
nc.zonePodEvictor[k].Try(func(value scheduler.TimedValue) (bool,
5. time.Duration) {
6. node, err := nc.nodeLister.Get(value.Value)
7. ......
8. nodeUID, _ := value.UID.(string)
remaining, err := nodeutil.DeletePods(nc.kubeClient, nc.recorder,
9. value.Value, nodeUID, nc.daemonSetStore)
10. if err != nil {
utilruntime.HandleError(fmt.Errorf("unable to evict node %q:
11. %v", value.Value, err))
12. return false, 0
13. }
14. ......
15.
16. if node != nil {
17. zone := utilnode.GetZoneKey(node)
18. evictionsNumber.WithLabelValues(zone).Inc()
19. }
20.
21. return true, 0
22. })
23. }
24. }

nc.monitorNodeHealth

上面已经介绍了无论是否启用了 TaintBasedEvictions 特性,需要打 taint 或者驱逐 pod 的


node 都会被放在 zoneNoExecuteTainter 或者 zonePodEvictor 队列中,而
nc.monitorNodeHealth 就是这两个队列中数据的生产者。 nc.monitorNodeHealth 的主要功
能是持续监控 node 的状态,当 node 处于异常状态时更新 node 的 taint 以及 node 上 pod
的状态或者直接驱逐 node 上的 pod,此外还会为集群下的所有 node 划分 zoneStates 并为每
个 zoneStates 设置对应的驱逐速率。

本文档使用 书栈网 · BookStack.CN 构建 - 86 -


node controller 源码分析

nc.monitorNodeHealth 主要逻辑为:

1、从 nodeLister 中获取所有的 node;


2、NodeLifecycleController 根据自身 knownNodeSet 列表中的数据调用
nc.classifyNodes 将 node 分为三类:added、deleted、
newZoneRepresentatives,added 表示新增的,deleted 表示被删除的,
newZoneRepresentatives 代表该 node 不存在 zoneStates,
NodeLifecycleController 会为每一个 node 划分一个 zoneStates,zoneStates 有
Initial、Normal、FullDisruption、PartialDisruption 四种,新增加的 node 默认
的 zoneStates 为 Initial,其余的几个 zoneStates 分别对应着不同的驱逐速率;
3、对于 newZoneRepresentatives 中 node 列表,调用
nc.addPodEvictorForNewZone 将 node 添加到对应的的 zoneStates 中,然后根据是否
启用了 TaintBasedEvictions 特性将 node 分别加入到 zonePodEvictor 或
zoneNoExecuteTainter 列表中,若启用了则加入到 zoneNoExecuteTainter 列表中否则
加入到 zonePodEvictor 中;
4、对应 added 列表中的 node,首先将其加入到 knownNodeSet 列表中,然后调用
nc.addPodEvictorForNewZone 将该 node 添加到对应的 zoneStates 中,判断是否启用
了 TaintBasedEvictions 特性,若启用了则调用 nc.markNodeAsReachable 移除该
node 上的 UnreachableTaint 和 NotReadyTaint ,并从 zoneNoExecuteTainter 中
移除该 node,表示为该 node 进行一次初始化,若未启用 TaintBasedEvictions 特性则
调用 nc.cancelPodEviction 将该 node 从 zonePodEvictor 中删除;
5、对于 deleted 列表中的 node,将其从 knownNodeSet 列表中删除;
6、遍历所有的 nodes:
7、调用 nc.tryUpdateNodeHealth 获取该 node 的 gracePeriod、
observedReadyCondition、currentReadyCondition,observedReadyCondition 可
以理解为 node 上一次的状态, currentReadyCondition 为本次的状态;
8、检查 node 是否在中断检查中被排除,主要判断当启用 LegacyNodeRoleBehavior 或
NodeDisruptionExclusion 特性时,node 是否存在对应的标签,如果该 node 没有被排
除,则将其对应的 zone 加入到 zoneToNodeConditions 中;
9、当该 node 的 currentReadyCondition 不为空时,检查
observedReadyCondition,即检查上一次的状态:
1、若 observedReadyCondition 为 false,此时若启用了 TaintBasedEvictions
时,为其添加 NotReadyTaint 并且确保 node 不存在 UnreachableTaint 。若未
启用 TaintBasedEvictions 则判断距 node 上一次 readyTransitionTimestamp
的时间是否超过了 podEvictionTimeout (默认 5 分钟),若超过则将 node 加入到
zonePodEvictor 队列中,最终会驱逐 node 上的所有 pod;
2、若 observedReadyCondition 为 unknown,此时若启用了
TaintBasedEvictions 时,则为 node 添加 UnreachableTaint 并且确保 node
不会有 NotReadyTaint 。若未启用 TaintBasedEvictions 则判断距 node 上一次
probeTimestamp 的时间是否超过了 podEvictionTimeout (默认 5 分钟),若超过

本文档使用 书栈网 · BookStack.CN 构建 - 87 -


node controller 源码分析

则将 node 加入到 zonePodEvictor 队列中,最终会驱逐 node 上的所有 pod;


3、若 observedReadyCondition 为 true 时,此时若启用了
TaintBasedEvictions 时,调用 nc.markNodeAsReachable 移除 node 上的
NotReadyTaint 和 UnreachableTaint ,若未启用 TaintBasedEvictions 则将
node 从 zonePodEvictor 队列中移除; 此处主要是判断是否启用了
TaintBasedEvictions 特性,然后根据 node 的 ReadyCondition 判断是否直接驱
逐 node 上的 pod 还是为 node 打 taint 等待 taintManager 驱逐 node 上的
pod;
10、最后判断当 node ReadyCondition 由 true 变为 false 时,调用
nodeutil.MarkAllPodsNotReady 将该node 上的所有 pod 标记为 notReady;
11、调用 nc.handleDisruption 处理中断情况,为不同 zoneState 设置驱逐的速度;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:664

1. func (nc *Controller) monitorNodeHealth() error {


2. // 1、从 nodeLister 获取所有 node
3. nodes, err := nc.nodeLister.List(labels.Everything())
4. if err != nil {
5. return err
6. }
7.
8. // 2、根据 controller knownNodeSet 中的记录将 node 分为三类
9. added, deleted, newZoneRepresentatives := nc.classifyNodes(nodes)
10.
11. // 3、为没有 zone 的 node 添加对应的 zone
12. for i := range newZoneRepresentatives {
13. nc.addPodEvictorForNewZone(newZoneRepresentatives[i])
14. }
15.
16. // 4、将新增加的 node 添加到 knownNodeSet 中并且对 node 进行初始化
17. for i := range added {
18. ......
19. nc.knownNodeSet[added[i].Name] = added[i]
20. nc.addPodEvictorForNewZone(added[i])
21. if nc.useTaintBasedEvictions {
22. nc.markNodeAsReachable(added[i])
23. } else {
24. nc.cancelPodEviction(added[i])
25. }
26. }
27.
28. // 5、将 deleted 列表中的 node 从 knownNodeSet 中删除

本文档使用 书栈网 · BookStack.CN 构建 - 88 -


node controller 源码分析

29. for i := range deleted {


30. ......
31. delete(nc.knownNodeSet, deleted[i].Name)
32. }
33.
34. zoneToNodeConditions := map[string][]*v1.NodeCondition{}
35. for i := range nodes {
36. var gracePeriod time.Duration
37. var observedReadyCondition v1.NodeCondition
38. var currentReadyCondition *v1.NodeCondition
39. node := nodes[i].DeepCopy()
40.
// 6、获取 node 的 gracePeriod, observedReadyCondition,
41. currentReadyCondition
if err := wait.PollImmediate(retrySleepTime,
42. retrySleepTime*scheduler.NodeHealthUpdateRetry, func() (bool, error) {
gracePeriod, observedReadyCondition, currentReadyCondition, err =
43. nc.tryUpdateNodeHealth(node)
44. if err == nil {
45. return true, nil
46. }
47. name := node.Name
node, err = nc.kubeClient.CoreV1().Nodes().Get(name,
48. metav1.GetOptions{})
49. if err != nil {
50. return false, err
51. }
52. return false, nil
53. }); err != nil {
54. ......
55. }
56.
57. // 7、若 node 没有被排除则加入到 zoneToNodeConditions 列表中
58. if !isNodeExcludedFromDisruptionChecks(node) {
zoneToNodeConditions[utilnode.GetZoneKey(node)] =
59. append(zoneToNodeConditions[utilnode.GetZoneKey(node)], currentReadyCondition)
60. }
61.
62. decisionTimestamp := nc.now()
63.
64. // 8、根据 observedReadyCondition 为 node 添加不同的 taint
65. if currentReadyCondition != nil {
66. switch observedReadyCondition.Status {

本文档使用 书栈网 · BookStack.CN 构建 - 89 -


node controller 源码分析

67.
68. case v1.ConditionFalse:
69. // 9、false 状态添加 NotReady taint
70. if nc.useTaintBasedEvictions {
if taintutils.TaintExists(node.Spec.Taints,
71. UnreachableTaintTemplate) {
72. taintToAdd := *NotReadyTaintTemplate
if !nodeutil.SwapNodeControllerTaint(nc.kubeClient,
73. []*v1.Taint{&taintToAdd}, []*v1.Taint{UnreachableTaintTemplate}, node) {
74. ......
75. }
76. } else if nc.markNodeForTainting(node) {
77. ......
78. }
79. // 10、或者当超过 podEvictionTimeout 后直接驱逐 node 上的 pod
80. } else {
if
decisionTimestamp.After(nc.nodeHealthMap[node.Name].readyTransitionTimestamp.Add(
81. {
82. if nc.evictPods(node) {
83. ......
84. }
85. }
86. }
87. case v1.ConditionUnknown:
88. // 11、unknown 状态时添加 UnreachableTaint
89. if nc.useTaintBasedEvictions {
if taintutils.TaintExists(node.Spec.Taints,
90. NotReadyTaintTemplate) {
91. taintToAdd := *UnreachableTaintTemplate
if !nodeutil.SwapNodeControllerTaint(nc.kubeClient,
92. []*v1.Taint{&taintToAdd}, []*v1.Taint{NotReadyTaintTemplate}, node) {
93. ......
94. }
95. } else if nc.markNodeForTainting(node) {
96. ......
97. }
98. } else {
if
decisionTimestamp.After(nc.nodeHealthMap[node.Name].probeTimestamp.Add(nc.podEvictionTi
99. {
100. if nc.evictPods(node) {
101. ......

本文档使用 书栈网 · BookStack.CN 构建 - 90 -


node controller 源码分析

102. }
103. }
104. }
105. case v1.ConditionTrue:
106. // 12、true 状态时移除所有 UnreachableTaint 和 NotReadyTaint
107. if nc.useTaintBasedEvictions {
108. removed, err := nc.markNodeAsReachable(node)
109. if err != nil {
110. ......
111. }
112. // 13、从 PodEviction 队列中移除
113. } else {
114. if nc.cancelPodEviction(node) {
115. ......
116. }
117. }
118. }
119.
// 14、ReadyCondition 由 true 变为 false 时标记 node 上的 pod 为
120. notready
if currentReadyCondition.Status != v1.ConditionTrue &&
121. observedReadyCondition.Status == v1.ConditionTrue {
nodeutil.RecordNodeStatusChange(nc.recorder, node,
122. "NodeNotReady")
if err = nodeutil.MarkAllPodsNotReady(nc.kubeClient, node); err
123. != nil {
utilruntime.HandleError(fmt.Errorf("Unable to mark all pods
124. NotReady on node %v: %v", node.Name, err))
125. }
126. }
127. }
128. }
129. // 15、处理中断情况
130. nc.handleDisruption(zoneToNodeConditions, nodes)
131.
132. return nil
133. }

nc.tryUpdateNodeHealth

nc.tryUpdateNodeHealth 会根据当前获取的 node status 更新 nc.nodeHealthMap 中的


数据, nc.nodeHealthMap 保存 node 最近一次的状态,并会根据 nc.nodeHealthMap 判断

本文档使用 书栈网 · BookStack.CN 构建 - 91 -


node controller 源码分析

node 是否已经处于 unknown 状态。

nc.tryUpdateNodeHealth 的主要逻辑为:

1、获取当前 node 的 ReadyCondition 作为 currentReadyCondition,若


ReadyCondition 为空则此 node 可能未上报 status,此时为该 node fake 一个
observedReadyCondition 且其 status 为 Unknown,将其 gracePeriod 设为
nodeStartupGracePeriod,否则 observedReadyCondition 设为
currentReadyCondition 且 gracePeriod 为 nodeMonitorGracePeriod,然后在
nc.nodeHealthMap 中更新 node 的 Status;
2、若 ReadyCondition 存在,则将 observedReadyCondition 置为当前
ReadyCondition,gracePeriod 设为 40s;
3、计算 node 当前的 nodeHealthData,nodeHealthData 中保存了 node 最近一次的状
态,包含 probeTimestamp、readyTransitionTimestamp、status、lease 四个字段。
从 nc.nodeHealthMap 中获取 node 的 condition 和 lease 信息,更新
savedNodeHealth 中 status、probeTimestamp、readyTransitionTimestamp,若启
用了 NodeLease 特性也会更新 NodeHealth 中的 lease 以及 probeTimestamp,最后
将当前计算出 savedNodeHealth 保存到 nc.nodeHealthMap 中;
4、通过获取到的 savedNodeHealth 检查 node 状态,若 NodeReady condition 或者
lease 对象更新时间超过 gracePeriod,则更新 node 的 Ready、MemoryPressure、
DiskPressure、PIDPressure 为 Unknown,若当前计算出来的 node status 与上一次
的 status 不一致则同步到 apiserver,并且更新 nodeHealthMap;
5、最后返回 gracePeriod、observedReadyCondition、currentReadyCondition;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:851

func (nc *Controller) tryUpdateNodeHealth(node *v1.Node) (time.Duration,


1. v1.NodeCondition, *v1.NodeCondition, error) {
2. var gracePeriod time.Duration
3. var observedReadyCondition v1.NodeCondition
_, currentReadyCondition := nodeutil.GetNodeCondition(&node.Status,
4. v1.NodeReady)
5.
6. // 1、若 currentReadyCondition 为 nil 则 fake 一个 observedReadyCondition
7. if currentReadyCondition == nil {
8. observedReadyCondition = v1.NodeCondition{
9. Type: v1.NodeReady,
10. Status: v1.ConditionUnknown,
11. LastHeartbeatTime: node.CreationTimestamp,
12. LastTransitionTime: node.CreationTimestamp,
13. }
14. gracePeriod = nc.nodeStartupGracePeriod

本文档使用 书栈网 · BookStack.CN 构建 - 92 -


node controller 源码分析

15. if _, found := nc.nodeHealthMap[node.Name]; found {


16. nc.nodeHealthMap[node.Name].status = &node.Status
17. } else {
18. nc.nodeHealthMap[node.Name] = &nodeHealthData{
19. status: &node.Status,
20. probeTimestamp: node.CreationTimestamp,
21. readyTransitionTimestamp: node.CreationTimestamp,
22. }
23. }
24. } else {
25. observedReadyCondition = *currentReadyCondition
26. gracePeriod = nc.nodeMonitorGracePeriod
27. }
28.
29. // 2、savedNodeHealth 中保存 node 最近的一次状态
30. savedNodeHealth, found := nc.nodeHealthMap[node.Name]
31.
32. var savedCondition *v1.NodeCondition
33. var savedLease *coordv1beta1.Lease
34. if found {
_, savedCondition = nodeutil.GetNodeCondition(savedNodeHealth.status,
35. v1.NodeReady)
36. savedLease = savedNodeHealth.lease
37. }
38.
// 3、根据 savedCondition 以及 currentReadyCondition 更新 savedNodeHealth 中的
39. 数据
40. if !found {
41. savedNodeHealth = &nodeHealthData{
42. status: &node.Status,
43. probeTimestamp: nc.now(),
44. readyTransitionTimestamp: nc.now(),
45. }
46. } else if savedCondition == nil && currentReadyCondition != nil {
47. savedNodeHealth = &nodeHealthData{
48. status: &node.Status,
49. probeTimestamp: nc.now(),
50. readyTransitionTimestamp: nc.now(),
51. }
52. } else if savedCondition != nil && currentReadyCondition == nil {
53. savedNodeHealth = &nodeHealthData{
54. status: &node.Status,
55. probeTimestamp: nc.now(),

本文档使用 书栈网 · BookStack.CN 构建 - 93 -


node controller 源码分析

56. readyTransitionTimestamp: nc.now(),


57. }
} else if savedCondition != nil && currentReadyCondition != nil &&
58. savedCondition.LastHeartbeatTime != currentReadyCondition.LastHeartbeatTime {
59. var transitionTime metav1.Time
if savedCondition.LastTransitionTime !=
60. currentReadyCondition.LastTransitionTime {
61. transitionTime = nc.now()
62. } else {
63. transitionTime = savedNodeHealth.readyTransitionTimestamp
64. }
65.
66. savedNodeHealth = &nodeHealthData{
67. status: &node.Status,
68. probeTimestamp: nc.now(),
69. readyTransitionTimestamp: transitionTime,
70. }
71. }
72.
73. // 4、判断是否启用了 nodeLease 功能
74. var observedLease *coordv1beta1.Lease
75. if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
observedLease, _ =
76. nc.leaseLister.Leases(v1.NamespaceNodeLease).Get(node.Name)
if observedLease != nil && (savedLease == nil ||
77. savedLease.Spec.RenewTime.Before(observedLease.Spec.RenewTime)) {
78. savedNodeHealth.lease = observedLease
79. savedNodeHealth.probeTimestamp = nc.now()
80. }
81. }
82. nc.nodeHealthMap[node.Name] = savedNodeHealth
83.
84. // 5、检查 node 是否已经超过 gracePeriod 时间没有上报状态了
85. if nc.now().After(savedNodeHealth.probeTimestamp.Add(gracePeriod)) {
86. nodeConditionTypes := []v1.NodeConditionType{
87. v1.NodeReady,
88. v1.NodeMemoryPressure,
89. v1.NodeDiskPressure,
90. v1.NodePIDPressure,
91. }
92. nowTimestamp := nc.now()
93.

本文档使用 书栈网 · BookStack.CN 构建 - 94 -


node controller 源码分析

// 6、若 node 超过 gracePeriod 时间没有上报状态将其所有 Condition 设置


94. unknown
95. for _, nodeConditionType := range nodeConditionTypes {
_, currentCondition := nodeutil.GetNodeCondition(&node.Status,
96. nodeConditionType)
97. if currentCondition == nil {
node.Status.Conditions = append(node.Status.Conditions,
98. v1.NodeCondition{
99. Type: nodeConditionType,
100. Status: v1.ConditionUnknown,
101. Reason: "NodeStatusNeverUpdated",
102. Message: "Kubelet never posted node status.",
103. LastHeartbeatTime: node.CreationTimestamp,
104. LastTransitionTime: nowTimestamp,
105. })
106. } else {
107. if currentCondition.Status != v1.ConditionUnknown {
108. currentCondition.Status = v1.ConditionUnknown
109. currentCondition.Reason = "NodeStatusUnknown"
currentCondition.Message = "Kubelet stopped posting node
110. status."
111. currentCondition.LastTransitionTime = nowTimestamp
112. }
113. }
114. }
115.
116. // 7、更新 node 最新状态至 apiserver 并更新 nodeHealthMap 中的数据
_, currentReadyCondition = nodeutil.GetNodeCondition(&node.Status,
117. v1.NodeReady)
if !apiequality.Semantic.DeepEqual(currentReadyCondition,
118. &observedReadyCondition) {
if _, err := nc.kubeClient.CoreV1().Nodes().UpdateStatus(node); err
119. != nil {
return gracePeriod, observedReadyCondition,
120. currentReadyCondition, err
121. }
122. nc.nodeHealthMap[node.Name] = &nodeHealthData{
123. status: &node.Status,
probeTimestamp:
124. nc.nodeHealthMap[node.Name].probeTimestamp,
125. readyTransitionTimestamp: nc.now(),
126. lease: observedLease,
127. }

本文档使用 书栈网 · BookStack.CN 构建 - 95 -


node controller 源码分析

return gracePeriod, observedReadyCondition, currentReadyCondition,


128. nil
129. }
130. }
131.
132. return gracePeriod, observedReadyCondition, currentReadyCondition, nil
133. }

nc.handleDisruption

monitorNodeHealth 中会为每个 node 划分 zone 并设置


zoneState, nc.handleDisruption 的目的是当集群中不同 zone 下出现多个 unhealthy
node 时会 zone 设置不同的驱逐速率。

nc.handleDisruption 主要逻辑为:

1、设置 allAreFullyDisrupted 默认值为 true,根据 zoneToNodeConditions 中的


数据,判断当前所有 zone 是否都为 FullDisruption 状态;
2、遍历 zoneToNodeConditions 首先调用 nc.computeZoneStateFunc 计算每个 zone
的状态,分为三种 fullyDisrupted (zone 下所有 node 都处于 notReady 状
态)、 partiallyDisrupted (notReady node 占比 >= unhealthyZoneThreshold 的
值且 node 数超过三个)、 normal (以上两种情况之外)。若 newState 不为
stateFullDisruption 将 allAreFullyDisrupted 设为 false,将 newState 保存在
newZoneStates 中;
3、将 allWasFullyDisrupted 默认值设置为 true,根据 zoneStates 中
nodeCondition 的数据,判断上一次观察到的所有 zone 是否都为 FullDisruption 状
态;
4、如果所有 zone 都为 FullyDisrupted 直接停止所有的驱逐工作,因为此时可能处于网络
中断的状态;
5、如果 allAreFullyDisrupted 为 true,allWasFullyDisrupted 为 false,说明
从非 FullyDisrupted 切换到了 FullyDisrupted 模式,此时需要停止所有 node 的驱
逐工作,首先去掉 node 上的 taint 并设置所有zone的对应 zoneNoExecuteTainter 或
者 zonePodEvictor 的 Rate Limeter 为0,最后更新所有 zone 的状态为
FullDisruption ;
6、如果 allWasFullyDisrupted 为 true,allAreFullyDisrupted 为 false,说明
集群从 FullyDisrupted 变为 非 FullyDisrupted 模式,此时首先更新
nc.nodeHealthMap 中所有 node 的 probeTimestamp 和
readyTransitionTimestamp 为当前时间,然后调用 nc.setLimiterInZone 重置每个
zone 的驱逐速率;
7、如果 allWasFullyDisrupted为false 且 allAreFullyDisrupted 为false,即集
群状态保持为非 FullDisruption 时,此时根据 zone 的 state 为每个 zone 设置默认

本文档使用 书栈网 · BookStack.CN 构建 - 96 -


node controller 源码分析

的驱逐速率;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:1017

func (nc *Controller) handleDisruption(zoneToNodeConditions map[string]


1. []*v1.NodeCondition, nodes []*v1.Node) {
2. newZoneStates := map[string]ZoneState{}
3. allAreFullyDisrupted := true
4.
5. // 1、判断当前所有 zone 是否都为 FullDisruption 状态
6. for k, v := range zoneToNodeConditions {
7. zoneSize.WithLabelValues(k).Set(float64(len(v)))
8. // 2、计算 zone state 以及 unhealthy node
9. unhealthy, newState := nc.computeZoneStateFunc(v)
zoneHealth.WithLabelValues(k).Set(float64(100*(len(v)-unhealthy)) /
10. float64(len(v)))
11. unhealthyNodes.WithLabelValues(k).Set(float64(unhealthy))
12. if newState != stateFullDisruption {
13. allAreFullyDisrupted = false
14. }
15. newZoneStates[k] = newState
16. if _, had := nc.zoneStates[k]; !had {
17. nc.zoneStates[k] = stateInitial
18. }
19. }
20.
21. // 3、判断上一次观察到的所有 zone 是否都为 FullDisruption 状态
22. allWasFullyDisrupted := true
23. for k, v := range nc.zoneStates {
24. if _, have := zoneToNodeConditions[k]; !have {
25. zoneSize.WithLabelValues(k).Set(0)
26. zoneHealth.WithLabelValues(k).Set(100)
27. unhealthyNodes.WithLabelValues(k).Set(0)
28. delete(nc.zoneStates, k)
29. continue
30. }
31. if v != stateFullDisruption {
32. allWasFullyDisrupted = false
33. break
34. }
35. }
36. // 4、若存在一个不为 FullyDisrupted
37. if !allAreFullyDisrupted || !allWasFullyDisrupted {

本文档使用 书栈网 · BookStack.CN 构建 - 97 -


node controller 源码分析

38. // 5、如果 allAreFullyDisrupted 为 true,则 allWasFullyDisrupted 为 false


39. // 说明从非 FullyDisrupted 切换到了 FullyDisrupted 模式
40. if allAreFullyDisrupted {
41. for i := range nodes {
42. if nc.useTaintBasedEvictions {
43. _, err := nc.markNodeAsReachable(nodes[i])
44. if err != nil {
klog.Errorf("Failed to remove taints from Node %v",
45. nodes[i].Name)
46. }
47. } else {
48. nc.cancelPodEviction(nodes[i])
49. }
50. }
51.
52. for k := range nc.zoneStates {
53. if nc.useTaintBasedEvictions {
54. nc.zoneNoExecuteTainter[k].SwapLimiter(0)
55. } else {
56. nc.zonePodEvictor[k].SwapLimiter(0)
57. }
58. }
59. for k := range nc.zoneStates {
60. nc.zoneStates[k] = stateFullDisruption
61. }
62. return
63. }
64. // 6、如果 allWasFullyDisrupted 为 true,则 allAreFullyDisrupted 为 false
65. // 说明 cluster 从 FullyDisrupted 切换为非 FullyDisrupted 模式
66. if allWasFullyDisrupted {
67. now := nc.now()
68. for i := range nodes {
69. v := nc.nodeHealthMap[nodes[i].Name]
70. v.probeTimestamp = now
71. v.readyTransitionTimestamp = now
72. nc.nodeHealthMap[nodes[i].Name] = v
73. }
74.
75. for k := range nc.zoneStates {
nc.setLimiterInZone(k, len(zoneToNodeConditions[k]),
76. newZoneStates[k])
77. nc.zoneStates[k] = newZoneStates[k]
78. }

本文档使用 书栈网 · BookStack.CN 构建 - 98 -


node controller 源码分析

79. return
80. }
81.
82. // 7、根据 zoneState 为每个 zone 设置驱逐速率
83. for k, v := range nc.zoneStates {
84. newState := newZoneStates[k]
85. if v == newState {
86. continue
87. }
88.
89. nc.setLimiterInZone(k, len(zoneToNodeConditions[k]), newState)
90. nc.zoneStates[k] = newState
91. }
92. }
93. }

nc.computeZoneStateFunc

nc.computeZoneStateFunc 是计算 zone state 的方法,该方法会计算每个 zone 下


notReady 的 node 并将 zone 分为三种:

fullyDisrupted :zone 下所有 node 都处于 notReady 状态;


partiallyDisrupted :notReady node 占比 >= unhealthyZoneThreshold 的值(默认
为0.55,通过 --unhealthy-zone-threshold 设置)且 notReady node 数超过2个;
normal :以上两种情况之外的;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:1262

func (nc *Controller) ComputeZoneState(nodeReadyConditions []*v1.NodeCondition)


1. (int, ZoneState) {
2. readyNodes := 0
3. notReadyNodes := 0
4. for i := range nodeReadyConditions {
if nodeReadyConditions[i] != nil && nodeReadyConditions[i].Status ==
5. v1.ConditionTrue {
6. readyNodes++
7. } else {
8. notReadyNodes++
9. }
10. }
11. switch {
12. case readyNodes == 0 && notReadyNodes > 0:
13. return notReadyNodes, stateFullDisruption

本文档使用 书栈网 · BookStack.CN 构建 - 99 -


node controller 源码分析

case notReadyNodes > 2 &&


float32(notReadyNodes)/float32(notReadyNodes+readyNodes) >=
14. nc.unhealthyZoneThreshold:
15. return notReadyNodes, statePartialDisruption
16. default:
17. return notReadyNodes, stateNormal
18. }
19. }

nc.setLimiterInZone

nc.setLimiterInZone 方法会根据不同的 zoneState 设置对应的驱逐速率:

stateNormal :驱逐速率为 evictionLimiterQPS(默认为0.1,可以通过 --node-


eviction-rate 参数指定)的值,即每隔 10s 清空一个节点;
statePartialDisruption :如果当前 zone size 大于 nc.largeClusterThreshold (默
认为 50,通过 --large-cluster-size-threshold 设置),则设置为
secondaryEvictionLimiterQPS(默认为 0.01,可以通过 --secondary-node-
eviction-rate 指定),否则设置为 0;
stateFullDisruption :为 evictionLimiterQPS(默认为0.1,可以通过 --node-
eviction-rate 参数指定)的值;

k8s.io/kubernetes/pkg/controller/nodelifecycle/node_lifecycle_controller.go:1115

func (nc *Controller) setLimiterInZone(zone string, zoneSize int, state


1. ZoneState) {
2. switch state {
3. case stateNormal:
4. if nc.useTaintBasedEvictions {
5. nc.zoneNoExecuteTainter[zone].SwapLimiter(nc.evictionLimiterQPS)
6. } else {
7. nc.zonePodEvictor[zone].SwapLimiter(nc.evictionLimiterQPS)
8. }
9. case statePartialDisruption:
10. if nc.useTaintBasedEvictions {
11. nc.zoneNoExecuteTainter[zone].SwapLimiter(
12. nc.enterPartialDisruptionFunc(zoneSize))
13. } else {
14. nc.zonePodEvictor[zone].SwapLimiter(
15. nc.enterPartialDisruptionFunc(zoneSize))
16. }
17. case stateFullDisruption:
18. if nc.useTaintBasedEvictions {

本文档使用 书栈网 · BookStack.CN 构建 - 100 -


node controller 源码分析

19. nc.zoneNoExecuteTainter[zone].SwapLimiter(
20. nc.enterFullDisruptionFunc(zoneSize))
21. } else {
22. nc.zonePodEvictor[zone].SwapLimiter(
23. nc.enterFullDisruptionFunc(zoneSize))
24. }
25. }
26. }

小结

monitorNodeHealth 中的主要流程如下所示:

1. monitorNodeHealth
2. |
3. |
4. useTaintBasedEvictions
5. |
6. |
7. ---------------------------------------------
8. yes | | no
9. | |
10. v v
11. addPodEvictorForNewZone evictPods
12. | |
13. | |
14. v v
15. zoneNoExecuteTainter zonePodEvictor
16. (RateLimitedTimedQueue) (RateLimitedTimedQueue)
17. | |
18. | |
19. | |
20. v v
21. doNoExecuteTaintingPass doEvictionPass
22. (consumer) (consumer)

NodeLifecycleController 中三个核心组件之间的交互流程如下所示:

1. monitorNodeHealth
2. |
3. |
4. | 为 node 添加 NoExecute taint

本文档使用 书栈网 · BookStack.CN 构建 - 101 -


node controller 源码分析

5. |
6. |
7. v 为 node 添加
8. watch nodeList NoSchedule taint
taintManager ------> APIServer <-----------
9. nc.doNodeProcessingPassWorker
10. |
11. |
12. |
13. v
14. 驱逐 node 上不容忍
15. node taint 的 pod

至此,NodeLifecycleController 的核心代码已经分析完。

总结
本文主要分析了 NodeLifecycleController 的设计与实现,NodeLifecycleController 主要
是监控 node 状态,当 node 异常时驱逐 node 上的 pod,其行为与其他组件有一定关系,node
的状态由 kubelet 上报,node 异常时为 node 添加 taint 标签后,scheduler 调度 pod 也
会有对应的行为。为了保证由于网络等问题引起的 pod 驱逐行为,NodeLifecycleController 会
为 node 进行分区并会为每个区设置不同的驱逐速率,即实际上会以 rate-limited 的方式添加
taint,在某些情况下可以避免 pod 被大量驱逐。

此外,NodeLifecycleController 还会对外暴露多个 metrics,包括 zoneHealth、


zoneSize、unhealthyNodes、evictionsNumber 等,便于用户查看集群下 node 的状态。

参考:

https://kubernetes.io/zh/docs/concepts/configuration/taint-and-toleration/

https://kubernetes.io/docs/reference/command-line-tools-reference/feature-
gates/

本文档使用 书栈网 · BookStack.CN 构建 - 102 -


job controller 源码分析

job 在 kubernetes 中主要用来处理离线任务,job 直接管理 pod,可以创建一个或多个 pod 并


会确保指定数量的 pod 运行完成。kubernetes 中有两种类型的 job,分别为 cronjob 和
batchjob,cronjob 类似于定时任务是定时触发的而 batchjob 创建后会直接运行,本文主要介
绍 batchjob,下面简称为 job。

job 的基本功能

创建

job 的一个示例如下所示:

1. apiVersion: batch/v1
2. kind: Job
3. metadata:
4. name: pi
5. spec:
6. backoffLimit: 6 // 标记为 failed 前的重试次数,默认为 6
completions: 4 // 要完成job 的 pod 数,若没有设定该值则默认等于
7. parallelism 的值
8. parallelism: 2 // 任意时间最多可以启动多少个 pod 同时运行,默认为 1
9. activeDeadlineSeconds: 120 // job 运行时间
10. ttlSecondsAfterFinished: 60 // job 在运行完成后 60 秒就会自动删除掉
11. template:
12. spec:
13. containers:
14. - command:
15. - sh
16. - -c
17. - 'echo ''scale=5000; 4*a(1)'' | bc -l '
18. image: resouer/ubuntu-bc
19. name: pi
20. restartPolicy: Never

扩缩容

job 不支持运行时扩缩容,job 在创建后其 spec.completions 字段也不支持修改。

删除

通常系统中已执行完成的 job 不再需要,将它们保留在系统中会占用一定的资源,需要进行回收,


pod 在执行完任务后会进入到 Completed 状态,删除 job 也会清除其创建的 pod。

本文档使用 书栈网 · BookStack.CN 构建 - 103 -


job controller 源码分析

1. $ kubectl get pod


2. pi-gdrwr 0/1 Completed 0 10m
3. pi-rjphf 0/1 Completed 0 10m
4.
5. $ kubectl delete job pi

自动清理机制

每次 job 执行完成后手动回收非常麻烦,k8s 在 v1.12 版本中加入了 TTLAfterFinished


feature-gates,启用该特性后会启动一个 TTL 控制器,在创建 job 时指定后可在 job 运行完
成后自动回收相关联的 pod,如上文中的 yaml 所示,创建 job 时指定了
ttlSecondsAfterFinished: 60 ,job 在执行完成后停留 60s 会被自动回收, 若
ttlSecondsAfterFinished 设置为 0 则表示在 job 执行完成后立刻回收。当 TTL 控制器清理
job 时,它将级联删除 job,即 pod 和 job 一起被删除。不过该特性截止目前还是 Alpha 版
本,请谨慎使用。

job controller 源码分析

kubernetes 版本:v1.16

在上节介绍了 job 的基本操作后,本节会继续深入源码了解其背后的设计与实现。

startJobController

首先还是直接看 jobController 的启动方法 startJobController ,该方法中调用


NewJobController 初始化 jobController 然后调用 Run 方法启动 jobController。
从初始化流程中可以看到 JobController 监听 pod 和 job 两种资源,其中
ConcurrentJobSyncs 默认值为 5。

k8s.io/kubernetes/cmd/kube-controller-manager/app/batch.go:33

1. func startJobController(ctx ControllerContext) (http.Handler, bool, error) {


if !ctx.AvailableResources[schema.GroupVersionResource{Group: "batch",
2. Version: "v1", Resource: "jobs"}] {
3. return nil, false, nil
4. }
5. go job.NewJobController(
6. ctx.InformerFactory.Core().V1().Pods(),
7. ctx.InformerFactory.Batch().V1().Jobs(),
8. ctx.ClientBuilder.ClientOrDie("job-controller"),
9. ).Run(int(ctx.ComponentConfig.JobController.ConcurrentJobSyncs), ctx.Stop)
10. return nil, true, nil
11. }

本文档使用 书栈网 · BookStack.CN 构建 - 104 -


job controller 源码分析

Run

以下是 jobController 的 Run 方法,其中核心逻辑是调用 jm.worker 执行 syncLoop


操作,worker 方法是 syncJob 方法的别名,最终调用的是 syncJob 。

k8s.io/kubernetes/pkg/controller/job/job_controller.go:139

1. func (jm *JobController) Run(workers int, stopCh <-chan struct{}) {


2. defer utilruntime.HandleCrash()
3. defer jm.queue.ShutDown()
4.
5. klog.Infof("Starting job controller")
6. defer klog.Infof("Shutting down job controller")
7.
if !cache.WaitForNamedCacheSync("job", stopCh, jm.podStoreSynced,
8. jm.jobStoreSynced) {
9. return
10. }
11.
12. for i := 0; i < workers; i++ {
13. go wait.Until(jm.worker, time.Second, stopCh)
14. }
15.
16. <-stopCh
17. }

syncJob

syncJob 是 jobController 的核心方法,其主要逻辑为:

1、从 lister 中获取 job 对象;

2、判断 job 是否已经执行完成,当 job 的 .status.conditions 中有 Complete


或 Failed 的 type 且对应的 status 为 true 时表示该 job 已经执行完成,例如:

1. status:
2. completionTime: "2019-12-18T14:16:47Z"
3. conditions:
4. - lastProbeTime: "2019-12-18T14:16:47Z"
5. lastTransitionTime: "2019-12-18T14:16:47Z"
6. status: "True" // status 为 true
7. type: Complete // Complete

本文档使用 书栈网 · BookStack.CN 构建 - 105 -


job controller 源码分析

8. startTime: "2019-12-18T14:15:35Z"
9. succeeded: 2

3、获取 job 重试的次数;


4、调用 jm.expectations.SatisfiedExpectations 判断 job 是否需能进行 sync 操
作,Expectations 机制在之前写的” ReplicaSetController 源码分析“一文中详细讲解
过,其主要判断条件如下:
1、该 key 在 ControllerExpectations 中的 adds 和 dels 都 <= 0,即调用
apiserver 的创建和删除接口没有失败过;
2、该 key 在 ControllerExpectations 中已经超过 5min 没有更新了;
3、该 key 在 ControllerExpectations 中不存在,即该对象是新创建的;
4、调用 GetExpectations 方法失败,内部错误;
5、调用 jm.getPodsForJob 通过 selector 获取 job 关联的 pod,若有孤儿 pod 的
label 与 job 的能匹配则进行关联,若已关联的 pod label 有变化则解除与 job 的关联
关系;
6、分别计算 active 、 succeeded 、 failed 状态的 pod 数;
7、判断 job 是否为首次启动,若首次启动其 job.Status.StartTime 为空,此时首先设置
startTime,然后检查是否有 job.Spec.ActiveDeadlineSeconds 是否为空,若不为空则将
其再加入到延迟队列中,等待 ActiveDeadlineSeconds 时间后会再次触发 sync 操作;
8、判断 job 的重试次数是否超过了 job.Spec.BackoffLimit (默认是6次),有两个判断方
法一是 job 的重试次数以及 job 的状态,二是当 job 的 restartPolicy 为
OnFailure 时 container 的重启次数,两者任一个符合都说明 job 处于 failed 状态且
原因为 BackoffLimitExceeded ;
9、判断 job 的运行时间是否达到 job.Spec.ActiveDeadlineSeconds 中设定的值,若已达
到则说明 job 此时处于 failed 状态且原因为 DeadlineExceeded ;
10、根据以上判断如果 job 处于 failed 状态,则调用 jm.deleteJobPods 并发删除所有
active pods ;
11、若非 failed 状态,根据 jobNeedsSync 判断是否要进行同步,若需要同步则调用
jm.manageJob 进行同步;
12、通过检查 job.Spec.Completions 判断 job 是否已经运行完成,若
job.Spec.Completions 字段没有设置值则只要有一个 pod 运行完成该 job 就为
Completed 状态,若设置了 job.Spec.Completions 会通过判断已经运行完成状态的 pod
即 succeeded pod 数是否大于等于该值;
13、通过以上判断若 job 运行完成了,则更新 job.Status.Conditions 和
job.Status.CompletionTime 字段;
14、如果 job 的 status 有变化,将 job 的 status 更新到 apiserver;

在 syncJob 中又调用了 jm.manageJob 处理非 failed 状态下的 sync 操作,下面主


要分析一下该方法。

本文档使用 书栈网 · BookStack.CN 构建 - 106 -


job controller 源码分析

k8s.io/kubernetes/pkg/controller/job/job_controller.go:436

1. func (jm *JobController) syncJob(key string) (bool, error) {


2. // 1、计算每次 sync 的运行时间
3. startTime := time.Now()
4. defer func() {
klog.V(4).Infof("Finished syncing job %q (%v)", key,
5. time.Since(startTime))
6. }()
7.
8. ns, name, err := cache.SplitMetaNamespaceKey(key)
9. if err != nil {
10. return false, err
11. }
12. if len(ns) == 0 || len(name) == 0 {
return false, fmt.Errorf("invalid job key %q: either namespace or name
13. is missing", key)
14. }
15.
16. // 2、从 lister 中获取 job 对象
17. sharedJob, err := jm.jobLister.Jobs(ns).Get(name)
18. if err != nil {
19. if errors.IsNotFound(err) {
20. klog.V(4).Infof("Job has been deleted: %v", key)
21. jm.expectations.DeleteExpectations(key)
22. return true, nil
23. }
24. return false, err
25. }
26. job := *sharedJob
27.
28. // 3、判断 job 是否已经执行完成
29. if IsJobFinished(&job) {
30. return true, nil
31. }
32.
33. // 4、获取 job 重试的次数
34. previousRetry := jm.queue.NumRequeues(key)
35.
36. // 5、判断 job 是否能进行 sync 操作
37. jobNeedsSync := jm.expectations.SatisfiedExpectations(key)
38.
39. // 6、获取 job 关联的所有 pod

本文档使用 书栈网 · BookStack.CN 构建 - 107 -


job controller 源码分析

40. pods, err := jm.getPodsForJob(&job)


41. if err != nil {
42. return false, err
43. }
44.
45. // 7、分别计算 active、succeeded、failed 状态的 pod 数
46. activePods := controller.FilterActivePods(pods)
47. active := int32(len(activePods))
48. succeeded, failed := getStatus(pods)
49. conditions := len(job.Status.Conditions)
50.
51. // 8、判断 job 是否为首次启动
52. if job.Status.StartTime == nil {
53. now := metav1.Now()
54. job.Status.StartTime = &now
55. // 9、判断是否设定了 ActiveDeadlineSeconds 值
56. if job.Spec.ActiveDeadlineSeconds != nil {
klog.V(4).Infof("Job %s have ActiveDeadlineSeconds will sync after
57. %d seconds",
58. key, *job.Spec.ActiveDeadlineSeconds)
jm.queue.AddAfter(key,
59. time.Duration(*job.Spec.ActiveDeadlineSeconds)*time.Second)
60. }
61. }
62.
63. var manageJobErr error
64. jobFailed := false
65. var failureReason string
66. var failureMessage string
67.
68. // 10、判断 job 的重启次数是否已达到上限,即处于 BackoffLimitExceeded
69. jobHaveNewFailure := failed > job.Status.Failed
exceedsBackoffLimit := jobHaveNewFailure && (active !=
70. *job.Spec.Parallelism) &&
71. (int32(previousRetry)+1 > *job.Spec.BackoffLimit)
72.
73. if exceedsBackoffLimit || pastBackoffLimitOnFailure(&job, pods) {
74. jobFailed = true
75. failureReason = "BackoffLimitExceeded"
76. failureMessage = "Job has reached the specified backoff limit"
77. } else if pastActiveDeadline(&job) {
78. jobFailed = true
79. failureReason = "DeadlineExceeded"

本文档使用 书栈网 · BookStack.CN 构建 - 108 -


job controller 源码分析

80. failureMessage = "Job was active longer than specified deadline"


81. }
82.
83. // 11、如果处于 failed 状态,则调用 jm.deleteJobPods 并发删除所有 active pods
84. if jobFailed {
85. errCh := make(chan error, active)
86. jm.deleteJobPods(&job, activePods, errCh)
87. select {
88. case manageJobErr = <-errCh:
89. if manageJobErr != nil {
90. break
91. }
92. default:
93. }
94.
95. failed += active
96. active = 0
job.Status.Conditions = append(job.Status.Conditions,
97. newCondition(batch.JobFailed, failureReason, failureMessage))
jm.recorder.Event(&job, v1.EventTypeWarning, failureReason,
98. failureMessage)
99. } else {
100.
101. // 12、若非 failed 状态,根据 jobNeedsSync 判断是否要进行同步
102. if jobNeedsSync && job.DeletionTimestamp == nil {
103. active, manageJobErr = jm.manageJob(activePods, succeeded, &job)
104. }
105.
106. // 13、检查 job.Spec.Completions 判断 job 是否已经运行完成
107. completions := succeeded
108. complete := false
109. if job.Spec.Completions == nil {
110. if succeeded > 0 && active == 0 {
111. complete = true
112. }
113. } else {
114. if completions >= *job.Spec.Completions {
115. complete = true
116. if active > 0 {
jm.recorder.Event(&job, v1.EventTypeWarning,
"TooManyActivePods", "Too many active pods running after completion count
117. reached")
118. }

本文档使用 书栈网 · BookStack.CN 构建 - 109 -


job controller 源码分析

119. if completions > *job.Spec.Completions {


jm.recorder.Event(&job, v1.EventTypeWarning,
"TooManySucceededPods", "Too many succeeded pods running after completion count
120. reached")
121. }
122. }
123. }
124.
// 14、若 job 运行完成了,则更新 job.Status.Conditions 和
125. job.Status.CompletionTime 字段
126. if complete {
job.Status.Conditions = append(job.Status.Conditions,
127. newCondition(batch.JobComplete, "", ""))
128. now := metav1.Now()
129. job.Status.CompletionTime = &now
130. }
131. }
132.
133. forget := false
134. if job.Status.Succeeded < succeeded {
135. forget = true
136. }
137.
138. // 15、如果 job 的 status 有变化,将 job 的 status 更新到 apiserver
if job.Status.Active != active || job.Status.Succeeded != succeeded ||
139. job.Status.Failed != failed || len(job.Status.Conditions) != conditions {
140. job.Status.Active = active
141. job.Status.Succeeded = succeeded
142. job.Status.Failed = failed
143.
144. if err := jm.updateHandler(&job); err != nil {
145. return forget, err
146. }
147.
148. if jobHaveNewFailure && !IsJobFinished(&job) {
return forget, fmt.Errorf("failed pod(s) detected for job key %q",
149. key)
150. }
151.
152. forget = true
153. }
154.
155. return forget, manageJobErr

本文档使用 书栈网 · BookStack.CN 构建 - 110 -


job controller 源码分析

156. }

jm.manageJob

jm.manageJob 它主要做的事情就是根据 job 配置的并发数来确认当前处于 active 的 pods


数量是否合理,如果不合理的话则进行调整,其主要逻辑为:

1、首先获取 job 的 active pods 数与可运行的 pod 数即 job.Spec.Parallelism ;

2、判断如果处于 active 状态的 pods 数大于 job 设置的并发数


job.Spec.Parallelism ,则并发删除多余的 active pods,需要删除的 active pods
是有一定的优先级的,删除的优先级为:

1、判断是否绑定了 node:Unassigned < assigned;


2、判断 pod phase:PodPending < PodUnknown < PodRunning;
3、判断 pod 状态:Not ready < ready;
4、若 pod 都为 ready,则按运行时间排序,运行时间最短会被删除:empty time <
less time < more time;
5、根据 pod 重启次数排序:higher restart counts < lower restart
counts;
6、按 pod 创建时间进行排序:Empty creation time pods < newer pods <
older pods;

3、若处于 active 状态的 pods 数小于 job 设置的并发数,则需要根据 job 的配置计算


pod 的 diff 数并进行创建,计算方法与 completions 、 parallelism 以及
succeeded 的 pods 数有关,计算出 diff 数后会进行批量创建,创建的 pod 数依次为
1、2、4、8……,呈指数级增长,job 创建 pod 的方式与 rs 创建 pod 是类似的,但是此处
并没有限制在一个 syncLoop 中创建 pod 的上限值,创建完 pod 后会将结果记录在 job
的 expectations 中,此处并非所有的 pod 都能创建成功,若超时错误会直接忽略,因其
他错误创建失败的 pod 会记录在 expectations 中, expectations 机制的主要目的是
减少不必要的 sync 操作,至于其详细的说明可以参考之前写的 ” ReplicaSetController
源码分析“ 一文;

k8s.io/kubernetes/pkg/controller/job/job_controller.go:684

func (jm *JobController) manageJob(activePods []*v1.Pod, succeeded int32, job


1. *batch.Job) (int32, error) {
2. // 1、获取 job 的 active pods 数与可运行的 pod 数
3. var activeLock sync.Mutex
4. active := int32(len(activePods))
5. parallelism := *job.Spec.Parallelism
6. jobKey, err := controller.KeyFunc(job)
7. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 111 -


job controller 源码分析

utilruntime.HandleError(fmt.Errorf("Couldn't get key for job %#v: %v",


8. job, err))
9. return 0, nil
10. }
11.
12. var errCh chan error
13. // 2、如果处于 active 状态的 pods 数大于 job 设置的并发数
14. if active > parallelism {
15. diff := active - parallelism
16. errCh = make(chan error, diff)
17. jm.expectations.ExpectDeletions(jobKey, int(diff))
klog.V(4).Infof("Too many pods running job %q, need %d, deleting %d",
18. jobKey, parallelism, diff)
19.
20. // 3、对 activePods 按以上 6 种策略进行排序
21. sort.Sort(controller.ActivePods(activePods))
22.
23. // 4、并发删除多余的 active pods
24. active -= diff
25. wait := sync.WaitGroup{}
26. wait.Add(int(diff))
27. for i := int32(0); i < diff; i++ {
28. go func(ix int32) {
29. defer wait.Done()
if err := jm.podControl.DeletePod(job.Namespace,
30. activePods[ix].Name, job); err != nil {
31. defer utilruntime.HandleError(err)
32.
klog.V(2).Infof("Failed to delete %v, decrementing
33. expectations for job %q/%q", activePods[ix].Name, job.Namespace, job.Name)
34. jm.expectations.DeletionObserved(jobKey)
35. activeLock.Lock()
36. active++
37. activeLock.Unlock()
38. errCh <- err
39. }
40. }(i)
41. }
42. wait.Wait()
43.
44. // 5、若处于 active 状态的 pods 数小于 job 设置的并发数,则需要创建出新的 pod
45. } else if active < parallelism {
46. // 6、首先计算出 diff 数

本文档使用 书栈网 · BookStack.CN 构建 - 112 -


job controller 源码分析

// 若 job.Spec.Completions == nil && succeeded pods > 0, 则diff =


47. 0;
// 若 job.Spec.Completions == nil && succeeded pods = 0,则diff =
48. Parallelism;
// 若 job.Spec.Completions != nil 则diff等于(job.Spec.Completions -
49. succeeded - active)和 parallelism 中的最小值(非负值);
50. wantActive := int32(0)
51. if job.Spec.Completions == nil {
52. if succeeded > 0 {
53. wantActive = active
54. } else {
55. wantActive = parallelism
56. }
57. } else {
58. wantActive = *job.Spec.Completions - succeeded
59. if wantActive > parallelism {
60. wantActive = parallelism
61. }
62. }
63. diff := wantActive - active
64. if diff < 0 {
utilruntime.HandleError(fmt.Errorf("More active than wanted: job
65. %q, want %d, have %d", jobKey, wantActive, active))
66. diff = 0
67. }
68. if diff == 0 {
69. return active, nil
70. }
71. jm.expectations.ExpectCreations(jobKey, int(diff))
72. errCh = make(chan error, diff)
klog.V(4).Infof("Too few pods running job %q, need %d, creating %d",
73. jobKey, wantActive, diff)
74. active += diff
75. wait := sync.WaitGroup{}
76.
77. // 7、批量创建 pod,呈指数级增长
for batchSize := int32(integer.IntMin(int(diff),
controller.SlowStartInitialBatchSize)); diff > 0; batchSize =
78. integer.Int32Min(2*batchSize, diff) {
79. errorCount := len(errCh)
80. wait.Add(int(batchSize))
81. for i := int32(0); i < batchSize; i++ {
82. go func() {

本文档使用 书栈网 · BookStack.CN 构建 - 113 -


job controller 源码分析

83. defer wait.Done()


err :=
jm.podControl.CreatePodsWithControllerRef(job.Namespace, &job.Spec.Template,
84. job, metav1.NewControllerRef(job, controllerKind))
85. // 8、调用 apiserver 创建时忽略 Timeout 错误
86. if err != nil && errors.IsTimeout(err) {
87. return
88. }
89. if err != nil {
90. defer utilruntime.HandleError(err)
klog.V(2).Infof("Failed creation, decrementing
91. expectations for job %q/%q", job.Namespace, job.Name)
92. jm.expectations.CreationObserved(jobKey)
93. activeLock.Lock()
94. active--
95. activeLock.Unlock()
96. errCh <- err
97. }
98. }()
99. }
100. wait.Wait()
101.
102. // 9、若有创建失败的操作记录在 expectations 中
103. skippedPods := diff - batchSize
104. if errorCount < len(errCh) && skippedPods > 0 {
klog.V(2).Infof("Slow-start failure. Skipping creation of %d
pods, decrementing expectations for job %q/%q", skippedPods, job.Namespace,
105. job.Name)
106. active -= skippedPods
107. for i := int32(0); i < skippedPods; i++ {
108. jm.expectations.CreationObserved(jobKey)
109. }
110. break
111. }
112. diff -= batchSize
113. }
114. }
115. select {
116. case err := <-errCh:
117. if err != nil {
118. return active, err
119. }
120. default:

本文档使用 书栈网 · BookStack.CN 构建 - 114 -


job controller 源码分析

121. }
122.
123. return active, nil
124. }

总结
以上就是 jobController 源码中主要的逻辑,从上文分析可以看到 jobController 的代码比较
清晰,若看过前面写的几个 controller 分析会发现每个 controller 在功能实现上有很多类似的
地方。

本文档使用 书栈网 · BookStack.CN 构建 - 115 -


garbage collector controller 源码分析

在前面几篇关于 controller 源码分析的文章中多次提到了当删除一个对象时,其对应的


controller 并不会执行删除对象的操作,在 kubernetes 中对象的回收操作是由
GarbageCollectorController 负责的,其作用就是当删除一个对象时,会根据指定的删除策略回
收该对象及其依赖对象,本文会深入分析垃圾收集背后的实现。

kubernetes 中的删除策略
kubernetes 中有三种删除策略: Orphan 、 Foreground 和 Background ,三种删除策略
的意义分别为:

Orphan 策略:非级联删除,删除对象时,不会自动删除它的依赖或者是子对象,这些依赖被
称作是原对象的孤儿对象,例如当执行以下命令时会使用 Orphan 策略进行删除,此时 ds
的依赖对象 controllerrevision 不会被删除;

1. $ kubectl delete ds/nginx-ds --cascade=false

Background 策略:在该模式下,kubernetes 会立即删除该对象,然后垃圾收集器会在后


台删除这些该对象的依赖对象;

Foreground 策略:在该模式下,对象首先进入“删除中”状态,即会设置对象的
deletionTimestamp 字段并且对象的 metadata.finalizers 字段包含了值
“foregroundDeletion”,此时该对象依然存在,然后垃圾收集器会删除该对象的所有依赖对
象,垃圾收集器在删除了所有“Blocking” 状态的依赖对象(指其子对象中
ownerReference.blockOwnerDeletion=true 的对象)之后,然后才会删除对象本身;

在 v1.9 以前的版本中,大部分 controller 默认的删除策略为 Orphan ,从 v1.9 开始,对


于 apps/v1 下的资源默认使用 Background 模式。以上三种删除策略都可以在删除对象时通过
设置 deleteOptions.propagationPolicy 字段进行指定,如下所示:

$ curl -k -v -XDELETE -H "Accept: application/json" -H "Content-Type:


application/json" -d '{"propagationPolicy":"Foreground"}'
'https://192.168.99.108:8443/apis/apps/v1/namespaces/default/daemonsets/nginx-
1. ds'

finalizer 机制

finalizer 是在删除对象时设置的一个 hook,其目的是为了让对象在删除前确认其子对象已经被完


全删除,k8s 中默认有两种 finalizer: OrphanFinalizer 和 ForegroundFinalizer ,
finalizer 存在于对象的 ObjectMeta 中,当一个对象的依赖对象被删除后其对应的
finalizers 字段也会被移除,只有 finalizers 字段为空时,apiserver 才会删除该对象。

1. {

本文档使用 书栈网 · BookStack.CN 构建 - 116 -


garbage collector controller 源码分析

2. ......
3. "metadata": {
4. ......
5. "finalizers": [
6. "foregroundDeletion"
7. ]
8. }
9. ......
10. }

此外,finalizer 不仅仅支持以上两种字段,在使用自定义 controller 时也可以在 CR 中设置


自定义的 finalizer 标识。

GarbageCollectorController 源码分析

kubernetes 版本:v1.16

GarbageCollectorController 负责回收 kubernetes 中的资源,要回收 kubernetes 中所


有资源首先得监控所有资源,GarbageCollectorController 会监听集群中所有可删除资源产生的
所有事件,这些事件会被放入到一个队列中,然后 controller 会启动多个 goroutine 处理队列
中的事件,若为删除事件会根据对象的删除策略删除关联的对象,对于非删除事件会更新对象之间的依
赖关系。

startGarbageCollectorController

首先还是看 GarbageCollectorController 的启动方法


startGarbageCollectorController ,其主要逻辑为:

1、初始化 discoveryClient,discoveryClient 主要用来获取集群中的所有资源;


2、调用 garbagecollector.GetDeletableResources 获取集群内所有可删除的资源对象,支
持 “delete”, “list”, “watch” 三种操作的 resource 称为 deletableResource ;
3、调用 garbagecollector.NewGarbageCollector 初始化 garbageCollector 对象;
4、调用 garbageCollector.Run 启动 garbageCollector;
5、调用 garbageCollector.Sync 监听集群中的 DeletableResources ,当出现新的
DeletableResources 时同步到 monitors 中,确保监控集群中的所有资源;
6、调用 garbagecollector.NewDebugHandler 注册 debug 接口,用来提供集群内所有对
象的关联关系;

k8s.io/kubernetes/cmd/kube-controller-manager/app/core.go:443

func startGarbageCollectorController(ctx ControllerContext) (http.Handler,


1. bool, error) {

本文档使用 书栈网 · BookStack.CN 构建 - 117 -


garbage collector controller 源码分析

2. if !ctx.ComponentConfig.GarbageCollectorController.EnableGarbageCollector {
3. return nil, false, nil
4. }
5. // 1、初始化 discoveryClient
6. gcClientset := ctx.ClientBuilder.ClientOrDie("generic-garbage-collector")
discoveryClient :=
7. cacheddiscovery.NewMemCacheClient(gcClientset.Discovery())
8.
9. config := ctx.ClientBuilder.ConfigOrDie("generic-garbage-collector")
10. metadataClient, err := metadata.NewForConfig(config)
11. if err != nil {
12. return nil, true, err
13. }
14.
15. // 2、获取 deletableResource
deletableResources :=
16. garbagecollector.GetDeletableResources(discoveryClient)
17. ignoredResources := make(map[schema.GroupResource]struct{})
for _, r := range
18. ctx.ComponentConfig.GarbageCollectorController.GCIgnoredResources {
ignoredResources[schema.GroupResource{Group: r.Group, Resource:
19. r.Resource}] = struct{}{}
20. }
21.
22. // 3、初始化 garbageCollector 对象
23. garbageCollector, err := garbagecollector.NewGarbageCollector(
24. ......
25. )
26. if err != nil {
return nil, true, fmt.Errorf("failed to start the generic garbage
27. collector: %v", err)
28. }
29. // 4、启动 garbage collector
workers :=
30. int(ctx.ComponentConfig.GarbageCollectorController.ConcurrentGCSyncs)
31. go garbageCollector.Run(workers, ctx.Stop)
32.
33. // 5、监听集群中的 DeletableResources
34. go garbageCollector.Sync(gcClientset.Discovery(), 30*time.Second, ctx.Stop)
35.
36. // 6、注册 debug 接口
37. return garbagecollector.NewDebugHandler(garbageCollector), true, nil
38. }

本文档使用 书栈网 · BookStack.CN 构建 - 118 -


garbage collector controller 源码分析

在 startGarbageCollectorController 中主要调用了四种方
法 garbagecollector.NewGarbageCollector 、 garbageCollector.Run 、 garbageCollector
.Sync 和 garbagecollector.NewDebugHandler 来完成核心功能,下面主要针对这四种方法进
行说明。

garbagecollector.NewGarbageCollector

NewGarbageCollector 的主要功能是初始化 GarbageCollector 和 GraphBuilder 对象,


并调用 gb.syncMonitors 方法初始化 deletableResources 中所有 resource controller
的 informer。GarbageCollector 的主要作用是启动 GraphBuilder 以及启动所有的消费者,
GraphBuilder 的主要作用是启动所有的生产者。

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:74

1. func NewGarbageCollector(......) (*GarbageCollector, error) {


2. ......
3. gc := &GarbageCollector{
4. ......
5. }
6. gb := &GraphBuilder{
7. ......
8. }
9. if err := gb.syncMonitors(deletableResources); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to sync all monitors: %v",
10. err))
11. }
12. gc.dependencyGraphBuilder = gb
13.
14. return gc, nil
15. }

gb.syncMonitors

syncMonitors 的主要作用是初始化各个资源对象的 informer,并调用 gb.controllerFor


为每种资源注册 eventHandler,此处每种资源被称为 monitors,因为为每种资源注册
eventHandler 时,对于 AddFunc、UpdateFunc 和 DeleteFunc 都会将对应的 event push
到 graphChanges 队列中,每种资源对象的 informer 都作为生产者。

k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:179

func (gb *GraphBuilder) syncMonitors(resources


1. map[schema.GroupVersionResource]struct{}) error {
2. gb.monitorLock.Lock()

本文档使用 书栈网 · BookStack.CN 构建 - 119 -


garbage collector controller 源码分析

3. defer gb.monitorLock.Unlock()
4.
5. ......
6. for resource := range resources {
7. if _, ok := gb.ignoredResources[resource.GroupResource()]; ok {
8. continue
9. }
10. ......
11. kind, err := gb.restMapper.KindFor(resource)
12. if err != nil {
errs = append(errs, fmt.Errorf("couldn't look up resource %q: %v",
13. resource, err))
14. continue
15. }
16. // 为 resource 的 controller 注册 eventHandler
17. c, s, err := gb.controllerFor(resource, kind)
18. if err != nil {
errs = append(errs, fmt.Errorf("couldn't start monitor for resource
19. %q: %v", resource, err))
20. continue
21. }
22. current[resource] = &monitor{store: s, controller: c}
23. added++
24. }
25. gb.monitors = current
26.
27. for _, monitor := range toRemove {
28. if monitor.stopCh != nil {
29. close(monitor.stopCh)
30. }
31. }
32. return utilerrors.NewAggregate(errs)
33. }

gb.controllerFor

在 gb.controllerFor 中主要是为每个 deletableResources 的 informer 注册


eventHandler,此处就可以看到真正的生产者了。

k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:127

func (gb *GraphBuilder) controllerFor(resource schema.GroupVersionResource,


1. kind schema.GroupVersionKind) (cache.Controller, cache.Store, error) {

本文档使用 书栈网 · BookStack.CN 构建 - 120 -


garbage collector controller 源码分析

2. handlers := cache.ResourceEventHandlerFuncs{
3. AddFunc: func(obj interface{}) {
4. event := &event{
5. eventType: addEvent,
6. obj: obj,
7. gvk: kind,
8. }
9. // 将对应的 event push 到 graphChanges 队列中
10. gb.graphChanges.Add(event)
11. },
12. UpdateFunc: func(oldObj, newObj interface{}) {
13. event := &event{
14. eventType: updateEvent,
15. obj: newObj,
16. oldObj: oldObj,
17. gvk: kind,
18. }
19. // 将对应的 event push 到 graphChanges 队列中
20. gb.graphChanges.Add(event)
21. },
22. DeleteFunc: func(obj interface{}) {
if deletedFinalStateUnknown, ok := obj.
23. (cache.DeletedFinalStateUnknown); ok {
24. obj = deletedFinalStateUnknown.Obj
25. }
26. event := &event{
27. eventType: deleteEvent,
28. obj: obj,
29. gvk: kind,
30. }
31. // 将对应的 event push 到 graphChanges 队列中
32. gb.graphChanges.Add(event)
33. },
34. }
35. shared, err := gb.sharedInformers.ForResource(resource)
36. if err != nil {
37. return nil, nil, err
38. }
shared.Informer().AddEventHandlerWithResyncPeriod(handlers,
39. ResourceResyncTime)
40. return shared.Informer().GetController(), shared.Informer().GetStore(), nil
41. }

本文档使用 书栈网 · BookStack.CN 构建 - 121 -


garbage collector controller 源码分析

至此 NewGarbageCollector 的功能已经分析完了,在 NewGarbageCollector 中初始化了两


个对象 GarbageCollector 和 GraphBuilder,然后在 gb.syncMonitors 中初始化了所有
deletableResources 的 informer,为每个 informer 添加 eventHandler 并将监听到的所
有 event push 到 graphChanges 队列中,此处每个 informer 都被称为 monitor,所有
informer 都被称为生产者。graphChanges 是 GraphBuilder 中的一个对象,GraphBuilder
的主要功能是作为一个生产者,其会处理 graphChanges 中的所有事件并进行分类,将事件放入到
attemptToDelete 和 attemptToOrphan 两个队列中,具体处理逻辑下文讲述。

NewGarbageCollector 中的调用逻辑如下所示:

1. |--> ctx.ClientBuilder.
2. | ClientOrDie
3. |
4. |
5. |--> cacheddiscovery.
6. | NewMemCacheClient
|
7. |--> gb.sharedInformers.
|
8. | ForResource
|
9. |
startGarbage ----|--> garbagecollector. --> gb.syncMonitors -->
10. gb.controllerFor --|
CollectorController | NewGarbageCollector
11. |
|
12. |
|
13. |--> shared.Informer().
|
14. AddEventHandlerWithResyncPeriod
15. |--> garbageCollector.Run
16. |
17. |
18. |--> garbageCollector.Sync
19. |
20. |
21. |--> garbagecollector.NewDebugHandler

garbageCollector.Run

上文已经详述了 NewGarbageCollector 的主要功能,然后继续分析

本文档使用 书栈网 · BookStack.CN 构建 - 122 -


garbage collector controller 源码分析

startGarbageCollectorController 中的第二个核心方法
garbageCollector.Run , garbageCollector.Run 的主要作用是启动所有的生产者和消费者,
其首先会调用 gc.dependencyGraphBuilder.Run 启动所有的生产者,即 monitors,然后再启
动一个 goroutine 处理 graphChanges 队列中的事件并分别放到 attemptToDelete 和
attemptToOrphan 两个队列中,dependencyGraphBuilder 即上文提到的
GraphBuilder, run 方法会调用 gc.runAttemptToDeleteWorker 和
gc.runAttemptToOrphanWorker 启动多个 goroutine 处理 attemptToDelete 和
attemptToOrphan 两个队列中的事件。

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:124

1. func (gc *GarbageCollector) Run(workers int, stopCh <-chan struct{}) {


2. defer utilruntime.HandleCrash()
3. defer gc.attemptToDelete.ShutDown()
4. defer gc.attemptToOrphan.ShutDown()
5. defer gc.dependencyGraphBuilder.graphChanges.ShutDown()
6.
7. defer klog.Infof("Shutting down garbage collector controller")
8.
// 1、调用 gc.dependencyGraphBuilder.Run 启动所有的 monitors 即 informers,并且
启动一个 goroutine 处理 graphChanges 中的事件将其分别放到 GraphBuilder 的
9. attemptToDelete 和 attemptToOrphan 两个 队列中;
10. go gc.dependencyGraphBuilder.Run(stopCh)
11.
12. // 2、等待 informers 的 cache 同步完成
if !cache.WaitForNamedCacheSync("garbage collector", stopCh,
13. gc.dependencyGraphBuilder.IsSynced) {
14. return
15. }
16.
17. for i := 0; i < workers; i++ {
// 3、启动多个 goroutine 调用 gc.runAttemptToDeleteWorker 处理
18. attemptToDelete 中的事件
19. go wait.Until(gc.runAttemptToDeleteWorker, 1*time.Second, stopCh)
// 4、启动多个 goroutine 调用 gc.runAttemptToOrphanWorker 处理
20. attemptToDelete 中的事件
21. go wait.Until(gc.runAttemptToOrphanWorker, 1*time.Second, stopCh)
22. }
23.
24. <-stopCh
25. }

本文档使用 书栈网 · BookStack.CN 构建 - 123 -


garbage collector controller 源码分析

Run 方法中调用了 gc.dependencyGraphBuilder.Run 来完成 GraphBuilder 的启动。

gc.dependencyGraphBuilder.Run

GraphBuilder 在 garbageCollector 整个环节中起到承上启下的作用,首先看一下


GraphBuilder 对象的结构:

1. type GraphBuilder struct {


2. restMapper meta.RESTMapper
3.
4. // informers
5. monitors monitors
6. monitorLock sync.RWMutex
7.
// 当 kube-controller-manager 中所有的 controllers 都启动后,informersStarted
8. 会被 close 掉
// informersStarted 会被 close 掉的调用程序在 kube-controller-manager 的启动流
9. 程中
10. informersStarted <-chan struct{}
11.
12. stopCh <-chan struct{}
13.
14. // 当调用 GraphBuilder 的 run 方法时,running 会被设置为 true
15. running bool
16.
17. metadataClient metadata.Interface
18.
19. // informers 监听到的事件会放在 graphChanges 中
20. graphChanges workqueue.RateLimitingInterface
21.
22. // 维护所有对象的依赖关系
23. uidToNode *concurrentUIDToNode
24.
// GarbageCollector 作为消费者要处理 attemptToDelete 和 attemptToOrphan 两个队
25. 列中的事件
26. attemptToDelete workqueue.RateLimitingInterface
27. attemptToOrphan workqueue.RateLimitingInterface
28.
29. absentOwnerCache *UIDCache
30. sharedInformers controller.InformerFactory
31. // 不需要被 gc 的资源
32. ignoredResources map[schema.GroupResource]struct{}
33. }

本文档使用 书栈网 · BookStack.CN 构建 - 124 -


garbage collector controller 源码分析

uidToNode

此处有必要先说明一下 uidToNode 的功能,uidToNode 数据结构中维护着所有对象的依赖关系,


此处的依赖关系是指比如当创建一个 deployment 时会创建对应的 rs 以及 pod,pod 的 owner
就是 rs,rs 的 owner 是 deployment,rs 的 dependents 是其关联的所有 pod,
deployment 的 dependents 是其关联的所有 rs。

uidToNode 中的 node 不是指 k8s 中的 node 节点,而是将 graphChanges 中的 event 转


换为 node 对象,k8s 中所有 object 之间的级联关系是通过 node 的概念来维护的,
garbageCollector 在后续的处理中会直接使用 node 对象,node 对象定义如下:

1. type concurrentUIDToNode struct {


2. uidToNodeLock sync.RWMutex
3. uidToNode map[types.UID]*node
4. }
5.
6. type node struct {
7. identity objectReference
8.
9. dependentsLock sync.RWMutex
10. // 其依赖项指 metadata.ownerReference 中的对象
11. dependents map[*node]struct{}
12.
13. deletingDependents bool
14. deletingDependentsLock sync.RWMutex
15.
16. beingDeleted bool
17. beingDeletedLock sync.RWMutex
18.
19. // 当 virtual 值为 true 时,此时不确定该对象是否存在于 apiserver 中
20. virtual bool
21. virtualLock sync.RWMutex
22.
23. // 对象本身的 OwnerReference 列表
24. owners []metav1.OwnerReference
25. }

GraphBuilder 主要有三个功能:

1、监控集群中所有的可删除资源;
2、基于 informers 中的资源在 uidToNode 数据结构中维护着所有对象的依赖关系;
3、处理 graphChanges 中的事件并放到 attemptToDelete 和 attemptToOrphan 两个

本文档使用 书栈网 · BookStack.CN 构建 - 125 -


garbage collector controller 源码分析

队列中;

上文已经说了 gc.dependencyGraphBuilder.Run 的功能,启动所有的 informers 然后再启动


一个 goroutine 处理 graphChanges 队列中的事件并分别放到 attemptToDelete 和
attemptToOrphan 两个队列中,代码如下所示:

k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:281

1. func (gb *GraphBuilder) Run(stopCh <-chan struct{}) {


2. klog.Infof("GraphBuilder running")
3. defer klog.Infof("GraphBuilder stopping")
4.
5. gb.monitorLock.Lock()
6. gb.stopCh = stopCh
7. gb.running = true
8. gb.monitorLock.Unlock()
9.
10. gb.startMonitors()
11.
12. // 调用 gb.runProcessGraphChanges
13. // 此处为死循环,除非收到 stopCh 信号,否则下面的代码不会被执行到
14. wait.Until(gb.runProcessGraphChanges, 1*time.Second, stopCh)
15.
16. // 若执行到此处说明收到了 stopCh 的信号,此时需要停止所有的 running monitors
17. gb.monitorLock.Lock()
18. defer gb.monitorLock.Unlock()
19. monitors := gb.monitors
20. stopped := 0
21. for _, monitor := range monitors {
22. if monitor.stopCh != nil {
23. stopped++
24. close(monitor.stopCh)
25. }
26. }
27.
28. gb.monitors = nil
29. }

gc.dependencyGraphBuilder.Run 的核心是调用了 gb.startMonitors 和


gb.runProcessGraphChanges 两个方法来完成主要功能,继续看这两个方法的主要逻辑。

gb.startMonitors

本文档使用 书栈网 · BookStack.CN 构建 - 126 -


garbage collector controller 源码分析

startMonitors 的功能很简单就是启动所有的 informers,代码如下所示:

k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:232

1. func (gb *GraphBuilder) startMonitors() {


2. gb.monitorLock.Lock()
3. defer gb.monitorLock.Unlock()
4.
5. // 1、当 GraphBuilder 调用 run 方法后,running 会设置为 true
6. if !gb.running {
7. return
8. }
9.
10. // 2、当 kube-controller-manager 中所有的 controllers 在启动流程中都启动后
11. // 会 close 掉 informersStarted
12. <-gb.informersStarted
13.
14. // 3、启动所有 informer
15. monitors := gb.monitors
16. started := 0
17. for _, monitor := range monitors {
18. if monitor.stopCh == nil {
19. monitor.stopCh = make(chan struct{})
20. gb.sharedInformers.Start(gb.stopCh)
21. go monitor.Run()
22. started++
23. }
24. }
25. }

gb.runProcessGraphChanges

runProcessGraphChanges 方法的主要功能是处理 graphChanges 中的事件将其分别放到


GraphBuilder 的 attemptToDelete 和 attemptToOrphan 两个队列中,代码主要逻辑为:

1、从 graphChanges 队列中取出一个 item 即 event;


2、获取 event 的 accessor,accessor 是一个 object 的 meta.Interface,里面包
含访问 object meta 中所有字段的方法;
3、通过 accessor 获取 UID 判断 uidToNode 中是否存在该 object;
4、若 uidToNode 中不存在该 node 且该事件是 addEvent 或 updateEvent,则为该
object 创建对应的 node,并调用 gb.insertNode 将该 node 加到 uidToNode 中,然
后将该 node 添加到其 owner 的 dependents 中,执行完 gb.insertNode 中的操作后
再调用 gb.processTransitions 方法判断该对象是否处于删除状态,若处于删除状态会判断

本文档使用 书栈网 · BookStack.CN 构建 - 127 -


garbage collector controller 源码分析

该对象是以 orphan 模式删除还是以 foreground 模式删除,若以 orphan 模式删除,


则将该 node 加入到 attemptToOrphan 队列中,若以 foreground 模式删除则将该对象
以及其所有 dependents 都加入到 attemptToDelete 队列中;

5、若 uidToNode 中存在该 node 且该事件是 addEvent 或 updateEvent 时,此时可能


是一个 update 操作,调用 referencesDiffs 方法检查该对象的 OwnerReferences
字段是否有变化,若有变化(1)调用 gb.addUnblockedOwnersToDeleteQueue 将被删除以及
更新的 owner 对应的 node 加入到 attemptToDelete 中,因为此时该 node 中已被删除
或更新的 owner 可能处于删除状态且阻塞在该 node 处,此时有三种方式避免该 node 的
owner 处于删除阻塞状态,一是等待该 node 被删除,二是将该 node 自身对应 owner 的
OwnerReferences 字段删除,三是将该 node OwnerReferences 字段中对应 owner
的 BlockOwnerDeletion 设置为 false;(2)更新该 node 的 owners 列表;(3)若有
新增的 owner,将该 node 加入到新 owner 的 dependents 中;(4) 若有被删除的
owner,将该 node 从已删除 owner 的 dependents 中删除;以上操作完成后,检查该
node 是否处于删除状态并进行标记,最后调用 gb.processTransitions 方法检查该
node 是否要被删除;

举个例子,若以 foreground 模式删除 deployment 时,deployment 的 dependents


列表中有对应的 rs,那么 deployment 的删除会阻塞住等待其依赖 rs 的删除,此时 rs 有
三种方法不阻塞 deployment 的删除操作,一是 rs 对象被删除,二是删除 rs 对象
OwnerReferences 字段中对应的 deployment,三是将 rs 对象 OwnerReferences 字
段中对应的 deployment 配置 BlockOwnerDeletion 设置为 false,文末会有示例演示
该操作。

6、若该事件为 deleteEvent,首先从 uidToNode 中删除该对象,然后从该 node 所有


owners 的 dependents 中删除该对象,将该 node 所有的 dependents 加入到
attemptToDelete 队列中,最后检查该 node 的所有 owners,若有处于删除状态的
owner,此时该 owner 可能处于删除阻塞状态正在等待该 node 的删除,将该 owner 加入
到 attemptToDelete 中;

总结一下,当从 graphChanges 中取出 event 时,不管是什么 event,主要完成三件时,首先都


会将 event 转化为 uidToNode 中的 node 对象,其次一是更新 uidToNode 中维护的依赖关
系,二是更新该 node 的 owners 以及 owners 的 dependents,三是检查该 node 的
owners 是否要被删除以及该 node 的 dependents 是否要被删除,若需要删除则根据 node 的
删除策略将其添加到 attemptToOrphan 或者 attemptToDelete 队列中;

k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:526

1. func (gb *GraphBuilder) runProcessGraphChanges() {


2. for gb.processGraphChanges() {
3. }
4. }

本文档使用 书栈网 · BookStack.CN 构建 - 128 -


garbage collector controller 源码分析

5.
6. func (gb *GraphBuilder) processGraphChanges() bool {
7. // 1、从 graphChanges 取出一个 event
8. item, quit := gb.graphChanges.Get()
9. if quit {
10. return false
11. }
12. defer gb.graphChanges.Done(item)
13. event, ok := item.(*event)
14. if !ok {
15. utilruntime.HandleError(fmt.Errorf("expect a *event, got %v", item))
16. return true
17. }
18. obj := event.obj
19. accessor, err := meta.Accessor(obj)
20. if err != nil {
21. utilruntime.HandleError(fmt.Errorf("cannot access obj: %v", err))
22. return true
23. }
24.
25. // 2、若存在 node 对象,从 uidToNode 中取出该 event 的 node 对象
26. existingNode, found := gb.uidToNode.Read(accessor.GetUID())
27. if found {
28. existingNode.markObserved()
29. }
30. switch {
31. // 3、若 event 为 add 或 update 类型以及对应的 node 对象不存在时
case (event.eventType == addEvent || event.eventType == updateEvent) &&
32. !found:
33. // 4、为 node 创建 event 对象
34. newNode := &node{
35. ......
36. }
37. // 5、在 uidToNode 中添加该 node 对象
38. gb.insertNode(newNode)
39.
40. // 6、检查并处理 node 的删除操作
41. gb.processTransitions(event.oldObj, accessor, newNode)
42.
43. // 7、若 event 为 add 或 update 类型以及对应的 node 对象存在时
case (event.eventType == addEvent || event.eventType == updateEvent) &&
44. found:

本文档使用 书栈网 · BookStack.CN 构建 - 129 -


garbage collector controller 源码分析

added, removed, changed := referencesDiffs(existingNode.owners,


45. accessor.GetOwnerReferences())
46. // 8、若 node 的 owners 有变化
47. if len(added) != 0 || len(removed) != 0 || len(changed) != 0 {
48.
49. gb.addUnblockedOwnersToDeleteQueue(removed, changed)
50. // 9、更新 uidToNode 中的 owners
51. existingNode.owners = accessor.GetOwnerReferences()
52. // 10、添加更新后 Owners 对应的 dependent
53. gb.addDependentToOwners(existingNode, added)
54. // 11、移除旧 owners 对应的 dependents
55. gb.removeDependentFromOwners(existingNode, removed)
56. }
57.
58. // 12、检查是否处于删除状态
59. if beingDeleted(accessor) {
60. existingNode.markBeingDeleted()
61. }
62. // 13、检查并处理 node 的删除操作
63. gb.processTransitions(event.oldObj, accessor, existingNode)
64.
65. // 14、若为 delete event
66. case event.eventType == deleteEvent:
67. if !found {
68. return true
69. }
70. // 15、从 uidToNode 中删除该 node
71. gb.removeNode(existingNode)
72. existingNode.dependentsLock.RLock()
73. defer existingNode.dependentsLock.RUnlock()
74. if len(existingNode.dependents) > 0 {
75. gb.absentOwnerCache.Add(accessor.GetUID())
76. }
77. // 16、删除该 node 的 dependents
78. for dep := range existingNode.dependents {
79. gb.attemptToDelete.Add(dep)
80. }
81. // 17、删除该 node 处于删除阻塞状态的 owner
82. for _, owner := range existingNode.owners {
83. ownerNode, found := gb.uidToNode.Read(owner.UID)
84. if !found || !ownerNode.isDeletingDependents() {
85. continue

本文档使用 书栈网 · BookStack.CN 构建 - 130 -


garbage collector controller 源码分析

86. }
87. gb.attemptToDelete.Add(ownerNode)
88. }
89. }
90. return true
91. }

processTransitions

上述在处理 add 或 update event 时最后都调用了 processTransitions 方法检查 node 是


否处于删除状态,若处于删除状态会通过其删除策略将 node 放到 attemptToOrphan 或
attemptToDelete 队列中。

k8s.io/kubernetes/pkg/controller/garbagecollector/graph_builder.go:509

func (gb *GraphBuilder) processTransitions(oldObj interface{}, newAccessor


1. metav1.Object, n *node) {
2. if startsWaitingForDependentsOrphaned(oldObj, newAccessor) {
3. gb.attemptToOrphan.Add(n)
4. return
5. }
6. if startsWaitingForDependentsDeleted(oldObj, newAccessor) {
7. n.markDeletingDependents()
8. for dep := range n.dependents {
9. gb.attemptToDelete.Add(dep)
10. }
11. gb.attemptToDelete.Add(n)
12. }
13. }

gc.runAttemptToDeleteWorker

runAttemptToDeleteWorker 是执行删除 attemptToDelete 中 node 的方法,其主要逻辑


为:

1、调用 gc.attemptToDeleteItem 删除 node;


2、若删除失败则重新加入到 attemptToDelete 队列中进行重试;

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:280

1. func (gc *GarbageCollector) runAttemptToDeleteWorker() {


2. for gc.attemptToDeleteWorker() {
3. }
4. }

本文档使用 书栈网 · BookStack.CN 构建 - 131 -


garbage collector controller 源码分析

5.
6. func (gc *GarbageCollector) attemptToDeleteWorker() bool {
7. item, quit := gc.attemptToDelete.Get()
8. gc.workerLock.RLock()
9. defer gc.workerLock.RUnlock()
10. if quit {
11. return false
12. }
13. defer gc.attemptToDelete.Done(item)
14. n, ok := item.(*node)
15. if !ok {
16. utilruntime.HandleError(fmt.Errorf("expect *node, got %#v", item))
17. return true
18. }
19. err := gc.attemptToDeleteItem(n)
20. if err != nil {
21. if _, ok := err.(*restMappingError); ok {
22. klog.V(5).Infof("error syncing item %s: %v", n, err)
23. } else {
utilruntime.HandleError(fmt.Errorf("error syncing item %s: %v", n,
24. err))
25. }
26. gc.attemptToDelete.AddRateLimited(item)
27. } else if !n.isObserved() {
28. gc.attemptToDelete.AddRateLimited(item)
29. }
30. return true
31. }

gc.runAttemptToDeleteWorker 中调用了 gc.attemptToDeleteItem 执行实际的删除操作。

gc.attemptToDeleteItem

gc.attemptToDeleteItem 的主要逻辑为:

1、判断 node 是否处于删除状态;


2、从 apiserver 获取该 node 最新的状态,该 node 可能为 virtual node,若为
virtual node 则从 apiserver 中获取不到该 node 的对象,此时会将该 node 重新加入
到 graphChanges 队列中,再次处理该 node 时会将其从 uidToNode 中删除;
3、判断该 node 最新状态的 uid 是否等于本地缓存中的 uid,若不匹配说明该 node 已更
新过此时将其设置为 virtual node 并重新加入到 graphChanges 队列中,再次处理该
node 时会将其从 uidToNode 中删除;

本文档使用 书栈网 · BookStack.CN 构建 - 132 -


garbage collector controller 源码分析

4、通过 node 的 deletingDependents 字段判断该 node 当前是否处于删除


dependents 的状态,若该 node 处于删除 dependents 的状态则调用
processDeletingDependentsItem 方法检查 node 的 blockingDependents 是否被完
全删除,若 blockingDependents 已完全被删除则删除该 node 对应的 finalizer,若
blockingDependents 还未删除完,将未删除的 blockingDependents 加入到
attemptToDelete 中;

上文中在 GraphBuilder 处理 graphChanges 中的事件时,若发现 node 处于删除状态,


会将 node 的 dependents 加入到 attemptToDelete 中并标记 node 的
deletingDependents 为 true;

5、调用 gc.classifyReferences 将 node 的 ownerReferences 分类为 solid ,


dangling , waitingForDependentsDeletion 三类: dangling (owner 不存
在)、 waitingForDependentsDeletion (owner 存在,owner 处于删除状态且正在等待其
dependents 被删除)、 solid (至少有一个 owner 存在且不处于删除状态);
6、对以上分类进行不同的处理,若 solid 不为 0 即当前 node 至少存在一个 owner,该
对象还不能被回收,此时需要将 dangling 和 waitingForDependentsDeletion 列表中的
owner 从 node 的 ownerReferences 删除,即已经被删除或等待删除的引用从对象中删
掉;
7、第二种情况是该 node 的 owner 处于 waitingForDependentsDeletion 状态并且
node 的 dependents 未被完全删除,该 node 需要等待删除完所有的 dependents 后才
能被删除;
8、第三种情况就是该 node 已经没有任何 dependents 了,此时按照 node 中声明的删除
策略调用 apiserver 的接口删除即可;

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:404

1. func (gc *GarbageCollector) attemptToDeleteItem(item *node) error {


2. // 1、判断 node 是否处于删除状态
3. if item.isBeingDeleted() && !item.isDeletingDependents() {
4. return nil
5. }
6.
7. // 2、从 apiserver 获取该 node 最新的状态
8. latest, err := gc.getObject(item.identity)
9. switch {
10. case errors.IsNotFound(err):
11. gc.dependencyGraphBuilder.enqueueVirtualDeleteEvent(item.identity)
12. item.markObserved()
13. return nil
14. case err != nil:
15. return err

本文档使用 书栈网 · BookStack.CN 构建 - 133 -


garbage collector controller 源码分析

16. }
17.
18. // 3、判断该 node 最新状态的 uid 是否等于本地缓存中的 uid
19. if latest.GetUID() != item.identity.UID {
20. gc.dependencyGraphBuilder.enqueueVirtualDeleteEvent(item.identity)
21. item.markObserved()
22. return nil
23. }
24.
25. // 4、判断该 node 当前是否处于删除 dependents 状态中
26. if item.isDeletingDependents() {
27. return gc.processDeletingDependentsItem(item)
28. }
29.
30. // 5、检查 node 是否还存在 ownerReferences
31. ownerReferences := latest.GetOwnerReferences()
32. if len(ownerReferences) == 0 {
33. return nil
34. }
35.
36. // 6、对 ownerReferences 进行分类
solid, dangling, waitingForDependentsDeletion, err :=
37. gc.classifyReferences(item, ownerReferences)
38. if err != nil {
39. return err
40. }
41. switch {
42. // 7、存在不处于删除状态的 owner
43. case len(solid) != 0:
44. if len(dangling) == 0 && len(waitingForDependentsDeletion) == 0 {
45. return nil
46. }
ownerUIDs := append(ownerRefsToUIDs(dangling),
47. ownerRefsToUIDs(waitingForDependentsDeletion)...)
patch := deleteOwnerRefStrategicMergePatch(item.identity.UID,
48. ownerUIDs...)
49. _, err = gc.patch(item, patch, func(n *node) ([]byte, error) {
50. return gc.deleteOwnerRefJSONMergePatch(n, ownerUIDs...)
51. })
52. return err
53. // 8、node 的 owner 处于 waitingForDependentsDeletion 状态并且 node
54. // 的 dependents 未被完全删除

本文档使用 书栈网 · BookStack.CN 构建 - 134 -


garbage collector controller 源码分析

case len(waitingForDependentsDeletion) != 0 && item.dependentsLength() !=


55. 0:
56. deps := item.getDependents()
57. // 9、删除 dependents
58. for _, dep := range deps {
59. if dep.isDeletingDependents() {
60. patch, err := item.unblockOwnerReferencesStrategicMergePatch()
61. if err != nil {
62. return err
63. }
if _, err := gc.patch(item, patch,
64. gc.unblockOwnerReferencesJSONMergePatch); err != nil {
65. return err
66. }
67. break
68. }
69. }
70. // 10、以 Foreground 模式删除 node 对象
71. policy := metav1.DeletePropagationForeground
72. return gc.deleteObject(item.identity, &policy)
// 11、该 node 已经没有任何依赖了,按照 node 中声明的删除策略调用 apiserver 的接口删
73. 除
74. default:
75. var policy metav1.DeletionPropagation
76. switch {
77. case hasOrphanFinalizer(latest):
78. policy = metav1.DeletePropagationOrphan
79. case hasDeleteDependentsFinalizer(latest):
80. policy = metav1.DeletePropagationForeground
81. default:
82. policy = metav1.DeletePropagationBackground
83. }
84. return gc.deleteObject(item.identity, &policy)
85. }
86. }

gc.runAttemptToOrphanWorker

runAttemptToOrphanWorker 是处理以 orphan 模式删除的 node,主要逻辑为:

1、调用 gc.orphanDependents 删除 owner 所有 dependents OwnerReferences 中


的 owner 字段;
2、调用 gc.removeFinalizer 删除 owner 的 orphan Finalizer;

本文档使用 书栈网 · BookStack.CN 构建 - 135 -


garbage collector controller 源码分析

3、以上两步中若有失败的会进行重试;

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:574

1. func (gc *GarbageCollector) runAttemptToOrphanWorker() {


2. for gc.attemptToOrphanWorker() {
3. }
4. }
5.
6. func (gc *GarbageCollector) attemptToOrphanWorker() bool {
7. item, quit := gc.attemptToOrphan.Get()
8. gc.workerLock.RLock()
9. defer gc.workerLock.RUnlock()
10. if quit {
11. return false
12. }
13. defer gc.attemptToOrphan.Done(item)
14. owner, ok := item.(*node)
15. if !ok {
16. return true
17. }
18. owner.dependentsLock.RLock()
19. dependents := make([]*node, 0, len(owner.dependents))
20. for dependent := range owner.dependents {
21. dependents = append(dependents, dependent)
22. }
23. owner.dependentsLock.RUnlock()
24. err := gc.orphanDependents(owner.identity, dependents)
25. if err != nil {
26. gc.attemptToOrphan.AddRateLimited(item)
27. return true
28. }
29. // 更新 owner, 从 finalizers 列表中移除 "orphaningFinalizer"
30. err = gc.removeFinalizer(owner, metav1.FinalizerOrphanDependents)
31. if err != nil {
32. gc.attemptToOrphan.AddRateLimited(item)
33. }
34. return true
35. }

garbageCollector.Sync

garbageCollector.Sync 是 startGarbageCollectorController 中的第三个核心方法,主

本文档使用 书栈网 · BookStack.CN 构建 - 136 -


garbage collector controller 源码分析

要功能是周期性的查询集群中所有的资源,过滤出 deletableResources ,然后对比已经监控的


deletableResources 和当前获取到的 deletableResources 是否一致,若不一致则更新
GraphBuilder 的 monitors 并重新启动 monitors 监控所有的 deletableResources ,该
方法的主要逻辑为:

1、通过调用 GetDeletableResources 获取集群内所有的 deletableResources 作为


newResources, deletableResources 指支持 “delete”, “list”, “watch” 三种操作
的 resource,包括 CR;
2、检查 oldResources, newResources 是否一致,不一致则需要同步;
3、调用 gc.resyncMonitors 同步 newResources,在 gc.resyncMonitors 中会重新
调用 GraphBuilder 的 syncMonitors 和 startMonitors 两个方法完成 monitors
的刷新;
4、等待 newResources informer 中的 cache 同步完成;
5、将 newResources 作为 oldResources,继续进行下一轮的同步;

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:164

func (gc *GarbageCollector) Sync(discoveryClient


discovery.ServerResourcesInterface, period time.Duration, stopCh <-chan
1. struct{}) {
2. oldResources := make(map[schema.GroupVersionResource]struct{})
3. wait.Until(func() {
4. // 1、获取集群内所有的 DeletableResources 作为 newResources
5. newResources := GetDeletableResources(discoveryClient)
6.
7. if len(newResources) == 0 {
8. return
9. }
10.
11. // 2、判断集群中的资源是否有变化
12. if reflect.DeepEqual(oldResources, newResources) {
13. return
14. }
15.
16. gc.workerLock.Lock()
17. defer gc.workerLock.Unlock()
18.
19. // 3、开始更新 GraphBuilder 中的 monitors
20. attempt := 0
21. wait.PollImmediateUntil(100*time.Millisecond, func() (bool, error) {
22. attempt++
23.

本文档使用 书栈网 · BookStack.CN 构建 - 137 -


garbage collector controller 源码分析

24. if attempt > 1 {


25. newResources = GetDeletableResources(discoveryClient)
26. if len(newResources) == 0 {
27. return false, nil
28. }
29. }
30.
31. gc.restMapper.Reset()
32. // 4、调用 gc.resyncMonitors 同步 newResources
33. if err := gc.resyncMonitors(newResources); err != nil {
34. return false, nil
35. }
36.
37. // 5、等待所有 monitors 的 cache 同步完成
if !cache.WaitForNamedCacheSync("garbage collector",
38. waitForStopOrTimeout(stopCh, period), gc.dependencyGraphBuilder.IsSynced) {
39. return false, nil
40. }
41.
42. return true, nil
43. }, stopCh)
44.
45. // 6、更新 oldResources
46. oldResources = newResources
47. }, period, stopCh)
48. }

garbageCollector.Sync 中主要调用了两个方法,一是调用 GetDeletableResources 获取集


群中所有的可删除资源,二是调用 gc.resyncMonitors 更新 GraphBuilder 中 monitors。

GetDeletableResources

在 GetDeletableResources 中首先通过调用 discoveryClient.ServerPreferredResources


方法获取集群内所有的 resource 信息,然后通过调用 discovery.FilteredBy 过滤出支持
“delete”, “list”, “watch” 三种方法的 resource 作为 deletableResources 。

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:636

func GetDeletableResources(discoveryClient discovery.ServerResourcesInterface)


1. map[schema.GroupVersionResource]struct{} {
// 1、调用 discoveryClient.ServerPreferredResources 方法获取集群内所有的
2. resource 信息
3. preferredResources, err := discoveryClient.ServerPreferredResources()

本文档使用 书栈网 · BookStack.CN 构建 - 138 -


garbage collector controller 源码分析

4. if err != nil {
5. if discovery.IsGroupDiscoveryFailedError(err) {
6. ......
7. } else {
8. ......
9. }
10. }
11. if preferredResources == nil {
12. return map[schema.GroupVersionResource]struct{}{}
13. }
14. // 2、调用 discovery.FilteredBy 过滤出 deletableResources
deletableResources :=
discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"delete",
15. "list", "watch"}}, preferredResources)
deletableGroupVersionResources := map[schema.GroupVersionResource]struct{}
16. {}
17. for _, rl := range deletableResources {
18. gv, err := schema.ParseGroupVersion(rl.GroupVersion)
19. if err != nil {
20. continue
21. }
22. for i := range rl.APIResources {
deletableGroupVersionResources[schema.GroupVersionResource{Group:
gv.Group, Version: gv.Version, Resource: rl.APIResources[i].Name}] =
23. struct{}{}
24. }
25. }
26.
27. return deletableGroupVersionResources
28. }

ServerPreferredResources

ServerPreferredResources 的主要功能是获取集群内所有的 resource 以及其 group、


version、verbs 信息,该方法的主要逻辑为:

1、调用 ServerGroups 方法获取集群内所有的 GroupList, ServerGroups 方法首先从


apiserver 通过 /api URL 获取当前版本下所有可用的 APIVersions ,再通过 /apis
URL 获取 所有可用的 APIVersions 以及其下的所有 APIGroupList ;
2、调用 fetchGroupVersionResources 通过 serverGroupList 再获取到对应的
resource;
3、将获取到的 version、group、resource 构建成标准格式添加到
metav1.APIResourceList 中;

本文档使用 书栈网 · BookStack.CN 构建 - 139 -


garbage collector controller 源码分析

k8s.io/kubernetes/staging/src/k8s.io/client-go/discovery/discovery_client.go:285

func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList,


1. error) {
2. // 1、获取集群内所有的 GroupList
3. serverGroupList, err := d.ServerGroups()
4. if err != nil {
5. return nil, err
6. }
7.
8. // 2、通过 serverGroupList 获取到对应的 resource
groupVersionResources, failedGroups := fetchGroupVersionResources(d,
9. serverGroupList)
10.
11. result := []*metav1.APIResourceList{}
grVersions := map[schema.GroupResource]string{} //
12. selected version of a GroupResource
grAPIResources := map[schema.GroupResource]*metav1.APIResource{} //
13. selected APIResource for a GroupResource
gvAPIResourceLists := map[schema.GroupVersion]*metav1.APIResourceList{} //
14. blueprint for a APIResourceList for later grouping
15.
16. // 3、格式化 resource
17. for _, apiGroup := range serverGroupList.Groups {
18. for _, version := range apiGroup.Versions {
groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version:
19. version.Version}
20.
21. apiResourceList, ok := groupVersionResources[groupVersion]
22. if !ok {
23. continue
24. }
25.
26. emptyAPIResourceList := metav1.APIResourceList{
27. GroupVersion: version.GroupVersion,
28. }
29. gvAPIResourceLists[groupVersion] = &emptyAPIResourceList
30. result = append(result, &emptyAPIResourceList)
31.
32. for i := range apiResourceList.APIResources {
33. apiResource := &apiResourceList.APIResources[i]
34. if strings.Contains(apiResource.Name, "/") {
35. continue

本文档使用 书栈网 · BookStack.CN 构建 - 140 -


garbage collector controller 源码分析

36. }
gv := schema.GroupResource{Group: apiGroup.Name, Resource:
37. apiResource.Name}
if _, ok := grAPIResources[gv]; ok && version.Version !=
38. apiGroup.PreferredVersion.Version {
39. continue
40. }
41. grVersions[gv] = version.Version
42. grAPIResources[gv] = apiResource
43. }
44. }
45. }
46.
47. for groupResource, apiResource := range grAPIResources {
48. version := grVersions[groupResource]
groupVersion := schema.GroupVersion{Group: groupResource.Group,
49. Version: version}
50. apiResourceList := gvAPIResourceLists[groupVersion]
apiResourceList.APIResources = append(apiResourceList.APIResources,
51. *apiResource)
52. }
53.
54. if len(failedGroups) == 0 {
55. return result, nil
56. }
57.
58. return result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
59. }

GetDeletableResources 方法中的调用流程为:

1. |--> d.ServerGroups
2. |
3. |--> discoveryClient. --|
4. | ServerPreferredResources |
| |-->
5. fetchGroupVersionResources
6. GetDeletableResources --|
7. |
8. |--> discovery.FilteredBy

gc.resyncMonitors

本文档使用 书栈网 · BookStack.CN 构建 - 141 -


garbage collector controller 源码分析

gc.resyncMonitors 的主要功能是更新 GraphBuilder 的 monitors 并重新启动 monitors


监控所有的 deletableResources,GraphBuilder 的 syncMonitors 和
startMonitors 方法在前面的流程中已经分析过,此处不再详细说明。

k8s.io/kubernetes/pkg/controller/garbagecollector/garbagecollector.go:116

func (gc *GarbageCollector) resyncMonitors(deletableResources map[schema.


1. GroupVersionResource]struct{}) error {
if err := gc.dependencyGraphBuilder.syncMonitors(deletableResources); err
2. != nil {
3. return err
4. }
5. gc.dependencyGraphBuilder.startMonitors()
6. return nil
7. }

garbagecollector.NewDebugHandler

garbagecollector.NewDebugHandler 主要功能是对外提供一个接口供用户查询当前集群中所有资
源的依赖关系,依赖关系可以以图表的形式展示。

func startGarbageCollectorController(ctx ControllerContext) (http.Handler,


1. bool, error) {
2. ......
3. return garbagecollector.NewDebugHandler(garbageCollector), true, nil
4. }

具体使用方法如下所示:

$ curl http://192.168.99.108:10252/debug/controllers/garbagecollector/graph >


1. tmp.dot
2.
$ curl http://192.168.99.108:10252/debug/controllers/garbagecollector/graph\?
3. uid=f9555d53-2b5f-4702-9717-54a313ed4fe8 > tmp.dot
4.
5. // 生成 svg 文件
6. $ dot -Tsvg -o graph.svg tmp.dot
7.
8. // 然后在浏览器中打开 svg 文件

依赖关系图如下所示:

本文档使用 书栈网 · BookStack.CN 构建 - 142 -


garbage collector controller 源码分析

示例
在此处会有一个小示例验证一下源码中的删除阻塞逻辑,当以 Foreground 策略删除一个对象时,
该对象会处于阻塞状态等待其依依赖被删除,此时有三种方式避免该对象处于删除阻塞状态,一是将依
赖对象直接删除,二是将依赖对象自身的 OwnerReferences 中 owner 字段删除,三是将该依赖
对象 OwnerReferences 字段中对应 owner 的 BlockOwnerDeletion 设置为 false,下面
会验证下这三种方式,首先创建一个 deployment,deployment 创建出的 rs 默认不会有
foregroundDeletion finalizers ,此时使用 kubectl edit 手动加上 foregroundDeletion
finalizers ,当 deployment 正常运行时,如下所示:

1. $ kubectl get deployment nginx-deployment


2. NAME READY UP-TO-DATE AVAILABLE AGE
3. nginx-deployment 2/2 2 2 43s
4.
5. $ kubectl get rs nginx-deployment-69b6b4c5cd
6. NAME DESIRED CURRENT READY AGE
7. nginx-deployment-69b6b4c5cd 2 2 2 57s
8.
9. $ kubectl get pod
10. NAME READY STATUS RESTARTS AGE
11. nginx-deployment-69b6b4c5cd-26dsn 1/1 Running 0 66s
12. nginx-deployment-69b6b4c5cd-6rqqc 1/1 Running 0 64s
13.
14. $ kubectl edit rs nginx-deployment-69b6b4c5cd
15.
16. // deployment 关联的 rs 对象
17. apiVersion: apps/v1
18. kind: ReplicaSet
19. metadata:
20. name: nginx-deployment-69b6b4c5cd
21. namespace: default

本文档使用 书栈网 · BookStack.CN 构建 - 143 -


garbage collector controller 源码分析

22. ownerReferences:
23. - apiVersion: apps/v1
24. blockOwnerDeletion: true
25. controller: true
26. kind: Deployment
27. name: nginx-deployment
28. uid: 40a1044e-03d1-48bc-8806-cb79d781c946
29. finalizers:
30. - foregroundDeletion // 为 rs 手动添加的 Foreground 策略
31. ......
32. spec:
33. replicas: 2
34. ......
35. status:
36. ......

当 deployment、rs、pod 都处于正常运行状态且 deployment 关联的 rs 使用 Foreground


删除策略时,然后验证源码中提到的三种方法,验证时需要模拟一个依赖对象无法删除的场景,当然这
个也很好模拟,三种场景如下所示:

1、当 pod 所在的 node 处于 Ready 状态时,以 Foreground 策略删除 deploment,


因为 rs 关联的 pod 会直接被删除,rs 也会被正常删除,此时 deployment 也会直接被删
除;
2、当 pod 所在的 node 处于 NotReady 状态时,以 Foreground 策略删除
deploment,此时因 rs 关联的 pod 无法被删除,rs 会一直处于删除阻塞状态,
deployment 由于 rs 无法被删除也会处于删除阻塞状态,此时更新 rs 去掉其
ownerReferences 中对应的 deployment 部分,deployment 会因无依赖对象被成功删
除;
3、和 2 同样的场景,node 处于 NotReady 状态时,以 Foreground 策略删除
deploment,deployment 和 rs 将处于删除阻塞状态,此时将 rs ownerReferences 中
关联 deployment 的 blockOwnerDeletion 字段设置为 false,可以看到 deployment
会因无 block 依赖对象被成功删除;

1. $ systemctl stop kubelet


2.
3. // node 处于 NotReady 状态
4. $ kubectl get node
5. NAME STATUS ROLES AGE VERSION
6. minikube NotReady master 6d11h v1.16.2
7.
8. // 以 Foreground 策略删除 deployment

本文档使用 书栈网 · BookStack.CN 构建 - 144 -


garbage collector controller 源码分析

$ curl -k -v -XDELETE -H "Accept: application/json" -H "Content-Type:


application/json" -d '{"propagationPolicy":"Foreground"}'
'https://192.168.99.108:8443/apis/apps/v1/namespaces/default/deployments/nginx-
9. deployment'

总结
GarbageCollectorController 是一种典型的生产者消费者模型,所有 deletableResources
的 informer 都是生产者,每种资源的 informer 监听到变化后都会将对应的事件 push 到
graphChanges 中,graphChanges 是 GraphBuilder 对象中的一个数据结构,GraphBuilder
会启动另外的 goroutine 对 graphChanges 中的事件进行分类并放在其 attemptToDelete 和
attemptToOrphan 两个队列中,garbageCollector 会启动多个 goroutine 对
attemptToDelete 和 attemptToOrphan 两个队列中的事件进行处理,处理的结果就是回收一些
需要被删除的对象。最后,再用一个流程图总结一下 GarbageCollectorController 的主要流程:

1. monitors (producer)
2. |
3. |
4. ∨
5. graphChanges queue
6. |
7. |
8. ∨
9. processGraphChanges
10. |
11. |
12. ∨
13. -------------------------------
14. | |
15. | |
16. ∨ ∨
17. attemptToDelete queue attemptToOrphan queue
18. | |
19. | |
20. ∨ ∨
21. AttemptToDeleteWorker AttemptToOrphanWorker
22. (consumer) (consumer)

本文档使用 书栈网 · BookStack.CN 构建 - 145 -


daemonset controller 源码分析

在前面的文章中已经分析过 deployment、statefulset 两个重要对象了,本文会继续分析


kubernetes 中另一个重要的对象 daemonset,在 kubernetes 中 daemonset 类似于 linux
上的守护进程会运行在每一个 node 上,在实际场景中,一般会将日志采集或者网络插件采用
daemonset 的方式部署。

DaemonSet 的基本操作

创建

daemonset 在创建后会在每个 node 上都启动一个 pod。

1. $ kubectl create -f nginx-ds.yaml

扩缩容

由于 daemonset 是在每个 node 上启动一个 pod,其不存在扩缩容操作,副本数量跟 node 数量


保持一致。

更新

daemonset 有两种更新策略 OnDelete 和 RollingUpdate ,默认为 RollingUpdate 。


滚动更新时,需要指定 .spec.updateStrategy.rollingUpdate.maxUnavailable (默认为1)和
.spec.minReadySeconds (默认为 0)。

1. // 更新镜像
2. $ kubectl set image ds/nginx-ds nginx-ds=nginx:1.16
3.
4. // 查看更新状态
5. $ kubectl rollout status ds/nginx-ds

回滚

在 statefulset 源码分析一节已经提到过 controllerRevision 这个对象了,其主要用来保存


历史版本信息,在更新以及回滚操作时使用,daemonset controller 也是使用
controllerrevision 保存历史版本信息,在回滚时会使用历史 controllerrevision 中的信
息替换 daemonset 中 Spec.Template 。

1. // 查看 ds 历史版本信息
2. $ kubectl get controllerrevision
3. NAME CONTROLLER REVISION AGE
4. nginx-ds-5c4b75bdbb daemonset.apps/nginx-ds 2 122m
5. nginx-ds-7cd7798dcd daemonset.apps/nginx-ds 1 133m

本文档使用 书栈网 · BookStack.CN 构建 - 146 -


daemonset controller 源码分析

6.
7. // 回滚到版本 1
8. $ kubectl rollout undo daemonset nginx-ds --to-revision=1
9.
10. // 查看回滚状态
11. $ kubectl rollout status ds/nginx-ds

暂停

daemonset 目前不支持暂停操作。

删除

daemonset 也支持两种删除操作。

1. // 非级联删除
2. $ kubectl delete ds/nginx-ds --cascade=false
3.
4. // 级联删除
5. $ kubectl delete ds/nginx-ds

DaemonSetController 源码分析

kubernetes 版本:v1.16

首先还是看 startDaemonSetController 方法,在此方法中会初始化 DaemonSetsController


对象并调用 Run 方法启动 daemonset controller,从该方法中可以看出 daemonset
controller 会监听 daemonsets 、 controllerRevision 、 pod 和 node 四种对象资
源的变动。其中 ConcurrentDaemonSetSyncs 的默认值为 2。

k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:36

func startDaemonSetController(ctx ControllerContext) (http.Handler, bool,


1. error) {
if !ctx.AvailableResources[schema.GroupVersionResource{Group: "apps",
2. Version: "v1", Resource: "daemonsets"}] {
3. return nil, false, nil
4. }
5. dsc, err := daemon.NewDaemonSetsController(
6. ctx.InformerFactory.Apps().V1().DaemonSets(),
7. ctx.InformerFactory.Apps().V1().ControllerRevisions(),
8. ctx.InformerFactory.Core().V1().Pods(),
9. ctx.InformerFactory.Core().V1().Nodes(),

本文档使用 书栈网 · BookStack.CN 构建 - 147 -


daemonset controller 源码分析

10. ctx.ClientBuilder.ClientOrDie("daemon-set-controller"),
11. flowcontrol.NewBackOff(1*time.Second, 15*time.Minute),
12. )
13. if err != nil {
return nil, true, fmt.Errorf("error creating DaemonSets controller:
14. %v", err)
15. }
go
dsc.Run(int(ctx.ComponentConfig.DaemonSetController.ConcurrentDaemonSetSyncs),
16. ctx.Stop)
17. return nil, true, nil
18. }

在 Run 方法中会启动两个操作,一个就是 dsc.runWorker 执行的 sync 操作,另一个就是


dsc.failedPodsBackoff.GC 执行的 gc 操作,主要逻辑为:

1、等待 informer 缓存同步完成;


2、启动两个 goroutine 分别执行 dsc.runWorker ;
3、启动一个 goroutine 每分钟执行一次 dsc.failedPodsBackoff.GC ,从
startDaemonSetController 方法中可以看到 failedPodsBackoff 的 duration为1s,
max duration为15m, failedPodsBackoff 的主要作用是当发现 daemon pod 状态为
failed 时,会定时重启该 pod;

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:263

1. func (dsc *DaemonSetsController) Run(workers int, stopCh <-chan struct{}) {


2. defer utilruntime.HandleCrash()
3. defer dsc.queue.ShutDown()
4.
5. defer klog.Infof("Shutting down daemon sets controller")
6.
if !cache.WaitForNamedCacheSync("daemon sets", stopCh, dsc.podStoreSynced,
7. dsc.nodeStoreSynced, dsc.historyStoreSynced, dsc.dsStoreSynced) {
8. return
9. }
10.
11. for i := 0; i < workers; i++ {
12. // sync 操作
13. go wait.Until(dsc.runWorker, time.Second, stopCh)
14. }
15.
16. // GC 操作
17. go wait.Until(dsc.failedPodsBackoff.GC, BackoffGCInterval, stopCh)

本文档使用 书栈网 · BookStack.CN 构建 - 148 -


daemonset controller 源码分析

18.
19. <-stopCh
20. }

syncDaemonSet

daemonset 中 pod 的创建与删除是与 node 相关联的,所以每次执行 sync 操作时需要遍历所有


的 node 进行判断。 syncDaemonSet 的主要逻辑为:

1、通过 key 获取 ns 和 name;


2、从 dsLister 中获取 ds 对象;
3、从 nodeLister 获取所有 node;
4、获取 dsKey;
5、判断 ds 是否处于删除状态;
6、调用 constructHistory 获取 current 和 old controllerRevision ;
7、调用 dsc.expectations.SatisfiedExpectations 判断是否满足 expectations 机
制, expectations 机制的目的就是减少不必要的 sync 操作,关于 expectations 机制
的详细说明可以参考笔者以前写的 “replicaset controller 源码分析”一文;
8、调用 dsc.manage 执行实际的 sync 操作;
9、判断是否为更新操作,并执行对应的更新操作逻辑;
10、调用 dsc.cleanupHistory 根据 spec.revisionHistoryLimit 字段清理过期的
controllerrevision ;
11、调用 dsc.updateDaemonSetStatus 更新 ds 状态;

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:1212

1. func (dsc *DaemonSetsController) syncDaemonSet(key string) error {


2. ......
3.
4. // 1、通过 key 获取 ns 和 name
5. namespace, name, err := cache.SplitMetaNamespaceKey(key)
6. if err != nil {
7. return err
8. }
9.
10. // 2、从 dsLister 中获取 ds 对象
11. ds, err := dsc.dsLister.DaemonSets(namespace).Get(name)
12. if errors.IsNotFound(err) {
13. dsc.expectations.DeleteExpectations(key)
14. return nil
15. }
16. ......

本文档使用 书栈网 · BookStack.CN 构建 - 149 -


daemonset controller 源码分析

17.
18. // 3、从 nodeLister 获取所有 node
19. nodeList, err := dsc.nodeLister.List(labels.Everything())
20. ......
21.
22. everything := metav1.LabelSelector{}
23. if reflect.DeepEqual(ds.Spec.Selector, &everything) {
dsc.eventRecorder.Eventf(ds, v1.EventTypeWarning, SelectingAllReason,
24. "This daemon set is selecting all pods. A non-empty selector is required. ")
25. return nil
26. }
27.
28. // 4、获取 dsKey
29. dsKey, err := controller.KeyFunc(ds)
30. if err != nil {
31. return fmt.Errorf("couldn't get key for object %#v: %v", ds, err)
32. }
33.
34. // 5、判断 ds 是否处于删除状态
35. if ds.DeletionTimestamp != nil {
36. return nil
37. }
38.
39. // 6、获取 current 和 old controllerRevision
40. cur, old, err := dsc.constructHistory(ds)
41. if err != nil {
return fmt.Errorf("failed to construct revisions of DaemonSet: %v",
42. err)
43. }
44. hash := cur.Labels[apps.DefaultDaemonSetUniqueLabelKey]
45.
46. // 7、判断是否满足 expectations 机制
47. if !dsc.expectations.SatisfiedExpectations(dsKey) {
48. return dsc.updateDaemonSetStatus(ds, nodeList, hash, false)
49. }
50.
51. // 8、执行实际的 sync 操作
52. err = dsc.manage(ds, nodeList, hash)
53. if err != nil {
54. return err
55. }
56.
57. // 9、判断是否为更新操作,并执行对应的更新操作

本文档使用 书栈网 · BookStack.CN 构建 - 150 -


daemonset controller 源码分析

58. if dsc.expectations.SatisfiedExpectations(dsKey) {
59. switch ds.Spec.UpdateStrategy.Type {
60. case apps.OnDeleteDaemonSetStrategyType:
61. case apps.RollingUpdateDaemonSetStrategyType:
62. err = dsc.rollingUpdate(ds, nodeList, hash)
63. }
64. if err != nil {
65. return err
66. }
67. }
68. // 10、清理过期的 controllerrevision
69. err = dsc.cleanupHistory(ds, old)
70. if err != nil {
71. return fmt.Errorf("failed to clean up revisions of DaemonSet: %v", err)
72. }
73.
74. // 11、更新 ds 状态
75. return dsc.updateDaemonSetStatus(ds, nodeList, hash, true)
76. }

syncDaemonSet 中主要有 manage 、 rollingUpdate 和 updateDaemonSetStatus 三个


方法,分别对应创建、更新与状态同步,下面主要来分析这三个方法。

manage

manage 主要是用来保证 ds 的 pod 数正常运行在每一个 node 上,其主要逻辑为:

1、调用 dsc.getNodesToDaemonPods 获取已存在 daemon pod 与 node 的映射关系;


2、遍历所有 node,调用 dsc.podsShouldBeOnNode 方法来确定在给定的节点上需要创建还
是删除 daemon pod;
3、判断是否启动了 ScheduleDaemonSetPods feature-gates 特性,若启动了则需要删除通
过默认调度器已经调度到不存在 node 上的 daemon pod;
4、调用 dsc.syncNodes 为对应的 node 创建 daemon pod 以及删除多余的 pods;

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:952

func (dsc *DaemonSetsController) manage(ds *apps.DaemonSet, nodeList


1. []*v1.Node, hash string) error {
2. // 1、获取已存在 daemon pod 与 node 的映射关系
3. nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
4. ......
5.
6. // 2、判断每一个 node 是否需要运行 daemon pod

本文档使用 书栈网 · BookStack.CN 构建 - 151 -


daemonset controller 源码分析

7. var nodesNeedingDaemonPods, podsToDelete []string


8. for _, node := range nodeList {
nodesNeedingDaemonPodsOnNode, podsToDeleteOnNode, err :=
9. dsc.podsShouldBeOnNode(
10. node, nodeToDaemonPods, ds)
11.
12. if err != nil {
13. continue
14. }
15.
nodesNeedingDaemonPods = append(nodesNeedingDaemonPods,
16. nodesNeedingDaemonPodsOnNode...)
17. podsToDelete = append(podsToDelete, podsToDeleteOnNode...)
18. }
19.
// 3、判断是否启动了 ScheduleDaemonSetPods feature-gates 特性,若启用了则对不存在
20. node 上的
21. // daemon pod 进行删除
22. if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
podsToDelete = append(podsToDelete,
23. getUnscheduledPodsWithoutNode(nodeList, nodeToDaemonPods)...)
24. }
25.
26. // 4、为对应的 node 创建 daemon pod 以及删除多余的 pods
if err = dsc.syncNodes(ds, podsToDelete, nodesNeedingDaemonPods, hash); err
27. != nil {
28. return err
29. }
30.
31. return nil
32. }

在 manage 方法中又调用了 getNodesToDaemonPods 、 podsShouldBeOnNode 和


syncNodes 三个方法,继续来看这几种方法的作用。

getNodesToDaemonPods

getNodesToDaemonPods 是用来获取已存在 daemon pod 与 node 的映射关系,并且会通过


adopt/orphan 方法关联以及释放对应的 pod。

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:820

func (dsc *DaemonSetsController) getNodesToDaemonPods(ds *apps.DaemonSet)


1. (map[string][]*v1.Pod, error) {

本文档使用 书栈网 · BookStack.CN 构建 - 152 -


daemonset controller 源码分析

2. claimedPods, err := dsc.getDaemonPods(ds)


3. if err != nil {
4. return nil, err
5. }
6. nodeToDaemonPods := make(map[string][]*v1.Pod)
7. for _, pod := range claimedPods {
8. nodeName, err := util.GetTargetNodeName(pod)
9. if err != nil {
klog.Warningf("Failed to get target node name of Pod %v/%v in
10. DaemonSet %v/%v",
11. pod.Namespace, pod.Name, ds.Namespace, ds.Name)
12. continue
13. }
14.
15. nodeToDaemonPods[nodeName] = append(nodeToDaemonPods[nodeName], pod)
16. }
17.
18. return nodeToDaemonPods, nil
19. }

podsShouldBeOnNode

podsShouldBeOnNode 方法用来确定在给定的节点上需要创建还是删除 daemon pod,主要逻辑


为:

1、调用 dsc.nodeShouldRunDaemonPod 判断该 node 是否需要运行 daemon pod 以及


pod 能不能调度成功,该方法返回三个值 wantToRun , shouldSchedule ,
shouldContinueRunning ;
2、通过判断 wantToRun , shouldSchedule , shouldContinueRunning 将需要创建
daemon pod 的 node 列表以及需要删除的 pod 列表获取到, wantToRun 主要检查的是
selector、taints 等是否匹配, shouldSchedule 主要检查 node 上的资源是否充
足, shouldContinueRunning 默认为 true;

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:866

func (dsc *DaemonSetsController) podsShouldBeOnNode(...)


1. (nodesNeedingDaemonPods, podsToDelete []string, err error) {
2. // 1、判断该 node 是否需要运行 daemon pod 以及能不能调度成功
wantToRun, shouldSchedule, shouldContinueRunning, err :=
3. dsc.nodeShouldRunDaemonPod(node, ds)
4. if err != nil {
5. return
6. }

本文档使用 书栈网 · BookStack.CN 构建 - 153 -


daemonset controller 源码分析

7. // 2、获取该节点上的指定ds的pod列表
8. daemonPods, exists := nodeToDaemonPods[node.Name]
9. dsKey, err := cache.MetaNamespaceKeyFunc(ds)
10. if err != nil {
11. utilruntime.HandleError(err)
12. return
13. }
14.
15. // 3、从 suspended list 中移除在该节点上 ds 的 pod
16. dsc.removeSuspendedDaemonPods(node.Name, dsKey)
17.
18. switch {
19. // 4、对于需要创建 pod 但是不能调度 pod 的 node,先把 pod 放入到 suspended 队列中
20. case wantToRun && !shouldSchedule:
21. dsc.addSuspendedDaemonPods(node.Name, dsKey)
22. // 5、需要创建 pod 且 pod 未运行,则创建 pod
23. case shouldSchedule && !exists:
24. nodesNeedingDaemonPods = append(nodesNeedingDaemonPods, node.Name)
25. // 6、需要 pod 一直运行
26. case shouldContinueRunning:
27. var daemonPodsRunning []*v1.Pod
28. for _, pod := range daemonPods {
29. if pod.DeletionTimestamp != nil {
30. continue
31. }
32. // 7、如果 pod 运行状态为 failed,则删除该 pod
33. if pod.Status.Phase == v1.PodFailed {
34. backoffKey := failedPodsBackoffKey(ds, node.Name)
35.
36. now := dsc.failedPodsBackoff.Clock.Now()
inBackoff :=
37. dsc.failedPodsBackoff.IsInBackOffSinceUpdate(backoffKey, now)
38. if inBackoff {
39. delay := dsc.failedPodsBackoff.Get(backoffKey)
40. dsc.enqueueDaemonSetAfter(ds, delay)
41. continue
42. }
43.
44. dsc.failedPodsBackoff.Next(backoffKey, now)
45. podsToDelete = append(podsToDelete, pod.Name)
46. } else {
47. daemonPodsRunning = append(daemonPodsRunning, pod)

本文档使用 书栈网 · BookStack.CN 构建 - 154 -


daemonset controller 源码分析

48. }
49. }
50. // 8、如果节点上已经运行 daemon pod 数 > 1,保留运行时间最长的 pod,其余的删除
51. if len(daemonPodsRunning) > 1 {
52. sort.Sort(podByCreationTimestampAndPhase(daemonPodsRunning))
53. for i := 1; i < len(daemonPodsRunning); i++ {
54. podsToDelete = append(podsToDelete, daemonPodsRunning[i].Name)
55. }
56. }
57. // 9、如果 pod 不需要继续运行但 pod 已存在则需要删除 pod
58. case !shouldContinueRunning && exists:
59. for _, pod := range daemonPods {
60. if pod.DeletionTimestamp != nil {
61. continue
62. }
63. podsToDelete = append(podsToDelete, pod.Name)
64. }
65. }
66.
67. return nodesNeedingDaemonPods, podsToDelete, nil
68. }

然后继续看 nodeShouldRunDaemonPod 方法的主要逻辑:

1、调用 NewPod 为该 node 构建一个 daemon pod object;


2、判断 ds 是否指定了 .Spec.Template.Spec.NodeName 字段;
3、调用 dsc.simulate 执行 GeneralPredicates 预选算法检查该 node 是否能够调度
成功;
4、判断 GeneralPredicates 预选算法执行后的 reasons 确定 wantToRun ,
shouldSchedule , shouldContinueRunning 的值;

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:1337

func (dsc *DaemonSetsController) nodeShouldRunDaemonPod(node *v1.Node, ds


*apps.DaemonSet) (wantToRun, shouldSchedule, shouldContinueRunning bool, err
1. error) {
2. // 1、构建 daemon pod object
3. newPod := NewPod(ds, node.Name)
4.
5. wantToRun, shouldSchedule, shouldContinueRunning = true, true, true
6. // 2、判断 ds 是否指定了 node,若指定了且不为当前 node 直接返回 false
if !(ds.Spec.Template.Spec.NodeName == "" || ds.Spec.Template.Spec.NodeName
7. == node.Name) {

本文档使用 书栈网 · BookStack.CN 构建 - 155 -


daemonset controller 源码分析

8. return false, false, false, nil


9. }
10.
11. // 3、执行 GeneralPredicates 预选算法
12. reasons, nodeInfo, err := dsc.simulate(newPod, node, ds)
13. if err != nil {
14. ......
15. }
16.
17. // 4、检查预选算法执行的结果
18. var insufficientResourceErr error
19. for _, r := range reasons {
20. switch reason := r.(type) {
21. case *predicates.InsufficientResourceError:
22. insufficientResourceErr = reason
23. case *predicates.PredicateFailureError:
24. var emitEvent bool
25. switch reason {
26. case
27. predicates.ErrNodeSelectorNotMatch,
28. predicates.ErrPodNotMatchHostName,
29. predicates.ErrNodeLabelPresenceViolated,
30.
31. predicates.ErrPodNotFitsHostPorts:
32. return false, false, false, nil
33. case predicates.ErrTaintsTolerationsNotMatch:
fitsNoExecute, _, err :=
34. predicates.PodToleratesNodeNoExecuteTaints(newPod, nil, nodeInfo)
35. if err != nil {
36. return false, false, false, err
37. }
38. if !fitsNoExecute {
39. return false, false, false, nil
40. }
41. wantToRun, shouldSchedule = false, false
42. case
43. predicates.ErrDiskConflict,
44. predicates.ErrVolumeZoneConflict,
45. predicates.ErrMaxVolumeCountExceeded,
46. predicates.ErrNodeUnderMemoryPressure,
47. predicates.ErrNodeUnderDiskPressure:
48. shouldSchedule = false

本文档使用 书栈网 · BookStack.CN 构建 - 156 -


daemonset controller 源码分析

49. emitEvent = true


50. case
51. predicates.ErrPodAffinityNotMatch,
52. predicates.ErrServiceAffinityViolated:
53.
return false, false, false, fmt.Errorf("unexpected reason:
54. DaemonSet Predicates should not return reason %s", reason.GetReason())
55. default:
wantToRun, shouldSchedule, shouldContinueRunning = false,
56. false, false
57. emitEvent = true
58. }
59. ......
60. }
61. }
62.
63. if shouldSchedule && insufficientResourceErr != nil {
dsc.eventRecorder.Eventf(ds, v1.EventTypeWarning,
FailedPlacementReason, "failed to place pod on %q: %s", node.ObjectMeta.Name,
64. insufficientResourceErr.Error())
65. shouldSchedule = false
66. }
67. return
68. }

syncNodes

syncNodes 方法主要是为需要 daemon pod 的 node 创建 pod 以及删除多余的 pod,其主要


逻辑为:

1、将 createDiff 和 deleteDiff 与 burstReplicas 进行比较, burstReplicas


默认值为 250 即每个 syncLoop 中创建或者删除的 pod 数最多为 250 个,若超过其值则
剩余需要创建或者删除的 pod 在下一个 syncLoop 继续操作;
2、将 createDiff 和 deleteDiff 写入到 expectations 中;
3、并发创建 pod,创建 pod 有两种方法:(1)创建的 pod 不经过默认调度器,直接指定了
pod 的运行节点(即设定 pod.Spec.NodeName );(2)若启用了 ScheduleDaemonSetPods
feature-gates 特性,则使用默认调度器进行创建 pod,通过 nodeAffinity 来保证每个节
点都运行一个 pod;
4、并发删除 deleteDiff 中的所有 pod;

ScheduleDaemonSetPods 是一个 feature-gates 特性,其出现在 v1.11 中,在 v1.12 中


处于 Beta 版本,v1.17 为 GA 版。最初 daemonset controller 只有一种创建 pod 的方
法,即直接指定 pod 的 spec.NodeName 字段,但是目前这种方式已经暴露了许多问题,在以后

本文档使用 书栈网 · BookStack.CN 构建 - 157 -


daemonset controller 源码分析

的发展中社区还是希望能通过默认调度器进行调度,所以才出现了第二种方式,原因主要有以下五点:

1、DaemonSet 无法感知 node 上资源的变化 (#46935, #58868):当 pod 第一次因资源


不够无法创建时,若其他 pod 退出后资源足够时 DaemonSet 无法感知到;
2、Daemonset 无法支持 Pod Affinity 和 Pod AntiAffinity 的功能(#29276);
3、在某些功能上需要实现和 scheduler 重复的代码逻辑, 例如:critical pods
(#42028), tolerant/taint;
4、当 DaemonSet 的 Pod 创建失败时难以 debug,例如:资源不足时,对于 pending
pod 最好能打一个 event 说明;
5、多个组件同时调度时难以实现抢占机制:这也是无法通过横向扩展调度器提高调度吞吐量的一
个原因;

更详细的原因可以参考社区的文档:schedule-DS-pod-by-scheduler.md。

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:990

func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete,


1. nodesNeedingDaemonPods []string, hash string) error {
2. ......
3.
4. // 1、设置 burstReplicas
5. createDiff := len(nodesNeedingDaemonPods)
6. deleteDiff := len(podsToDelete)
7.
8. if createDiff > dsc.burstReplicas {
9. createDiff = dsc.burstReplicas
10. }
11. if deleteDiff > dsc.burstReplicas {
12. deleteDiff = dsc.burstReplicas
13. }
14.
15. // 2、写入到 expectations 中
16. dsc.expectations.SetExpectations(dsKey, createDiff, deleteDiff)
17.
18. errCh := make(chan error, createDiff+deleteDiff)
19. createWait := sync.WaitGroup{}
20. generation, err := util.GetTemplateGeneration(ds)
21. if err != nil {
22. generation = nil
23. }
24. template := util.CreatePodTemplate(ds.Spec.Template, generation, hash)
25.
26. // 3、并发创建 pod,创建的 pod 数依次为 1, 2, 4, 8, ...

本文档使用 书栈网 · BookStack.CN 构建 - 158 -


daemonset controller 源码分析

batchSize := integer.IntMin(createDiff,
27. controller.SlowStartInitialBatchSize)
for pos := 0; createDiff > pos; batchSize, pos =
28. integer.IntMin(2*batchSize, createDiff-(pos+batchSize)), pos+batchSize {
29. errorCount := len(errCh)
30. createWait.Add(batchSize)
31. for i := pos; i < pos+batchSize; i++ {
32. go func(ix int) {
33. defer createWait.Done()
34. var err error
35.
36. podTemplate := template.DeepCopy()
37.
// 4、若启动了 ScheduleDaemonSetPods 功能,则通过 kube-scheduler 创
38. 建 pod
if
39. utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
podTemplate.Spec.Affinity =
40. util.ReplaceDaemonSetPodNodeNameNodeAffinity(
41. podTemplate.Spec.Affinity, nodesNeedingDaemonPods[ix])
42.
err =
43. dsc.podControl.CreatePodsWithControllerRef(ds.Namespace, podTemplate,
44. ds, metav1.NewControllerRef(ds, controllerKind))
45. } else {
46. // 5、否则直接设置 pod 的 .spec.NodeName 创建 pod
err =
dsc.podControl.CreatePodsOnNode(nodesNeedingDaemonPods[ix], ds.Namespace,
47. podTemplate,
48. ds, metav1.NewControllerRef(ds, controllerKind))
49. }
50.
51. // 6、创建 pod 时忽略 timeout err
52. if err != nil && errors.IsTimeout(err) {
53. return
54. }
55. if err != nil {
56. dsc.expectations.CreationObserved(dsKey)
57. errCh <- err
58. utilruntime.HandleError(err)
59. }
60. }(i)
61. }

本文档使用 书栈网 · BookStack.CN 构建 - 159 -


daemonset controller 源码分析

62. createWait.Wait()
63.
64. // 7、将创建失败的 pod 数记录到 expectations 中
65. skippedPods := createDiff - (batchSize + pos)
66. if errorCount < len(errCh) && skippedPods > 0 {
67. dsc.expectations.LowerExpectations(dsKey, skippedPods, 0)
68. break
69. }
70. }
71.
72. // 8、并发删除 deleteDiff 中的 pod
73. deleteWait := sync.WaitGroup{}
74. deleteWait.Add(deleteDiff)
75. for i := 0; i < deleteDiff; i++ {
76. go func(ix int) {
77. defer deleteWait.Done()
if err := dsc.podControl.DeletePod(ds.Namespace, podsToDelete[ix],
78. ds); err != nil {
79. dsc.expectations.DeletionObserved(dsKey)
80. errCh <- err
81. utilruntime.HandleError(err)
82. }
83. }(i)
84. }
85. deleteWait.Wait()
86. errors := []error{}
87. close(errCh)
88. for err := range errCh {
89. errors = append(errors, err)
90. }
91. return utilerrors.NewAggregate(errors)
92. }

RollingUpdate

daemonset update 的方式有两种 OnDelete 和 RollingUpdate ,当为 OnDelete 时


需要用户手动删除每一个 pod 后完成更新操作,当为 RollingUpdate 时,daemonset
controller 会自动控制升级进度。

当为 RollingUpdate 时,主要逻辑为:

1、获取 daemonset pod 与 node 的映射关系;


2、根据 controllerrevision 的 hash 值获取所有未更新的 pods;

本文档使用 书栈网 · BookStack.CN 构建 - 160 -


daemonset controller 源码分析

3、获取 maxUnavailable , numUnavailable 的 pod 数值, maxUnavailable 是从


ds 的 rollingUpdate 字段中获取的默认值为 1, numUnavailable 的值是通过
daemonset pod 与 node 的映射关系计算每个 node 下是否有 available pod 得到的;
4、通过 oldPods 获取 oldAvailablePods , oldUnavailablePods 的 pod 列表;
5、遍历 oldUnavailablePods 列表将需要删除的 pod 追加到 oldPodsToDelete 数组
中。 oldUnavailablePods 列表中的 pod 分为两种,一种处于更新中,即删除状态,一种处
于未更新且异常状态,处于异常状态的都需要被删除;
6、遍历 oldAvailablePods 列表,此列表中的 pod 都处于正常运行状态,根据
maxUnavailable 值确定是否需要删除该 pod 并将需要删除的 pod 追加到
oldPodsToDelete 数组中;
7、调用 dsc.syncNodes 删除 oldPodsToDelete 数组中的 pods, syncNodes 方法在
manage 阶段已经分析过,此处不再详述;

rollingUpdate 的结果是找出需要删除的 pods 并进行删除,被删除的 pod 在下一个


syncLoop 中会通过 manage 方法使用最新版本的 daemonset template 进行创建,整个滚
动更新的过程是通过先删除再创建的方式一步步完成更新的,每次操作都是严格按照
maxUnavailable 的值确定需要删除的 pod 数。

k8s.io/kubernetes/pkg/controller/daemon/update.go:43

1. func (dsc *DaemonSetsController) rollingUpdate(......) error {


2. // 1、获取 daemonset pod 与 node 的映射关系
3. nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
4. ......
5.
6. // 2、获取所有未更新的 pods
7. _, oldPods := dsc.getAllDaemonSetPods(ds, nodeToDaemonPods, hash)
8.
9. // 3、计算 maxUnavailable, numUnavailable 的 pod 数值
maxUnavailable, numUnavailable, err := dsc.getUnavailableNumbers(ds,
10. nodeList, nodeToDaemonPods)
11. if err != nil {
12. return fmt.Errorf("couldn't get unavailable numbers: %v", err)
13. }
oldAvailablePods, oldUnavailablePods :=
14. util.SplitByAvailablePods(ds.Spec.MinReadySeconds, oldPods)
15.
16. // 4、将非 running 状态的 pods 加入到 oldPodsToDelete 中
17. var oldPodsToDelete []string
18. for _, pod := range oldUnavailablePods {
19. if pod.DeletionTimestamp != nil {
20. continue

本文档使用 书栈网 · BookStack.CN 构建 - 161 -


daemonset controller 源码分析

21. }
22. oldPodsToDelete = append(oldPodsToDelete, pod.Name)
23. }
24. // 5、根据 maxUnavailable 值确定是否需要删除 pod
25. for _, pod := range oldAvailablePods {
26. if numUnavailable >= maxUnavailable {
27. break
28. }
29. oldPodsToDelete = append(oldPodsToDelete, pod.Name)
30. numUnavailable++
31. }
32. // 6、调用 syncNodes 方法删除 oldPodsToDelete 数组中的 pods
33. return dsc.syncNodes(ds, oldPodsToDelete, []string{}, hash)
34. }

总结一下, manage 方法中的主要流程为:

1. |-> dsc.getNodesToDaemonPods
2. |
3. |
4. manage ---- |-> dsc.podsShouldBeOnNode ---> dsc.nodeShouldRunDaemonPod
5. |
6. |
7. |-> dsc.syncNodes

updateDaemonSetStatus

updateDaemonSetStatus 是 syncDaemonSet 中最后执行的方法,主要是用来计算 ds


status subresource 中的值并更新其 status。status 如下所示:

1. status:
2. currentNumberScheduled: 1 // 已经运行了 DaemonSet Pod的节点数量
3. desiredNumberScheduled: 1 // 需要运行该DaemonSet Pod的节点数量
4. numberMisscheduled: 0 // 不需要运行 DeamonSet Pod 但是已经运行了的节点数量
5. numberReady: 0 // DaemonSet Pod状态为Ready的节点数量
numberAvailable: 1 // DaemonSet Pod状态为Ready且运行时间超过
6. // Spec.MinReadySeconds 的节点数量
numberUnavailable: 0 // desiredNumberScheduled - numberAvailable
7. 的节点数量
8. observedGeneration: 3
9. updatedNumberScheduled: 1 // 已经完成DaemonSet Pod更新的节点数量

本文档使用 书栈网 · BookStack.CN 构建 - 162 -


daemonset controller 源码分析

updateDaemonSetStatus 主要逻辑为:

1、调用 dsc.getNodesToDaemonPods 获取已存在 daemon pod 与 node 的映射关系;


2、遍历所有 node,调用 dsc.nodeShouldRunDaemonPod 判断该 node 是否需要运行
daemon pod,然后计算 status 中的部分字段值;
3、调用 storeDaemonSetStatus 更新 ds status subresource;
4、判断 ds 是否需要 resync;

k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:1152

1. func (dsc *DaemonSetsController) updateDaemonSetStatus(......) error {


2. // 1、获取已存在 daemon pod 与 node 的映射关系
3. nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
4. ......
5.
var desiredNumberScheduled, currentNumberScheduled, numberMisscheduled,
6. numberReady, updatedNumberScheduled, numberAvailable int
7. for _, node := range nodeList {
8. // 2、判断该 node 是否需要运行 daemon pod
9. wantToRun, _, _, err := dsc.nodeShouldRunDaemonPod(node, ds)
10. if err != nil {
11. return err
12. }
13.
14. scheduled := len(nodeToDaemonPods[node.Name]) > 0
15. // 3、计算 status 中的字段值
16. if wantToRun {
17. desiredNumberScheduled++
18. if scheduled {
19. currentNumberScheduled++
20. daemonPods, _ := nodeToDaemonPods[node.Name]
21. sort.Sort(podByCreationTimestampAndPhase(daemonPods))
22. pod := daemonPods[0]
23. if podutil.IsPodReady(pod) {
24. numberReady++
if podutil.IsPodAvailable(pod, ds.Spec.MinReadySeconds,
25. metav1.Now()) {
26. numberAvailable++
27. }
28. }
29.
30. generation, err := util.GetTemplateGeneration(ds)
31. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 163 -


daemonset controller 源码分析

32. generation = nil


33. }
34. if util.IsPodUpdated(pod, hash, generation) {
35. updatedNumberScheduled++
36. }
37. }
38. } else {
39. if scheduled {
40. numberMisscheduled++
41. }
42. }
43. }
44. numberUnavailable := desiredNumberScheduled - numberAvailable
45. // 4、更新 daemonset status subresource
err =
storeDaemonSetStatus(dsc.kubeClient.AppsV1().DaemonSets(ds.Namespace), ds,
desiredNumberScheduled, currentNumberScheduled, numberMisscheduled,
numberReady, updatedNumberScheduled, numberAvailable, numberUnavailable,
46. updateObservedGen)
47. if err != nil {
return fmt.Errorf("error storing status for daemon set %#v: %v", ds,
48. err)
49. }
50.
51. // 5、判断 ds 是否需要 resync
52. if ds.Spec.MinReadySeconds > 0 && numberReady != numberAvailable {
dsc.enqueueDaemonSetAfter(ds,
53. time.Duration(ds.Spec.MinReadySeconds)*time.Second)
54. }
55. return nil
56. }

最后,再总结一下 syncDaemonSet 方法的主要流程:

1. |-> dsc.getNodesToDaemonPods
2. |
3. |
|-> manage -->|-> dsc.podsShouldBeOnNode --->
4. dsc.nodeShouldRunDaemonPod
5. | |
6. | |
7. syncDaemonSet --> | |-> dsc.syncNodes
8. |

本文档使用 书栈网 · BookStack.CN 构建 - 164 -


daemonset controller 源码分析

9. |-> rollingUpdate
10. |
11. |
12. |-> updateDaemonSetStatus

总结
在 daemonset controller 中可以看到许多功能都是 deployment 和 statefulset 已有的。
在创建 pod 的流程与 replicaset controller 创建 pod 的流程是相似的,都使用了
expectations 机制并且限制了在一个 syncLoop 中最多创建或删除的 pod 数。更新方式与
statefulset 一样都有 OnDelete 和 RollingUpdate 两种, OnDelete 方式与
statefulset 相似,都需要手动删除对应的 pod,而 RollingUpdate 方式与 statefulset
和 deployment 都有点区别, RollingUpdate 方式更新时不支持暂停操作并且 pod 是先删除再
创建的顺序进行。版本控制方式与 statefulset 的一样都是使用 controllerRevision 。最后
要说的一点是在 v1.12 及以后的版本中,使用 daemonset 创建的 pod 已不再使用直接指定
.spec.nodeName 的方式绕过调度器进行调度,而是走默认调度器通过 nodeAffinity 的方式调
度到每一个节点上。

参考:

https://yq.aliyun.com/articles/702305

本文档使用 书栈网 · BookStack.CN 构建 - 165 -


statefulset controller 源码分析

Statefulset 的基本功能
statefulset 旨在与有状态的应用及分布式系统一起使用,statefulset 中的每个 pod 拥有一个
唯一的身份标识,并且所有 pod 名都是按照 {0..N-1} 的顺序进行编号。本文会主要分析
statefulset controller 的设计与实现,在分析源码前先介绍一下 statefulset 的基本使
用。

创建

对于一个拥有 N 个副本的 statefulset,pod 是按照 {0..N-1}的序号顺序创建的,并且会等待


前一个 pod 变为 Running & Ready 后才会启动下一个 pod。

1. $ kubectl create -f sts.yaml


2.
3. $ kubectl get pod -o wide -w
4. NAME READY STATUS RESTARTS AGE IP NODE
5. web-0 0/1 ContainerCreating 0 20s <none> minikube
6. web-0 1/1 Running 0 3m1s 10.1.0.8 minikube
7.
8. web-1 0/1 Pending 0 0s <none> <none>
9. web-1 0/1 ContainerCreating 0 2s <none> minikube
10. web-1 1/1 Running 0 4s 10.1.0.9 minikube

扩容

statefulset 扩容时 pod 也是顺序创建的,编号与前面的 pod 相接。

1. $ kubectl scale sts web --replicas=4


2. statefulset.apps/web scaled
3.
4. $ kubectl get pod -o wide -w
5. ......
6. web-2 0/1 Pending 0 0s <none> <none>
7. web-2 0/1 ContainerCreating 0 1s <none> minikube
8. web-2 1/1 Running 0 4s 10.1.0.10 minikube
9.
10. web-3 0/1 Pending 0 0s <none> <none>
11. web-3 0/1 ContainerCreating 0 1s <none> minikube
12. web-3 1/1 Running 0 4s 10.1.0.11 minikube

缩容

本文档使用 书栈网 · BookStack.CN 构建 - 166 -


statefulset controller 源码分析

缩容时控制器会按照与 pod 序号索引相反的顺序每次删除一个 pod,在删除下一个 pod 前会等待上


一个被完全删除。

1. $ kubectl scale sts web --replicas=2


2.
3. $ kubectl get pod -o wide -w
4. ......
5. web-3 1/1 Terminating 0 8m25s 10.1.0.11 minikube
6. web-3 0/1 Terminating 0 8m27s <none> minikube
7.
8. web-2 1/1 Terminating 0 8m31s 10.1.0.10 minikube
9. web-2 0/1 Terminating 0 8m33s 10.1.0.10 minikube

更新

更新策略由 statefulset 中的 spec.updateStrategy.type 字段决定,可以指定为


OnDelete 或者 RollingUpdate , 默认的更新策略为 RollingUpdate 。当使
用 RollingUpdate 更新策略更新所有 pod 时采用与序号索引相反的顺序进行更新,即最先删除序
号最大的 pod 并根据更新策略中的 partition 参数来进行分段更新,控制器会更新所有序号大
于或等于 partition 的 pod,等该区间内的 pod 更新完成后需要再次设定 partition 的
值以此来更新剩余的 pod,最终 partition 被设置为 0 时代表更新完成了所有的 pod。在更新
过程中,如果一个序号小于 partition 的 pod 被删除或者终止,controller 依然会使用更新
前的配置重新创建。

1. // 使用 RollingUpdate 策略更新
$ kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path":
2. "/spec/template/spec/containers/0/image", "value":"nginx:1.16"}]'
3.
4. statefulset.apps/web patched
5.
6. $ kubectl rollout status sts/web
7. Waiting for 1 pods to be ready...
Waiting for partitioned roll out to finish: 1 out of 2 new pods have been
8. updated...
9. Waiting for 1 pods to be ready...
10. partitioned roll out complete: 2 new pods have been updated...

如果 statefulset 的 .spec.updateStrategy.type 字段被设置为 OnDelete ,在更新


statefulset 时,statefulset controller 将不会自动更新其 pod。你必须手动删除 pod,
此时 statefulset controller 在重新创建 pod 时,使用修改过的 .spec.template 的内
容创建新 pod。

本文档使用 书栈网 · BookStack.CN 构建 - 167 -


statefulset controller 源码分析

1. // 使用 OnDelete 方式更新
$ kubectl patch statefulset nginx --type='json' -p='[{"op": "replace", "path":
2. "/spec/template/spec/containers/0/image", "value":"nginx:1.9"}]'
3.
4. // 删除 web-1
5. $ kubectl delete pod web-1
6.
7. // 查看 web-0 与 web-1 的镜像版本,此时发现 web-1 已经变为最新版本 nginx:1.9 了
$ kubectl get pod -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}
8. {"\t"}{.spec.containers[0].image}{"\n"}{end}'
9. web-0 nginx:1.16
10. web-1 nginx:1.9

使用滚动更新策略时你必须以某种策略不段更新 partition 值来进行升级,类似于金丝雀部署方


式,升级对于 pod 名称来说是逆序。使用非滚动更新方式式,需要手动删除对应的 pod,升级可以是
无序的。

回滚

statefulset 和 deployment 一样也支持回滚操作,statefulset 也保存了历史版本,和


deployment 一样利用 .spec.revisionHistoryLimit 字段设置保存多少个历史版本,但
statefulset 的回滚并不是自动进行的,回滚操作也仅仅是进行了一次发布更新,和发布更新的策略
一样,更新 statefulset 后需要按照对应的策略手动删除 pod 或者修改 partition 字段以
达到回滚 pod 的目的。

1. // 查看 sts 的历史版本
2. $ kubectl rollout history statefulset web
3. statefulset.apps/web
4. REVISION
5. 0
6. 0
7. 5
8. 6
9.
10. $ kubectl get controllerrevision
11. NAME CONTROLLER REVISION AGE
12. web-6c4c79564f statefulset.apps/web 6 11m
13. web-c47b9997f statefulset.apps/web 5 4h13m
14.
15. // 回滚至最近的一个版本
16. $ kubectl rollout undo statefulset web --to-revision=5

本文档使用 书栈网 · BookStack.CN 构建 - 168 -


statefulset controller 源码分析

因为 statefulset 的使用对象是有状态服务,大部分有状态副本集都会用到持久存储,
statefulset 下的每个 pod 正常情况下都会关联一个 pv 对象,对 statefulset 对象回滚非常
容易,但其使用的 pv 中保存的数据无法回滚,所以在生产环境中进行回滚时需要谨慎操作,
statefulset、pod、pvc 和 pv 关系图如下所示:

删除

statefulset 同时支持级联和非级联删除。使用非级联方式删除 statefulset 时,


statefulset 的 pod 不会被删除。使用级联删除时,statefulset 和它关联的 pod 都会被删
除。对于级联与非级联删除,在删除时需要指定删除选项( orphan 、 background 或者
foreground )进行区分。

1. // 1、非级联删除
2. $ kubectl delete statefulset web --cascade=false
3.
4. // 删除 sts 后 pod 依然处于运行中
5. $ kubectl get pod
6. NAME READY STATUS RESTARTS AGE
7. web-0 1/1 Running 0 4m38s
8. web-1 1/1 Running 0 17m
9.
10. // 重新创建 sts 后,会再次关联所有的 pod
11. $ kubectl create -f sts.yaml
12.
13. $ kubectl get sts
14. NAME READY AGE
15. web 2/2 28s

在级联删除 statefulset 时,会将所有的 pod 同时删掉,statefulset 控制器会首先进行一个


类似缩容的操作,pod 按照和他们序号索引相反的顺序每次终止一个。在终止一个 pod 前,
statefulset 控制器会等待 pod 后继者被完全终止。

本文档使用 书栈网 · BookStack.CN 构建 - 169 -


statefulset controller 源码分析

1. // 2、级联删除
2. $ kubectl delete statefulset web
3.
4. $ kubectl get pod -o wide -w
5. ......
web-0 1/1 Terminating 0 17m 10.1.0.18 minikube <none>
6. <none>
web-1 1/1 Terminating 0 36m 10.1.0.15 minikube <none>
7. <none>
web-1 0/1 Terminating 0 36m 10.1.0.15 minikube <none>
8. <none>
web-0 0/1 Terminating 0 17m 10.1.0.18 minikube <none>
9. <none>

Pod 管理策略

statefulset 的默认管理策略是 OrderedReady ,该策略遵循上文展示的顺序性保证。


statefulset 还有另外一种管理策略 Parallel , Parallel 管理策略告诉 statefulset
控制器并行的终止所有 pod,在启动或终止另一个 pod 前,不必等待这些 pod 变成 Running &
Ready 或者完全终止状态,但是 Parallel 仅仅支持在 OnDelete 策略下生效,下文会在源
码中具体分析。

StatefulSetController 源码分析

kubernetes 版本:v1.16

startStatefulSetController 是 statefulSetController 的启动方法,其中调用


NewStatefulSetController 进行初始化 controller 对象然后调用 Run 方法启动
controller。其中 ConcurrentStatefulSetSyncs 默认值为 5。

k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:55

func startStatefulSetController(ctx ControllerContext) (http.Handler, bool,


1. error) {
if !ctx.AvailableResources[schema.GroupVersionResource{Group: "apps",
2. Version: "v1", Resource: "statefulsets"}] {
3. return nil, false, nil
4. }
5. go statefulset.NewStatefulSetController(
6. ctx.InformerFactory.Core().V1().Pods(),
7. ctx.InformerFactory.Apps().V1().StatefulSets(),
8. ctx.InformerFactory.Core().V1().PersistentVolumeClaims(),
9. ctx.InformerFactory.Apps().V1().ControllerRevisions(),

本文档使用 书栈网 · BookStack.CN 构建 - 170 -


statefulset controller 源码分析

10. ctx.ClientBuilder.ClientOrDie("statefulset-controller"),

).Run(int(ctx.ComponentConfig.StatefulSetController.ConcurrentStatefulSetSyncs),
11. ctx.Stop)
12. return nil, true, nil
13. }

当 controller 启动后会通过 informer 同步 cache 并监听 pod 和 statefulset 对象的变


更事件,informer 的处理流程此处不再详细讲解,最后会执行 sync 方法, sync 方法是每
个 controller 的核心方法,下面直接看 statefulset controller 的 sync 方法。

sync

sync 方法的主要逻辑为:

1、根据 ns/name 获取 sts 对象;


2、获取 sts 的 selector;
3、调用 ssc.adoptOrphanRevisions 检查是否有孤儿 controllerrevisions 对象,若
有且能匹配 selector 的则添加 ownerReferences 进行关联,已关联但 label 不匹配的
则进行释放;
4、调用 ssc.getPodsForStatefulSet 通过 selector 获取 sts 关联的 pod,若有孤儿
pod 的 label 与 sts 的能匹配则进行关联,若已关联的 pod label 有变化则解除与 sts
的关联关系;
5、最后调用 ssc.syncStatefulSet 执行真正的 sync 操作;

k8s.io/kubernetes/pkg/controller/statefulset/stateful_set.go:408

1. func (ssc *StatefulSetController) sync(key string) error {


2. ......
3.
4. namespace, name, err := cache.SplitMetaNamespaceKey(key)
5. if err != nil {
6. return err
7. }
8.
9. // 1、获取 sts 对象
10. set, err := ssc.setLister.StatefulSets(namespace).Get(name)
11. ......
12.
13. selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector)
14. ......
15.
16. // 2、关联以及释放 sts 的 controllerrevisions

本文档使用 书栈网 · BookStack.CN 构建 - 171 -


statefulset controller 源码分析

17. if err := ssc.adoptOrphanRevisions(set); err != nil {


18. return err
19. }
20.
21. // 3、获取 sts 所关联的 pod
22. pods, err := ssc.getPodsForStatefulSet(set, selector)
23. if err != nil {
24. return err
25. }
26.
27. return ssc.syncStatefulSet(set, pods)
28. }

syncStatefulSet

在 syncStatefulSet 中仅仅是调用了 ssc.control.UpdateStatefulSet 方法进行处


理。 ssc.control.UpdateStatefulSet 会调用 defaultStatefulSetControl 的
UpdateStatefulSet 方法, defaultStatefulSetControl 是 statefulset controller
中另外一个对象,主要负责处理 statefulset 的更新。

k8s.io/kubernetes/pkg/controller/statefulset/stateful_set.go:448

func (ssc *StatefulSetController) syncStatefulSet(set *apps.StatefulSet, pods


1. []*v1.Pod) error {
2. ......
3. if err := ssc.control.UpdateStatefulSet(set.DeepCopy(), pods); err != nil {
4. return err
5. }
6. ......
7. return nil
8. }

UpdateStatefulSet 方法的主要逻辑如下所示:

1、获取历史 revisions;
2、计算 currentRevision 和 updateRevision ,若 sts 处于更新过程中则
currentRevision 和 updateRevision 值不同;
3、调用 ssc.updateStatefulSet 执行实际的 sync 操作;
4、调用 ssc.updateStatefulSetStatus 更新 status subResource;
5、根据 sts 的 spec.revisionHistoryLimit 字段清理过期的 controllerrevision ;

在基本操作的回滚阶段提到了过,sts 通过 controllerrevision 保存历史版本,类似于

本文档使用 书栈网 · BookStack.CN 构建 - 172 -


statefulset controller 源码分析

deployment 的 replicaset,与 replicaset 不同的是 controllerrevision 仅用于回滚阶


段,在 sts 的滚动升级过程中是通过 currentRevision 和 updateRevision 来进行控制并
不会用到 controllerrevision 。

k8s.io/kubernetes/pkg/controller/statefulset/stateful_set_control.go:75

func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet,


1. pods []*v1.Pod) error {
2.
3. // 1、获取历史 revisions
4. revisions, err := ssc.ListRevisions(set)
5. if err != nil {
6. return err
7. }
8. history.SortControllerRevisions(revisions)
9.
10. // 2、计算 currentRevision 和 updateRevision
currentRevision, updateRevision, collisionCount, err :=
11. ssc.getStatefulSetRevisions(set, revisions)
12. if err != nil {
13. return err
14. }
15.
16. // 3、执行实际的 sync 操作
status, err := ssc.updateStatefulSet(set, currentRevision, updateRevision,
17. collisionCount, pods)
18. if err != nil {
19. return err
20. }
21.
22. // 4、更新 sts 状态
23. err = ssc.updateStatefulSetStatus(set, status)
24. if err != nil {
25. return err
26. }
27. ......
28.
29. // 5、清理过期的历史版本
return ssc.truncateHistory(set, pods, revisions, currentRevision,
30. updateRevision)
31. }

updateStatefulSet
本文档使用 书栈网 · BookStack.CN 构建 - 173 -
statefulset controller 源码分析

updateStatefulSet 是 sync 操作中的核心方法,对于 statefulset 的创建、扩缩容、更


新、删除等操作都会在这个方法中完成,以下是其主要逻辑:

1、分别获取 currentRevision 和 updateRevision 对应的的 statefulset


object;
2、构建 status 对象;
3、将 statefulset 的 pods 按 ord(ord 为 pod name 中的序号)的值分到 replicas
和 condemned 两个数组中,0 <= ord < Spec.Replicas 的放到 replicas 组,ord
>= Spec.Replicas 的放到 condemned 组,replicas 组代表可用的 pod,condemned
组是需要删除的 pod;
4、找出 replicas 和 condemned 组中的 unhealthy pod,healthy pod 指 running
& ready 并且不处于删除状态;
5、判断 sts 是否处于删除状态;
6、遍历 replicas 数组,确保 replicas 数组中的容器处于 running & ready 状态,其
中处于 failed 状态的容器删除重建,未创建的容器则直接创建,最后检查 pod 的信息是否
与 statefulset 的匹配,若不匹配则更新 pod 的状态。在此过程中每一步操作都会检查
monotonic 的值,即 sts 是否设置了 Parallel 参数,若设置了则循环处理 replicas
中的所有 pod,否则每次处理一个 pod,剩余 pod 则在下一个 syncLoop 继续进行处理;
7、按 pod 名称逆序删除 condemned 数组中的 pod,删除前也要确保 pod 处于
running & ready 状态,在此过程中也会检查 monotonic 的值,以此来判断是顺序删除还
是在下一个 syncLoop 中继续进行处理;
8、判断 sts 的更新策略 .Spec.UpdateStrategy.Type ,若为 OnDelete 则直接返回;
9、此时更新策略为 RollingUpdate ,更新序号大于等于
.Spec.UpdateStrategy.RollingUpdate.Partition 的 pod,在 RollingUpdate 时,并
不会关注 monotonic 的值,都是顺序进行处理且等待当前 pod 删除成功后才继续删除小于
上一个 pod 序号的 pod,所以 Parallel 的策略在滚动更新时无法使用。

updateStatefulSet 这个方法中包含了 statefulset 的创建、删除、扩若容、更新等操作,在


源码层面对于各个功能无法看出明显的界定,没有 deployment sync 方法中写的那么清晰,下面还
是按 statefulset 的功能再分析一下具体的操作:

创建:在创建 sts 后,sts 对象已被保存至 etcd 中,此时 sync 操作仅仅是创建出需要的


pod,即执行到第 6 步就会结束;
扩缩容:对于扩若容操作仅仅是创建或者删除对应的 pod,在操作前也会判断所有 pod 是否处
于 running & ready 状态,然后进行对应的创建/删除操作,在上面的步骤中也会执行到第 6
步就结束了;
更新:可以看出在第六步之后的所有操作就是与更新相关的了,所以更新操作会执行完整个方
法,在更新过程中通过 pod 的 currentRevision 和 updateRevision 来计算
currentReplicas 、 updatedReplicas 的值,最终完成所有 pod 的更新;
删除:删除操作就比较明显了,会止于第五步,但是在此之前检查 pod 状态以及分组的操作确

本文档使用 书栈网 · BookStack.CN 构建 - 174 -


statefulset controller 源码分析

实是多余的;

k8s.io/kubernetes/pkg/controller/statefulset/stateful_set_control.go:255

func (ssc *defaultStatefulSetControl) updateStatefulSet(......)


1. (*apps.StatefulSetStatus, error) {
2. // 1、分别获取 currentRevision 和 updateRevision 对应的的 statefulset object
3. currentSet, err := ApplyRevision(set, currentRevision)
4. if err != nil {
5. return nil, err
6. }
7. updateSet, err := ApplyRevision(set, updateRevision)
8. if err != nil {
9. return nil, err
10. }
11.
12. // 2、计算 status
13. status := apps.StatefulSetStatus{}
14. status.ObservedGeneration = set.Generation
15. status.CurrentRevision = currentRevision.Name
16. status.UpdateRevision = updateRevision.Name
17. status.CollisionCount = new(int32)
18. *status.CollisionCount = collisionCount
19.
20.
21. // 3、将 statefulset 的 pods 按 ord(ord 为 pod name 中的序数)的值
22. // 分到 replicas 和 condemned 两个数组中
23. replicaCount := int(*set.Spec.Replicas)
24. replicas := make([]*v1.Pod, replicaCount)
25. condemned := make([]*v1.Pod, 0, len(pods))
26. unhealthy := 0
27. firstUnhealthyOrdinal := math.MaxInt32
28.
29. var firstUnhealthyPod *v1.Pod
30.
31. // 4、计算 status 字段中的值,将 pod 分配到 replicas和condemned两个数组中
32. for i := range pods {
33. status.Replicas++
34.
35. if isRunningAndReady(pods[i]) {
36. status.ReadyReplicas++
37. }
38.

本文档使用 书栈网 · BookStack.CN 构建 - 175 -


statefulset controller 源码分析

39. if isCreated(pods[i]) && !isTerminating(pods[i]) {


40. if getPodRevision(pods[i]) == currentRevision.Name {
41. status.CurrentReplicas++
42. }
43. if getPodRevision(pods[i]) == updateRevision.Name {
44. status.UpdatedReplicas++
45. }
46. }
47.
48. if ord := getOrdinal(pods[i]); 0 <= ord && ord < replicaCount {
49. replicas[ord] = pods[i]
50. } else if ord >= replicaCount {
51. condemned = append(condemned, pods[i])
52. }
53. }
54.
// 5、检查 replicas数组中 [0,set.Spec.Replicas) 下标是否有缺失的 pod,若有缺失的则
55. 创建对应的 pod object
// 在 newVersionedStatefulSetPod 中会判断是使用 currentSet 还是 updateSet 来创
56. 建
57. for ord := 0; ord < replicaCount; ord++ {
58. if replicas[ord] == nil {
59. replicas[ord] = newVersionedStatefulSetPod(
60. currentSet,
61. updateSet,
62. currentRevision.Name,
63. updateRevision.Name, ord)
64. }
65. }
66.
67. // 6、对 condemned 数组进行排序
68. sort.Sort(ascendingOrdinal(condemned))
69.
70. // 7、根据 ord 在 replicas 和 condemned 数组中找出 first unhealthy Pod
71. for i := range replicas {
72. if !isHealthy(replicas[i]) {
73. unhealthy++
74. if ord := getOrdinal(replicas[i]); ord < firstUnhealthyOrdinal {
75. firstUnhealthyOrdinal = ord
76. firstUnhealthyPod = replicas[i]
77. }
78. }
79. }

本文档使用 书栈网 · BookStack.CN 构建 - 176 -


statefulset controller 源码分析

80.
81. for i := range condemned {
82. if !isHealthy(condemned[i]) {
83. unhealthy++
84. if ord := getOrdinal(condemned[i]); ord < firstUnhealthyOrdinal {
85. firstUnhealthyOrdinal = ord
86. firstUnhealthyPod = condemned[i]
87. }
88. }
89. }
90.
91. ......
92.
93. // 8、判断是否处于删除中
94. if set.DeletionTimestamp != nil {
95. return &status, nil
96. }
97.
98. // 9、默认设置为非并行模式
99. monotonic := !allowsBurst(set)
100.
101.
102. // 10、确保 replicas 数组中所有的 pod 是 running 的
103. for i := range replicas {
104. // 11、对于 failed 的 pod 删除并重新构建 pod object
105. if isFailed(replicas[i]) {
106. ......
if err := ssc.podControl.DeleteStatefulPod(set, replicas[i]); err
107. != nil {
108. return &status, err
109. }
110. if getPodRevision(replicas[i]) == currentRevision.Name {
111. status.CurrentReplicas--
112. }
113. if getPodRevision(replicas[i]) == updateRevision.Name {
114. status.UpdatedReplicas--
115. }
116. status.Replicas--
117. replicas[i] = newVersionedStatefulSetPod(
118. currentSet,
119. updateSet,
120. currentRevision.Name,

本文档使用 书栈网 · BookStack.CN 构建 - 177 -


statefulset controller 源码分析

121. updateRevision.Name,
122. i)
123. }
124.
125. // 12、如果 pod.Status.Phase 不为“” 说明该 pod 未创建,则直接重新创建该 pod
126. if !isCreated(replicas[i]) {
if err := ssc.podControl.CreateStatefulPod(set, replicas[i]); err
127. != nil {
128. return &status, err
129. }
130. status.Replicas++
131. if getPodRevision(replicas[i]) == currentRevision.Name {
132. status.CurrentReplicas++
133. }
134. if getPodRevision(replicas[i]) == updateRevision.Name {
135. status.UpdatedReplicas++
136. }
137.
// 13、如果为Parallel,直接return status结束;如果为OrderedReady,循环处
138. 理下一个pod。
139. if monotonic {
140. return &status, nil
141. }
142. continue
143. }
144.
// 14、如果pod正在删除(pod.DeletionTimestamp不为nil),且
145. Spec.PodManagementPolicy不
146. // 为Parallel,直接return status结束,结束后会在下一个 syncLoop 继续进行处理,
147. // pod 状态的改变会触发下一次 syncLoop
148. if isTerminating(replicas[i]) && monotonic {
149. ......
150. return &status, nil
151. }
152.
// 15、如果pod状态不是Running & Ready,且Spec.PodManagementPolicy不为
153. Parallel,
154. // 直接return status结束
155. if !isRunningAndReady(replicas[i]) && monotonic {
156. ......
157. return &status, nil
158. }
159.

本文档使用 书栈网 · BookStack.CN 构建 - 178 -


statefulset controller 源码分析

160. // 16、检查 pod 的信息是否与 statefulset 的匹配,若不匹配则更新 pod 的状态


if identityMatches(set, replicas[i]) && storageMatches(set,
161. replicas[i]) {
162. continue
163. }
164.
165. replica := replicas[i].DeepCopy()
if err := ssc.podControl.UpdateStatefulPod(updateSet, replica); err !=
166. nil {
167. return &status, err
168. }
169. }
170.
171.
172. // 17、逆序处理 condemned 中的 pod
173. for target := len(condemned) - 1; target >= 0; target-- {
174.
175. // 18、如果pod正在删除,检查 Spec.PodManagementPolicy 的值,如果为Parallel,
176. // 循环处理下一个pod 否则直接退出
177. if isTerminating(condemned[target]) {
178. ......
179. if monotonic {
180. return &status, nil
181. }
182. continue
183. }
184.
185.
186. // 19、不满足以下条件说明该 pod 是更新前创建的,正处于创建中
if !isRunningAndReady(condemned[target]) && monotonic &&
187. condemned[target] != firstUnhealthyPod {
188. ......
189. return &status, nil
190. }
191.
192. // 20、否则直接删除该 pod
if err := ssc.podControl.DeleteStatefulPod(set, condemned[target]); err
193. != nil {
194. return &status, err
195. }
196. if getPodRevision(condemned[target]) == currentRevision.Name {
197. status.CurrentReplicas--
198. }

本文档使用 书栈网 · BookStack.CN 构建 - 179 -


statefulset controller 源码分析

199. if getPodRevision(condemned[target]) == updateRevision.Name {


200. status.UpdatedReplicas--
201. }
202.
203. // 21、如果为 OrderedReady 方式则返回否则继续处理下一个 pod
204. if monotonic {
205. return &status, nil
206. }
207. }
208.
209. // 22、对于 OnDelete 策略直接返回
210. if set.Spec.UpdateStrategy.Type == apps.OnDeleteStatefulSetStrategyType {
211. return &status, nil
212. }
213.
214. // 23、若为 RollingUpdate 策略,则倒序处理 replicas数组中下标大于等于
215. // Spec.UpdateStrategy.RollingUpdate.Partition 的 pod
216. updateMin := 0
217. if set.Spec.UpdateStrategy.RollingUpdate != nil {
218. updateMin = int(*set.Spec.UpdateStrategy.RollingUpdate.Partition)
219. }
220.
221. for target := len(replicas) - 1; target >= updateMin; target-- {
// 24、如果Pod的Revision 不等于 updateRevision,且 pod 没有处于删除状态则直接
222. 删除 pod
if getPodRevision(replicas[target]) != updateRevision.Name &&
223. !isTerminating(replicas[target]) {
224. ......
225. err := ssc.podControl.DeleteStatefulPod(set, replicas[target])
226. status.CurrentReplicas--
227. return &status, err
228. }
229.
230. // 25、如果 pod 非 healthy 状态直接返回
231. if !isHealthy(replicas[target]) {
232. return &status, nil
233. }
234. }
235. return &status, nil
236. }

总结

本文档使用 书栈网 · BookStack.CN 构建 - 180 -


statefulset controller 源码分析

本文分析了 statefulset controller 的主要功能,statefulset 在设计上有很多功能与


deployment 是类似的,但其主要是用来部署有状态应用的,statefulset 中的 pod 名称存在顺
序性和唯一性,同时每个 pod 都使用了 pv 和 pvc 来存储状态,在创建、删除、更新操作中都会按
照 pod 的顺序进行。

参考:

https://github.com/kubernetes/kubernetes/issues/78007

https://github.com/kubernetes/kubernetes/issues/67250

https://www.cnblogs.com/linuxk/p/9767736.html

本文档使用 书栈网 · BookStack.CN 构建 - 181 -


deployment controller 源码分析

在前面的文章中已经分析过 kubernetes 中多个组件的源码了,本章会继续解读 kube-


controller-manager 源码,kube-controller-manager 中有数十个 controller,本文会分
析最常用到的 deployment controller。

deployment 的功能
deployment 是 kubernetes 中用来部署无状态应用的一个对象,也是最常用的一种对象。

deployment、replicaSet 和 pod 之间的关系

deployment 的本质是控制 replicaSet,replicaSet 会控制 pod,然后由 controller 驱动


各个对象达到期望状态。

DeploymentController 是 Deployment 资源的控制器,其通过 DeploymentInformer、


ReplicaSetInformer、PodInformer 监听三种资源,当三种资源变化时会触发
DeploymentController 中的 syncLoop 操作。

deployment 的基本功能

下面通过命令行操作展示一下 deployment 的基本功能。

本文档使用 书栈网 · BookStack.CN 构建 - 182 -


deployment controller 源码分析

以下是 deployment 的一个示例文件:

1. apiVersion: apps/v1
2. kind: Deployment
3. metadata:
4. name: nginx-deployment
5. spec:
6. progressDeadlineSeconds: 600 // 执行操作的超时时间
7. replicas: 20
8. revisionHistoryLimit: 10 // 保存的历史版本数量
9. selector:
10. matchLabels:
11. app: nginx-deployment
12. strategy:
13. rollingUpdate:
14. maxSurge: 25% // 升级过程中最多可以比原先设置多出的 pod 数量
15. maxUnavailable: 25% // 升级过程中最多有多少个 pod 处于无法提供服务的状态
16. type: RollingUpdate // 更新策略
17. template:
18. metadata:
19. labels:
20. app: nginx-deployment
21. spec:
22. containers:
23. - name: nginx-deployment
24. image: nginx:1.9
25. imagePullPolicy: IfNotPresent
26. ports:
27. - containerPort: 80

创建

1. $ kubectl create -f nginx-dep.yaml --record


2.
3. $ kubectl get deployment
4. NAME READY UP-TO-DATE AVAILABLE AGE
5. nginx-deployment 20/20 20 20 22h
6.
7. $ kubectl get rs
8. NAME DESIRED CURRENT READY AGE
9. nginx-deployment-68b649bd8b 20 20 20 22h

本文档使用 书栈网 · BookStack.CN 构建 - 183 -


deployment controller 源码分析

滚动更新

1. $ kubectl set image deploy/nginx-deployment nginx-deployment=nginx:1.9.3


2.
3. $ kubectl rollout status deployment/nginx-deployment

回滚

1. // 查看历史版本
2. $ kubectl rollout history deployment/nginx-deployment
3. deployment.extensions/nginx-deployment
4. REVISION CHANGE-CAUSE
5. 4 <none>
6. 5 <none>
7.
8. // 指定版本回滚
9. $ kubectl rollout undo deployment/nginx-deployment --to-revision=2

扩缩容

1. $ kubectl scale deployment nginx-deployment --replicas 10


2. deployment.extensions/nginx-deployment scaled

暂停与恢复

1. $ kubectl rollout pause deployment/nginx-deployment


2. $ kubectl rollout resume deploy nginx-deployment

删除

1. $ kubectl delete deployment nginx-deployment

以上是 deployment 的几个常用操作,下面会结合源码分析这几个操作都是如何实现的。

deployment controller 源码分析

kubernetes 版本:v1.16

在控制器模式下,每次操作对象都会触发一次事件,然后 controller 会进行一次 syncLoop 操


作,controller 是通过 informer 监听事件以及进行 ListWatch 操作的,关于 informer 的
基础知识可以参考以前写的文章。

本文档使用 书栈网 · BookStack.CN 构建 - 184 -


deployment controller 源码分析

deployment controller 启动流程

kube-controller-manager 中所有 controller 的启动都是在 Run 方法中完成初始化并启


动的。在 Run 中会调用 run 函数,run 函数的主要流程有:

1、调用 NewControllerInitializers 初始化所有 controller


2、调用 StartControllers 启动所有 controller

k8s.io/kubernetes/cmd/kube-controller-manager/app/controllermanager.go:158

1. func Run(c *config.CompletedConfig, stopCh <-chan struct{}) error {


2. ......
3. run := func(ctx context.Context) {
4. ......
5. // 1.调用 NewControllerInitializers 初始化所有 controller
6. // 2.调用 StartControllers 启动所有 controller
if err := StartControllers(controllerContext,
saTokenControllerInitFunc,
NewControllerInitializers(controllerContext.LoopMode), unsecuredMux); err !=
7. nil {
8. klog.Fatalf("error starting controllers: %v", err)
9. }
10. ......
11. select {}
12. }
13. ......
14. }

NewControllerInitializers 中定义了所有的 controller 以及 start controller 对应


的方法。deployment controller 对应的启动方法是 startDeploymentController 。

k8s.io/kubernetes/cmd/kube-controller-manager/app/controllermanager.go:373

func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc


1. {
2. controllers := map[string]InitFunc{}
3.
4. ......
5. controllers["deployment"] = startDeploymentController
6. ......
7. }

在 startDeploymentController 中对 deploymentController 进行了初始化,并执行

本文档使用 书栈网 · BookStack.CN 构建 - 185 -


deployment controller 源码分析

dc.Run() 方法启动了 controller。

k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:82

func startDeploymentController(ctx ControllerContext) (http.Handler, bool,


1. error) {
2. ......
3.
4. // 初始化 controller
5. dc, err := deployment.NewDeploymentController(
6. ctx.InformerFactory.Apps().V1().Deployments(),
7. ctx.InformerFactory.Apps().V1().ReplicaSets(),
8. ctx.InformerFactory.Core().V1().Pods(),
9. ctx.ClientBuilder.ClientOrDie("deployment-controller"),
10. )
11. ......
12.
13. // 启动 controller
go
dc.Run(int(ctx.ComponentConfig.DeploymentController.ConcurrentDeploymentSyncs),
14. ctx.Stop)
15. return nil, true, nil
16. }

ctx.ComponentConfig.DeploymentController.ConcurrentDeploymentSyncs 指定了
deployment controller 中工作的 goroutine 数量,默认值为 5,即会启动五个 goroutine
从 workqueue 中取出 object 并进行 sync 操作,该参数的默认值定义在
k8s.io/kubernetes/pkg/controller/deployment/config/v1alpha1/defaults.go 中。

dc.Run 方法会执行 ListWatch 操作并根据对应的事件执行 syncLoop。

k8s.io/kubernetes/pkg/controller/deployment/deployment_controller.go:148

1. func (dc *DeploymentController) Run(workers int, stopCh <-chan struct{}) {


2. ......
3.
4. // 1、等待 informer cache 同步完成
if !cache.WaitForNamedCacheSync("deployment", stopCh, dc.dListerSynced,
5. dc.rsListerSynced, dc.podListerSynced) {
6. return
7. }
8.
9. // 2、启动 5 个 goroutine

本文档使用 书栈网 · BookStack.CN 构建 - 186 -


deployment controller 源码分析

10. for i := 0; i < workers; i++ {


11. // 3、在每个 goroutine 中每秒执行一次 dc.worker 方法
12. go wait.Until(dc.worker, time.Second, stopCh)
13. }
14.
15. <-stopCh
16. }

dc.worker 会调用 syncHandler 进行 sync 操作。

1. func (dc *DeploymentController) worker() {


2. for dc.processNextWorkItem() {
3. }
4. }
5.
6. func (dc *DeploymentController) processNextWorkItem() bool {
7. key, quit := dc.queue.Get()
8. if quit {
9. return false
10. }
11. defer dc.queue.Done(key)
12.
13. // 若 workQueue 中有任务则进行处理
14. err := dc.syncHandler(key.(string))
15. dc.handleErr(err, key)
16.
17. return true
18. }

syncHandler 是 controller 的核心逻辑,下面会进行详细说明。至此,对于 deployment


controller 的启动流程已经分析完,再来看一下 deployment controller 启动过程中的整个调
用链,如下所示:

Run() --> run() --> NewControllerInitializers() --> StartControllers() -->


startDeploymentController() --> deployment.NewDeploymentController() -->
1. deployment.Run()
2. --> deployment.syncDeployment()

deployment controller 在初始化时指定了 dc.syncHandler = dc.syncDeployment ,所以该


函数名为 syncDeployment ,本文开头介绍 deployment 中的基本操作都是在
syncDeployment 中完成的。

本文档使用 书栈网 · BookStack.CN 构建 - 187 -


deployment controller 源码分析

syncDeployment 的主要流程如下所示:

1、调用 getReplicaSetsForDeployment 获取集群中与 Deployment 相关的


ReplicaSet,若发现匹配但没有关联 deployment 的 rs 则通过设置 ownerReferences
字段与 deployment 关联,已关联但不匹配的则删除对应的 ownerReferences;
2、调用 getPodMapForDeployment 获取当前 Deployment 对象关联的 pod,并根据
rs.UID 对上述 pod 进行分类;
3、通过判断 deployment 的 DeletionTimestamp 字段确认是否为删除操作;
4、执行 checkPausedConditions 检查 deployment 是否为 pause 状态并添加合适
的 condition ;
5、调用 getRollbackTo 函数检查 Deployment 是否
有 Annotations:"deprecated.deployment.rollback.to" 字段,如果有,调用
dc.rollback 方法执行 rollback 操作;
6、调用 dc.isScalingEvent 方法检查是否处于 scaling 状态中;
7、最后检查是否为更新操作,并根据更新策略 Recreate 或 RollingUpdate 来执行对应
的操作;

k8s.io/kubernetes/pkg/controller/deployment/deployment_controller.go:562

1. func (dc *DeploymentController) syncDeployment(key string) error {


2. ......
3. namespace, name, err := cache.SplitMetaNamespaceKey(key)
4. if err != nil {
5. return err
6. }
7.
8. // 1、从 informer cache 中获取 deployment 对象
9. deployment, err := dc.dLister.Deployments(namespace).Get(name)
10. if errors.IsNotFound(err) {
11. ......
12. }
13.
14. ......
15. d := deployment.DeepCopy()
16.
17. // 2、判断 selecor 是否为空
18. everything := metav1.LabelSelector{}
19. if reflect.DeepEqual(d.Spec.Selector, &everything) {
20. ......
21. return nil
22. }
23.

本文档使用 书栈网 · BookStack.CN 构建 - 188 -


deployment controller 源码分析

24.
25. // 3、获取 deployment 对应的所有 rs,通过 LabelSelector 进行匹配
26. rsList, err := dc.getReplicaSetsForDeployment(d)
27. if err != nil {
28. return err
29. }
30.
31. // 4、获取当前 Deployment 对象关联的 pod,并根据 rs.UID 对 pod 进行分类
32. podMap, err := dc.getPodMapForDeployment(d, rsList)
33. if err != nil {
34. return err
35. }
36.
37. // 5、如果该 deployment 处于删除状态,则更新其 status
38. if d.DeletionTimestamp != nil {
39. return dc.syncStatusOnly(d, rsList)
40. }
41.
42. // 6、检查是否处于 pause 状态
43. if err = dc.checkPausedConditions(d); err != nil {
44. return err
45. }
46.
47. if d.Spec.Paused {
48. return dc.sync(d, rsList)
49. }
50.
51. // 7、检查是否为回滚操作
52. if getRollbackTo(d) != nil {
53. return dc.rollback(d, rsList)
54. }
55.
56. // 8、检查 deployment 是否处于 scale 状态
57. scalingEvent, err := dc.isScalingEvent(d, rsList)
58. if err != nil {
59. return err
60. }
61. if scalingEvent {
62. return dc.sync(d, rsList)
63. }
64.
65. // 9、更新操作

本文档使用 书栈网 · BookStack.CN 构建 - 189 -


deployment controller 源码分析

66. switch d.Spec.Strategy.Type {


67. case apps.RecreateDeploymentStrategyType:
68. return dc.rolloutRecreate(d, rsList, podMap)
69. case apps.RollingUpdateDeploymentStrategyType:
70. return dc.rolloutRolling(d, rsList)
71. }
return fmt.Errorf("unexpected deployment strategy type: %s",
72. d.Spec.Strategy.Type)
73. }

可以看出对于 deployment 的删除、暂停恢复、扩缩容以及更新操作都是在 syncDeployment 方


法中进行处理的,最终是通过调用 syncStatusOnly、sync、rollback、rolloutRecreate、
rolloutRolling 这几个方法来处理的,其中 syncStatusOnly 和 sync 都是更新
Deployment 的 Status,rollback 是用来回滚的,rolloutRecreate 和 rolloutRolling
是根据不同的更新策略来更新 Deployment 的,下面就来看看这些操作的具体实现。

从 syncDeployment 中也可知以上几个操作的优先级为:

1. delete > pause > rollback > scale > rollout

举个例子,当在 rollout 操作时可以执行 pause 操作,在 pause 状态时也可直接执行删除操


作。

删除

syncDeployment 中首先处理的是删除操作,删除操作是由客户端发起的,首先会在对象的
metadata 中设置 DeletionTimestamp 字段。

1. func (dc *DeploymentController) syncDeployment(key string) error {


2. ......
3. if d.DeletionTimestamp != nil {
4. return dc.syncStatusOnly(d, rsList)
5. }
6. ......
7. }

当 controller 检查到该对象有了 DeletionTimestamp 字段时会调用 dc.syncStatusOnly


执行对应的删除逻辑,该方法首先获取 newRS 以及所有的 oldRSs,然后会调用
syncDeploymentStatus 方法。

k8s.io/kubernetes/pkg/controller/deployment/sync.go:48

本文档使用 书栈网 · BookStack.CN 构建 - 190 -


deployment controller 源码分析

func (dc *DeploymentController) syncStatusOnly(d *apps.Deployment, rsList


1. []*apps.ReplicaSet) error {
2. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
3. if err != nil {
4. return err
5. }
6.
7. allRSs := append(oldRSs, newRS)
8. return dc.syncDeploymentStatus(allRSs, newRS, d)
9. }

syncDeploymentStatus 首先通过 newRS 和 allRSs 计算 deployment 当前的 status,然


后和 deployment 中的 status 进行比较,若二者有差异则更新 deployment 使用最新的
status, syncDeploymentStatus 在后面的多种操作中都会被用到。

k8s.io/kubernetes/pkg/controller/deployment/sync.go:469

func (dc *DeploymentController) syncDeploymentStatus(allRSs []*apps.ReplicaSet,


1. newRS *apps.ReplicaSet, d *apps.Deployment) error {
2. newStatus := calculateStatus(allRSs, newRS, d)
3.
4. if reflect.DeepEqual(d.Status, newStatus) {
5. return nil
6. }
7.
8. newDeployment := d
9. newDeployment.Status = newStatus
_, err :=
10. dc.client.AppsV1().Deployments(newDeployment.Namespace).UpdateStatus(newDeployment
11. return err
12. }

calculateStatus 如下所示,主要是通过 allRSs 以及 deployment 的状态计算出最新的


status。

k8s.io/kubernetes/pkg/controller/deployment/sync.go:483

func calculateStatus(allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet,


1. deployment *apps.Deployment) apps.DeploymentStatus {
availableReplicas :=
2. deploymentutil.GetAvailableReplicaCountForReplicaSets(allRSs)
3. totalReplicas := deploymentutil.GetReplicaCountForReplicaSets(allRSs)

本文档使用 书栈网 · BookStack.CN 构建 - 191 -


deployment controller 源码分析

4. unavailableReplicas := totalReplicas - availableReplicas


5.
6. if unavailableReplicas < 0 {
7. unavailableReplicas = 0
8. }
9.
10. status := apps.DeploymentStatus{
11. ObservedGeneration: deployment.Generation,
Replicas:
12. deploymentutil.GetActualReplicaCountForReplicaSets(allRSs),
UpdatedReplicas:
13. deploymentutil.GetActualReplicaCountForReplicaSets([]*apps.ReplicaSet{newRS}),
ReadyReplicas:
14. deploymentutil.GetReadyReplicaCountForReplicaSets(allRSs),
15. AvailableReplicas: availableReplicas,
16. UnavailableReplicas: unavailableReplicas,
17. CollisionCount: deployment.Status.CollisionCount,
18. }
19.
20. conditions := deployment.Status.Conditions
21. for i := range conditions {
22. status.Conditions = append(status.Conditions, conditions[i])
23. }
24.
25. conditions := deployment.Status.Conditions
26. for i := range conditions {
27. status.Conditions = append(status.Conditions, conditions[i])
28. }
29.
30. ......
31. return status
32. }

以上就是 controller 中处理删除逻辑的主要流程,通过上述代码可知,当删除 deployment 对象


时,仅仅是判断该对象中是否存在 metadata.DeletionTimestamp 字段,然后进行一次状态同
步,并没有看到删除 deployment、rs、pod 对象的操作,其实删除对象并不是在此处进行而是在
kube-controller-manager 的垃圾回收器(garbagecollector controller)中完成的,对于
garbagecollector controller 会在后面的文章中进行说明,此外在删除对象时还需要指定一个
删除选项(orphan、background 或者 foreground)来说明该对象如何删除。

暂停和恢复

暂停以及恢复两个操作都是通过更新 deployment spec.paused 字段实现的,下面直接看它的具

本文档使用 书栈网 · BookStack.CN 构建 - 192 -


deployment controller 源码分析

体实现。

1. func (dc *DeploymentController) syncDeployment(key string) error {


2. ......
3. // pause 操作
4. if d.Spec.Paused {
5. return dc.sync(d, rsList)
6. }
7.
8. if getRollbackTo(d) != nil {
9. return dc.rollback(d, rsList)
10. }
11.
12. // scale 操作
13. scalingEvent, err := dc.isScalingEvent(d, rsList)
14. if err != nil {
15. return err
16. }
17. if scalingEvent {
18. return dc.sync(d, rsList)
19. }
20. ......
21. }

当触发暂停操作时,会调用 sync 方法进行操作, sync 方法的主要逻辑如下所示:

1、获取 newRS 和 oldRSs;


2、根据 newRS 和 oldRSs 判断是否需要 scale 操作;
3、若处于暂停状态且没有执行回滚操作,则根据 deployment 的
.spec.revisionHistoryLimit 中的值清理多余的 rs;
4、最后执行 syncDeploymentStatus 更新 status;

func (dc *DeploymentController) sync(d *apps.Deployment, rsList


1. []*apps.ReplicaSet) error {
2. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
3. if err != nil {
4. return err
5. }
6. if err := dc.scale(d, newRS, oldRSs); err != nil {
7. return err
8. }
9.

本文档使用 书栈网 · BookStack.CN 构建 - 193 -


deployment controller 源码分析

10. if d.Spec.Paused && getRollbackTo(d) == nil {


11. if err := dc.cleanupDeployment(oldRSs, d); err != nil {
12. return err
13. }
14. }
15.
16. allRSs := append(oldRSs, newRS)
17. return dc.syncDeploymentStatus(allRSs, newRS, d)
18. }

上文已经提到过 deployment controller 在一个 syncLoop 中各种操作是有优先级,而 pause


> rollback > scale > rollout,通过文章开头的命令行参数也可以看出,暂停和恢复操作只有
在 rollout 时才会生效,再结合源码分析,虽然暂停操作下不会执行到 scale 相关的操作,但是
pause 与 scale 都是调用 sync 方法完成的,且在 sync 方法中会首先检查 scale 操作
是否完成,也就是说在 pause 操作后并不是立即暂停所有操作,例如,当执行滚动更新操作后立即执
行暂停操作,此时滚动更新的第一个周期并不会立刻停止而是会等到滚动更新的第一个周期完成后才会
处于暂停状态,在下文的滚动更新一节会有例子进行详细的分析,至于 scale 操作在下文也会进行详
细分析。

syncDeploymentStatus 方法以及相关的代码在上文的删除操作中已经解释过了,此处不再进行分
析。

回滚

kubernetes 中的每一个 Deployment 资源都包含有 revision 这个概念,并且其


.spec.revisionHistoryLimit 字段指定了需要保留的历史版本数,默认为10,每个版本都会对应
一个 rs,若发现集群中有大量 0/0 rs 时请不要删除它,这些 rs 对应的都是 deployment 的历
史版本,否则会导致无法回滚。当一个 deployment 的历史 rs 数超过指定数时,deployment
controller 会自动清理。

当在客户端触发回滚操作时,controller 会调用 getRollbackTo 进行判断并调用


rollback 执行对应的回滚操作。

1. func (dc *DeploymentController) syncDeployment(key string) error {


2. ......
3. if getRollbackTo(d) != nil {
4. return dc.rollback(d, rsList)
5. }
6. ......
7. }

getRollbackTo 通过判断 deployment 是否存在 rollback 对应的注解然后获取其值作为目标

本文档使用 书栈网 · BookStack.CN 构建 - 194 -


deployment controller 源码分析

版本。

1. func getRollbackTo(d *apps.Deployment) *extensions.RollbackConfig {


2. // annotations 为 "deprecated.deployment.rollback.to"
3. revision := d.Annotations[apps.DeprecatedRollbackTo]
4. if revision == "" {
5. return nil
6. }
7. revision64, err := strconv.ParseInt(revision, 10, 64)
8. if err != nil {
9. return nil
10. }
11. return &extensions.RollbackConfig{
12. Revision: revision64,
13. }
14. }

rollback 方法的主要逻辑如下:

1、获取 newRS 和 oldRSs;


2、调用 getRollbackTo 获取 rollback 的 revision;
3、判断 revision 以及对应的 rs 是否存在,若 revision 为 0,则表示回滚到上一个版
本;
4、若存在对应的 rs,则调用 rollbackToTemplate 方法将 rs.Spec.Template 赋值给
d.Spec.Template ,否则放弃回滚操作;

k8s.io/kubernetes/pkg/controller/deployment/rollback.go:32

func (dc *DeploymentController) rollback(d *apps.Deployment, rsList


1. []*apps.ReplicaSet) error {
2. // 1、获取 newRS 和 oldRSs
newRS, allOldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList,
3. true)
4. if err != nil {
5. return err
6. }
7.
8. allRSs := append(allOldRSs, newRS)
9. // 2、调用 getRollbackTo 获取 rollback 的 revision
10. rollbackTo := getRollbackTo(d)
11.
12. // 3、判断 revision 以及对应的 rs 是否存在,若 revision 为 0,则表示回滚到最新的版本
13. if rollbackTo.Revision == 0 {

本文档使用 书栈网 · BookStack.CN 构建 - 195 -


deployment controller 源码分析

if rollbackTo.Revision = deploymentutil.LastRevision(allRSs);
14. rollbackTo.Revision == 0 {
15. // 4、清除回滚标志放弃回滚操作
16. return dc.updateDeploymentAndClearRollbackTo(d)
17. }
18. }
19. for _, rs := range allRSs {
20. v, err := deploymentutil.Revision(rs)
21. if err != nil {
22. ......
23. }
24.
25. if v == rollbackTo.Revision {
26. // 5、调用 rollbackToTemplate 进行回滚操作
27. performedRollback, err := dc.rollbackToTemplate(d, rs)
28. if performedRollback && err == nil {
29. ......
30. }
31. return err
32. }
33. }
34.
35. return dc.updateDeploymentAndClearRollbackTo(d)
36. }

rollbackToTemplate 会判断 deployment.Spec.Template 和 rs.Spec.Template 是否


相等,若相等则无需回滚,否则使用 rs.Spec.Template 替换 deployment.Spec.Template ,
然后更新 deployment 的 spec 并清除回滚标志。

k8s.io/kubernetes/pkg/controller/deployment/rollback.go:75

func (dc *DeploymentController) rollbackToTemplate(d *apps.Deployment, rs


1. *apps.ReplicaSet) (bool, error) {
2. performedRollback := false
3. // 1、比较 d.Spec.Template 和 rs.Spec.Template 是否相等
4. if !deploymentutil.EqualIgnoreHash(&d.Spec.Template, &rs.Spec.Template) {
5. // 2、替换 d.Spec.Template
6. deploymentutil.SetFromReplicaSetTemplate(d, rs.Spec.Template)
7.
8. // 3、设置 annotation
9. deploymentutil.SetDeploymentAnnotationsTo(d, rs)
10. performedRollback = true
11. } else {

本文档使用 书栈网 · BookStack.CN 构建 - 196 -


deployment controller 源码分析

dc.emitRollbackWarningEvent(d,
12. deploymentutil.RollbackTemplateUnchanged, eventMsg)
13. }
14.
15. // 4、更新 deployment 并清除回滚标志
16. return performedRollback, dc.updateDeploymentAndClearRollbackTo(d)
17. }

回滚操作其实就是通过 revision 找到对应的 rs,然后使用 rs.Spec.Template 替换


deployment.Spec.Template 最后驱动 replicaSet 和 pod 达到期望状态即完成了回滚操作,
在最新版中,这种使用注解方式指定回滚版本的方法即将被废弃。

扩缩容

当执行 scale 操作时,首先会通过 isScalingEvent 方法判断是否为扩缩容操作,然后通过


dc.sync 方法来执行实际的扩缩容动作。

1. func (dc *DeploymentController) syncDeployment(key string) error {


2. ......
3. // scale 操作
4. scalingEvent, err := dc.isScalingEvent(d, rsList)
5. if err != nil {
6. return err
7. }
8. if scalingEvent {
9. return dc.sync(d, rsList)
10. }
11. ......
12. }

isScalingEvent 的主要逻辑如下所示:

1、获取所有的 rs;
2、过滤出 activeRS,rs.Spec.Replicas > 0 的为 activeRS;
3、判断 rs 的 desired 值是否等于 deployment.Spec.Replicas,若不等于则需要为
rs 进行 scale 操作;

k8s.io/kubernetes/pkg/controller/deployment/sync.go:526

1. func (dc *DeploymentController) isScalingEvent(......) (bool, error) {


2. // 1、获取所有 rs
3. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
4. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 197 -


deployment controller 源码分析

5. return false, err


6. }
7. allRSs := append(oldRSs, newRS)
8.
9. // 2、过滤出 activeRS 并进行比较
10. for _, rs := range controller.FilterActiveReplicaSets(allRSs) {
// 3、获取 rs annotation 中 deployment.kubernetes.io/desired-replicas 的
11. 值
12. desired, ok := deploymentutil.GetDesiredReplicasAnnotation(rs)
13. if !ok {
14. continue
15. }
16. // 4、判断是否需要 scale 操作
17. if desired != *(d.Spec.Replicas) {
18. return true, nil
19. }
20. }
21. return false, nil
22. }

在通过 isScalingEvent 判断为 scale 操作时会调用 sync 方法执行,主要逻辑如下:

1、获取 newRS 和 oldRSs;


2、调用 scale 方法进行扩缩容操作;
3、同步 deployment 的状态;

func (dc *DeploymentController) sync(d *apps.Deployment, rsList


1. []*apps.ReplicaSet) error {
2. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
3. if err != nil {
4. return err
5. }
6. if err := dc.scale(d, newRS, oldRSs); err != nil {
7. return err
8. }
9.
10. ......
11. allRSs := append(oldRSs, newRS)
12. return dc.syncDeploymentStatus(allRSs, newRS, d)
13. }

sync 方法中会调用 scale 方法执行扩容操作,其主要逻辑为:

本文档使用 书栈网 · BookStack.CN 构建 - 198 -


deployment controller 源码分析

1、通过 FindActiveOrLatest 获取 activeRS 或者最新的 rs,此时若只有一个 rs 说明


本次操作仅为 scale 操作,则调用 scaleReplicaSetAndRecordEvent 对 rs 进行 scale
操作,否则此时存在多个 activeRS;
2、判断 newRS 是否已达到期望副本数,若达到则将所有的 oldRS 缩容到 0;
3、若 newRS 还未达到期望副本数,且存在多个 activeRS,说明此时的操作有可能是升级与
扩缩容操作同时进行,若 deployment 的更新操作为 RollingUpdate 那么 scale 操作也
需要按比例进行:
通过 FilterActiveReplicaSets 获取所有活跃的 ReplicaSet 对象;
调用 GetReplicaCountForReplicaSets 计算当前 Deployment 对应 ReplicaSet
持有的全部 Pod 副本个数;
计算 Deployment 允许创建的最大 Pod 数量;
判断是扩容还是缩容并对 allRSs 按时间戳进行正向或者反向排序;
计算每个 rs 需要增加或者删除的副本数;
更新 rs 对象;
4、若为 recreat 则需要等待更新完成后再进行 scale 操作;

k8s.io/kubernetes/pkg/controller/deployment/sync.go:294

1. func (dc *DeploymentController) scale(......) error {


// 1、在滚动更新过程中 第一个 rs 的 replicas 数量= maxSuger + dep.spec.Replicas
2. ,
3. // 更新完成后 pod 数量会多出 maxSurge 个,此处若检测到则应缩减回去
if activeOrLatest := deploymentutil.FindActiveOrLatest(newRS, oldRSs);
4. activeOrLatest != nil {
5. if *(activeOrLatest.Spec.Replicas) == *(deployment.Spec.Replicas) {
6. return nil
7. }
8. // 2、只更新 rs annotation 以及为 deployment 设置 events
_, _, err := dc.scaleReplicaSetAndRecordEvent(activeOrLatest, *
9. (deployment.Spec.Replicas), deployment)
10. return err
11. }
12.
// 3、当调用 IsSaturated 方法发现当前的 Deployment 对应的副本数量已经达到期望状态时
13. 就
14. // 将所有历史版本 rs 持有的副本缩容为 0
15. if deploymentutil.IsSaturated(deployment, newRS) {
16. for _, old := range controller.FilterActiveReplicaSets(oldRSs) {
if _, _, err := dc.scaleReplicaSetAndRecordEvent(old, 0,
17. deployment); err != nil {
18. return err
19. }

本文档使用 书栈网 · BookStack.CN 构建 - 199 -


deployment controller 源码分析

20. }
21. return nil
22. }
23.
24. // 4、此时说明 当前的 rs 副本并没有达到期望状态并且存在多个活跃的 rs 对象,
// 若 deployment 的更新策略为滚动更新,需要按照比例分别对各个活跃的 rs 进行扩容或者缩
25. 容
26. if deploymentutil.IsRollingUpdate(deployment) {
27. allRSs := controller.FilterActiveReplicaSets(append(oldRSs, newRS))
28. allRSsReplicas := deploymentutil.GetReplicaCountForReplicaSets(allRSs)
29.
30. allowedSize := int32(0)
31.
32. // 5、计算最大可以创建出的 pod 数
33. if *(deployment.Spec.Replicas) > 0 {
allowedSize = *(deployment.Spec.Replicas) +
34. deploymentutil.MaxSurge(*deployment)
35. }
36.
37. // 6、计算需要扩容的 pod 数
38. deploymentReplicasToAdd := allowedSize - allRSsReplicas
39.
// 7、如果 deploymentReplicasToAdd > 0,ReplicaSet 将按照从新到旧的顺序依次
40. 进行扩容;
// 如果 deploymentReplicasToAdd < 0,ReplicaSet 将按照从旧到新的顺序依次进行
41. 缩容;
// 若 > 0,则需要先扩容 newRS,但当在先扩容然后立刻缩容时,若 <0,则需要先删除
42. oldRS 的 pod
43. var scalingOperation string
44. switch {
45. case deploymentReplicasToAdd > 0:
46. sort.Sort(controller.ReplicaSetsBySizeNewer(allRSs))
47. scalingOperation = "up"
48.
49. case deploymentReplicasToAdd < 0:
50. sort.Sort(controller.ReplicaSetsBySizeOlder(allRSs))
51. scalingOperation = "down"
52. }
53. deploymentReplicasAdded := int32(0)
54. nameToSize := make(map[string]int32)
55.
56. // 8、遍历所有的 rs,计算每个 rs 需要扩容或者缩容到的期望副本数
57. for i := range allRSs {

本文档使用 书栈网 · BookStack.CN 构建 - 200 -


deployment controller 源码分析

58. rs := allRSs[i]
59.
60. if deploymentReplicasToAdd != 0 {
61. // 9、调用 GetProportion 估算出 rs 需要扩容或者缩容的副本数
proportion := deploymentutil.GetProportion(rs, *deployment,
62. deploymentReplicasToAdd, deploymentReplicasAdded)
63.
64. nameToSize[rs.Name] = *(rs.Spec.Replicas) + proportion
65. deploymentReplicasAdded += proportion
66. } else {
67. nameToSize[rs.Name] = *(rs.Spec.Replicas)
68. }
69. }
70.
71. // 10、遍历所有的 rs,第一个最活跃的 rs.Spec.Replicas 加上上面循环中计算出
72. // 其他 rs 要加或者减的副本数,然后更新所有 rs 的 rs.Spec.Replicas
73. for i := range allRSs {
74. rs := allRSs[i]
75.
76. // 11、要扩容或者要删除的 rs 已经达到了期望状态
77. if i == 0 && deploymentReplicasToAdd != 0 {
78. leftover := deploymentReplicasToAdd - deploymentReplicasAdded
79. nameToSize[rs.Name] = nameToSize[rs.Name] + leftover
80. if nameToSize[rs.Name] < 0 {
81. nameToSize[rs.Name] = 0
82. }
83. }
84.
85. // 12、对 rs 进行 scale 操作
if _, _, err := dc.scaleReplicaSet(rs, nameToSize[rs.Name],
86. deployment, scalingOperation); err != nil {
87. return err
88. }
89. }
90. }
91. return nil
92. }

上述方法中有一个重要的操作就是在第 9 步调用 GetProportion 方法估算出 rs 需要扩容或者


缩容的副本数,该方法中计算副本数的逻辑如下所示:

k8s.io/kubernetes/pkg/controller/deployment/util/deployment_util.go:466

本文档使用 书栈网 · BookStack.CN 构建 - 201 -


deployment controller 源码分析

func GetProportion(rs *apps.ReplicaSet, d apps.Deployment,


1. deploymentReplicasToAdd, deploymentReplicasAdded int32) int32 {
if rs == nil || *(rs.Spec.Replicas) == 0 || deploymentReplicasToAdd == 0 ||
2. deploymentReplicasToAdd == deploymentReplicasAdded {
3. return int32(0)
4. }
5.
6. // 调用 getReplicaSetFraction 方法
7. rsFraction := getReplicaSetFraction(*rs, d)
8. allowed := deploymentReplicasToAdd - deploymentReplicasAdded
9.
10. if deploymentReplicasToAdd > 0 {
11. return integer.Int32Min(rsFraction, allowed)
12. }
13. return integer.Int32Max(rsFraction, allowed)
14. }
15.
16. func getReplicaSetFraction(rs apps.ReplicaSet, d apps.Deployment) int32 {
17. if *(d.Spec.Replicas) == int32(0) {
18. return -*(rs.Spec.Replicas)
19. }
20.
21. deploymentReplicas := *(d.Spec.Replicas) + MaxSurge(d)
22. annotatedReplicas, ok := getMaxReplicasAnnotation(&rs)
23. if !ok {
24. annotatedReplicas = d.Status.Replicas
25. }
26.
27. // 计算 newRSSize 的公式
newRSsize := (float64(*(rs.Spec.Replicas) * deploymentReplicas)) /
28. float64(annotatedReplicas)
29.
30. // 返回最终计算出的结果
31. return integer.RoundToInt32(newRSsize) - *(rs.Spec.Replicas)
32. }

滚动更新

deployment 的更新方式有两种,其中滚动更新是最常用的,下面就看看其具体的实现。

1. func (dc *DeploymentController) syncDeployment(key string) error {


2. ......

本文档使用 书栈网 · BookStack.CN 构建 - 202 -


deployment controller 源码分析

3. switch d.Spec.Strategy.Type {
4. case apps.RecreateDeploymentStrategyType:
5. return dc.rolloutRecreate(d, rsList, podMap)
6. case apps.RollingUpdateDeploymentStrategyType:
7. // 调用 rolloutRolling 执行滚动更新
8. return dc.rolloutRolling(d, rsList)
9. }
10. ......
11. }

通过判断 d.Spec.Strategy.Type ,当更新操作为 rolloutRolling 时,会调用


rolloutRolling 方法进行操作,具体的逻辑如下所示:

1、调用 getAllReplicaSetsAndSyncRevision 获取所有的 rs,若没有 newRS 则创建;


2、调用 reconcileNewReplicaSet 判断是否需要对 newRS 进行 scaleUp 操作;
3、如果需要 scaleUp,更新 Deployment 的 status,添加相关的 condition,直接返
回;
4、调用 reconcileOldReplicaSets 判断是否需要为 oldRS 进行 scaleDown 操作;
5、如果两者都不是则滚动升级很可能已经完成,此时需要检查 deployment status 是否已
经达到期望状态,并且根据 deployment.Spec.RevisionHistoryLimit 的值清理 oldRSs;

1. func (dc *DeploymentController) rolloutRolling(......) error {


2. // 1、获取所有的 rs,若没有 newRS 则创建
3. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, true)
4. if err != nil {
5. return err
6. }
7. allRSs := append(oldRSs, newRS)
8.
9. // 2、执行 scale up 操作
10. scaledUp, err := dc.reconcileNewReplicaSet(allRSs, newRS, d)
11. if err != nil {
12. return err
13. }
14. if scaledUp {
15. return dc.syncRolloutStatus(allRSs, newRS, d)
16. }
17.
18. // 3、执行 scale down 操作
scaledDown, err := dc.reconcileOldReplicaSets(allRSs,
19. controller.FilterActiveReplicaSets(oldRSs), newRS, d)
20. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 203 -


deployment controller 源码分析

21. return err


22. }
23. if scaledDown {
24. return dc.syncRolloutStatus(allRSs, newRS, d)
25. }
26.
27. // 4、清理过期的 rs
28. if deploymentutil.DeploymentComplete(d, &d.Status) {
29. if err := dc.cleanupDeployment(oldRSs, d); err != nil {
30. return err
31. }
32. }
33.
34. // 5、同步 deployment status
35. return dc.syncRolloutStatus(allRSs, newRS, d)
36. }

reconcileNewReplicaSet 主要逻辑如下:

1、判断 newRS.Spec.Replicas 和 deployment.Spec.Replicas 是否相等,如果相等则


直接返回,说明已经达到期望状态;
2、若 newRS.Spec.Replicas > deployment.Spec.Replicas ,则说明 newRS 副本数已
经超过期望值,调用 dc.scaleReplicaSetAndRecordEvent 进行 scale down;
3、此时 newRS.Spec.Replicas < deployment.Spec.Replicas ,调用
NewRSNewReplicas 为 newRS 计算所需要的副本数,计算原则遵守 maxSurge 和
maxUnavailable 的约束;
4、调用 scaleReplicaSetAndRecordEvent 更新 newRS 对象,设置
rs.Spec.Replicas、rs.Annotations[DesiredReplicasAnnotation] 以及
rs.Annotations[MaxReplicasAnnotation] ;

k8s.io/kubernetes/pkg/controller/deployment/rolling.go:69

1. func (dc *DeploymentController) reconcileNewReplicaSet(......) (bool, error) {


2. // 1、判断副本数是否已达到了期望值
3. if *(newRS.Spec.Replicas) == *(deployment.Spec.Replicas) {
4. return false, nil
5. }
6.
7. // 2、判断是否需要 scale down 操作
8. if *(newRS.Spec.Replicas) > *(deployment.Spec.Replicas) {
scaled, _, err := dc.scaleReplicaSetAndRecordEvent(newRS, *
9. (deployment.Spec.Replicas), deployment)

本文档使用 书栈网 · BookStack.CN 构建 - 204 -


deployment controller 源码分析

10. return scaled, err


11. }
12.
13. // 3、计算 newRS 所需要的副本数
newReplicasCount, err := deploymentutil.NewRSNewReplicas(deployment,
14. allRSs, newRS)
15. if err != nil {
16. return false, err
17. }
18.
19. // 4、如果需要 scale ,则更新 rs 的 annotation 以及 rs.Spec.Replicas
scaled, _, err := dc.scaleReplicaSetAndRecordEvent(newRS, newReplicasCount,
20. deployment)
21. return scaled, err
22. }

NewRSNewReplicas 是为 newRS 计算所需要的副本数,该方法主要逻辑为:

1、判断更新策略;
2、计算 maxSurge 值;
3、通过 allRSs 计算 currentPodCount 的值;
4、最后计算 scaleUpCount 值;

k8s.io/kubernetes/pkg/controller/deployment/util/deployment_util.go:814

1. func NewRSNewReplicas(......) (int32, error) {


2. switch deployment.Spec.Strategy.Type {
3. case apps.RollingUpdateDeploymentStrategyType:
4. // 1、计算 maxSurge 值
maxSurge, err :=
intstrutil.GetValueFromIntOrPercent(deployment.Spec.Strategy.RollingUpdate.MaxSurge
5. int(*(deployment.Spec.Replicas)), true)
6. if err != nil {
7. return 0, err
8. }
9.
10. // 2、累加 rs.Spec.Replicas 获取 currentPodCount
11. currentPodCount := GetReplicaCountForReplicaSets(allRSs)
12. maxTotalPods := *(deployment.Spec.Replicas) + int32(maxSurge)
13. if currentPodCount >= maxTotalPods {
14. return *(newRS.Spec.Replicas), nil
15. }
16.

本文档使用 书栈网 · BookStack.CN 构建 - 205 -


deployment controller 源码分析

17. // 3、计算 scaleUpCount


18. scaleUpCount := maxTotalPods - currentPodCount
scaleUpCount = int32(integer.IntMin(int(scaleUpCount), int(*
19. (deployment.Spec.Replicas)-*(newRS.Spec.Replicas))))
20.
21. return *(newRS.Spec.Replicas) + scaleUpCount, nil
22. case apps.RecreateDeploymentStrategyType:
23. return *(deployment.Spec.Replicas), nil
24. default:
return 0, fmt.Errorf("deployment type %v isn't supported",
25. deployment.Spec.Strategy.Type)
26. }
27. }

reconcileOldReplicaSets 的主要逻辑如下:

1、通过 oldRSs 和 allRSs 获取 oldPodsCount 和 allPodsCount;


2、计算 deployment 的 maxUnavailable、minAvailable、
newRSUnavailablePodCount、maxScaledDown 值,当 deployment 的 maxSurge 和
maxUnavailable 值为百分数时,计算 maxSurge 向上取整而 maxUnavailable 则向下取
整;
3、清理异常的 rs;
4、计算 oldRS 的 scaleDownCount;

func (dc *DeploymentController) reconcileOldReplicaSets(......) (bool, error)


1. {
2. // 1、计算 oldPodsCount
3. oldPodsCount := deploymentutil.GetReplicaCountForReplicaSets(oldRSs)
4. if oldPodsCount == 0 {
5. return false, nil
6. }
7.
8. // 2、计算 allPodsCount
9. allPodsCount := deploymentutil.GetReplicaCountForReplicaSets(allRSs)
10.
11. // 3、计算 maxScaledDown
12. maxUnavailable := deploymentutil.MaxUnavailable(*deployment)
13. minAvailable := *(deployment.Spec.Replicas) - maxUnavailable
newRSUnavailablePodCount := *(newRS.Spec.Replicas) -
14. newRS.Status.AvailableReplicas
15. maxScaledDown := allPodsCount - minAvailable - newRSUnavailablePodCount
16. if maxScaledDown <= 0 {

本文档使用 书栈网 · BookStack.CN 构建 - 206 -


deployment controller 源码分析

17. return false, nil


18. }
19.
20. // 4、清理异常的 rs
oldRSs, cleanupCount, err := dc.cleanupUnhealthyReplicas(oldRSs,
21. deployment, maxScaledDown)
22. if err != nil {
23. return false, nil
24. }
25.
26. allRSs = append(oldRSs, newRS)
27.
28. // 5、缩容 old rs
scaledDownCount, err := dc.scaleDownOldReplicaSetsForRollingUpdate(allRSs,
29. oldRSs, deployment)
30. if err != nil {
31. return false, nil
32. }
33.
34. totalScaledDown := cleanupCount + scaledDownCount
35. return totalScaledDown > 0, nil
36. }

通过上面的代码可以看出,滚动更新过程中主要是通过调用 reconcileNewReplicaSet 对 newRS


不断扩容,调用 reconcileOldReplicaSets 对 oldRS 不断缩容,最终达到期望状态,并且在整
个升级过程中,都严格遵守 maxSurge 和 maxUnavailable 的约束。

不论是在 scale up 或者 scale down 中都是调用 scaleReplicaSetAndRecordEvent 执行,


而 scaleReplicaSetAndRecordEvent 又会调用 scaleReplicaSet 来执行,两个操作都是更
新 rs 的 annotations 以及 rs.Spec.Replicas。

1. scale down
2.
3. or --> dc.scaleReplicaSetAndRecordEvent() --> dc.scaleReplicaSet()
4.
5. scale up

滚动更新示例

上面的代码看起来非常的枯燥,只看源码其实并不能完全理解整个滚动升级的流程,此处举个例子说明
一下:

创建一个 nginx-deployment 有10 个副本,等 10 个 pod 都启动完成后如下所示:

本文档使用 书栈网 · BookStack.CN 构建 - 207 -


deployment controller 源码分析

1. $ kubectl create -f nginx-dep.yaml


2.
3. $ kubectl get rs
4. NAME DESIRED CURRENT READY AGE
5. nginx-deployment-68b649bd8b 10 10 10 72m

然后更新 nginx-deployment 的镜像,默认使用滚动更新的方式:

1. $ kubectl set image deploy/nginx-deployment nginx-deployment=nginx:1.9.3

此时通过源码可知会计算该 deployment 的 maxSurge、maxUnavailable 和 maxAvailable


的值,分别为 3、2 和 13,计算方法如下所示:

1. // 向上取整为 3
2. maxSurge = replicas * deployment.spec.strategy.rollingUpdate.maxSurge(25%)= 2.5
3.
4. // 向下取整为 2
maxUnavailable = replicas *
5. deployment.spec.strategy.rollingUpdate.maxUnavailable(25%)= 2.5
6.
7. maxAvailable = replicas(10) + MaxSurge(3) = 13

如上面代码所说,更新时首先创建 newRS,然后为其设定 replicas,计算 newRS replicas 值


的方法在 NewRSNewReplicas 中,此时计算出 replicas 结果为 3,然后更新 deployment 的
annotation,创建 events,本次 syncLoop 完成。等到下一个 syncLoop 时,所有 rs 的
replicas 已经达到最大值 10 + 3 = 13,此时需要 scale down oldRSs 了,scale down
的数量是通过以下公式得到的:

1. // 13 = 10 + 3
2. allPodsCount := deploymentutil.GetReplicaCountForReplicaSets(allRSs)
3.
4. // 8 = 10 - 2
5. minAvailable := *(deployment.Spec.Replicas) - maxUnavailable
6.
7. // ???
newRSUnavailablePodCount := *(newRS.Spec.Replicas) -
8. newRS.Status.AvailableReplicas
9.
10. // 13 - 8 - ???
11. maxScaledDown := allPodsCount - minAvailable - newRSUnavailablePodCount

本文档使用 书栈网 · BookStack.CN 构建 - 208 -


deployment controller 源码分析

allPodsCount 是 allRSs 的 replicas 之和此时为 13,minAvailable 为 8 ,


newRSUnavailablePodCount 此时不确定,但是值在 [0,3] 中,此时假设 newRS 的三个 pod
还处于 containerCreating 状态,则newRSUnavailablePodCount 为 3,根据以上公式计算
所知 maxScaledDown 为 2,则 oldRS 需要 scale down 2 个 pod,其 replicas 需要改为
8,此时该 syncLoop 完成。下一个 syncLoop 时在 scaleUp 处计算得知 scaleUpCount =
maxTotalPods - currentPodCount,13-3-8=2, 此时 newRS 需要更新 replicase 增加
2。以此轮询直到 newRS replicas 扩容到 10,oldRSs replicas 缩容至 0。

对于上面的示例,可以使用 kubectl get rs -w 进行观察,以下为输出:

1. $ kubectl get rs -w
2. NAME DESIRED CURRENT READY AGE
3. nginx-deployment-68b649bd8b 10 0 0 0s
4. nginx-deployment-68b649bd8b 10 10 0 0s
5. nginx-deployment-68b649bd8b 10 10 10 13s
6.
7. nginx-deployment-689bff574f 3 0 0 0s
8.
9. nginx-deployment-68b649bd8b 8 10 10 14s
10.
11. nginx-deployment-689bff574f 3 0 0 0s
12. nginx-deployment-689bff574f 3 3 3 1s
13.
14. nginx-deployment-689bff574f 5 3 0 0s
15.
16. nginx-deployment-68b649bd8b 8 8 8 14s
17.
18. nginx-deployment-689bff574f 5 3 0 0s
19. nginx-deployment-689bff574f 5 5 0 0s
20.
21. nginx-deployment-689bff574f 5 5 5 6s
22. ......

重新创建

deployment 的另一种更新策略 recreate 就比较简单粗暴了,当更新策略为 Recreate 时,


deployment 先将所有旧的 rs 缩容到 0,并等待所有 pod 都删除后,再创建新的 rs。

1. func (dc *DeploymentController) syncDeployment(key string) error {


2. ......
3. switch d.Spec.Strategy.Type {
4. case apps.RecreateDeploymentStrategyType:

本文档使用 书栈网 · BookStack.CN 构建 - 209 -


deployment controller 源码分析

5. return dc.rolloutRecreate(d, rsList, podMap)


6. case apps.RollingUpdateDeploymentStrategyType:
7. return dc.rolloutRolling(d, rsList)
8. }
9. ......
10. }

rolloutRecreate 方法主要逻辑为:

1、获取 newRS 和 oldRSs;


2、缩容 oldRS replicas 至 0;
3、创建 newRS;
4、扩容 newRS;
5、同步 deployment 状态;

1. func (dc *DeploymentController) rolloutRecreate(......) error {


2. // 1、获取所有 rs
3. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
4. if err != nil {
5. return err
6. }
7. allRSs := append(oldRSs, newRS)
8. activeOldRSs := controller.FilterActiveReplicaSets(oldRSs)
9.
10. // 2、缩容 oldRS
11. scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(activeOldRSs, d)
12. if err != nil {
13. return err
14. }
15. if scaledDown {
16. return dc.syncRolloutStatus(allRSs, newRS, d)
17. }
18.
19. if oldPodsRunning(newRS, oldRSs, podMap) {
20. return dc.syncRolloutStatus(allRSs, newRS, d)
21. }
22.
23. // 3、创建 newRS
24. if newRS == nil {
newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(d, rsList,
25. true)
26. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 210 -


deployment controller 源码分析

27. return err


28. }
29. allRSs = append(oldRSs, newRS)
30. }
31. // 4、扩容 newRS
32. if _, err := dc.scaleUpNewReplicaSetForRecreate(newRS, d); err != nil {
33. return err
34. }
35.
36. // 5、清理过期的 RS
37. if util.DeploymentComplete(d, &d.Status) {
38. if err := dc.cleanupDeployment(oldRSs, d); err != nil {
39. return err
40. }
41. }
42.
43. // 6、同步 deployment 状态
44. return dc.syncRolloutStatus(allRSs, newRS, d)
45. }

判断 deployment 是否存在 newRS 是在 deploymentutil.FindNewReplicaSet 方法中进行判


断的,对比 rs.Spec.Template 和 deployment.Spec.Template 中字段的 hash 值是否相
等以此进行确定,在上面的几个操作中也多次用到了该方法,此处说明一下。

dc.getAllReplicaSetsAndSyncRevision() --> dc.getNewReplicaSet() -->


1. deploymentutil.FindNewReplicaSet() --> EqualIgnoreHash()

EqualIgnoreHash 方法如下所示:

k8s.io/kubernetes/pkg/controller/deployment/util/deployment_util.go:633

1. func EqualIgnoreHash(template1, template2 *v1.PodTemplateSpec) bool {


2. t1Copy := template1.DeepCopy()
3. t2Copy := template2.DeepCopy()
4. // Remove hash labels from template.Labels before comparing
5. delete(t1Copy.Labels, apps.DefaultDeploymentUniqueLabelKey)
6. delete(t2Copy.Labels, apps.DefaultDeploymentUniqueLabelKey)
7. return apiequality.Semantic.DeepEqual(t1Copy, t2Copy)
8. }

以上就是对 deployment recreate 更新策略源码的分析,需要注意的是,该策略会导致服务一段


时间不可用,当 oldRS 缩容为 0,newRS 才开始创建,此时无可用的 pod,所以在生产环境中请慎

本文档使用 书栈网 · BookStack.CN 构建 - 211 -


deployment controller 源码分析

用该更新策略。

总结
本文主要介绍了 deployment 的基本功能以及从源码角度分析其实现,deployment 主要有更新、
回滚、扩缩容、暂停与恢复几个主要的功能。从源码中可以看到 deployment 在升级过程中一直会修
改 rs 的 replicas 以及 annotation 最终达到最终期望的状态,但是整个过程中并没有体现出
pod 的创建与删除,从开头三者的关系图中可知是 rs 控制 pod 的变化,在下篇文章中会继续介绍
rs 是如何控制 pod 的变化。

参考:

https://my.oschina.net/u/3797264/blog/2966086

https://draveness.me/kubernetes-deployment

本文档使用 书栈网 · BookStack.CN 构建 - 212 -


replicaset controller 源码分析

在前面的文章中已经介绍了 deployment controller 的设计与实现,deployment 控制的是


replicaset,而 replicaset 控制 pod 的创建与删除,deployment 通过控制 replicaset
实现了滚动更新、回滚等操作。而 replicaset 会直接控制 pod 的创建与删除,本文会继续从源码
层面分析 replicaset 的设计与实现。

在分析源码前先考虑一下 replicaset 的使用场景,在平时的操作中其实我们并不会直接操作


replicaset,replicaset 也仅有几个简单的操作,创建、删除、更新等,但其地位是非常重要
的,replicaset 的主要功能就是通过 add/del pod 来达到期望的状态。

ReplicaSetController 源码分析

kubernetes 版本: v1.16

启动流程

首先来看 replicaSetController 对象初始化以及启动的代码,在


startReplicaSetController 中有两个比较重要的变量:

BurstReplicas:用来控制在一个 syncLoop 过程中 rs 最多能创建的 pod 数量,设置上


限值是为了避免单个 rs 影响整个系统,默认值为 500;
ConcurrentRSSyncs:指的是需要启动多少个 goroutine 处理 informer 队列中的对象,
默认值为 5;

k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:69

func startReplicaSetController(ctx ControllerContext) (http.Handler, bool,


1. error) {
if !ctx.AvailableResources[schema.GroupVersionResource{Group: "apps",
2. Version: "v1", Resource: "replicasets"}] {
3. return nil, false, nil
4. }
5. go replicaset.NewReplicaSetController(
6. ctx.InformerFactory.Apps().V1().ReplicaSets(),
7. ctx.InformerFactory.Core().V1().Pods(),
8. ctx.ClientBuilder.ClientOrDie("replicaset-controller"),
9. replicaset.BurstReplicas,
).Run(int(ctx.ComponentConfig.ReplicaSetController.ConcurrentRSSyncs),
10. ctx.Stop)
11. return nil, true, nil
12. }

下面是 replicaSetController 初始化的具体步骤,可以看到其会监听 pod 以及 rs 两个对象


的事件。

本文档使用 书栈网 · BookStack.CN 构建 - 213 -


replicaset controller 源码分析

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:109

1. func NewReplicaSetController(......) *ReplicaSetController {


2. ......
3. // 1、此处调用 NewBaseController
return NewBaseController(rsInformer, podInformer, kubeClient,
4. burstReplicas,
5. apps.SchemeGroupVersion.WithKind("ReplicaSet"),
6. "replicaset_controller",
7. "replicaset",
8. controller.RealPodControl{
9. KubeClient: kubeClient,
Recorder: eventBroadcaster.NewRecorder(scheme.Scheme,
10. v1.EventSource{Component: "replicaset-controller"}),
11. },
12. )
13. }
14.
15. func NewBaseController(......) *ReplicaSetController {
16. ......
17. // 2、ReplicaSetController 初始化
18. rsc := &ReplicaSetController{
19. GroupVersionKind: gvk,
20. kubeClient: kubeClient,
21. podControl: podControl,
22. burstReplicas: burstReplicas,
23. // 3、expectations 的初始化
expectations:
24. controller.NewUIDTrackingControllerExpectations(controller.NewControllerExpectations
queue:
workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(),
25. queueName),
26. }
27.
28. // 4、rsInformer 中注册的 EventHandler
29. rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
30. AddFunc: rsc.enqueueReplicaSet,
31. UpdateFunc: rsc.updateRS,
32. DeleteFunc: rsc.enqueueReplicaSet,
33. })
34. ......
35.
36. // 5、podInformer 中注册的 EventHandler

本文档使用 书栈网 · BookStack.CN 构建 - 214 -


replicaset controller 源码分析

37. podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
38. AddFunc: rsc.addPod,
39. UpdateFunc: rsc.updatePod,
40. DeleteFunc: rsc.deletePod,
41. })
42. ......
43.
44. return rsc
45. }

replicaSetController 初始化完成后会调用 Run 方法启动 5 个 goroutine 处理


informer 队列中的事件并进行 sync 操作,kube-controller-manager 中每个 controller
的启动操作都是如下所示流程。

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:177

1. func (rsc *ReplicaSetController) Run(workers int, stopCh <-chan struct{}) {


2. ......
3.
4. // 1、等待 informer 同步缓存
if !cache.WaitForNamedCacheSync(rsc.Kind, stopCh, rsc.podListerSynced,
5. rsc.rsListerSynced) {
6. return
7. }
8.
9. // 2、启动 5 个 goroutine 执行 worker 方法
10. for i := 0; i < workers; i++ {
11. go wait.Until(rsc.worker, time.Second, stopCh)
12. }
13.
14. <-stopCh
15. }
16.
17. // 3、worker 方法中调用 rocessNextWorkItem
18. func (rsc *ReplicaSetController) worker() {
19. for rsc.processNextWorkItem() {
20. }
21. }
22.
23. func (rsc *ReplicaSetController) processNextWorkItem() bool {
24. // 4、从队列中取出对象
25. key, quit := rsc.queue.Get()

本文档使用 书栈网 · BookStack.CN 构建 - 215 -


replicaset controller 源码分析

26. if quit {
27. return false
28. }
29. defer rsc.queue.Done(key)
30.
31. // 5、执行 sync 操作
32. err := rsc.syncHandler(key.(string))
33. ......
34.
35. return true
36. }

EventHandler

初始化 replicaSetController 时,其中有一个 expectations 字段,这是 rs 中一个比较


特殊的机制,为了说清楚 expectations,先来看一下 controller 中所注册的
eventHandler,replicaSetController 会 watch pod 和 replicaSet 两个对象,
eventHandler 中注册了对这两种对象的 add、update、delete 三个操作。

addPod

1、判断 pod 是否处于删除状态;


2、获取该 pod 关联的 rs 以及 rsKey,入队 rs 并更新 rsKey 的 expectations;
3、若 pod 对象没体现出关联的 rs 则为孤儿 pod,遍历 rsList 查找匹配的 rs,若该
rs.Namespace == pod.Namespace 并且 rs.Spec.Selector 匹配 pod.Labels,则说
明该 pod 应该与此 rs 关联,将匹配的 rs 入队;

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:255

1. func (rsc *ReplicaSetController) addPod(obj interface{}) {


2. pod := obj.(*v1.Pod)
3.
4. if pod.DeletionTimestamp != nil {
5. rsc.deletePod(pod)
6. return
7. }
8.
9. // 1、获取 pod 所关联的 rs
10. if controllerRef := metav1.GetControllerOf(pod); controllerRef != nil {
11. rs := rsc.resolveControllerRef(pod.Namespace, controllerRef)
12. if rs == nil {
13. return
14. }

本文档使用 书栈网 · BookStack.CN 构建 - 216 -


replicaset controller 源码分析

15. rsKey, err := controller.KeyFunc(rs)


16. if err != nil {
17. return
18. }
19. // 2、更新 expectations,rsKey 的 add - 1
20. rsc.expectations.CreationObserved(rsKey)
21. rsc.enqueueReplicaSet(rs)
22. return
23. }
24.
25.
26. rss := rsc.getPodReplicaSets(pod)
27. if len(rss) == 0 {
28. return
29. }
30.
31. for _, rs := range rss {
32. rsc.enqueueReplicaSet(rs)
33. }
34. }

updatePod

1、如果 pod label 改变或者处于删除状态,则直接删除;


2、如果 pod 的 OwnerReference 发生改变,此时 oldRS 需要创建 pod,将 oldRS 入
队;
3、获取 pod 关联的 rs,入队 rs,若 pod 当前处于 ready 并非 available 状态,则
会再次将该 rs 加入到延迟队列中,因为 pod 从 ready 到 available 状态需要触发一次
status 的更新;
4、否则为孤儿 pod,遍历 rsList 查找匹配的 rs,若找到则将 rs 入队;

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:298

1. func (rsc *ReplicaSetController) updatePod(old, cur interface{}) {


2. curPod := cur.(*v1.Pod)
3. oldPod := old.(*v1.Pod)
4. if curPod.ResourceVersion == oldPod.ResourceVersion {
5. return
6. }
7.
8. // 1、如果 pod label 改变或者处于删除状态,则直接删除
9. labelChanged := !reflect.DeepEqual(curPod.Labels, oldPod.Labels)
10. if curPod.DeletionTimestamp != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 217 -


replicaset controller 源码分析

11. rsc.deletePod(curPod)
12. if labelChanged {
13. rsc.deletePod(oldPod)
14. }
15. return
16. }
17.
18. // 2、如果 pod 的 OwnerReference 发生改变,将 oldRS 入队
19. curControllerRef := metav1.GetControllerOf(curPod)
20. oldControllerRef := metav1.GetControllerOf(oldPod)
controllerRefChanged := !reflect.DeepEqual(curControllerRef,
21. oldControllerRef)
22. if controllerRefChanged && oldControllerRef != nil {
if rs := rsc.resolveControllerRef(oldPod.Namespace, oldControllerRef);
23. rs != nil {
24. rsc.enqueueReplicaSet(rs)
25. }
26. }
27.
28. // 3、获取 pod 关联的 rs,入队 rs
29. if curControllerRef != nil {
30. rs := rsc.resolveControllerRef(curPod.Namespace, curControllerRef)
31. if rs == nil {
32. return
33. }
34.
35. rsc.enqueueReplicaSet(rs)
if !podutil.IsPodReady(oldPod) && podutil.IsPodReady(curPod) &&
36. rs.Spec.MinReadySeconds > 0 {
rsc.enqueueReplicaSetAfter(rs,
37. (time.Duration(rs.Spec.MinReadySeconds)*time.Second)+time.Second)
38. }
39. return
40. }
41.
42.
43. // 4、查找匹配的 rs
44. if labelChanged || controllerRefChanged {
45. rss := rsc.getPodReplicaSets(curPod)
46. if len(rss) == 0 {
47. return
48. }
49. for _, rs := range rss {

本文档使用 书栈网 · BookStack.CN 构建 - 218 -


replicaset controller 源码分析

50. rsc.enqueueReplicaSet(rs)
51. }
52. }
53. }

deletePod

1、确认该对象是否为 pod;
2、判断是否为孤儿 pod;
3、获取其对应的 rs 以及 rsKey;
4、更新 expectations 中 rsKey 的 del 值;
5、将 rs 入队;

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:372

1. func (rsc *ReplicaSetController) deletePod(obj interface{}) {


2. pod, ok := obj.(*v1.Pod)
3.
4. if !ok {
5. ......
6. }
7.
8. controllerRef := metav1.GetControllerOf(pod)
9. if controllerRef == nil {
10. return
11. }
12. rs := rsc.resolveControllerRef(pod.Namespace, controllerRef)
13. if rs == nil {
14. return
15. }
16. rsKey, err := controller.KeyFunc(rs)
17. if err != nil {
18. return
19. }
20. // 更新 expectations,该 rsKey 的 del - 1
21. rsc.expectations.DeletionObserved(rsKey, controller.PodKey(pod))
22. rsc.enqueueReplicaSet(rs)
23. }

AddRS 和 DeleteRS

以上两个操作仅仅是将对应的 rs 入队。

UpdateRS

本文档使用 书栈网 · BookStack.CN 构建 - 219 -


replicaset controller 源码分析

其实 updateRS 也仅仅是将对应的 rs 进行入队,不过多了一个打印日志的操作,如下所示:

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:232

1. func (rsc *ReplicaSetController) updateRS(old, cur interface{}) {


2. oldRS := old.(*apps.ReplicaSet)
3. curRS := cur.(*apps.ReplicaSet)
4.
5. if *(oldRS.Spec.Replicas) != *(curRS.Spec.Replicas) {
klog.V(4).Infof("%v %v updated. Desired pod count change: %d->%d",
6. rsc.Kind, curRS.Name, *(oldRS.Spec.Replicas), *(curRS.Spec.Replicas))
7. }
8. rsc.enqueueReplicaSet(cur)
9. }

至于 expectations 机制会在下文进行分析。

syncReplicaSet

syncReplicaSet 是 controller 的核心方法,它会驱动 controller 所控制的对象达到期望状


态,主要逻辑如下所示:

1、根据 ns/name 获取 rs 对象;


2、调用 expectations.SatisfiedExpectations 判断是否需要执行真正的 sync 操作;
3、获取所有 pod list;
4、根据 pod label 进行过滤获取与该 rs 关联的 pod 列表,对于其中的孤儿 pod 若与该
rs label 匹配则进行关联,若已关联的 pod 与 rs label 不匹配则解除关联关系;
5、调用 manageReplicas 进行同步 pod 操作,add/del pod;
6、计算 rs 当前的 status 并进行更新;
7、若 rs 设置了 MinReadySeconds 字段则将该 rs 加入到延迟队列中;

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:562

1. func (rsc *ReplicaSetController) syncReplicaSet(key string) error {


2. ......
3.
4. namespace, name, err := cache.SplitMetaNamespaceKey(key)
5. if err != nil {
6. return err
7. }
8.
9. // 1、根据 ns/name 从 informer cache 中获取 rs 对象,

本文档使用 书栈网 · BookStack.CN 构建 - 220 -


replicaset controller 源码分析

10. // 若 rs 已经被删除则直接删除 expectations 中的对象


11. rs, err := rsc.rsLister.ReplicaSets(namespace).Get(name)
12. if errors.IsNotFound(err) {
13. rsc.expectations.DeleteExpectations(key)
14. return nil
15. }
16. ......
17.
18. // 2、判断该 rs 是否需要执行 sync 操作
19. rsNeedsSync := rsc.expectations.SatisfiedExpectations(key)
20. selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)
21. if err != nil {
22. ......
23. }
24.
25. // 3、获取所有 pod list
26. allPods, err := rsc.podLister.Pods(rs.Namespace).List(labels.Everything())
27. ......
28.
29. // 4、过滤掉异常 pod,处于删除状态或者 failed 状态的 pod 都为非 active 状态
30. filteredPods := controller.FilterActivePods(allPods)
31.
// 5、检查所有 pod,根据 pod 并进行 adopt 与 release 操作,最后获取与该 rs 关联的
32. pod list
33. filteredPods, err = rsc.claimPods(rs, selector, filteredPods)
34. ......
35.
36. // 6、若需要 sync 则执行 manageReplicas 创建/删除 pod
37. var manageReplicasErr error
38. if rsNeedsSync && rs.DeletionTimestamp == nil {
39. manageReplicasErr = rsc.manageReplicas(filteredPods, rs)
40. }
41. rs = rs.DeepCopy()
42. // 7、计算 rs 当前的 status
43. newStatus := calculateStatus(rs, filteredPods, manageReplicasErr)
44.
45. // 8、更新 rs status
updatedRS, err :=
updateReplicaSetStatus(rsc.kubeClient.AppsV1().ReplicaSets(rs.Namespace), rs,
46. newStatus)
47.
48.
49. // 9、判断是否需要将 rs 加入到延迟队列中

本文档使用 书栈网 · BookStack.CN 构建 - 221 -


replicaset controller 源码分析

50. if manageReplicasErr == nil && updatedRS.Spec.MinReadySeconds > 0 &&


51. updatedRS.Status.ReadyReplicas == *(updatedRS.Spec.Replicas) &&
52. updatedRS.Status.AvailableReplicas != *(updatedRS.Spec.Replicas) {
rsc.enqueueReplicaSetAfter(updatedRS,
53. time.Duration(updatedRS.Spec.MinReadySeconds)*time.Second)
54. }
55. return manageReplicasErr
56. }

在 syncReplicaSet 方法中有几个重要的操作分别
为: rsc.expectations.SatisfiedExpectations 、 rsc.manageReplicas 、 calculateStatus
,下面一一进行分析。

SatisfiedExpectations

该方法主要判断 rs 是否需要执行真正的同步操作,若需要 add/del pod 或者 expectations


已过期则需要进行同步操作。

k8s.io/kubernetes/pkg/controller/controller_utils.go:181

func (r *ControllerExpectations) SatisfiedExpectations(controllerKey string)


1. bool {
2. // 1、若该 key 存在时,判断是否满足条件或者是否超过同步周期
3. if exp, exists, err := r.GetExpectations(controllerKey); exists {
4. if exp.Fulfilled() {
5. return true
6. } else if exp.isExpired() {
7. return true
8. } else {
9. return false
10. }
11. } else if err != nil {
12. ......
13. } else {
14. // 2、该 rs 可能为新创建的,需要进行 sync
15. ......
16. }
17. return true
18. }
19.
20. // 3、若 add <= 0 且 del <= 0 说明本地观察到的状态已经为期望状态了
21. func (e *ControlleeExpectations) Fulfilled() bool {
22. return atomic.LoadInt64(&e.add) <= 0 && atomic.LoadInt64(&e.del) <= 0

本文档使用 书栈网 · BookStack.CN 构建 - 222 -


replicaset controller 源码分析

23. }
24.
25. // 4、判断 key 是否过期,ExpectationsTimeout 默认值为 5 * time.Minute
26. func (exp *ControlleeExpectations) isExpired() bool {
27. return clock.RealClock{}.Since(exp.timestamp) > ExpectationsTimeout
28. }

manageReplicas

manageReplicas 是最核心的方法,它会计算 replicaSet 需要创建或者删除多少个 pod 并调用


apiserver 的接口进行操作,在此阶段仅仅是调用 apiserver 的接口进行创建,并不保证 pod
成功运行,如果在某一轮,未能成功创建的所有 Pod 对象,则不再创建剩余的 pod。一个周期内最多
只能创建或删除 500 个 pod,若超过上限值未创建完成的 pod 数会在下一个 syncLoop 继续进行
处理。

该方法主要逻辑如下所示:

1、计算已存在 pod 数与期望数的差异;


2、如果 diff < 0 说明 rs 实际的 pod 数未达到期望值需要继续创建 pod,首先会将需要
创建的 pod 数在 expectations 中进行记录,然后调用 slowStartBatch 创建所需要的
pod,slowStartBatch 以指数级增长的方式批量创建 pod,创建 pod 过程中若出现
timeout err 则忽略,若为其他 err 则终止创建操作并更新 expectations;
3、如果 diff > 0 说明可能是一次缩容操作需要删除多余的 pod,如果需要删除全部的 pod
则直接进行删除,否则会通过 getPodsToDelete 方法筛选出需要删除的 pod,具体的筛选策
略在下文会将到,然后并发删除这些 pod,对于删除失败操作也会记录在 expectations 中;

在 slowStartBatch 中会调用 rsc.podControl.CreatePodsWithControllerRef 方法创建


pod,若创建 pod 失败会判断是否为创建超时错误,或者可能是超时后失败,但此时认为超时并不影
响后续的批量创建动作,大家知道,创建 pod 操作提交到 apiserver 后会经过认证、鉴权、以及
动态访问控制三个步骤,此过程有可能会超时,即使真的创建失败了,等到 expectations 过期后在
下一个 syncLoop 时会重新创建。

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:459

1. func (rsc *ReplicaSetController) manageReplicas(......) error {


2. // 1、计算已存在 pod 数与期望数的差异
3. diff := len(filteredPods) - int(*(rs.Spec.Replicas))
4. rsKey, err := controller.KeyFunc(rs)
5. if err != nil {
6. ......
7. }
8. 2、如果 <0,则需要创建 pod
9. if diff < 0 {

本文档使用 书栈网 · BookStack.CN 构建 - 223 -


replicaset controller 源码分析

10. diff *= -1
11. 3、判断需要创建的 pod 数是否超过单次 sync 上限值 500
12. if diff > rsc.burstReplicas {
13. diff = rsc.burstReplicas
14. }
15.
16. 4、在 expectations 中进行记录,若该 key 已经存在会进行覆盖
17. rsc.expectations.ExpectCreations(rsKey, diff)
18.
19. 5、调用 slowStartBatch 创建所需要的 pod
successfulCreations, err := slowStartBatch(diff,
20. controller.SlowStartInitialBatchSize, func() error {
err := rsc.podControl.CreatePodsWithControllerRef(rs.Namespace,
21. &rs.Spec.Template, rs, metav1.NewControllerRef(rs, rsc.GroupVersionKind))
22. // 6、若为 timeout err 则忽略
23. if err != nil && errors.IsTimeout(err) {
24. return nil
25. }
26. return err
27. })
28.
29. // 7、计算未创建的 pod 数,并记录在 expectations 中
// 若 pod 创建成功,informer watch 到事件后会在 addPod handler 中更新
30. expectations
31. if skippedPods := diff - successfulCreations; skippedPods > 0 {
32. for i := 0; i < skippedPods; i++ {
33. rsc.expectations.CreationObserved(rsKey)
34. }
35. }
36. return err
37. } else if diff > 0 {
38. // 8、若 diff >0 说明需要删除多创建的 pod
39. if diff > rsc.burstReplicas {
40. diff = rsc.burstReplicas
41. }
42.
43. // 9、getPodsToDelete 会按照一定的策略找出需要删除的 pod 列表
44. podsToDelete := getPodsToDelete(filteredPods, diff)
45.
46. // 10、在 expectations 中进行记录,若该 key 已经存在会进行覆盖
47. rsc.expectations.ExpectDeletions(rsKey, getPodKeys(podsToDelete))
48.
49. // 11、进行并发删除的操作

本文档使用 书栈网 · BookStack.CN 构建 - 224 -


replicaset controller 源码分析

50. errCh := make(chan error, diff)


51. var wg sync.WaitGroup
52. wg.Add(diff)
53. for _, pod := range podsToDelete {
54. go func(targetPod *v1.Pod) {
55. defer wg.Done()
if err := rsc.podControl.DeletePod(rs.Namespace,
56. targetPod.Name, rs); err != nil {
57. podKey := controller.PodKey(targetPod)
58. // 12、某次删除操作若失败会记录在 expectations 中
59. rsc.expectations.DeletionObserved(rsKey, podKey)
60. errCh <- err
61. }
62. }(pod)
63. }
64. wg.Wait()
65.
66. // 13、返回其中一条 err
67. select {
68. case err := <-errCh:
69. if err != nil {
70. return err
71. }
72. default:
73. }
74. }
75.
76. return nil
77. }

slowStartBatch 会批量创建出已计算出的 diff pod 数,创建的 pod 数依次为 1、2、4、


8……,呈指数级增长,其方法如下所示:

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:658

func slowStartBatch(count int, initialBatchSize int, fn func() error) (int,


1. error) {
2. remaining := count
3. successes := 0
for batchSize := integer.IntMin(remaining, initialBatchSize); batchSize >
4. 0; batchSize = integer.IntMin(2*batchSize, remaining) {
5. errCh := make(chan error, batchSize)
6. var wg sync.WaitGroup

本文档使用 书栈网 · BookStack.CN 构建 - 225 -


replicaset controller 源码分析

7. wg.Add(batchSize)
8. for i := 0; i < batchSize; i++ {
9. go func() {
10. defer wg.Done()
11. if err := fn(); err != nil {
12. errCh <- err
13. }
14. }()
15. }
16. wg.Wait()
17. curSuccesses := batchSize - len(errCh)
18. successes += curSuccesses
19. if len(errCh) > 0 {
20. return successes, <-errCh
21. }
22. remaining -= batchSize
23. }
24. return successes, nil
25. }

若 diff > 0 时再删除 pod 阶段会调用 getPodsToDelete 对 pod 进行筛选操作,此阶段会选


出最劣质的 pod,下面是用到的 6 种筛选方法:

1、判断是否绑定了 node:Unassigned < assigned;


2、判断 pod phase:PodPending < PodUnknown < PodRunning;
3、判断 pod 状态:Not ready < ready;
4、若 pod 都为 ready,则按运行时间排序,运行时间最短会被删除:empty time < less
time < more time;
5、根据 pod 重启次数排序:higher restart counts < lower restart counts;
6、按 pod 创建时间进行排序:Empty creation time pods < newer pods < older
pods;

上面的几个排序规则遵循互斥原则,从上到下进行匹配,符合条件则排序完成,代码如下所示:

k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:684

1. func getPodsToDelete(filteredPods []*v1.Pod, diff int) []*v1.Pod {


2. if diff < len(filteredPods) {
3. sort.Sort(controller.ActivePods(filteredPods))
4. }
5. return filteredPods[:diff]
6. }

本文档使用 书栈网 · BookStack.CN 构建 - 226 -


replicaset controller 源码分析

k8s.io/kubernetes/pkg/controller/controller_utils.go:735

1. type ActivePods []*v1.Pod


2.
3. func (s ActivePods) Len() int { return len(s) }
4. func (s ActivePods) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
5.
6. func (s ActivePods) Less(i, j int) bool {
7. // 1. Unassigned < assigned
if s[i].Spec.NodeName != s[j].Spec.NodeName && (len(s[i].Spec.NodeName) ==
8. 0 || len(s[j].Spec.NodeName) == 0) {
9. return len(s[i].Spec.NodeName) == 0
10. }
11.
12. // 2. PodPending < PodUnknown < PodRunning
m := map[v1.PodPhase]int{v1.PodPending: 0, v1.PodUnknown: 1, v1.PodRunning:
13. 2}
14. if m[s[i].Status.Phase] != m[s[j].Status.Phase] {
15. return m[s[i].Status.Phase] < m[s[j].Status.Phase]
16. }
17.
18. // 3. Not ready < ready
19. if podutil.IsPodReady(s[i]) != podutil.IsPodReady(s[j]) {
20. return !podutil.IsPodReady(s[i])
21. }
22.
23. // 4. Been ready for empty time < less time < more time
if podutil.IsPodReady(s[i]) && podutil.IsPodReady(s[j]) &&
24. !podReadyTime(s[i]).Equal(podReadyTime(s[j])) {
25. return afterOrZero(podReadyTime(s[i]), podReadyTime(s[j]))
26. }
27.
// 5. Pods with containers with higher restart counts < lower restart
28. counts
29. if maxContainerRestarts(s[i]) != maxContainerRestarts(s[j]) {
30. return maxContainerRestarts(s[i]) > maxContainerRestarts(s[j])
31. }
32.
33. // 6. Empty creation time pods < newer pods < older pods
34. if !s[i].CreationTimestamp.Equal(&s[j].CreationTimestamp) {
35. return afterOrZero(&s[i].CreationTimestamp, &s[j].CreationTimestamp)
36. }
37. return false

本文档使用 书栈网 · BookStack.CN 构建 - 227 -


replicaset controller 源码分析

38. }

calculateStatus

calculateStatus 会通过当前 pod 的状态计算出 rs 中 status 字段值,status 字段如下所


示:

1. status:
2. availableReplicas: 10
3. fullyLabeledReplicas: 10
4. observedGeneration: 1
5. readyReplicas: 10
6. replicas: 10

k8s.io/kubernetes/pkg/controller/replicaset/replica_set_utils.go:85

1. func calculateStatus(......) apps.ReplicaSetStatus {


2. newStatus := rs.Status
3. fullyLabeledReplicasCount := 0
4. readyReplicasCount := 0
5. availableReplicasCount := 0
templateLabel :=
6. labels.Set(rs.Spec.Template.Labels).AsSelectorPreValidated()
7. for _, pod := range filteredPods {
8. if templateLabel.Matches(labels.Set(pod.Labels)) {
9. fullyLabeledReplicasCount++
10. }
11. if podutil.IsPodReady(pod) {
12. readyReplicasCount++
if podutil.IsPodAvailable(pod, rs.Spec.MinReadySeconds,
13. metav1.Now()) {
14. availableReplicasCount++
15. }
16. }
17. }
18.
19. failureCond := GetCondition(rs.Status, apps.ReplicaSetReplicaFailure)
20. if manageReplicasErr != nil && failureCond == nil {
21. var reason string
22. if diff := len(filteredPods) - int(*(rs.Spec.Replicas)); diff < 0 {
23. reason = "FailedCreate"
24. } else if diff > 0 {
25. reason = "FailedDelete"

本文档使用 书栈网 · BookStack.CN 构建 - 228 -


replicaset controller 源码分析

26. }
cond := NewReplicaSetCondition(apps.ReplicaSetReplicaFailure,
27. v1.ConditionTrue, reason, manageReplicasErr.Error())
28. SetCondition(&newStatus, cond)
29. } else if manageReplicasErr == nil && failureCond != nil {
30. RemoveCondition(&newStatus, apps.ReplicaSetReplicaFailure)
31. }
32.
33. newStatus.Replicas = int32(len(filteredPods))
34. newStatus.FullyLabeledReplicas = int32(fullyLabeledReplicasCount)
35. newStatus.ReadyReplicas = int32(readyReplicasCount)
36. newStatus.AvailableReplicas = int32(availableReplicasCount)
37. return newStatus
38. }

expectations 机制

通过上面的分析可知,在 rs 每次入队后进行 sync 操作时,首先需要判断该 rs 是否满足


expectations 机制,那么这个 expectations 的目的是什么?其实,rs 除了有 informer 的
缓存外,还有一个本地缓存就是 expectations,expectations 会记录 rs 所有对象需要
add/del 的 pod 数量,若两者都为 0 则说明该 rs 所期望创建的 pod 或者删除的 pod 数已经
被满足,若不满足则说明某次在 syncLoop 中创建或者删除 pod 时有失败的操作,则需要等待
expectations 过期后再次同步该 rs。

通过上面对 eventHandler 的分析,再来总结一下触发 replicaSet 对象发生同步事件的条件:

1、与 rs 相关的:AddRS、UpdateRS、DeleteRS;
2、与 pod 相关的:AddPod、UpdatePod、DeletePod;
3、informer 二级缓存的同步;

但是所有的更新事件是否都需要执行 sync 操作?对于除 rs.Spec.Replicas 之外的更新操作其实


都没必要执行 sync 操作,因为 spec 其他字段和 status 的更新都不需要创建或者删除 pod。

在 sync 操作真正开始之前,依据 expectations 机制进行判断,确定是否要真正地启动一次


sync,因为在 eventHandler 阶段也会更新 expectations 值,从上面的 eventHandler 中
可以看到在 addPod 中会调用 rsc.expectations.CreationObserved 更新 rsKey 的
expectations,将其 add 值 -1,在 deletePod 中调用
rsc.expectations.DeletionObserved 将其 del 值 -1。所以等到 sync 时,若
controllerKey(name 或者 ns/name)满足 expectations 机制则进行 sync 操作,而
updatePod 并不会修改 expectations,所以,expectations 的设计就是当需要创建或删除
pod 才会触发对应的 sync 操作,expectations 机制的目的就是减少不必要的 sync 操作。

什么条件下 expectations 机制会满足?

本文档使用 书栈网 · BookStack.CN 构建 - 229 -


replicaset controller 源码分析

1、当 expectations 中不存在 rsKey 时,也就说首次创建 rs 时;


2、当 expectations 中 del 以及 add 值都为 0 时,即 rs 所需要创建或者删除的 pod
数都已满足;
3、当 expectations 过期时,即超过 5 分钟未进行 sync 操作;

最后再看一下 expectations 中用到的几个方法:

1. // 创建了一个 pod 说明 expectations 中对应的 key add 期望值需要减少一个 pod, add -1


2. CreationObserved(controllerKey string)
3.
4. // 删除了一个 pod 说明 expectations 中对应的 key del 期望值需要减少一个 pod, del - 1
5. DeletionObserved(controllerKey string)
6.
7. // 写入 key 需要 add 的 pod 数量
8. ExpectCreations(controllerKey string, adds int) error
9.
10. // 写入 key 需要 del 的 pod 数量
11. ExpectDeletions(controllerKey string, dels int) error
12.
13. // 删除该 key
14. DeleteExpectations(controllerKey string)

当在 syncLoop 中发现满足条件时,会执行 manageReplicas 方法,在 manageReplicas 中无


论是为 rs 创建还是删除 pod 都会调用 ExpectCreations 和 ExpectDeletions 为 rsKey
创建 expectations 对象。

总结
本文主要从源码层面分析了 replicaSetController 的设计与实现,但是不得不说其在设计方面考
虑了很多因素,文中只提到了笔者理解了或者思考后稍有了解的一些机制,至于其他设计思想还得自行
阅读代码体会。

下面以一个流程图总结下创建 rs 的主要流程。

1. SatisfiedExpectations
2. (expectations 中不存在
3. rsKey,rsNeedsSync
4. 为 true)
5. | 判断 add/del pod
6. | |
7. | ∨

本文档使用 书栈网 · BookStack.CN 构建 - 230 -


replicaset controller 源码分析

| 创建 expectations 对
8. 象,
9. | 并设置 add/del 值
10. ∨ |
11. create rs --> syncReplicaSet --> manageReplicas --> ∨
(为 rs 创建 pod) 调用
12. slowStartBatch 批量创建 pod/
| 删除筛选出的多余
13. pod
14. | |
15. | ∨
| 更新 expectations
16. 对象
17. ∨
18. updateReplicaSetStatus
19. (更新 rs 的 status
20. subResource)

参考:

https://keyla.vip/k8s/3-master/controller/replica-set/

本文档使用 书栈网 · BookStack.CN 构建 - 231 -


kube-scheduler 源码分析

kube-scheduler 的设计
Kube-scheduler 是 kubernetes 的核心组件之一,也是所有核心组件之间功能比较单一的,其代
码也相对容易理解。kube-scheduler 的目的就是为每一个 pod 选择一个合适的 node,整体流程
可以概括为三步,获取未调度的 podList,通过执行一系列调度算法为 pod 选择一个合适的
node,提交数据到 apiserver,其核心则是一系列调度算法的设计与执行。

官方对 kube-scheduler 的调度流程描述 The Kubernetes Scheduler:

1. For given pod:


2.
3. +---------------------------------------------+
4. | Schedulable nodes: |
5. | |
6. | +--------+ +--------+ +--------+ |
7. | | node 1 | | node 2 | | node 3 | |
8. | +--------+ +--------+ +--------+ |
9. | |
10. +-------------------+-------------------------+
11. |
12. |
13. v
14. +-------------------+-------------------------+
15.
16. Pred. filters: node 3 doesn't have enough resource
17.
18. +-------------------+-------------------------+
19. |
20. |
21. v
22. +-------------------+-------------------------+
23. | remaining nodes: |
24. | +--------+ +--------+ |
25. | | node 1 | | node 2 | |
26. | +--------+ +--------+ |
27. | |
28. +-------------------+-------------------------+
29. |
30. |
31. v
32. +-------------------+-------------------------+

本文档使用 书栈网 · BookStack.CN 构建 - 232 -


kube-scheduler 源码分析

33.
34. Priority function: node 1: p=2
35. node 2: p=5
36.
37. +-------------------+-------------------------+
38. |
39. |
40. v
41. select max{node priority} = node 2

kube-scheduler 目前包含两部分调度算法 predicates 和 priorities,首先执行


predicates 算法过滤部分 node 然后执行 priorities 算法为所有 node 打分,最后从所有
node 中选出分数最高的最为最佳的 node。

kube-scheduler 源码分析

kubernetes 版本: v1.16

kubernetes 中所有组件的启动流程都是类似的,首先会解析命令行参数、添加默认值,kube-
scheduler 的默认参数在
k8s.io/kubernetes/pkg/scheduler/apis/config/v1alpha1/defaults.go 中定义的。然后会执
行 run 方法启动主逻辑,下面直接看 kube-scheduler 的主逻辑 run 方法执行过程。

Run() 方法主要做了以下工作:

初始化 scheduler 对象
启动 kube-scheduler server,kube-scheduler 监听 10251 和 10259 端口,10251
端口不需要认证,可以获取 healthz metrics 等信息,10259 为安全端口,需要认证
启动所有的 informer
执行 sched.Run() 方法,执行主调度逻辑

k8s.io/kubernetes/cmd/kube-scheduler/app/server.go:160

func Run(cc schedulerserverconfig.CompletedConfig, stopCh <-chan struct{},


1. registryOptions ...Option) error {
2. ......
3. // 1、初始化 scheduler 对象
4. sched, err := scheduler.New(......)
5. if err != nil {
6. return err
7. }
8.
9. // 2、启动事件广播

本文档使用 书栈网 · BookStack.CN 构建 - 233 -


kube-scheduler 源码分析

10. if cc.Broadcaster != nil && cc.EventClient != nil {


11. cc.Broadcaster.StartRecordingToSink(stopCh)
12. }
13. if cc.LeaderElectionBroadcaster != nil && cc.CoreEventClient != nil {

cc.LeaderElectionBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface
14. cc.CoreEventClient.Events("")})
15. }
16.
17. ......
18. // 3、启动 http server
19. if cc.InsecureServing != nil {
20. separateMetrics := cc.InsecureMetricsServing != nil
handler := buildHandlerChain(newHealthzHandler(&cc.ComponentConfig,
21. separateMetrics, checks...), nil, nil)
22. if err := cc.InsecureServing.Serve(handler, 0, stopCh); err != nil {
23. return fmt.Errorf("failed to start healthz server: %v", err)
24. }
25. }
26. ......
27. // 4、启动所有 informer
28. go cc.PodInformer.Informer().Run(stopCh)
29. cc.InformerFactory.Start(stopCh)
30.
31. cc.InformerFactory.WaitForCacheSync(stopCh)
32.
33. run := func(ctx context.Context) {
34. sched.Run()
35. <-ctx.Done()
36. }
37.
ctx, cancel := context.WithCancel(context.TODO()) // TODO once Run()
38. accepts a context, it should be used here
39. defer cancel()
40. go func() {
41. select {
42. case <-stopCh:
43. cancel()
44. case <-ctx.Done():
45. }
46. }()
47.
48. // 5、选举 leader

本文档使用 书栈网 · BookStack.CN 构建 - 234 -


kube-scheduler 源码分析

49. if cc.LeaderElection != nil {


50. ......
51. }
52. // 6、执行 sched.Run() 方法
53. run(ctx)
54. return fmt.Errorf("finished without leader elect")
55. }

下面看一下 scheduler.New() 方法是如何初始化 scheduler 结构体的,该方法主要的功能是初


始化默认的调度算法以及默认的调度器 GenericScheduler。

创建 scheduler 配置文件
根据默认的 DefaultProvider 初始化 schedulerAlgorithmSource 然后加载默认的预选
及优选算法,然后初始化 GenericScheduler
若启动参数提供了 policy config 则使用其覆盖默认的预选及优选算法并初始化
GenericScheduler,不过该参数现已被弃用

k8s.io/kubernetes/pkg/scheduler/scheduler.go:166

1. func New(......) (*Scheduler, error) {


2. ......
3. // 1、创建 scheduler 的配置文件
4. configurator := factory.NewConfigFactory(&factory.ConfigFactoryArgs{
5. ......
6. })
7. var config *factory.Config
8. source := schedulerAlgorithmSource
9. // 2、加载默认的调度算法
10. switch {
11. case source.Provider != nil:
12. // 使用默认的 ”DefaultProvider“ 初始化 config
13. sc, err := configurator.CreateFromProvider(*source.Provider)
14. if err != nil {
return nil, fmt.Errorf("couldn't create scheduler using provider
15. %q: %v", *source.Provider, err)
16. }
17. config = sc
18. case source.Policy != nil:
19. // 通过启动时指定的 policy source 加载 config
20. ......
21. config = sc
22. default:

本文档使用 书栈网 · BookStack.CN 构建 - 235 -


kube-scheduler 源码分析

23. return nil, fmt.Errorf("unsupported algorithm source: %v", source)


24. }
25. // Additional tweaks to the config produced by the configurator.
26. config.Recorder = recorder
27. config.DisablePreemption = options.disablePreemption
28. config.StopEverything = stopCh
29.
30. // 3.创建 scheduler 对象
31. sched := NewFromConfig(config)
32. ......
33. return sched, nil
34. }

下面是 pod informer 的启动逻辑,只监听 status.phase 不为 succeeded 以及 failed 状


态的 pod,即非 terminating 的 pod。

k8s.io/kubernetes/pkg/scheduler/factory/factory.go:527

func NewPodInformer(client clientset.Interface, resyncPeriod time.Duration)


1. coreinformers.PodInformer {
2. selector := fields.ParseSelectorOrDie(
3. "status.phase!=" + string(v1.PodSucceeded) +
4. ",status.phase!=" + string(v1.PodFailed))
lw := cache.NewListWatchFromClient(client.CoreV1().RESTClient(),
5. string(v1.ResourcePods), metav1.NamespaceAll, selector)
6. return &podInformer{
informer: cache.NewSharedIndexInformer(lw, &v1.Pod{}, resyncPeriod,
7. cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}),
8. }
9. }

然后继续看 Run() 方法中最后执行的 sched.Run() 调度循环逻辑,若 informer 中的


cache 同步完成后会启动一个循环逻辑执行 sched.scheduleOne 方法。

k8s.io/kubernetes/pkg/scheduler/scheduler.go:313

1. func (sched *Scheduler) Run() {


2. if !sched.config.WaitForCacheSync() {
3. return
4. }
5.
6. go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything)
7. }

本文档使用 书栈网 · BookStack.CN 构建 - 236 -


kube-scheduler 源码分析

scheduleOne() 每次对一个 pod 进行调度,主要有以下步骤:

从 scheduler 调度队列中取出一个 pod,如果该 pod 处于删除状态则跳过


执行调度逻辑 sched.schedule() 返回通过预算及优选算法过滤后选出的最佳 node
如果过滤算法没有选出合适的 node,则返回 core.FitError
若没有合适的 node 会判断是否启用了抢占策略,若启用了则执行抢占机制
判断是否需要 VolumeScheduling 特性
执行 reserve plugin
pod 对应的 spec.NodeName 写上 scheduler 最终选择的 node,更新 scheduler
cache
请求 apiserver 异步处理最终的绑定操作,写入到 etcd
执行 permit plugin
执行 prebind plugin
执行 postbind plugin

k8s.io/kubernetes/pkg/scheduler/scheduler.go:515

1. func (sched *Scheduler) scheduleOne() {


2. fwk := sched.Framework
3.
4. pod := sched.NextPod()
5. if pod == nil {
6. return
7. }
8. // 1.判断 pod 是否处于删除状态
9. if pod.DeletionTimestamp != nil {
10. ......
11. }
12.
13. // 2.执行调度策略选择 node
14. start := time.Now()
15. pluginContext := framework.NewPluginContext()
16. scheduleResult, err := sched.schedule(pod, pluginContext)
17. if err != nil {
18. if fitError, ok := err.(*core.FitError); ok {
19. // 3.若启用抢占机制则执行
20. if sched.DisablePreemption {
21. ......
22. } else {
23. preemptionStartTime := time.Now()
24. sched.preempt(pluginContext, fwk, pod, fitError)

本文档使用 书栈网 · BookStack.CN 构建 - 237 -


kube-scheduler 源码分析

25. ......
26. }
27. ......
28. metrics.PodScheduleFailures.Inc()
29. } else {
30. klog.Errorf("error selecting node for pod: %v", err)
31. metrics.PodScheduleErrors.Inc()
32. }
33. return
34. }
35. ......
36. assumedPod := pod.DeepCopy()
37.
38. // 4.判断是否需要 VolumeScheduling 特性
allBound, err := sched.assumeVolumes(assumedPod,
39. scheduleResult.SuggestedHost)
40. if err != nil {
41. klog.Errorf("error assuming volumes: %v", err)
42. metrics.PodScheduleErrors.Inc()
43. return
44. }
45.
46. // 5.执行 "reserve" plugins
if sts := fwk.RunReservePlugins(pluginContext, assumedPod,
47. scheduleResult.SuggestedHost); !sts.IsSuccess() {
48. .....
49. }
50.
51. // 6.为 pod 设置 NodeName 字段,更新 scheduler 缓存
52. err = sched.assume(assumedPod, scheduleResult.SuggestedHost)
53. if err != nil {
54. ......
55. }
56.
57. // 7.异步请求 apiserver
58. go func() {
59. // Bind volumes first before Pod
60. if !allBound {
61. err := sched.bindVolumes(assumedPod)
62. if err != nil {
63. ......
64. return
65. }

本文档使用 书栈网 · BookStack.CN 构建 - 238 -


kube-scheduler 源码分析

66. }
67.
68. // 8.执行 "permit" plugins
permitStatus := fwk.RunPermitPlugins(pluginContext, assumedPod,
69. scheduleResult.SuggestedHost)
70. if !permitStatus.IsSuccess() {
71. ......
72. }
73. // 9.执行 "prebind" plugins
preBindStatus := fwk.RunPreBindPlugins(pluginContext, assumedPod,
74. scheduleResult.SuggestedHost)
75. if !preBindStatus.IsSuccess() {
76. ......
77. }
err := sched.bind(assumedPod, scheduleResult.SuggestedHost,
78. pluginContext)
79. ......
80. if err != nil {
81. ......
82. } else {
83. ......
84. // 10.执行 "postbind" plugins
fwk.RunPostBindPlugins(pluginContext, assumedPod,
85. scheduleResult.SuggestedHost)
86. }
87. }()
88. }

scheduleOne() 中通过调用 sched.schedule() 来执行预选与优选算法处理:

k8s.io/kubernetes/pkg/scheduler/scheduler.go:337

func (sched *Scheduler) schedule(pod *v1.Pod, pluginContext


1. *framework.PluginContext) (core.ScheduleResult, error) {
2. result, err := sched.Algorithm.Schedule(pod, pluginContext)
3. if err != nil {
4. ......
5. }
6. return result, err
7. }

sched.Algorithm 是一个 interface,主要包含四个方法,GenericScheduler 是其具体的实


现:

本文档使用 书栈网 · BookStack.CN 构建 - 239 -


kube-scheduler 源码分析

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:131

1. type ScheduleAlgorithm interface {


Schedule(*v1.Pod, *framework.PluginContext) (scheduleResult ScheduleResult,
2. err error)
Preempt(*framework.PluginContext, *v1.Pod, error) (selectedNode *v1.Node,
3. preemptedPods []*v1.Pod, cleanupNominatedPods []*v1.Pod, err error)
4. Predicates() map[string]predicates.FitPredicate
5. Prioritizers() []priorities.PriorityConfig
6. }

Schedule() :正常调度逻辑,包含预算与优选算法的执行
Preempt() :抢占策略,在 pod 调度发生失败的时候尝试抢占低优先级的 pod,函数返回发
生抢占的 node,被 抢占的 pods 列表,nominated node name 需要被移除的 pods 列表
以及 error
Predicates() :predicates 算法列表
Prioritizers() :prioritizers 算法列表

kube-scheduler 提供的默认调度为 DefaultProvider,DefaultProvider 配置的


predicates 和 priorities policies 在
k8s.io/kubernetes/pkg/scheduler/algorithmprovider/defaults/defaults.go 中定义,算法
具体实现是在 k8s.io/kubernetes/pkg/scheduler/algorithm/predicates/
和 k8s.io/kubernetes/pkg/scheduler/algorithm/priorities/ 中,默认的算法如下所示:

pkg/scheduler/algorithmprovider/defaults/defaults.go

1. func defaultPredicates() sets.String {


2. return sets.NewString(
3. predicates.NoVolumeZoneConflictPred,
4. predicates.MaxEBSVolumeCountPred,
5. predicates.MaxGCEPDVolumeCountPred,
6. predicates.MaxAzureDiskVolumeCountPred,
7. predicates.MaxCSIVolumeCountPred,
8. predicates.MatchInterPodAffinityPred,
9. predicates.NoDiskConflictPred,
10. predicates.GeneralPred,
11. predicates.CheckNodeMemoryPressurePred,
12. predicates.CheckNodeDiskPressurePred,
13. predicates.CheckNodePIDPressurePred,
14. predicates.CheckNodeConditionPred,
15. predicates.PodToleratesNodeTaintsPred,
16. predicates.CheckVolumeBindingPred,

本文档使用 书栈网 · BookStack.CN 构建 - 240 -


kube-scheduler 源码分析

17. )
18. }
19.
20. func defaultPriorities() sets.String {
21. return sets.NewString(
22. priorities.SelectorSpreadPriority,
23. priorities.InterPodAffinityPriority,
24. priorities.LeastRequestedPriority,
25. priorities.BalancedResourceAllocation,
26. priorities.NodePreferAvoidPodsPriority,
27. priorities.NodeAffinityPriority,
28. priorities.TaintTolerationPriority,
29. priorities.ImageLocalityPriority,
30. )
31. }

下面继续看 sched.Algorithm.Schedule() 调用具体调度算法的过程:

检查 pod pvc 信息
执行 prefilter plugins
获取 scheduler cache 的快照,每次调度 pod 时都会获取一次快照
执行 g.findNodesThatFit() 预选算法
执行 postfilter plugin
若 node 为 0 直接返回失败的 error,若 node 数为1 直接返回该 node
执行 g.priorityMetaProducer() 获取 metaPrioritiesInterface,计算 pod 的
metadata,检查该 node 上是否有相同 meta 的 pod
执行 PrioritizeNodes() 算法
执行 g.selectHost() 通过得分选择一个最佳的 node

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:186

func (g *genericScheduler) Schedule(pod *v1.Pod, pluginContext


1. *framework.PluginContext) (result ScheduleResult, err error) {
2. ......
3. // 1.检查 pod pvc
4. if err := podPassesBasicChecks(pod, g.pvcLister); err != nil {
5. return result, err
6. }
7.
8. // 2.执行 "prefilter" plugins
9. preFilterStatus := g.framework.RunPreFilterPlugins(pluginContext, pod)
10. if !preFilterStatus.IsSuccess() {

本文档使用 书栈网 · BookStack.CN 构建 - 241 -


kube-scheduler 源码分析

11. return result, preFilterStatus.AsError()


12. }
13.
14. // 3.获取 node 数量
15. numNodes := g.cache.NodeTree().NumNodes()
16. if numNodes == 0 {
17. return result, ErrNoNodesAvailable
18. }
19.
20. // 4.快照 node 信息
21. if err := g.snapshot(); err != nil {
22. return result, err
23. }
24.
25. // 5.执行预选算法
26. startPredicateEvalTime := time.Now()
filteredNodes, failedPredicateMap, filteredNodesStatuses, err :=
27. g.findNodesThatFit(pluginContext, pod)
28. if err != nil {
29. return result, err
30. }
31. // 6.执行 "postfilter" plugins
postfilterStatus := g.framework.RunPostFilterPlugins(pluginContext, pod,
32. filteredNodes, filteredNodesStatuses)
33. if !postfilterStatus.IsSuccess() {
34. return result, postfilterStatus.AsError()
35. }
36.
37. // 7.预选后没有合适的 node 直接返回
38. if len(filteredNodes) == 0 {
39. ......
40. }
41.
42. startPriorityEvalTime := time.Now()
43. // 8.若只有一个 node 则直接返回该 node
44. if len(filteredNodes) == 1 {
45. return ScheduleResult{
46. SuggestedHost: filteredNodes[0].Name,
47. EvaluatedNodes: 1 + len(failedPredicateMap),
48. FeasibleNodes: 1,
49. }, nil
50. }
51.

本文档使用 书栈网 · BookStack.CN 构建 - 242 -


kube-scheduler 源码分析

52. // 9.获取 pod meta 信息,执行优选算法


metaPrioritiesInterface := g.priorityMetaProducer(pod,
53. g.nodeInfoSnapshot.NodeInfoMap)
priorityList, err := PrioritizeNodes(pod, g.nodeInfoSnapshot.NodeInfoMap,
metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders,
54. g.framework, pluginContext)
55. if err != nil {
56. return result, err
57. }
58.
59. // 10.根据打分选择最佳的 node
60. host, err := g.selectHost(priorityList)
61. trace.Step("Selecting host done")
62. return ScheduleResult{
63. SuggestedHost: host,
64. EvaluatedNodes: len(filteredNodes) + len(failedPredicateMap),
65. FeasibleNodes: len(filteredNodes),
66. }, err
67. }

至此,scheduler 的整个过程分析完毕。

总结
本文主要对于 kube-scheduler v1.16 的调度流程进行了分析,但其中有大量的细节都暂未提及,
包括预选算法以及优选算法的具体实现、优先级与抢占调度的实现、framework 的使用及实现,因篇
幅有限,部分内容会在后文继续说明。

参考:

The Kubernetes Scheduler

scheduling design proposals

本文档使用 书栈网 · BookStack.CN 构建 - 243 -


kube-scheduler predicates 与 priorities 调度算法源码分析

在上篇文章kube-scheduler 源码分析中已经介绍了 kube-scheduler 的设计以及从源码角度分


析了其执行流程,这篇文章会专注介绍调度过程中 predicates 和 priorities 这两个调度策略主
要发生作用的阶段。

kubernetes 版本: v1.16

predicates 调度算法源码分析
predicates 算法主要是对集群中的 node 进行过滤,选出符合当前 pod 运行的 nodes。

调度算法说明

上节已经提到默认的调度算法在 pkg/scheduler/algorithmprovider/defaults/defaults.go 中定
义了:

1. func defaultPredicates() sets.String {


2. return sets.NewString(
3. predicates.NoVolumeZoneConflictPred,
4. predicates.MaxEBSVolumeCountPred,
5. predicates.MaxGCEPDVolumeCountPred,
6. predicates.MaxAzureDiskVolumeCountPred,
7. predicates.MaxCSIVolumeCountPred,
8. predicates.MatchInterPodAffinityPred,
9. predicates.NoDiskConflictPred,
10. predicates.GeneralPred,
11. predicates.CheckNodeMemoryPressurePred,
12. predicates.CheckNodeDiskPressurePred,
13. predicates.CheckNodePIDPressurePred,
14. predicates.CheckNodeConditionPred,
15. predicates.PodToleratesNodeTaintsPred,
16. predicates.CheckVolumeBindingPred,
17. )
18. }

下面是对默认调度算法的一些说明:

predicates 算法 说明

GeneralPred 包含 PodFitsResources、PodFitsHost,、
GeneralPred
PodFitsHostPorts、PodMatchNodeSelector 四种算法

NoDiskConflictPred 检查多个 Pod 声明挂载的持久化 Volume 是否有冲突

MaxGCEPDVolumeCountPred 检查 GCE 持久化 Volume 是否超过了一定数目

MaxAzureDiskVolumeCountPred 检查 Azure 持久化 Volume 是否超过了一定数目

本文档使用 书栈网 · BookStack.CN 构建 - 244 -


kube-scheduler predicates 与 priorities 调度算法源码分析

MaxCSIVolumeCountPred 检查 CSI 持久化 Volume 是否超过了一定数目(已废弃)

MaxEBSVolumeCountPred 检查 EBS 持久化 Volume 是否超过了一定数目

检查持久化 Volume 的 Zone(高可用域)标签是否与节点的 Zone


NoVolumeZoneConflictPred
标签相匹配

检查该 Pod 对应 PV 的 nodeAffinity 字段是否跟某个节点的标签


CheckVolumeBindingPred 相匹配,Local Persistent Volume(本地持久化卷)必须使用
nodeAffinity 来跟某个具体的节点绑定

检查 Node 的 Taint 机制,只有当 Pod 的 Toleration 字段与


PodToleratesNodeTaintsPred
Node 的 Taint 字段能够匹配时,这个 Pod 才能被调度到该节点上

检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)


MatchInterPodAffinityPred
和反亲密(anti-affinity)关系

CheckNodeConditionPred 检查 NodeCondition

CheckNodePIDPressurePred 检查 NodePIDPressure

CheckNodeDiskPressurePred 检查 NodeDiskPressure

CheckNodeMemoryPressurePred 检查 NodeMemoryPressure

默认的 predicates 调度算法主要分为五种类型:

1、第一种类型叫作 GeneralPredicates,包含 PodFitsResources、PodFitsHost、


PodFitsHostPorts、PodMatchNodeSelector 四种策略,其具体含义如下所示:

PodFitsHost:检查宿主机的名字是否跟 Pod 的 spec.nodeName 一致


PodFitsHostPorts:检查 Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用
的端口有冲突
PodMatchNodeSelector:检查 Pod 的 nodeSelector 或者 nodeAffinity 指定的节点
是否与节点匹配等
PodFitsResources:检查主机的资源是否满足 Pod 的需求,根据实际已经分配(Request)
的资源量做调度

kubelet 在启动 Pod 前,会执行一个 Admit 操作来进行二次确认,这里二次确认的规则就是执行


一遍 GeneralPredicates。

2、第二种类型是与 Volume 相关的过滤规则,主要有NoDiskConflictPred、


MaxGCEPDVolumeCountPred、MaxAzureDiskVolumeCountPred、
MaxCSIVolumeCountPred、MaxEBSVolumeCountPred、NoVolumeZoneConflictPred、
CheckVolumeBindingPred。

3、第三种类型是宿主机相关的过滤规则,主要是 PodToleratesNodeTaintsPred。

4、第四种类型是 Pod 相关的过滤规则,主要是 MatchInterPodAffinityPred。

5、第五种类型是新增的过滤规则,与宿主机的运行状况有关,主要有 CheckNodeCondition、
CheckNodeMemoryPressure、CheckNodePIDPressure、CheckNodeDiskPressure 四种。若
启用了 TaintNodesByCondition FeatureGates 则在 predicates 算法中会将该四种算法移

本文档使用 书栈网 · BookStack.CN 构建 - 245 -


kube-scheduler predicates 与 priorities 调度算法源码分析

除, TaintNodesByCondition 基于 node conditions 当 node 出现 pressure 时自动为


node 打上 taints 标签,该功能在 v1.8 引入,v1.12 成为 beta 版本,目前 v1.16 中也是
beta 版本,但在 v1.13 中该功能已默认启用。

predicates 调度算法也有一个顺序,要不然在一台资源已经严重不足的宿主机上,上来就开始计算
PodAffinityPredicate 是没有实际意义的,其默认顺序如下所示:

k8s.io/kubernetes/pkg/scheduler/algorithm/predicates/predicates.go:146

1. var (
predicatesOrdering = []string{CheckNodeConditionPred,
2. CheckNodeUnschedulablePred,
3. GeneralPred, HostNamePred, PodFitsHostPortsPred,
4. MatchNodeSelectorPred, PodFitsResourcesPred, NoDiskConflictPred,
PodToleratesNodeTaintsPred, PodToleratesNodeNoExecuteTaintsPred,
5. CheckNodeLabelPresencePred,
CheckServiceAffinityPred, MaxEBSVolumeCountPred,
6. MaxGCEPDVolumeCountPred, MaxCSIVolumeCountPred,
MaxAzureDiskVolumeCountPred, MaxCinderVolumeCountPred,
7. CheckVolumeBindingPred, NoVolumeZoneConflictPred,
CheckNodeMemoryPressurePred, CheckNodePIDPressurePred,
8. CheckNodeDiskPressurePred, EvenPodsSpreadPred, MatchInterPodAffinityPred}
9. )

源码分析

上节中已经说到调用预选以及优选算法的逻辑在
k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:189 中,

func (g *genericScheduler) Schedule(pod *v1.Pod, pluginContext


1. *framework.PluginContext) (result ScheduleResult, err error) {
2. ......
3.
4. // 执行 predicates 策略
filteredNodes, failedPredicateMap, filteredNodesStatuses, err :=
5. g.findNodesThatFit(pluginContext, pod)
6.
7. ......
8.
9. // 执行 priorities 策略
priorityList, err := PrioritizeNodes(pod, g.nodeInfoSnapshot.NodeInfoMap,
metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders,
10. g.framework, pluginContext)

本文档使用 书栈网 · BookStack.CN 构建 - 246 -


kube-scheduler predicates 与 priorities 调度算法源码分析

11.
12. ......
13.
14. return
15. }

findNodesThatFit() 是 predicates 策略的实际调用方法,其基本流程如下:

设定最多需要检查的节点数,作为预选节点数组的容量,避免总节点过多影响调度效率
通过 NodeTree() 不断获取下一个节点来判断该节点是否满足 pod 的调度条件
通过之前注册的各种 predicates 函数来判断当前节点是否符合 pod 的调度条件
最后返回满足调度条件的 node 列表,供下一步的优选操作

checkNode() 是一个校验 node 是否符合要求的函数,其实际调用到的核心函数


是 podFitsOnNode() ,再通过 workqueue() 并发执行 checkNode() 函数, workqueue()
会启动 16 个 goroutine 来并行计算需要筛选的 node 列表,其主要流程如下:

通过 cache 中的 NodeTree() 不断获取下一个 node


将当前 node 和 pod 传入 podFitsOnNode() 方法中来判断当前 node 是否符合要求
如果当前 node 符合要求就将当前 node 加入预选节点的数组中 filtered
如果当前 node 不满足要求,则加入到失败的数组中,并记录原因
通过 workqueue.ParallelizeUntil() 并发执行 checkNode() 函数,一旦找到足够的可行节点
数后就停止筛选更多节点
若配置了 extender 则再次进行过滤已筛选出的 node

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:464

func (g *genericScheduler) findNodesThatFit(pluginContext


*framework.PluginContext, pod *v1.Pod) ([]*v1.Node, FailedPredicateMap,
1. framework.NodeToStatusMap, error) {
2. var filtered []*v1.Node
3. failedPredicateMap := FailedPredicateMap{}
4. filteredNodesStatuses := framework.NodeToStatusMap{}
5.
6. if len(g.predicates) == 0 {
7. filtered = g.cache.ListNodes()
8. } else {
9. allNodes := int32(g.cache.NodeTree().NumNodes())
10. // 1.设定最多需要检查的节点数
11. numNodesToFind := g.numFeasibleNodesToFind(allNodes)
12.
13. filtered = make([]*v1.Node, numNodesToFind)
14. ......

本文档使用 书栈网 · BookStack.CN 构建 - 247 -


kube-scheduler predicates 与 priorities 调度算法源码分析

15.
16. // 2.获取该 pod 的 meta 值
17. meta := g.predicateMetaProducer(pod, g.nodeInfoSnapshot.NodeInfoMap)
18.
19. // 3.checkNode 为执行预选算法的函数
20. checkNode := func(i int) {
21. nodeName := g.cache.NodeTree().Next()
22.
23. // 4.podFitsOnNode 最终执行预选算法的函数
24. fits, failedPredicates, status, err := g.podFitsOnNode(
25. ......
26. )
27. if err != nil {
28. ......
29. }
30. if fits {
31. length := atomic.AddInt32(&filteredLen, 1)
32. if length > numNodesToFind {
33. cancel()
34. atomic.AddInt32(&filteredLen, -1)
35. } else {
filtered[length-1] =
36. g.nodeInfoSnapshot.NodeInfoMap[nodeName].Node()
37. }
38. } else {
39. ......
40. }
41. }
42.
43. // 5.启动 16 个 goroutine 并发执行 checkNode 函数
44. workqueue.ParallelizeUntil(ctx, 16, int(allNodes), checkNode)
45.
46. filtered = filtered[:filteredLen]
47. if len(errs) > 0 {
48. ......
49. }
50. }
51.
52. // 6.若配置了 extender 则再次进行过滤
53. if len(filtered) > 0 && len(g.extenders) != 0 {
54. ......
55. }

本文档使用 书栈网 · BookStack.CN 构建 - 248 -


kube-scheduler predicates 与 priorities 调度算法源码分析

56. return filtered, failedPredicateMap, filteredNodesStatuses, nil


57. }

然后继续看如何设定最多需要检查的节点数,此过程由 numFeasibleNodesToFind() 进行处理,基本


流程如下:

如果总的 node 节点小于 minFeasibleNodesToFind (默认为100)则直接返回总节点数


如果节点数超过 100,则取指定百分比 percentageOfNodesToScore (默认值为 50)的节点数
,当该百分比后的数目仍小于 minFeasibleNodesToFind ,则返回 minFeasibleNodesToFind
如果百分比后的数目大于 minFeasibleNodesToFind ,则返回该百分比的节点数

所以当节点数小于 100 时直接返回,大于 100 时只返回其总数的


50%。 percentageOfNodesToScore 参数在 v1.12 引入,默认值为 50,kube-scheduler 在
启动时可以设定该参数的值。

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:441

func (g *genericScheduler) numFeasibleNodesToFind(numAllNodes int32) (numNodes


1. int32) {
if numAllNodes < minFeasibleNodesToFind || g.percentageOfNodesToScore >=
2. 100 {
3. return numAllNodes
4. }
5.
6. adaptivePercentage := g.percentageOfNodesToScore
7. if adaptivePercentage <= 0 {
adaptivePercentage = schedulerapi.DefaultPercentageOfNodesToScore -
8. numAllNodes/125
9. if adaptivePercentage < minFeasibleNodesPercentageToFind {
10. adaptivePercentage = minFeasibleNodesPercentageToFind
11. }
12. }
13.
14. numNodes = numAllNodes * adaptivePercentage / 100
15. if numNodes < minFeasibleNodesToFind {
16. return minFeasibleNodesToFind
17. }
18.
19. return numNodes
20. }

pridicates 调度算法的核心是 podFitsOnNode() ,scheduler 的抢占机制也会执行该函


数, podFitsOnNode() 基本流程如下:

本文档使用 书栈网 · BookStack.CN 构建 - 249 -


kube-scheduler predicates 与 priorities 调度算法源码分析

遍历已经注册好的预选策略 predicates.Ordering() ,按顺序执行对应的策略函数


遍历执行每个策略函数,并返回是否合适,预选失败的原因和错误
如果预选函数执行失败,则加入预选失败的数组中,直接返回,后面的预选函数不会再执行
如果该 node 上存在 nominated pod 则执行两次预选函数

因为引入了抢占机制,此处主要说明一下执行两次预选函数的原因:

第一次循环,若该 pod 为抢占者( nominatedPods ),调度器会假设该 pod 已经运行在这个节点


上,然后更新 meta 和 nodeInfo , nominatedPods 是指执行了抢占机制且已经分配到了
node( pod.Status.NominatedNodeName 已被设定) 但是还没有真正运行起来的 pod,然后再执
行所有的预选函数。

第二次循环,不将 nominatedPods 加入到 node 内。

而只有这两遍 predicates 算法都能通过时,这个 pod 和 node 才会被认为是可以绑定(bind)


的。这样做是因为考虑到 pod affinity 等策略的执行,如果当前的 pod 与 nominatedPods 有
依赖关系就会有问题,因为 nominatedPods 不能保证一定可以调度且在已指定的 node 运行成功,
也可能出现被其他高优先级的 pod 抢占等问题,关于抢占问题下篇会详细介绍。

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:610

func (g *genericScheduler) podFitsOnNode(......) (bool,


1. []predicates.PredicateFailureReason, *framework.Status, error) {
2. var failedPredicates []predicates.PredicateFailureReason
3. var status *framework.Status
4.
5. podsAdded := false
6.
7. for i := 0; i < 2; i++ {
8. metaToUse := meta
9. nodeInfoToUse := info
10. if i == 0 {
11. // 1.第一次循环加入 NominatedPods,计算 meta, nodeInfo
podsAdded, metaToUse, nodeInfoToUse = addNominatedPods(pod, meta,
12. info, queue)
13. } else if !podsAdded || len(failedPredicates) != 0 {
14. break
15. }
16. // 2.按顺序执行所有预选函数
17. for _, predicateKey := range predicates.Ordering() {
18. var (
19. fit bool
20. reasons []predicates.PredicateFailureReason

本文档使用 书栈网 · BookStack.CN 构建 - 250 -


kube-scheduler predicates 与 priorities 调度算法源码分析

21. err error


22. )
23. if predicate, exist := predicateFuncs[predicateKey]; exist {
24. fit, reasons, err = predicate(pod, metaToUse, nodeInfoToUse)
25. if err != nil {
return false, []predicates.PredicateFailureReason{}, nil,
26. err
27. }
28.
29. // 3.任何一个预选函数执行失败则直接返回
30. if !fit {
31. failedPredicates = append(failedPredicates, reasons...)
32. if !alwaysCheckAllPredicates {
klog.V(5).Infoln("since alwaysCheckAllPredicates has
33. not been set, the predicate " +
"evaluation is short circuited and there are
34. chances " +
35. "of other predicates failing as well.")
36. break
37. }
38. }
39. }
40. }
41. // 4.执行 Filter Plugin
status = g.framework.RunFilterPlugins(pluginContext, pod,
42. info.Node().Name)
43. if !status.IsSuccess() && !status.IsUnschedulable() {
44. return false, failedPredicates, status, status.AsError()
45. }
46. }
47.
return len(failedPredicates) == 0 && status.IsSuccess(), failedPredicates,
48. status, nil
49. }

至此,关于 predicates 调度算法的执行过程已经分析完。

priorities 调度算法源码分析
priorities 调度算法是在 pridicates 算法后执行的,主要功能是对已经过滤出的 nodes 进行
打分并选出最佳的一个 node。

调度算法说明

本文档使用 书栈网 · BookStack.CN 构建 - 251 -


kube-scheduler predicates 与 priorities 调度算法源码分析

默认的调度算法在 pkg/scheduler/algorithmprovider/defaults/defaults.go 中定义了:

1. func defaultPriorities() sets.String {


2. return sets.NewString(
3. priorities.SelectorSpreadPriority,
4. priorities.InterPodAffinityPriority,
5. priorities.LeastRequestedPriority,
6. priorities.BalancedResourceAllocation,
7. priorities.NodePreferAvoidPodsPriority,
8. priorities.NodeAffinityPriority,
9. priorities.TaintTolerationPriority,
10. priorities.ImageLocalityPriority,
11. )
12. }

默认调度算法的一些说明:

priorities 算法 说明

按 service,rs,statefulset 归属计算 Node 上分布最少的同类


SelectorSpreadPriority
Pod数量,数量越少得分越高,默认权重为1

InterPodAffinityPriority pod 亲和性选择策略,默认权重为1

选择空闲资源(CPU 和 Memory)最多的节点,默认权重为1,其计算方
式为:score = (cpu((capacity-
LeastRequestedPriority
sum(requested))10/capacity) + memory((capacity-
sum(requested))10/capacity))/2

CPU、Memory 以及 Volume 资源分配最均衡的节点,默认权重为1,其


BalancedResourceAllocation 计算方式为:score = 10 -
variance(cpuFraction,memoryFraction,volumeFraction)*10

判断 node annotation 是否有


NodePreferAvoidPodsPriority scheduler.alpha.kubernetes.io/preferAvoidPods 标签,类
似于 taints 机制,过滤标签中定义类型的 pod,默认权重为10000

NodeAffinityPriority 节点亲和性选择策略,默认权重为1

Pod 是否容忍节点上的 Taint,优先调度到标记了 Taint 的节点,默


TaintTolerationPriority
认权重为1

ImageLocalityPriority 待调度 Pod 需要使用的镜像是否存在于该节点,默认权重为1

源码分析

执行 priorities 调度算法的逻辑是在 PrioritizeNodes() 函数中,其目的是执行每个


priority 函数为 node 打分,分数为 0-10,其功能主要有:

PrioritizeNodes() 通过并行运行各个优先级函数来对节点进行打分
每个优先级函数会给节点打分,打分范围为 0-10 分,0 表示优先级最低的节点,10表示优先
级最高的节点

本文档使用 书栈网 · BookStack.CN 构建 - 252 -


kube-scheduler predicates 与 priorities 调度算法源码分析

每个优先级函数有各自的权重
优先级函数返回的节点分数乘以权重以获得加权分数
最后计算所有节点的总加权分数

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:691

1. func PrioritizeNodes(......) (schedulerapi.HostPriorityList, error) {


2. // 1.检查是否有自定义配置
3. if len(priorityConfigs) == 0 && len(extenders) == 0 {
4. result := make(schedulerapi.HostPriorityList, 0, len(nodes))
5. for i := range nodes {
hostPriority, err := EqualPriorityMap(pod, meta,
6. nodeNameToInfo[nodes[i].Name])
7. if err != nil {
8. return nil, err
9. }
10. result = append(result, hostPriority)
11. }
12. return result, nil
13. }
14. ......
15.
results := make([]schedulerapi.HostPriorityList, len(priorityConfigs),
16. len(priorityConfigs))
17.
18. ......
19. // 2.使用 workqueue 启动 16 个 goroutine 并发为 node 打分
workqueue.ParallelizeUntil(context.TODO(), 16, len(nodes), func(index int)
20. {
21. nodeInfo := nodeNameToInfo[nodes[index].Name]
22. for i := range priorityConfigs {
23. if priorityConfigs[i].Function != nil {
24. continue
25. }
26.
27. var err error
results[i][index], err = priorityConfigs[i].Map(pod, meta,
28. nodeInfo)
29. if err != nil {
30. appendError(err)
31. results[i][index].Host = nodes[index].Name
32. }
33. }

本文档使用 书栈网 · BookStack.CN 构建 - 253 -


kube-scheduler predicates 与 priorities 调度算法源码分析

34. })
35.
36. // 3.执行自定义配置
37. for i := range priorityConfigs {
38. ......
39. }
40.
41. wg.Wait()
42. if len(errs) != 0 {
43. return schedulerapi.HostPriorityList{}, errors.NewAggregate(errs)
44. }
45.
46. // 4.运行 Score plugins
scoresMap, scoreStatus := framework.RunScorePlugins(pluginContext, pod,
47. nodes)
48. if !scoreStatus.IsSuccess() {
49. return schedulerapi.HostPriorityList{}, scoreStatus.AsError()
50. }
51.
52. result := make(schedulerapi.HostPriorityList, 0, len(nodes))
53. // 5.为每个 node 汇总分数
54. for i := range nodes {
result = append(result, schedulerapi.HostPriority{Host: nodes[i].Name,
55. Score: 0})
56. for j := range priorityConfigs {
57. result[i].Score += results[j][i].Score * priorityConfigs[j].Weight
58. }
59.
60. for j := range scoresMap {
61. result[i].Score += scoresMap[j][i].Score
62. }
63. }
64.
65. // 6.执行 extender
66. if len(extenders) != 0 && nodes != nil {
67. ......
68. }
69. ......
70. return result, nil
71. }

总结

本文档使用 书栈网 · BookStack.CN 构建 - 254 -


kube-scheduler predicates 与 priorities 调度算法源码分析

本文主要讲述了 kube-scheduler 中的 predicates 调度算法与 priorities 调度算法的执行


流程,可以看到 kube-scheduler 中有许多的调度策略,但是想要添加自己的策略并不容易,
scheduler 目前已经朝着提升性能与扩展性的方向演进了,其调度部分进行性能优化的一个最根本原
则就是尽最大可能将集群信息 cache 化,以便从根本上提高 predicates 和 priorities 调度
算法的执行效率。第二个就是在 bind 阶段进行异步处理,只会更新其 cache 里的 pod 和 node
的信息,这种基于“乐观”假设的 API 对象更新方式,在 kubernetes 里被称作 assume,如果这
次异步的 bind 过程失败了,其实也没有太大关系,等 scheduler cache 同步之后一切又恢复正
常了。除了上述的“cache 化”和“乐观绑定”,还有一个重要的设计,那就是“无锁化”,predicates
调度算法与 priorities 调度算法的执行都是并行的,只有在调度队列和 scheduler cache 进行
操作时,才需要加锁,而对调度队列的操作并不影响主流程。

参考:

https://kubernetes.io/docs/concepts/configuration/scheduling-framework/

predicates-ordering.md

本文档使用 书栈网 · BookStack.CN 构建 - 255 -


kube-scheduler 优先级与抢占机制源码分析

前面已经分析了 kube-scheduler 的代码逻辑以及 predicates 与 priorities 算法,本节会


继续讲 scheduler 中的一个重要机制,pod 优先级与抢占机制(Pod Priority and
Preemption),该功能是在 v1.8 中引入的,v1.11 中该功能为 beta 版本且默认启用了,v1.14
为 stable 版本。

kube-scheduler 源码分析
kube-scheduler predicates 与 priorities 调度算法源码分析

为什么要有优先级与抢占机制
正常情况下,当一个 pod 调度失败后,就会被暂时 “搁置” 处于 pending 状态,直到 pod 被
更新或者集群状态发生变化,调度器才会对这个 pod 进行重新调度。但在实际的业务场景中会存在在
线与离线业务之分,若在线业务的 pod 因资源不足而调度失败时,此时就需要离线业务下掉一部分为
在线业务提供资源,即在线业务要抢占离线业务的资源,此时就需要 scheduler 的优先级和抢占机
制了,该机制解决的是 pod 调度失败时该怎么办的问题,若该 pod 的优先级比较高此时并不会
被”搁置”,而是会”挤走”某个 node 上的一些低优先级的 pod,这样就可以保证高优先级的 pod 调
度成功。

优先级与抢占机制源码分析

kubernetes 版本: v1.16

抢占发生的原因,一定是一个高优先级的 pod 调度失败,我们称这个 pod 为“抢占者”,称被抢占的


pod 为“牺牲者”(victims)。而 kubernetes 调度器实现抢占算法的一个最重要的设计,就是在调
度队列的实现里,使用了两个不同的队列。

第一个队列叫作 activeQ,凡是在 activeQ 里的 pod,都是下一个调度周期需要调度的对象。所


以,当你在 kubernetes 集群里新创建一个 pod 的时候,调度器会将这个 pod 入队到 activeQ
里面,调度器不断从队列里出队(pop)一个 pod 进行调度,实际上都是从 activeQ 里出队的。

第二个队列叫作 unschedulableQ,专门用来存放调度失败的 pod,当一个 unschedulableQ 里


的 pod 被更新之后,调度器会自动把这个 pod 移动到 activeQ 里,从而给这些调度失败的 pod
“重新做人”的机会。

当 pod 拥有了优先级之后,高优先级的 pod 就可能会比低优先级的 pod 提前出队,从而尽早完成


调度过程。

k8s.io/kubernetes/pkg/scheduler/internal/queue/scheduling_queue.go

1. // NewSchedulingQueue initializes a priority queue as a new scheduling queue.


func NewSchedulingQueue(stop <-chan struct{}, fwk framework.Framework)
2. SchedulingQueue {
3. return NewPriorityQueue(stop, fwk)

本文档使用 书栈网 · BookStack.CN 构建 - 256 -


kube-scheduler 优先级与抢占机制源码分析

4. }
5. // NewPriorityQueue creates a PriorityQueue object.
func NewPriorityQueue(stop <-chan struct{}, fwk framework.Framework)
6. *PriorityQueue {
7. return NewPriorityQueueWithClock(stop, util.RealClock{}, fwk)
8. }
9.
// NewPriorityQueueWithClock creates a PriorityQueue which uses the passed
10. clock for time.
func NewPriorityQueueWithClock(stop <-chan struct{}, clock util.Clock, fwk
11. framework.Framework) *PriorityQueue {
12. comp := activeQComp
13. if fwk != nil {
14. if queueSortFunc := fwk.QueueSortFunc(); queueSortFunc != nil {
15. comp = func(podInfo1, podInfo2 interface{}) bool {
16. pInfo1 := podInfo1.(*framework.PodInfo)
17. pInfo2 := podInfo2.(*framework.PodInfo)
18.
19. return queueSortFunc(pInfo1, pInfo2)
20. }
21. }
22. }
23.
24. pq := &PriorityQueue{
25. clock: clock,
26. stop: stop,
27. podBackoff: NewPodBackoffMap(1*time.Second, 10*time.Second),
activeQ: util.NewHeapWithRecorder(podInfoKeyFunc, comp,
28. metrics.NewActivePodsRecorder()),
unschedulableQ:
29. newUnschedulablePodsMap(metrics.NewUnschedulablePodsRecorder()),
30. nominatedPods: newNominatedPodMap(),
31. moveRequestCycle: -1,
32. }
33. pq.cond.L = &pq.lock
pq.podBackoffQ = util.NewHeapWithRecorder(podInfoKeyFunc,
34. pq.podsCompareBackoffCompleted, metrics.NewBackoffPodsRecorder())
35.
36. pq.run()
37.
38. return pq
39. }

本文档使用 书栈网 · BookStack.CN 构建 - 257 -


kube-scheduler 优先级与抢占机制源码分析

前面的文章已经说了 scheduleOne() 是执行调度算法的主逻辑,其主要功能有:

调用 sched.schedule() ,即执行 predicates 算法和 priorities 算法


若执行失败,会返回 core.FitError
若开启了抢占机制,则执行抢占机制
……

k8s.io/kubernetes/pkg/scheduler/scheduler.go:516

1. func (sched *Scheduler) scheduleOne() {


2. ......
3. scheduleResult, err := sched.schedule(pod, pluginContext)
4. // predicates 算法和 priorities 算法执行失败
5. if err != nil {
6. if fitError, ok := err.(*core.FitError); ok {
7. // 是否开启抢占机制
8. if sched.DisablePreemption {
9. .......
10. } else {
11. // 执行抢占机制
12. preemptionStartTime := time.Now()
13. sched.preempt(pluginContext, fwk, pod, fitError)
14. ......
15. }
16. ......
17. } else {
18. ......
19. }
20. return
21. }
22. ......
23. }

我们主要来看其中的抢占机制, sched.preempt() 是执行抢占机制的主逻辑,主要功能有:

从 apiserver 获取 pod info


调用 sched.Algorithm.Preempt() 执行抢占逻辑,该函数会返回抢占成功的 node、被抢占的
pods(victims) 以及需要被移除已提名的 pods
更新 scheduler 缓存,为抢占者绑定 nodeName,即设定
pod.Status.NominatedNodeName
将 pod info 提交到 apiserver
删除被抢占的 pods

本文档使用 书栈网 · BookStack.CN 构建 - 258 -


kube-scheduler 优先级与抢占机制源码分析

删除被抢占 pods 的 NominatedNodeName 字段

可以看到当上述抢占过程发生时,抢占者并不会立刻被调度到被抢占的 node 上,调度器只会将抢占


者的 status.nominatedNodeName 字段设置为被抢占的 node 的名字。然后,抢占者会重新进入
下一个调度周期,在新的调度周期里来决定是不是要运行在被抢占的节点上,当然,即使在下一个调度
周期,调度器也不会保证抢占者一定会运行在被抢占的节点上。

这样设计的一个重要原因是调度器只会通过标准的 DELETE API 来删除被抢占的 pod,所以,这些


pod 必然是有一定的“优雅退出”时间(默认是 30s)的。而在这段时间里,其他的节点也是有可能变
成可调度的,或者直接有新的节点被添加到这个集群中来。所以,鉴于优雅退出期间集群的可调度性可
能会发生的变化,把抢占者交给下一个调度周期再处理,是一个非常合理的选择。而在抢占者等待被调
度的过程中,如果有其他更高优先级的 pod 也要抢占同一个节点,那么调度器就会清空原抢占者的
status.nominatedNodeName 字段,从而允许更高优先级的抢占者执行抢占,并且,这也使得原抢
占者本身也有机会去重新抢占其他节点。以上这些都是设置 nominatedNodeName 字段的主要目
的。

k8s.io/kubernetes/pkg/scheduler/scheduler.go:352

func (sched *Scheduler) preempt(pluginContext *framework.PluginContext, fwk


1. framework.Framework, preemptor *v1.Pod, scheduleErr error) (string, error) {
2. // 获取 pod info
3. preemptor, err := sched.PodPreemptor.GetUpdatedPod(preemptor)
4. if err != nil {
5. klog.Errorf("Error getting the updated preemptor pod object: %v", err)
6. return "", err
7. }
8.
9. // 执行抢占算法
node, victims, nominatedPodsToClear, err :=
10. sched.Algorithm.Preempt(pluginContext, preemptor, scheduleErr)
11. if err != nil {
12. ......
13. }
14. var nodeName = ""
15. if node != nil {
16. nodeName = node.Name
// 更新 scheduler 缓存,为抢占者绑定 nodename,即设定
17. pod.Status.NominatedNodeName
18. sched.SchedulingQueue.UpdateNominatedPodForNode(preemptor, nodeName)
19.
20. // 将 pod info 提交到 apiserver
21. err = sched.PodPreemptor.SetNominatedNodeName(preemptor, nodeName)
22. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 259 -


kube-scheduler 优先级与抢占机制源码分析

23. sched.SchedulingQueue.DeleteNominatedPodIfExists(preemptor)
24. return "", err
25. }
26. // 删除被抢占的 pods
27. for _, victim := range victims {
28. if err := sched.PodPreemptor.DeletePod(victim); err != nil {
29. return "", err
30. }
31. ......
32. }
33. }
34.
35. // 删除被抢占 pods 的 NominatedNodeName 字段
36. for _, p := range nominatedPodsToClear {
37. rErr := sched.PodPreemptor.RemoveNominatedNodeName(p)
38. if rErr != nil {
39. ......
40. }
41. }
42. return nodeName, err
43. }

preempt() 中会调用 sched.Algorithm.Preempt() 来执行实际抢占的算法,其主要功能有:

判断 err 是否为 FitError


调用 podEligibleToPreemptOthers() 确认 pod 是否有抢占其他 pod 的资格,若 pod 已经
抢占了低优先级的 pod,被抢占的 pod 处于 terminating 状态中,则不会继续进行抢占
如果确定抢占可以发生,调度器会把自己缓存的所有节点信息复制一份,然后使用这个副本来模
拟抢占过程
过滤预选失败的 node 列表,此处会检查 predicates 失败的原因,若存在
NodeSelectorNotMatch、PodNotMatchHostName 这些 error 则不能成为抢占者,如果
过滤出的候选 node 为空则返回抢占者作为 nominatedPodsToClear
获取 PodDisruptionBudget 对象
从预选失败的 node 列表中并发计算可以被抢占的 nodes,得到 nodeToVictims
若声明了 extenders 则调用 extenders 再次过滤 nodeToVictims
调用 pickOneNodeForPreemption() 从 nodeToVictims 中选出一个节点作为最佳候选人
移除低优先级 pod 的 Nominated ,更新这些 pod,移动到 activeQ 队列中,让调度器为
这些 pod 重新 bind node

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:320

本文档使用 书栈网 · BookStack.CN 构建 - 260 -


kube-scheduler 优先级与抢占机制源码分析

func (g *genericScheduler) Preempt(pluginContext *framework.PluginContext, pod


1. *v1.Pod, scheduleErr error) (*v1.Node, []*v1.Pod, []*v1.Pod, error) {
2. fitError, ok := scheduleErr.(*FitError)
3. if !ok || fitError == nil {
4. return nil, nil, nil, nil
5. }
// 判断 pod 是否支持抢占,若 pod 已经抢占了低优先级的 pod,被抢占的 pod 处于
6. terminating 状态中,则不会继续进行抢占
if !podEligibleToPreemptOthers(pod, g.nodeInfoSnapshot.NodeInfoMap,
7. g.enableNonPreempting) {
8. return nil, nil, nil, nil
9. }
10. // 从缓存中获取 node list
11. allNodes := g.cache.ListNodes()
12. if len(allNodes) == 0 {
13. return nil, nil, nil, ErrNoNodesAvailable
14. }
15. // 过滤 predicates 算法执行失败的 node 作为抢占的候选 node
16. potentialNodes := nodesWherePreemptionMightHelp(allNodes, fitError)
17. // 如果过滤出的候选 node 为空则返回抢占者作为 nominatedPodsToClear
18. if len(potentialNodes) == 0 {
19. return nil, nil, []*v1.Pod{pod}, nil
20. }
21. // 获取 PodDisruptionBudget objects
22. pdbs, err := g.pdbLister.List(labels.Everything())
23. if err != nil {
24. return nil, nil, nil, err
25. }
26. // 过滤出可以抢占的 node 列表
nodeToVictims, err := g.selectNodesForPreemption(pluginContext, pod,
27. g.nodeInfoSnapshot.NodeInfoMap, potentialNodes, g.predicates,
28. g.predicateMetaProducer, g.schedulingQueue, pdbs)
29. if err != nil {
30. return nil, nil, nil, err
31. }
32.
33. // 若有 extender 则执行
34. nodeToVictims, err = g.processPreemptionWithExtenders(pod, nodeToVictims)
35. if err != nil {
36. return nil, nil, nil, err
37. }
38.

本文档使用 书栈网 · BookStack.CN 构建 - 261 -


kube-scheduler 优先级与抢占机制源码分析

39. // 选出最佳的 node


40. candidateNode := pickOneNodeForPreemption(nodeToVictims)
41. if candidateNode == nil {
42. return nil, nil, nil, nil
43. }
44.
45. // 移除低优先级 pod 的 Nominated,更新这些 pod,移动到 activeQ 队列中,让调度器
46. // 为这些 pod 重新 bind node
47. nominatedPods := g.getLowerPriorityNominatedPods(pod, candidateNode.Name)
48. if nodeInfo, ok := g.nodeInfoSnapshot.NodeInfoMap[candidateNode.Name]; ok {
return nodeInfo.Node(), nodeToVictims[candidateNode].Pods,
49. nominatedPods, nil
50. }
51.
52. return nil, nil, nil, fmt.Errorf(
"preemption failed: the target node %s has been deleted from scheduler
53. cache",
54. candidateNode.Name)
55. }

该函数中调用了多个函数:

nodesWherePreemptionMightHelp() :过滤 predicates 算法执行失败的 node


selectNodesForPreemption() :过滤出可以抢占的 node 列表
pickOneNodeForPreemption() :选出最佳的 node
getLowerPriorityNominatedPods() :移除低优先级 pod 的 Nominated

selectNodesForPreemption() 从 prediacates 算法执行失败的 node 列表中来寻找可以被抢


占的 node,通过 workqueue.ParallelizeUntil() 并发执行 checkNode() 函数检查 node。

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:996

1. func (g *genericScheduler) selectNodesForPreemption(


2. ......
3. ) (map[*v1.Node]*schedulerapi.Victims, error) {
4. nodeToVictims := map[*v1.Node]*schedulerapi.Victims{}
5. var resultLock sync.Mutex
6.
7. meta := metadataProducer(pod, nodeNameToInfo)
8. // checkNode 函数
9. checkNode := func(i int) {
10. nodeName := potentialNodes[i].Name
11. var metaCopy predicates.PredicateMetadata

本文档使用 书栈网 · BookStack.CN 构建 - 262 -


kube-scheduler 优先级与抢占机制源码分析

12. if meta != nil {


13. metaCopy = meta.ShallowCopy()
14. }
15. // 调用 selectVictimsOnNode 函数进行检查
pods, numPDBViolations, fits := g.selectVictimsOnNode(pluginContext,
16. pod, metaCopy, nodeNameToInfo[nodeName], fitPredicates, queue, pdbs)
17. if fits {
18. resultLock.Lock()
19. victims := schedulerapi.Victims{
20. Pods: pods,
21. NumPDBViolations: numPDBViolations,
22. }
23. nodeToVictims[potentialNodes[i]] = &victims
24. resultLock.Unlock()
25. }
26. }
27. // 启动 16 个 goroutine 并发执行
workqueue.ParallelizeUntil(context.TODO(), 16, len(potentialNodes),
28. checkNode)
29. return nodeToVictims, nil
30. }

其中调用的 selectVictimsOnNode() 是来获取每个 node 上 victims pod 的,首先移除所有低


优先级的 pod 尝试抢占者是否可以调度成功,如果能够调度成功,然后基于 pod 是否有 PDB 被分
为两组 violatingVictims 和 nonViolatingVictims ,再对每一组的 pod 按优先级进行排
序。PDB(pod 中断预算)是 kubernetes 保证副本高可用的一个对象。

然后开始逐一”删除“ pod 即要删掉最少的 pod 数来完成这次抢占即可,先从


violatingVictims (有PDB)的一组中进行”删除“ pod,并且记录删除有 PDB pod 的数量,然后
再“删除” nonViolatingVictims 组中的 pod,每次”删除“一个 pod 都要检查一下抢占者是否
能够运行在该 node 上即执行一次预选策略,若执行预选策略失败则该 node 当前不满足抢占需要继
续”删除“ pod 并将该 pod 加入到 victims 中,直到”删除“足够多的 pod 可以满足抢占,最后
返回 victims 以及删除有 PDB pod 的数量。

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:1086

1. func (g *genericScheduler) selectVictimsOnNode(


2. ......
3. ) ([]*v1.Pod, int, bool) {
4. if nodeInfo == nil {
5. return nil, 0, false
6. }

本文档使用 书栈网 · BookStack.CN 构建 - 263 -


kube-scheduler 优先级与抢占机制源码分析

7.
8. potentialVictims := util.SortableList{CompFunc: util.MoreImportantPod}
9. nodeInfoCopy := nodeInfo.Clone()
10.
11. removePod := func(rp *v1.Pod) {
12. nodeInfoCopy.RemovePod(rp)
13. if meta != nil {
14. meta.RemovePod(rp, nodeInfoCopy.Node())
15. }
16. }
17. addPod := func(ap *v1.Pod) {
18. nodeInfoCopy.AddPod(ap)
19. if meta != nil {
20. meta.AddPod(ap, nodeInfoCopy)
21. }
22. }
23. // 先删除所有的低优先级 pod 检查是否能满足抢占 pod 的调度需求
24. podPriority := util.GetPodPriority(pod)
25. for _, p := range nodeInfoCopy.Pods() {
26. if util.GetPodPriority(p) < podPriority {
27. potentialVictims.Items = append(potentialVictims.Items, p)
28. removePod(p)
29. }
30. }
31. // 如果删除所有低优先级的 pod 不符合要求则直接过滤掉该 node
32. // podFitsOnNode 就是前文讲过用来执行预选函数的
if fits, _, _, err := g.podFitsOnNode(pluginContext, pod, meta,
33. nodeInfoCopy, fitPredicates, queue, false); !fits {
34. if err != nil {
35. ......
36. }
37. return nil, 0, false
38. }
39. var victims []*v1.Pod
40. numViolatingVictim := 0
41. potentialVictims.Sort()
42.
// 尝试尽量多地“删除”这些 pods,先从 PDB violating victims 中“删除”,再从 PDB
43. non-violating victims 中“删除”
violatingVictims, nonViolatingVictims :=
44. filterPodsWithPDBViolation(potentialVictims.Items, pdbs)
45.
46. // reprievePod 是“删除” pods 的函数

本文档使用 书栈网 · BookStack.CN 构建 - 264 -


kube-scheduler 优先级与抢占机制源码分析

47. reprievePod := func(p *v1.Pod) bool {


48. addPod(p)
49. // 同样也会调用 podFitsOnNode 再次执行 predicates 算法
fits, _, _, _ := g.podFitsOnNode(pluginContext, pod, meta,
50. nodeInfoCopy, fitPredicates, queue, false)
51. if !fits {
52. removePod(p)
53. // 加入到 victims 中
54. victims = append(victims, p)
55. }
56. return fits
57. }
58. // 删除 violatingVictims 中的 pod,同时也记录删除了多少个
59. for _, p := range violatingVictims {
60. if !reprievePod(p) {
61. numViolatingVictim++
62. }
63. }
64. // 删除 nonViolatingVictims 中的 pod
65. for _, p := range nonViolatingVictims {
66. reprievePod(p)
67. }
68. return victims, numViolatingVictim, true
69. }

pickOneNodeForPreemption() 用来选出最佳的 node 作为抢占者的 node,该函数主要基于 6


个原则:

PDB violations 值最小的 node


挑选具有高优先级较少的 node
对每个 node 上所有 victims 的优先级进项累加,选取最小的
如果多个 node 优先级总和相等,选择具有最小 victims 数量的 node
如果多个 node 优先级总和相等,选择具有高优先级且 pod 运行时间最短的
如果依据以上策略仍然选出了多个 node 则直接返回第一个 node

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:867

func pickOneNodeForPreemption(nodesToVictims
1. map[*v1.Node]*schedulerapi.Victims) *v1.Node {
2. if len(nodesToVictims) == 0 {
3. return nil
4. }
5. minNumPDBViolatingPods := math.MaxInt32

本文档使用 书栈网 · BookStack.CN 构建 - 265 -


kube-scheduler 优先级与抢占机制源码分析

6. var minNodes1 []*v1.Node


7. lenNodes1 := 0
8. for node, victims := range nodesToVictims {
9. if len(victims.Pods) == 0 {
10. // 若该 node 没有 victims 则返回
11. return node
12. }
13. numPDBViolatingPods := victims.NumPDBViolations
14. if numPDBViolatingPods < minNumPDBViolatingPods {
15. minNumPDBViolatingPods = numPDBViolatingPods
16. minNodes1 = nil
17. lenNodes1 = 0
18. }
19. if numPDBViolatingPods == minNumPDBViolatingPods {
20. minNodes1 = append(minNodes1, node)
21. lenNodes1++
22. }
23. }
24. if lenNodes1 == 1 {
25. return minNodes1[0]
26. }
27.
28. // 选出 PDB violating pods 数量最少的或者高优先级 victim 数量少的
29. minHighestPriority := int32(math.MaxInt32)
30. var minNodes2 = make([]*v1.Node, lenNodes1)
31. lenNodes2 := 0
32. for i := 0; i < lenNodes1; i++ {
33. node := minNodes1[i]
34. victims := nodesToVictims[node]
35. highestPodPriority := util.GetPodPriority(victims.Pods[0])
36. if highestPodPriority < minHighestPriority {
37. minHighestPriority = highestPodPriority
38. lenNodes2 = 0
39. }
40. if highestPodPriority == minHighestPriority {
41. minNodes2[lenNodes2] = node
42. lenNodes2++
43. }
44. }
45. if lenNodes2 == 1 {
46. return minNodes2[0]
47. }

本文档使用 书栈网 · BookStack.CN 构建 - 266 -


kube-scheduler 优先级与抢占机制源码分析

48. // 若多个 node 高优先级的 pod 同样少,则选出加权得分最小的


49. minSumPriorities := int64(math.MaxInt64)
50. lenNodes1 = 0
51. for i := 0; i < lenNodes2; i++ {
52. var sumPriorities int64
53. node := minNodes2[i]
54. for _, pod := range nodesToVictims[node].Pods {
sumPriorities += int64(util.GetPodPriority(pod)) +
55. int64(math.MaxInt32+1)
56. }
57. if sumPriorities < minSumPriorities {
58. minSumPriorities = sumPriorities
59. lenNodes1 = 0
60. }
61. if sumPriorities == minSumPriorities {
62. minNodes1[lenNodes1] = node
63. lenNodes1++
64. }
65. }
66. if lenNodes1 == 1 {
67. return minNodes1[0]
68. }
69. // 若多个 node 高优先级的 pod 数量同等且加权分数相等,则选出 pod 数量最少的
70. minNumPods := math.MaxInt32
71. lenNodes2 = 0
72. for i := 0; i < lenNodes1; i++ {
73. node := minNodes1[i]
74. numPods := len(nodesToVictims[node].Pods)
75. if numPods < minNumPods {
76. minNumPods = numPods
77. lenNodes2 = 0
78. }
79. if numPods == minNumPods {
80. minNodes2[lenNodes2] = node
81. lenNodes2++
82. }
83. }
84. if lenNodes2 == 1 {
85. return minNodes2[0]
86. }
87. // 若多个 node 的 pod 数量相等,则选出高优先级 pod 启动时间最短的
latestStartTime :=
88. util.GetEarliestPodStartTime(nodesToVictims[minNodes2[0]])

本文档使用 书栈网 · BookStack.CN 构建 - 267 -


kube-scheduler 优先级与抢占机制源码分析

89. if latestStartTime == nil {


90. return minNodes2[0]
91. }
92. nodeToReturn := minNodes2[0]
93. for i := 1; i < lenNodes2; i++ {
94. node := minNodes2[i]
earliestStartTimeOnNode :=
95. util.GetEarliestPodStartTime(nodesToVictims[node])
96. if earliestStartTimeOnNode == nil {
klog.Errorf("earliestStartTime is nil for node %s. Should not reach
97. here.", node)
98. continue
99. }
100. if earliestStartTimeOnNode.After(latestStartTime.Time) {
101. latestStartTime = earliestStartTimeOnNode
102. nodeToReturn = node
103. }
104. }
105.
106. return nodeToReturn
107. }

以上就是对抢占机制代码的一个通读。

优先级与抢占机制的使用
1、创建 PriorityClass 对象:

1. apiVersion: scheduling.k8s.io/v1
2. kind: PriorityClass
3. metadata:
4. name: high-priority
5. value: 1000000
6. globalDefault: false
7. description: "This priority class should be used for XYZ service pods only."

2、在 deployment、statefulset 或者 pod 中声明使用已有的 priorityClass 对象即可

在 pod 中使用:

1. apiVersion: v1
2. kind: Pod

本文档使用 书栈网 · BookStack.CN 构建 - 268 -


kube-scheduler 优先级与抢占机制源码分析

3. metadata:
4. labels:
5. app: nginx-a
6. name: nginx-a
7. spec:
8. containers:
9. - image: nginx:1.7.9
10. imagePullPolicy: IfNotPresent
11. name: nginx-a
12. ports:
13. - containerPort: 80
14. protocol: TCP
15. resources:
16. requests:
17. memory: "64Mi"
18. cpu: 5
19. limits:
20. memory: "128Mi"
21. cpu: 5
22. priorityClassName: high-priority

在 deployment 中使用:

1. template:
2. spec:
3. containers:
4. - image: nginx
5. name: nginx-deployment
6. priorityClassName: high-priority

3、测试过程中可以看到高优先级的 nginx-a 会抢占 nginx-5754944d6c 的资源:

1. $ kubectl get pod -o wide -w


NAME READY STATUS RESTARTS AGE IP NODE
2. NOMINATED NODE READINESS GATES
nginx-5754944d6c-9mnxa 1/1 Running 0 37s 10.244.1.4 test-
3. worker <none> <none>
nginx-a 0/1 Pending 0 0s <none> <none>
4. <none> <none>
nginx-a 0/1 Pending 0 0s <none> <none>
5. <none> <none>

本文档使用 书栈网 · BookStack.CN 构建 - 269 -


kube-scheduler 优先级与抢占机制源码分析

nginx-a 0/1 Pending 0 0s <none> <none>


6. test-worker <none>
nginx-5754944d6c-9mnxa 1/1 Terminating 0 45s 10.244.1.4
7. test-worker <none> <none>
nginx-5754944d6c-9mnxa 0/1 Terminating 0 46s 10.244.1.4
8. test-worker <none> <none>
nginx-5754944d6c-9mnxa 0/1 Terminating 0 47s 10.244.1.4
9. test-worker <none> <none>
nginx-5754944d6c-9mnxa 0/1 Terminating 0 47s 10.244.1.4
10. test-worker <none> <none>
nginx-a 0/1 Pending 0 2s <none>
11. test-worker test-worker <none>
nginx-a 0/1 ContainerCreating 0 2s <none>
12. test-worker <none> <none>
nginx-a 1/1 Running 0 4s
13. 10.244.1.5 test-worker <none> <none>

总结
这篇文章主要讲述 kube-scheduler 中的优先级与抢占机制,可以看到抢占机制比 predicates
与 priorities 算法都要复杂,其中的许多细节仍然没有提到,本文只是通读了大部分代码,某些代
码的实现需要精读,限于笔者时间的关系,对于 kube-scheduler 的代码暂时分享到此处。

参考:

https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/

本文档使用 书栈网 · BookStack.CN 构建 - 270 -


kubernetes service 原理解析

为什么需要 service
在 kubernetes 中,当创建带有多个副本的 deployment 时,kubernetes 会创建出多个 pod,
此时即一个服务后端有多个容器,那么在 kubernetes 中负载均衡怎么做,容器漂移后 ip 也会发
生变化,如何做服务发现以及会话保持?这就是 service 的作用,service 是一组具有相同
label pod 集合的抽象,集群内外的各个服务可以通过 service 进行互相通信,当创建一个
service 对象时也会对应创建一个 endpoint 对象,endpoint 是用来做容器发现的,service
只是将多个 pod 进行关联,实际的路由转发都是由 kubernetes 中的 kube-proxy 组件来实现,
因此,service 必须结合 kube-proxy 使用,kube-proxy 组件可以运行在 kubernetes 集群
中的每一个节点上也可以只运行在单独的几个节点上,其会根据 service 和 endpoints 的变动来
改变节点上 iptables 或者 ipvs 中保存的路由规则。

service 的工作原理

endpoints controller 是负责生成和维护所有 endpoints 对象的控制器,监听 service 和


对应 pod 的变化,更新对应 service 的 endpoints 对象。当用户创建 service 后
endpoints controller 会监听 pod 的状态,当 pod 处于 running 且准备就绪时,
endpoints controller 会将 pod ip 记录到 endpoints 对象中,因此,service 的容器发

本文档使用 书栈网 · BookStack.CN 构建 - 271 -


kubernetes service 原理解析

现是通过 endpoints 来实现的。而 kube-proxy 会监听 service 和 endpoints 的更新并调


用其代理模块在主机上刷新路由转发规则。

service 的负载均衡
上文已经提到 service 实际的路由转发都是由 kube-proxy 组件来实现的,service 仅以一种
VIP(ClusterIP) 的形式存在,kube-proxy 主要实现了集群内部从 pod 到 service 和集群
外部从 nodePort 到 service 的访问,kube-proxy 的路由转发规则是通过其后端的代理模块实
现的,kube-proxy 的代理模块目前有四种实现方案,userspace、iptables、ipvs、
kernelspace,其发展历程如下所示:

kubernetes v1.0:services 仅是一个“4层”代理,代理模块只有 userspace


kubernetes v1.1:Ingress API 出现,其代理“7层”服务,并且增加了 iptables 代理
模块
kubernetes v1.2:iptables 成为默认代理模式
kubernetes v1.8:引入 ipvs 代理模块
kubernetes v1.9:ipvs 代理模块成为 beta 版本
kubernetes v1.11:ipvs 代理模式 GA

在每种模式下都有自己的负载均衡策略,下文会详解介绍。

userspace 模式

在 userspace 模式下,访问服务的请求到达节点后首先进入内核 iptables,然后回到用户空间,


由 kube-proxy 转发到后端的 pod,这样流量从用户空间进出内核带来的性能损耗是不可接受的,
所以也就有了 iptables 模式。

为什么 userspace 模式要建立 iptables 规则,因为 kube-proxy 监听的端口在用户空间,这


个端口不是服务的访问端口也不是服务的 nodePort,因此需要一层 iptables 把访问服务的连接重
定向给 kube-proxy 服务。

本文档使用 书栈网 · BookStack.CN 构建 - 272 -


kubernetes service 原理解析

iptables 模式

iptables 模式是目前默认的代理方式,基于 netfilter 实现。当客户端请求 service 的


ClusterIP 时,根据 iptables 规则路由到各 pod 上,iptables 使用 DNAT 来完成转发,其
采用了随机数实现负载均衡。

iptables 模式与 userspace 模式最大的区别在于,iptables 模块使用 DNAT 模块实现了


service 入口地址到 pod 实际地址的转换,免去了一次内核态到用户态的切换,另一个与
userspace 代理模式不同的是,如果 iptables 代理最初选择的那个 pod 没有响应,它不会自动
重试其他 pod。

iptables 模式最主要的问题是在 service 数量大的时候会产生太多的 iptables 规则,使用非


增量式更新会引入一定的时延,大规模情况下有明显的性能问题。

本文档使用 书栈网 · BookStack.CN 构建 - 273 -


kubernetes service 原理解析

ipvs 模式

当集群规模比较大时,iptables 规则刷新会非常慢,难以支持大规模集群,因其底层路由表的实现是
链表,对路由规则的增删改查都要涉及遍历一次链表,ipvs 的问世正是解决此问题的,ipvs 是 LVS
的负载均衡模块,与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,
但是它却使用哈希表作为底层的数据结构并且工作在内核态,也就是说 ipvs 在重定向流量和同步代
理规则有着更好的性能,几乎允许无限的规模扩张。

ipvs 支持三种负载均衡模式:DR模式(Direct Routing)、NAT 模式(Network Address


Translation)、Tunneling(也称 ipip 模式)。三种模式中只有 NAT 支持端口映射,所以
ipvs 使用 NAT 模式。linux 内核原生的 ipvs 只支持 DNAT,当在数据包过滤,SNAT 和支持
NodePort 类型的服务这几个场景中ipvs 还是会使用 iptables。

此外,ipvs 也支持更多的负载均衡算法,例如:

rr:round-robin/轮询
lc:least connection/最少连接
dh:destination hashing/目标哈希
sh:source hashing/源哈希

本文档使用 书栈网 · BookStack.CN 构建 - 274 -


kubernetes service 原理解析

sed:shortest expected delay/预计延迟时间最短


nq:never queue/从不排队

userspace、iptables、ipvs 三种模式中默认的负载均衡策略都是通过 round-robin 算法来选


择后端 pod 的,在 service 中可以通过设置 service.spec.sessionAffinity 的值实现基于
客户端 ip 的会话亲和性, service.spec.sessionAffinity 的值默认为”None”,可以设置为
“ClientIP”,此外也可以使用
service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 设置会话保持时间。
kernelspace 主要是在 windows 下使用的,本文暂且不谈。

service 的类型
service 支持的类型也就是 kubernetes 中服务暴露的方式,默认有四种 ClusterIP、
NodePort、LoadBalancer、ExternelName,此外还有 Ingress,下面会详细介绍每种类型
service 的具体使用场景。

ClusterIP

ClusterIP 类型的 service 是 kubernetes 集群默认的服务暴露方式,它只能用于集群内部通


信,可以被各 pod 访问,其访问方式为:

1. pod ---> ClusterIP:ServicePort --> (iptables)DNAT --> PodIP:containePort

ClusterIP Service 类型的结构如下图所示:

本文档使用 书栈网 · BookStack.CN 构建 - 275 -


kubernetes service 原理解析

NodePort

如果你想要在集群外访问集群内部的服务,可以使用这种类型的 service,NodePort 类型的


service 会在集群内部署了 kube-proxy 的节点打开一个指定的端口,之后所有的流量直接发送到
这个端口,然后会被转发到 service 后端真实的服务进行访问。Nodeport 构建在 ClusterIP
上,其访问链路如下所示:

client ---> NodeIP:NodePort ---> ClusterIP:ServicePort ---> (iptables)DNAT --->


1. PodIP:containePort

其对应具体的 iptables 规则会在后文进行讲解。

本文档使用 书栈网 · BookStack.CN 构建 - 276 -


kubernetes service 原理解析

NodePort service 类型的结构如下图所示:

LoadBalancer

LoadBalancer 类型的 service 通常和云厂商的 LB 结合一起使用,用于将集群内部的服务暴露


到外网,云厂商的 LoadBalancer 会给用户分配一个 IP,之后通过该 IP 的流量会转发到你的
service 上。

LoadBalancer service 类型的结构如下图所示:

本文档使用 书栈网 · BookStack.CN 构建 - 277 -


kubernetes service 原理解析

ExternelName

通过 CNAME 将 service 与 externalName 的值(比如:foo.bar.example.com)映射起来,


这种方式用的比较少。

Ingress

Ingress 其实不是 service 的一个类型,但是它可以作用于多个 service,被称为 service 的


service,作为集群内部服务的入口,Ingress 作用在七层,可以根据不同的 url,将请求转发到不
同的 service 上。

本文档使用 书栈网 · BookStack.CN 构建 - 278 -


kubernetes service 原理解析

Ingress 的结构如下图所示:

service 的服务发现
虽然 service 的 endpoints 解决了容器发现问题,但不提前知道 service 的 Cluster IP,
怎么发现 service 服务呢?service 当前支持两种类型的服务发现机制,一种是通过环境变量,另
一种是通过 DNS。在这两种方案中,建议使用后者。

环境变量

当一个 pod 创建完成之后,kubelet 会在该 pod 中注册该集群已经创建的所有 service 相关的


环境变量,但是需要注意的是,在 service 创建之前的所有 pod 是不会注册该环境变量的,所以在
平时使用时,建议通过 DNS 的方式进行 service 之间的服务发现。

DNS

可以在集群中部署 CoreDNS 服务(旧版本的 kubernetes 群使用的是 kubeDNS), 来达到集群内


部的 pod 通过DNS 的方式进行集群内部各个服务之间的通讯。

当前 kubernetes 集群默认使用 CoreDNS 作为默认的 DNS 服务,主要原因是 CoreDNS 是基于


Plugin 的方式进行扩展的,简单,灵活,并且不完全被Kubernetes所捆绑。

本文档使用 书栈网 · BookStack.CN 构建 - 279 -


kubernetes service 原理解析

service 的使用

ClusterIP 方式

1. apiVersion: v1
2. kind: Service
3. metadata:
4. name: my-nginx
5. spec:
6. clusterIP: 10.105.146.177
7. ports:
8. - port: 80
9. protocol: TCP
10. targetPort: 8080
11. selector:
12. app: my-nginx
13. sessionAffinity: None
14. type: ClusterIP

NodePort 方式

1. apiVersion: v1
2. kind: Service
3. metadata:
4. name: my-nginx
5. spec:
6. ports:
7. - nodePort: 30090
8. port: 80
9. protocol: TCP
10. targetPort: 8080
11. selector:
12. app: my-nginx
13. sessionAffinity: None
14. type: NodePort

其中 nodeport 字段表示通过 nodeport 方式访问的端口, port 表示通过 service 方式


访问的端口, targetPort 表示 container port。

Headless service(就是没有 Cluster IP 的 service )

当不需要负载均衡以及单独的 ClusterIP 时,可以通过指定 spec.clusterIP 的值为 None

本文档使用 书栈网 · BookStack.CN 构建 - 280 -


kubernetes service 原理解析

来创建 Headless service,它会给一个集群内部的每个成员提供一个唯一的 DNS 域名来作为每个


成员的网络标识,集群内部成员之间使用域名通信。

1. apiVersion: v1
2. kind: Service
3. metadata:
4. name: my-nginx
5. spec:
6. clusterIP: None
7. ports:
8. - nodePort: 30090
9. port: 80
10. protocol: TCP
11. targetPort: 8080
12. selector:
13. app: my-nginx

总结
本文主要讲了 kubernetes 中 service 的原理、实现以及使用方式,service 目前主要有 5 种
服务暴露方式,service 的容器发现是通过 endpoints 来实现的,其服务发现主要是通过 DNS 实
现的,其负载均衡以及流量转发是通过 kube-proxy 实现的。在后面的文章我会继续介绍 kube-
proxy 的设计及实现。

参考:

https://www.cnblogs.com/xzkzzz/p/9559362.html

https://xigang.github.io/2019/07/21/kubernetes-service/

本文档使用 书栈网 · BookStack.CN 构建 - 281 -


kube-proxy 源码分析

上篇文章 kubernetes service 原理解析 已经分析了 service 原理以 kube-proxy 中三种模


式的原理,本篇文章会从源码角度分析 kube-proxy 的设计与实现。

kubernetes 版本: v1.16

kube-proxy 启动流程
前面的文章已经说过 kubernetes 中所有组件都是通过其 run() 方法启动主逻辑的, run()
方法调用之前会进行解析命令行参数、添加默认值等。下面就直接看 kube-proxy 的 run() 方
法:

若启动时指定了 --write-config-to 参数,kube-proxy 只将启动的默认参数写到指定的配


置文件中,然后退出
初始化 ProxyServer 对象
如果启动参数 --cleanup 设置为 true,则清理 iptables 和 ipvs 规则并退出

k8s.io/kubernetes/cmd/kube-proxy/app/server.go:290

1. func (o *Options) Run() error {


2. defer close(o.errCh)
3. // 1.如果指定了 --write-config-to 参数,则将默认的配置文件写到指定文件并退出
4. if len(o.WriteConfigTo) > 0 {
5. return o.writeConfigFile()
6. }
7.
8. // 2.初始化 ProxyServer 对象
9. proxyServer, err := NewProxyServer(o)
10. if err != nil {
11. return err
12. }
13.
14. // 3.如果启动参数 --cleanup 设置为 true,则清理 iptables 和 ipvs 规则并退出
15. if o.CleanupAndExit {
16. return proxyServer.CleanupAndExit()
17. }
18.
19. o.proxyServer = proxyServer
20. return o.runLoop()
21. }

Run() 方法中主要调用了 NewProxyServer() 方法来初始化 ProxyServer,然后会调用

本文档使用 书栈网 · BookStack.CN 构建 - 282 -


kube-proxy 源码分析

runLoop() 启动主循环,继续看初始化 ProxyServer 的具体实现:

初始化 iptables、ipvs 相关的 interface


若启用了 ipvs 则检查内核版本、ipvs 依赖的内核模块、ipset 版本,内核模块主要包
括: ip_vs , ip_vs_rr , ip_vs_wrr , ip_vs_sh , nf_conntrack_ipv4 , nf_conntrack
,若没有相关模块,kube-proxy 会尝试使用 modprobe 自动加载
根据 proxyMode 初始化 proxier,kube-proxy 启动后只运行一种 proxier

k8s.io/kubernetes/cmd/kube-proxy/app/server_others.go:57

1. func NewProxyServer(o *Options) (*ProxyServer, error) {


2. return newProxyServer(o.config, o.CleanupAndExit, o.master)
3. }
4.
5. func newProxyServer(
6. config *proxyconfigapi.KubeProxyConfiguration,
7. cleanupAndExit bool,
8. master string) (*ProxyServer, error) {
9. ......
10.
11. if c, err := configz.New(proxyconfigapi.GroupName); err == nil {
12. c.Set(config)
13. } else {
14. return nil, fmt.Errorf("unable to register configz: %s", err)
15. }
16.
17. ......
18.
19. // 1.关键依赖工具 iptables/ipvs/ipset/dbus
20. var iptInterface utiliptables.Interface
21. var ipvsInterface utilipvs.Interface
22. var kernelHandler ipvs.KernelHandler
23. var ipsetInterface utilipset.Interface
24. var dbus utildbus.Interface
25.
26. // 2.执行 linux 命令行的工具
27. execer := exec.New()
28.
29. // 3.初始化 iptables/ipvs/ipset/dbus 对象
30. dbus = utildbus.New()
31. iptInterface = utiliptables.New(execer, dbus, protocol)
32. kernelHandler = ipvs.NewLinuxKernelHandler()
33. ipsetInterface = utilipset.New(execer)

本文档使用 书栈网 · BookStack.CN 构建 - 283 -


kube-proxy 源码分析

34.
35. // 4.检查该机器是否支持使用 ipvs 模式
36. canUseIPVS, _ := ipvs.CanUseIPVSProxier(kernelHandler, ipsetInterface)
37. if canUseIPVS {
38. ipvsInterface = utilipvs.New(execer)
39. }
40.
41. if cleanupAndExit {
42. return &ProxyServer{
43. ......
44. }, nil
45. }
46.
47. // 5.初始化 kube client 和 event client
48. client, eventClient, err := createClients(config.ClientConnection, master)
49. if err != nil {
50. return nil, err
51. }
52. ......
53.
54. // 6.初始化 healthzServer
55. var healthzServer *healthcheck.HealthzServer
56. var healthzUpdater healthcheck.HealthzUpdater
57. if len(config.HealthzBindAddress) > 0 {
healthzServer =
healthcheck.NewDefaultHealthzServer(config.HealthzBindAddress,
58. 2*config.IPTables.SyncPeriod.Duration, recorder, nodeRef)
59. healthzUpdater = healthzServer
60. }
61.
62. // 7.proxier 是一个 interface,每种模式都是一个 proxier
63. var proxier proxy.Provider
64.
65. // 8.根据 proxyMode 初始化 proxier
proxyMode := getProxyMode(string(config.Mode), kernelHandler,
66. ipsetInterface, iptables.LinuxKernelCompatTester{})
67. ......
68.
69. if proxyMode == proxyModeIPTables {
70. klog.V(0).Info("Using iptables Proxier.")
71. if config.IPTables.MasqueradeBit == nil {
return nil, fmt.Errorf("unable to read IPTables MasqueradeBit from
72. config")

本文档使用 书栈网 · BookStack.CN 构建 - 284 -


kube-proxy 源码分析

73. }
74.
75. // 9.初始化 iptables 模式的 proxier
76. proxier, err = iptables.NewProxier(
77. .......
78. )
79. if err != nil {
80. return nil, fmt.Errorf("unable to create proxier: %v", err)
81. }
82. metrics.RegisterMetrics()
83. } else if proxyMode == proxyModeIPVS {
84. // 10.判断是够启用了 ipv6 双栈
85. if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) {
86. ......
87. // 11.初始化 ipvs 模式的 proxier
88. proxier, err = ipvs.NewDualStackProxier(
89. ......
90. )
91. } else {
92. proxier, err = ipvs.NewProxier(
93. ......
94. )
95. }
96. if err != nil {
97. return nil, fmt.Errorf("unable to create proxier: %v", err)
98. }
99. metrics.RegisterMetrics()
100. } else {
101. // 12.初始化 userspace 模式的 proxier
102. proxier, err = userspace.NewProxier(
103. ......
104. )
105. if err != nil {
106. return nil, fmt.Errorf("unable to create proxier: %v", err)
107. }
108. }
109.
110. iptInterface.AddReloadFunc(proxier.Sync)
111. return &ProxyServer{
112. ......
113. }, nil
114. }

本文档使用 书栈网 · BookStack.CN 构建 - 285 -


kube-proxy 源码分析

runLoop() 方法主要是启动 proxyServer。

k8s.io/kubernetes/cmd/kube-proxy/app/server.go:311

1. func (o *Options) runLoop() error {


2. // 1.watch 配置文件变化
3. if o.watcher != nil {
4. o.watcher.Run()
5. }
6.
7. // 2.以 goroutine 方式启动 proxyServer
8. go func() {
9. err := o.proxyServer.Run()
10. o.errCh <- err
11. }()
12.
13. for {
14. err := <-o.errCh
15. if err != nil {
16. return err
17. }
18. }
19. }

o.proxyServer.Run() 中会启动已经初始化好的所有服务:

设定进程 OOMScore,可通过命令行配置,默认值为 --oom-score-adj="-999"


启动 metric server 和 healthz server,两者分别监听 10256 和 10249 端口
设置内核参数 nf_conntrack_tcp_timeout_established 和
nf_conntrack_tcp_timeout_close_wait
将 proxier 注册到 serviceEventHandler、endpointsEventHandler 中
启动 informer 监听 service 和 endpoints 变化
执行 s.Proxier.SyncLoop() ,启动 proxier 主循环

k8s.io/kubernetes/cmd/kube-proxy/app/server.go:527

1. func (s *ProxyServer) Run() error {


2. ......
3.
4. // 1.进程 OOMScore,避免进程因 oom 被杀掉,此处默认值为 -999
5. var oomAdjuster *oom.OOMAdjuster
6. if s.OOMScoreAdj != nil {
7. oomAdjuster = oom.NewOOMAdjuster()

本文档使用 书栈网 · BookStack.CN 构建 - 286 -


kube-proxy 源码分析

if err := oomAdjuster.ApplyOOMScoreAdj(0, int(*s.OOMScoreAdj)); err !=


8. nil {
9. klog.V(2).Info(err)
10. }
11. }
12. ......
13.
14. // 2.启动 healthz server
15. if s.HealthzServer != nil {
16. s.HealthzServer.Run()
17. }
18.
19. // 3.启动 metrics server
20. if len(s.MetricsBindAddress) > 0 {
21. ......
22. go wait.Until(func() {
23. err := http.ListenAndServe(s.MetricsBindAddress, proxyMux)
24. if err != nil {
utilruntime.HandleError(fmt.Errorf("starting metrics server
25. failed: %v", err))
26. }
27. }, 5*time.Second, wait.NeverStop)
28. }
29.
// 4.配置 conntrack,设置内核参数 nf_conntrack_tcp_timeout_established 和
30. nf_conntrack_tcp_timeout_close_wait
31. if s.Conntracker != nil {
32. max, err := getConntrackMax(s.ConntrackConfiguration)
33. if err != nil {
34. return err
35. }
36. if max > 0 {
37. err := s.Conntracker.SetMax(max)
38. ......
39. }
40.
if s.ConntrackConfiguration.TCPEstablishedTimeout != nil &&
41. s.ConntrackConfiguration.TCPEstablishedTimeout.Duration > 0 {
timeout :=
42. int(s.ConntrackConfiguration.TCPEstablishedTimeout.Duration / time.Second)
if err := s.Conntracker.SetTCPEstablishedTimeout(timeout); err !=
43. nil {
44. return err

本文档使用 书栈网 · BookStack.CN 构建 - 287 -


kube-proxy 源码分析

45. }
46. }
if s.ConntrackConfiguration.TCPCloseWaitTimeout != nil &&
47. s.ConntrackConfiguration.TCPCloseWaitTimeout.Duration > 0 {
timeout :=
48. int(s.ConntrackConfiguration.TCPCloseWaitTimeout.Duration / time.Second)
if err := s.Conntracker.SetTCPCloseWaitTimeout(timeout); err != nil
49. {
50. return err
51. }
52. }
53. }
54.
55. ......
56.
57. // 5.启动 informer 监听 Services 和 Endpoints 或者 EndpointSlices 信息
informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client,
58. s.ConfigSyncPeriod,
59. informers.WithTweakListOptions(func(options *metav1.ListOptions) {
60. options.LabelSelector = labelSelector.String()
61. }))
62.
63.
64. // 6.将 proxier 注册到 serviceConfig、endpointsConfig 中
serviceConfig :=
config.NewServiceConfig(informerFactory.Core().V1().Services(),
65. s.ConfigSyncPeriod)
66. serviceConfig.RegisterEventHandler(s.Proxier)
67. go serviceConfig.Run(wait.NeverStop)
68.
69. if utilfeature.DefaultFeatureGate.Enabled(features.EndpointSlice) {
endpointSliceConfig :=
config.NewEndpointSliceConfig(informerFactory.Discovery().V1alpha1().EndpointSlices
70. s.ConfigSyncPeriod)
71. endpointSliceConfig.RegisterEventHandler(s.Proxier)
72. go endpointSliceConfig.Run(wait.NeverStop)
73. } else {
endpointsConfig :=
config.NewEndpointsConfig(informerFactory.Core().V1().Endpoints(),
74. s.ConfigSyncPeriod)
75. endpointsConfig.RegisterEventHandler(s.Proxier)
76. go endpointsConfig.Run(wait.NeverStop)
77. }

本文档使用 书栈网 · BookStack.CN 构建 - 288 -


kube-proxy 源码分析

78.
79. // 7.启动 informer
80. informerFactory.Start(wait.NeverStop)
81.
82. s.birthCry()
83.
84. // 8.启动 proxier 主循环
85. s.Proxier.SyncLoop()
86. return nil
87. }

回顾一下整个启动逻辑:

1. o.Run() --> o.runLoop() --> o.proxyServer.Run() --> s.Proxier.SyncLoop()

o.Run() 中调用了 NewProxyServer() 来初始化 proxyServer 对象,其中包括初始化每种


模式对应的 proxier,该方法最终会调用 s.Proxier.SyncLoop() 执行 proxier 的主循环。

proxier 的初始化
看完了启动流程的逻辑代码,接着再看一下各代理模式的初始化,上文已经提到每种模式都是一个
proxier,即要实现 proxy.Provider 对应的 interface,如下所示:

1. type Provider interface {


2. config.EndpointsHandler
3. config.EndpointSliceHandler
4. config.ServiceHandler
5.
6. Sync()
7. SyncLoop()
8. }

首先要实现 service、endpoints 和 endpointSlice 对应的 handler,也就是对


OnAdd 、 OnUpdate 、 OnDelete 、 OnSynced 四种方法的处理,详细的代码在下文进行
讲解。EndpointSlice 是在 v1.16 中新加入的一个 API。 Sync() 和 SyncLoop() 是主
要用来处理iptables 规则的方法。

iptables proxier 初始化


首先看 iptables 模式的 NewProxier() 方法,其函数的具体执行逻辑为:

本文档使用 书栈网 · BookStack.CN 构建 - 289 -


kube-proxy 源码分析

设置相关的内核参数 route_localnet 、 bridge-nf-call-iptables


生成 masquerade 标记
设置默认调度算法 rr
初始化 proxier 对象
使用 BoundedFrequencyRunner 初始化 proxier.syncRunner,将
proxier.syncProxyRules 方法注入, BoundedFrequencyRunner 是一个管理器用于执行
用户注入的函数,可以指定运行的时间策略。

k8s.io/kubernetes/pkg/proxy/iptables/proxier.go:249

1. func NewProxier(ipt utiliptables.Interface,


2. ......
3. ) (*Proxier, error) {
4. // 1.设置相关的内核参数
5. if val, _ := sysctl.GetSysctl(sysctlRouteLocalnet); val != 1 {
6. ......
7. }
8.
if val, err := sysctl.GetSysctl(sysctlBridgeCallIPTables); err == nil &&
9. val != 1 {
10. ......
11. }
12.
13. // 2.设置 masqueradeMark,默认为 0x00004000/0x00004000
14. // 用来标记 k8s 管理的报文,masqueradeBit 默认为 14
15. // 标记 0x4000 的报文(即 POD 发出的报文),在离开 Node 的时候需要进行 SNAT 转换
16. masqueradeValue := 1 << uint(masqueradeBit)
masqueradeMark := fmt.Sprintf("%#08x/%#08x", masqueradeValue,
17. masqueradeValue)
18.
19. ......
20.
endpointSlicesEnabled :=
21. utilfeature.DefaultFeatureGate.Enabled(features.EndpointSlice)
22.
23. healthChecker := healthcheck.NewServer(hostname, recorder, nil, nil)
24.
25. // 3.初始化 proxier
26. isIPv6 := ipt.IsIpv6()
27. proxier := &Proxier{
28. ......
29. }
30. burstSyncs := 2

本文档使用 书栈网 · BookStack.CN 构建 - 290 -


kube-proxy 源码分析

31.
32. // 4.初始化 syncRunner,BoundedFrequencyRunner 是一个定时执行器,会定时执行
// proxier.syncProxyRules 方法,syncProxyRules 是每个 proxier 实际刷新iptables
33. 规则的方法
proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner",
34. proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
35. return proxier, nil
36. }

ipvs proxier 初始化


ipvs NewProxier() 方法主要逻辑为:

设定内核参数, route_localnet 、 br_netfilter 、 bridge-nf-call-


iptables 、 conntrack 、 conn_reuse_mode 、 ip_forward 、 arp_ignore 、 arp_anno
unce 等
和 iptables 一样,对于 SNAT iptables 规则生成 masquerade 标记
设置默认调度算法 rr
初始化 proxier 对象
初始化 ipset 规则
初始化 syncRunner 将 proxier.syncProxyRules 方法注入
启动 gracefuldeleteManager 定时清理 RS (realServer) 记录

k8s.io/kubernetes/pkg/proxy/ipvs/proxier.go:316

1. func NewProxier(ipt utiliptables.Interface,


2. ......
3. ) (*Proxier, error) {
4.
5. // 1.设定内核参数
6. if val, _ := sysctl.GetSysctl(sysctlRouteLocalnet); val != 1 {
7. ......
8. }
9. ......
10.
11. // 2.生成 masquerade 标记
12. masqueradeValue := 1 << uint(masqueradeBit)
masqueradeMark := fmt.Sprintf("%#08x/%#08x", masqueradeValue,
13. masqueradeValue)
14.
15. // 3.设置默认调度算法 rr
16. if len(scheduler) == 0 {

本文档使用 书栈网 · BookStack.CN 构建 - 291 -


kube-proxy 源码分析

17. scheduler = DefaultScheduler


18. }
19.
healthChecker := healthcheck.NewServer(hostname, recorder, nil, nil) // use
20. default implementations of deps
21.
endpointSlicesEnabled :=
22. utilfeature.DefaultFeatureGate.Enabled(features.EndpointSlice)
23.
24. // 4.初始化 proxier
25. proxier := &Proxier{
26. ......
27. }
28. // 5.初始化 ipset 规则
29. proxier.ipsetList = make(map[string]*IPSet)
30. for _, is := range ipsetInfo {
proxier.ipsetList[is.name] = NewIPSet(ipset, is.name, is.setType,
31. isIPv6, is.comment)
32. }
33. burstSyncs := 2
34.
35. // 6.初始化 syncRunner
proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner",
36. proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
37.
38. // 7.启动 gracefuldeleteManager
39. proxier.gracefuldeleteManager.Run()
40. return proxier, nil
41. }

userspace proxier 初始化


userspace NewProxier() 方法主要逻辑为:

初始化 iptables 规则
初始化 proxier
初始化 syncRunner 将 proxier.syncProxyRules 方法注入

k8s.io/kubernetes/pkg/proxy/userspace/proxier.go:187

1. func NewProxier(......) (*Proxier, error) {


return NewCustomProxier(loadBalancer, listenIP, iptables, exec, pr,
2. syncPeriod, minSyncPeriod, udpIdleTimeout, nodePortAddresses, newProxySocket)

本文档使用 书栈网 · BookStack.CN 构建 - 292 -


kube-proxy 源码分析

3. }
4.
5. func NewCustomProxier(......) (*Proxier, error) {
6. ......
7.
8. // 1.设置打开文件数
9. err = setRLimit(64 * 1000)
10. if err != nil {
return nil, fmt.Errorf("failed to set open file handler limit: %v",
11. err)
12. }
13.
14. proxyPorts := newPortAllocator(pr)
15.
return createProxier(loadBalancer, listenIP, iptables, exec, hostIP,
16. proxyPorts, syncPeriod, minSyncPeriod, udpIdleTimeout, makeProxySocket)
17. }
18.
func createProxier(loadBalancer LoadBalancer, listenIP net.IP, iptables
iptables.Interface, exec utilexec.Interface, hostIP net.IP, proxyPorts
PortAllocator, syncPeriod, minSyncPeriod, udpIdleTimeout time.Duration,
19. makeProxySocket ProxySocketFunc) (*Proxier, error) {
20. if proxyPorts == nil {
21. proxyPorts = newPortAllocator(utilnet.PortRange{})
22. }
23.
24. // 2.初始化 iptables 规则
25. if err := iptablesInit(iptables); err != nil {
26. return nil, fmt.Errorf("failed to initialize iptables: %v", err)
27. }
28.
29. if err := iptablesFlush(iptables); err != nil {
30. return nil, fmt.Errorf("failed to flush iptables: %v", err)
31. }
32.
33. // 3.初始化 proxier
34. proxier := &Proxier{
35. ......
36. }
37.
38. // 4.初始化 syncRunner
proxier.syncRunner = async.NewBoundedFrequencyRunner("userspace-proxy-sync-
39. runner", proxier.syncProxyRules, minSyncPeriod, syncPeriod, numBurstSyncs)

本文档使用 书栈网 · BookStack.CN 构建 - 293 -


kube-proxy 源码分析

40. return proxier, nil


41. }

proxier 接口实现

handler 的实现
上文已经提到过每种 proxier 都需要实现 interface 中的几个方法,首先看一下
ServiceHandler 、 EndpointsHandler 和 EndpointSliceHandler 相关的,对于
service、endpoints 和 endpointSlices 三种对象都实现了
OnAdd 、 OnUpdate 、 OnDelete 和 OnSynced 方法。

1. // 1.service 相关的方法
2. func (proxier *Proxier) OnServiceAdd(service *v1.Service) {
3. proxier.OnServiceUpdate(nil, service)
4. }
5.
6. func (proxier *Proxier) OnServiceUpdate(oldService, service *v1.Service) {
if proxier.serviceChanges.Update(oldService, service) &&
7. proxier.isInitialized() {
8. proxier.syncRunner.Run()
9. }
10. }
11.
12. func (proxier *Proxier) OnServiceDelete(service *v1.Service) {
13. proxier.OnServiceUpdate(service, nil)
14. }
15.
16. func (proxier *Proxier) OnServiceSynced(){
17. ......
18. proxier.syncProxyRules()
19. }
20.
21. // 2.endpoints 相关的方法
22. func (proxier *Proxier) OnEndpointsAdd(endpoints *v1.Endpoints) {
23. proxier.OnEndpointsUpdate(nil, endpoints)
24. }
25.
func (proxier *Proxier) OnEndpointsUpdate(oldEndpoints, endpoints
26. *v1.Endpoints) {

本文档使用 书栈网 · BookStack.CN 构建 - 294 -


kube-proxy 源码分析

if proxier.endpointsChanges.Update(oldEndpoints, endpoints) &&


27. proxier.isInitialized() {
28. proxier.Sync()
29. }
30. }
31.
32. func (proxier *Proxier) OnEndpointsDelete(endpoints *v1.Endpoints) {
33. proxier.OnEndpointsUpdate(endpoints, nil)
34. }
35.
36. func (proxier *Proxier) OnEndpointsSynced() {
37. ......
38. proxier.syncProxyRules()
39. }
40.
41. // 3.endpointSlice 相关的方法
func (proxier *Proxier) OnEndpointSliceAdd(endpointSlice
42. *discovery.EndpointSlice) {
if proxier.endpointsChanges.EndpointSliceUpdate(endpointSlice, false) &&
43. proxier.isInitialized() {
44. proxier.Sync()
45. }
46. }
47.
func (proxier *Proxier) OnEndpointSliceUpdate(_, endpointSlice
48. *discovery.EndpointSlice) {
if proxier.endpointsChanges.EndpointSliceUpdate(endpointSlice, false) &&
49. proxier.isInitialized() {
50. proxier.Sync()
51. }
52. }
53.
func (proxier *Proxier) OnEndpointSliceDelete(endpointSlice
54. *discovery.EndpointSlice) {
if proxier.endpointsChanges.EndpointSliceUpdate(endpointSlice, true) &&
55. proxier.isInitialized() {
56. proxier.Sync()
57. }
58. }
59.
60. func (proxier *Proxier) OnEndpointSlicesSynced() {
61. ......
62. proxier.syncProxyRules()

本文档使用 书栈网 · BookStack.CN 构建 - 295 -


kube-proxy 源码分析

63. }

在启动逻辑的 Run() 方法中 proxier 已经被注册到了 serviceConfig、


endpointsConfig、endpointSliceConfig 中,当启动 informer,cache 同步完成后会调用
OnSynced() 方法,之后当 watch 到变化后会调用 proxier 中对应的 OnUpdate() 方法进
行处理, OnSynced() 会直接调用 proxier.syncProxyRules() 来刷新iptables 规则,而
OnUpdate() 会调用 proxier.syncRunner.Run() 方法,其最终也是调用
proxier.syncProxyRules() 方法刷新规则的,这种转换是在 BoundedFrequencyRunner 中
体现出来的,下面看一下具体实现。

Sync() 以及 SyncLoop() 的实现


每种 proxier 的 Sync() 以及 SyncLoop() 方法如下所示,都是调用 syncRunner 中的
相关方法,而 syncRunner 在前面的 NewProxier() 中已经说过了,syncRunner 是调用
async.NewBoundedFrequencyRunner() 方法初始化,至此,基本上可以确定了所有的核心都是在
BoundedFrequencyRunner 中实现的。

1. func NewProxier() (*Proxier, error) {


2. ......
proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner",
3. proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
4. ......
5. }
6.
7. // Sync()
8. func (proxier *Proxier) Sync() {
9. proxier.syncRunner.Run()
10. }
11.
12. // SyncLoop()
13. func (proxier *Proxier) SyncLoop() {
14. if proxier.healthzServer != nil {
15. proxier.healthzServer.UpdateTimestamp()
16. }
17. proxier.syncRunner.Loop(wait.NeverStop)
18. }

NewBoundedFrequencyRunner() 是其初始化的函数,其中的参数 minInterval 和


maxInterval 分别对应 proxier 中的 minSyncPeriod 和 syncPeriod ,两者的默认值
分别为 0s 和 30s,其值可以使用 --iptables-min-sync-period 和 --iptables-sync-
period 启动参数来指定。

本文档使用 书栈网 · BookStack.CN 构建 - 296 -


kube-proxy 源码分析

k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:134

func NewBoundedFrequencyRunner(name string, fn func(), minInterval, maxInterval


1. time.Duration, burstRuns int) *BoundedFrequencyRunner {
2. timer := realTimer{Timer: time.NewTimer(0)}
3. // 执行定时器
4. <-timer.C()
5. // 调用 construct() 函数
6. return construct(name, fn, minInterval, maxInterval, burstRuns, timer)
7. }
8.
func construct(name string, fn func(), minInterval, maxInterval time.Duration,
9. burstRuns int, timer timer) *BoundedFrequencyRunner {
10. if maxInterval < minInterval {
panic(fmt.Sprintf("%s: maxInterval (%v) must be >= minInterval (%v)",
11. name, maxInterval, minInterval))
12. }
13. if timer == nil {
14. panic(fmt.Sprintf("%s: timer must be non-nil", name))
15. }
16.
17. bfr := &BoundedFrequencyRunner{
18. name: name,
19. fn: fn, // 被调用的函数,proxier.syncProxyRules
20. minInterval: minInterval,
21. maxInterval: maxInterval,
22. run: make(chan struct{}, 1),
23. timer: timer,
24. }
25. // 由于默认的 minInterval = 0,此处使用 nullLimiter
26. if minInterval == 0 {
27. bfr.limiter = nullLimiter{}
28. } else {
29. // 采用“令牌桶”算法实现流控机制
30. qps := float32(time.Second) / float32(minInterval)
bfr.limiter = flowcontrol.NewTokenBucketRateLimiterWithClock(qps,
31. burstRuns, timer)
32. }
33. return bfr
34. }

在启动流程 Run() 方法最后调用的 s.Proxier.SyncLoop() 最终调用的是


BoundedFrequencyRunner 的 Loop() 方法,如下所示:

本文档使用 书栈网 · BookStack.CN 构建 - 297 -


kube-proxy 源码分析

k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:169

1. func (bfr *BoundedFrequencyRunner) Loop(stop <-chan struct{}) {


2. bfr.timer.Reset(bfr.maxInterval)
3. for {
4. select {
5. case <-stop:
6. bfr.stop()
7. return
8. case <-bfr.timer.C(): // 定时器
9. bfr.tryRun()
10. case <-bfr.run: // 接收 channel
11. bfr.tryRun()
12. }
13. }
14. }

proxier 的 OnUpdate() 中调用的 syncRunner.Run() 其实只是在 bfr.run 这个带


buffer 的 channel 中发送了一条数据,在 BoundedFrequencyRunner 的 Loop() 方法中
接收到该数据后会调用 bfr.tryRun() 进行处理:

k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:191

1. func (bfr *BoundedFrequencyRunner) Run() {


2. select {
3. case bfr.run <- struct{}{}: // 向 channel 发送信号
4. default:
5. }
6. }

而 tryRun() 方法才是最终调用 syncProxyRules() 刷新iptables 规则的。

k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:211

1. func (bfr *BoundedFrequencyRunner) tryRun() {


2. bfr.mu.Lock()
3. defer bfr.mu.Unlock()
4.
5. if bfr.limiter.TryAccept() {
6. // 执行 fn() 即 syncProxyRules() 刷新iptables 规则
7. bfr.fn()
8. bfr.lastRun = bfr.timer.Now()

本文档使用 书栈网 · BookStack.CN 构建 - 298 -


kube-proxy 源码分析

9. bfr.timer.Stop()
10. bfr.timer.Reset(bfr.maxInterval)
11. return
12. }
13.
14. elapsed := bfr.timer.Since(bfr.lastRun) // how long since last run
15. nextPossible := bfr.minInterval - elapsed // time to next possible run
16. nextScheduled := bfr.maxInterval - elapsed // time to next periodic run
17.
18. if nextPossible < nextScheduled {
19. bfr.timer.Stop()
20. bfr.timer.Reset(nextPossible)
21. }
22. }

通过以上分析可知, syncProxyRules() 是每个 proxier 的核心方法,启动 informer cache


同步完成后会直接调用 proxier.syncProxyRules() 刷新iptables 规则,之后如果 informer
watch 到相关对象的变化后会调用 BoundedFrequencyRunner 的 tryRun() 来刷新
iptables 规则,定时器每 30s 会执行一次iptables 规则的刷新。

总结
本文主要介绍了 kube-proxy 的启动逻辑以及三种模式 proxier 的初始化,还有最终调用刷新
iptables 规则的 BoundedFrequencyRunner,可以看到其中的代码写的很巧妙。而每种模式下的
iptables 规则是如何创建、刷新以及转发的是如何实现的会在后面的文章中进行分析。

本文档使用 书栈网 · BookStack.CN 构建 - 299 -


kube-proxy iptables 模式源码分析

iptables 的功能
在前面的文章中已经介绍过 iptable 的一些基本信息,本文会深入介绍 kube-proxy iptables
模式下的工作原理,本文中多处会与 iptables 的知识相关联,若没有 iptables 基础,请先自行
补充。

iptables 的功能:

流量转发:DNAT 实现 IP 地址和端口的映射;
负载均衡:statistic 模块为每个后端设置权重;
会话保持:recent 模块设置会话保持时间;

iptables 有五张表和五条链,五条链分别对应为:

PREROUTING 链:数据包进入路由之前,可以在此处进行 DNAT;


INPUT 链:一般处理本地进程的数据包,目的地址为本机;
FORWARD 链:一般处理转发到其他机器或者 network namespace 的数据包;
OUTPUT 链:原地址为本机,向外发送,一般处理本地进程的输出数据包;
POSTROUTING 链:发送到网卡之前,可以在此处进行 SNAT;

五张表分别为:

filter 表:用于控制到达某条链上的数据包是继续放行、直接丢弃(drop)还是拒绝
(reject);
nat 表:network address translation 网络地址转换,用于修改数据包的源地址和目的
地址;
mangle 表:用于修改数据包的 IP 头信息;
raw 表:iptables 是有状态的,其对数据包有链接追踪机制,连接追踪信息在
/proc/net/nf_conntrack 中可以看到记录,而 raw 是用来去除链接追踪机制的;
security 表:最不常用的表,用在 SELinux 上;

这五张表是对 iptables 所有规则的逻辑集群且是有顺序的,当数据包到达某一条链时会按表的顺序


进行处理,表的优先级为:raw、mangle、nat、filter、security。

iptables 的工作流程如下图所示:

本文档使用 书栈网 · BookStack.CN 构建 - 300 -


kube-proxy iptables 模式源码分析

kube-proxy 的 iptables 模式
kube-proxy 组件负责维护 node 节点上的防火墙规则和路由规则,在 iptables 模式下,会根据
service 以及 endpoints 对象的改变来实时刷新规则,kube-proxy 使用了 iptables 的

本文档使用 书栈网 · BookStack.CN 构建 - 301 -


kube-proxy iptables 模式源码分析

filter 表和 nat 表,并对 iptables 的链进行了扩充,自定义了 KUBE-SERVICES、KUBE-


EXTERNAL-SERVICES、KUBE-NODEPORTS、KUBE-POSTROUTING、KUBE-MARK-MASQ、KUBE-
MARK-DROP、KUBE-FORWARD 七条链,另外还新增了以“KUBE-SVC-xxx”和“KUBE-SEP-xxx”开头
的数个链,除了创建自定义的链以外还将自定义链插入到已有链的后面以便劫持数据包。

在 nat 表中自定义的链以及追加的链如下所示:

在 filter 表定义的链以及追加的链如下所示如下所示:

对于 KUBE-MARK-MASQ 链中所有规则设置了 kubernetes 独有的 MARK 标记,在 KUBE-


POSTROUTING 链中对 node 节点上匹配 kubernetes 独有 MARK 标记的数据包,进行 SNAT 处
理。

本文档使用 书栈网 · BookStack.CN 构建 - 302 -


kube-proxy iptables 模式源码分析

1. -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000

Kube-proxy 接着为每个服务创建 KUBE-SVC-xxx 链,并在 nat 表中将 KUBE-SERVICES 链中


每个目标地址是service 的数据包导入这个 KUBE-SVC-xxx 链,如果 endpoint 尚未创建,则
KUBE-SVC-xxx 链中没有规则,任何 incomming packets 在规则匹配失败后会被 KUBE-MARK-
DROP 进行标记然后再 FORWARD 链中丢弃。

这些自定义链与 iptables 的表结合后如下所示,笔者只画出了 PREROUTING 和 OUTPUT 链中追


加的链以及部分自定义链,因为 PREROUTING 和 OUTPUT 的首条 NAT 规则都先将所有流量导入
KUBE-SERVICE 链中,这样就截获了所有的入流量和出流量,进而可以对 k8s 相关流量进行重定向
处理。

kubernetes 自定义链中数据包的详细流转可以参考:

本文档使用 书栈网 · BookStack.CN 构建 - 303 -


kube-proxy iptables 模式源码分析

iptables 规则分析

clusterIP 访问方式

创建一个 clusterIP 访问方式的 service 以及带有两个副本,从 pod 中访问 clusterIP 的


iptables 规则流向为:

1. PREROUTING --> KUBE-SERVICE --> KUBE-SVC-XXX --> KUBE-SEP-XXX

访问流程如下所示:

1、对于进入 PREROUTING 链的都转到 KUBE-SERVICES 链进行处理;


2、在 KUBE-SERVICES 链,对于访问 clusterIP 为 10.110.243.155 的转发到 KUBE-
SVC-5SB6FTEHND4GTL2W;
3、访问 KUBE-SVC-5SB6FTEHND4GTL2W 的使用随机数负载均衡,并转发到 KUBE-SEP-
CI5ZO3FTK7KBNRMG 和 KUBE-SEP-OVNLTDWFHTHII4SC 上;
4、KUBE-SEP-CI5ZO3FTK7KBNRMG 和 KUBE-SEP-OVNLTDWFHTHII4SC 对应 endpoint
中的 pod 192.168.137.147 和 192.168.98.213,设置 mark 标记,进行 DNAT 并转发
到具体的 pod 上,如果某个 service 的 endpoints 中没有 pod,那么针对此 service

本文档使用 书栈网 · BookStack.CN 构建 - 304 -


kube-proxy iptables 模式源码分析

的请求将会被 drop 掉;

1. // 1.
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-
2. SERVICES
3.
4. // 2.
-A KUBE-SERVICES -d 10.110.243.155/32 -p tcp -m comment --comment "pks-
system/tenant-service: cluster IP" -m tcp --dport 7000 -j KUBE-SVC-
5. 5SB6FTEHND4GTL2W
6.
7. // 3.
-A KUBE-SVC-5SB6FTEHND4GTL2W -m statistic --mode random --probability
8. 0.50000000000 -j KUBE-SEP-CI5ZO3FTK7KBNRMG
9. -A KUBE-SVC-5SB6FTEHND4GTL2W -j KUBE-SEP-OVNLTDWFHTHII4SC
10.
11.
12. // 4.
13. -A KUBE-SEP-CI5ZO3FTK7KBNRMG -s 192.168.137.147/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-CI5ZO3FTK7KBNRMG -p tcp -m tcp -j DNAT --to-destination
14. 192.168.137.147:7000
15.
16. -A KUBE-SEP-OVNLTDWFHTHII4SC -s 192.168.98.213/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-OVNLTDWFHTHII4SC -p tcp -m tcp -j DNAT --to-destination
17. 192.168.98.213:7000

nodePort 方式

在 nodePort 方式下,会用到 KUBE-NODEPORTS 规则链,通过 iptables -t nat -L -n 可


以看到 KUBE-NODEPORTS 位于 KUBE-SERVICE 链的最后一个,iptables 在处理报文时会优先处
理目的 IP 为clusterIP 的报文,在前面的 KUBE-SVC-XXX 都匹配失败之后再去使用 nodePort
方式进行匹配。

创建一个 nodePort 访问方式的 service 以及带有两个副本,访问 nodeport 的 iptables 规


则流向为:

1、非本机访问

PREROUTING --> KUBE-SERVICE --> KUBE-NODEPORTS --> KUBE-SVC-XXX --> KUBE-SEP-


1. XXX

2、本机访问

本文档使用 书栈网 · BookStack.CN 构建 - 305 -


kube-proxy iptables 模式源码分析

1. OUTPUT --> KUBE-SERVICE --> KUBE-NODEPORTS --> KUBE-SVC-XXX --> KUBE-SEP-XXX

该服务的 nodePort 端口为 30070,其 iptables 访问规则和使用 clusterIP 方式访问有点类


似,不过 nodePort 方式会比 clusterIP 的方式多走一条链 KUBE-NODEPORTS,其会在 KUBE-
NODEPORTS 链设置 mark 标记并转发到 KUBE-SVC-5SB6FTEHND4GTL2W,nodeport 与
clusterIP 访问方式最后都是转发到了 KUBE-SVC-xxx 链。

1、经过 PREROUTING 转到 KUBE-SERVICES


2、经过 KUBE-SERVICES 转到 KUBE-NODEPORTS
3、经过 KUBE-NODEPORTS 转到 KUBE-SVC-5SB6FTEHND4GTL2W
4、经过 KUBE-SVC-5SB6FTEHND4GTL2W 转到 KUBE-SEP-CI5ZO3FTK7KBNRMG 和 KUBE-
SEP-VR562QDKF524UNPV
5、经过 KUBE-SEP-CI5ZO3FTK7KBNRMG 和 KUBE-SEP-VR562QDKF524UNPV 分别转到
192.168.137.147:7000 和 192.168.89.11:7000

1. // 1.
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-
2. SERVICES
3.
4. // 2.
5. ......
6. -A KUBE-SERVICES xxx
7. ......
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this
must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-
8. NODEPORTS
9.
10. // 3.
-A KUBE-NODEPORTS -p tcp -m comment --comment "pks-system/tenant-service:" -m
11. tcp --dport 30070 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "pks-system/tenant-service:" -m
12. tcp --dport 30070 -j KUBE-SVC-5SB6FTEHND4GTL2W
13.
14. // 4、
-A KUBE-SVC-5SB6FTEHND4GTL2W -m statistic --mode random --probability
15. 0.50000000000 -j KUBE-SEP-CI5ZO3FTK7KBNRMG
16. -A KUBE-SVC-5SB6FTEHND4GTL2W -j KUBE-SEP-VR562QDKF524UNPV
17.
18. // 5、
19. -A KUBE-SEP-CI5ZO3FTK7KBNRMG -s 192.168.137.147/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-CI5ZO3FTK7KBNRMG -p tcp -m tcp -j DNAT --to-destination
20. 192.168.137.147:7000

本文档使用 书栈网 · BookStack.CN 构建 - 306 -


kube-proxy iptables 模式源码分析

21. -A KUBE-SEP-VR562QDKF524UNPV -s 192.168.89.11/32 -j KUBE-MARK-MASQ


-A KUBE-SEP-VR562QDKF524UNPV -p tcp -m tcp -j DNAT --to-destination
22. 192.168.89.11:7000

其他访问方式对应的 iptables 规则可自行分析。

iptables 模式源码分析

kubernetes 版本:v1.16

上篇文章已经在源码方面做了许多铺垫,下面就直接看 kube-proxy iptables 模式的核心方法。


首先回顾一下 iptables 模式的调用流程,kube-proxy 根据给定的 proxyMode 初始化对应的
proxier 后会调用 Proxier.SyncLoop() 执行 proxier 的主循环,而其最终会调用
proxier.syncProxyRules() 刷新 iptables 规则。

proxier.SyncLoop() --> proxier.syncRunner.Loop()-->bfr.tryRun()-->bfr.fn()--


1. >proxier.syncProxyRules()

proxier.syncProxyRules() 这个函数比较长,大约 800 行,其中有许多冗余的代码,代码可读性


不佳,我们只需理解其基本流程即可,该函数的主要功能为:

更新proxier.endpointsMap,proxier.servieMap
创建自定义链
将当前内核中 filter 表和 nat 表中的全部规则导入到内存中
为每个 service 创建规则
为 clusterIP 设置访问规则
为 externalIP 设置访问规则
为 ingress 设置访问规则
为 nodePort 设置访问规则
为 endpoint 生成规则链
写入 DNAT 规则
删除不再使用的服务自定义链
使用 iptables-restore 同步规则

首先是更新 proxier.endpointsMap,proxier.servieMap 两个对象。

k8s.io/kubernetes/pkg/proxy/iptables/proxier.go:677

1. func (proxier *Proxier) syncProxyRules() {


2. ......
serviceUpdateResult := proxy.UpdateServiceMap(proxier.serviceMap,
3. proxier.serviceChanges)

本文档使用 书栈网 · BookStack.CN 构建 - 307 -


kube-proxy iptables 模式源码分析

endpointUpdateResult :=
4. proxier.endpointsMap.Update(proxier.endpointsChanges)
5.
6. staleServices := serviceUpdateResult.UDPStaleClusterIP
7. for _, svcPortName := range endpointUpdateResult.StaleServiceNames {
if svcInfo, ok := proxier.serviceMap[svcPortName]; ok && svcInfo != nil
8. && svcInfo.Protocol() == v1.ProtocolUDP {
9. staleServices.Insert(svcInfo.ClusterIP().String())
10. for _, extIP := range svcInfo.ExternalIPStrings() {
11. staleServices.Insert(extIP)
12. }
13. }
14. }
15. ......

然后创建所需要的 iptable 链:

1. for _, jump := range iptablesJumpChains {


2. // 创建自定义链
if _, err := proxier.iptables.EnsureChain(jump.table, jump.dstChain);
3. err != nil {
4. .....
5. }
6. args := append(jump.extraArgs,
7. ......
8. )
9. //插入到已有的链
if _, err := proxier.iptables.EnsureRule(utiliptables.Prepend,
10. jump.table, jump.srcChain, args...); err != nil {
11. ......
12. }
13. }

将当前内核中 filter 表和 nat 表中的全部规则临时导出到 buffer 中:

err := proxier.iptables.SaveInto(utiliptables.TableFilter,
1. proxier.existingFilterChainsData)
2. if err != nil {
3.
4. } else {
existingFilterChains =
utiliptables.GetChainLines(utiliptables.TableFilter,
5. proxier.existingFilterChainsData.Bytes())

本文档使用 书栈网 · BookStack.CN 构建 - 308 -


kube-proxy iptables 模式源码分析

6. }
7.
8. ......
err = proxier.iptables.SaveInto(utiliptables.TableNAT,
9. proxier.iptablesData)
10. if err != nil {
11.
12. } else {
existingNATChains = utiliptables.GetChainLines(utiliptables.TableNAT,
13. proxier.iptablesData.Bytes())
14. }
15.
16. writeLine(proxier.filterChains, "*filter")
17. writeLine(proxier.natChains, "*nat")

检查已经创建出的表是否存在:

for _, chainName := range []utiliptables.Chain{kubeServicesChain,


1. kubeExternalServicesChain, kubeForwardChain} {
2. if chain, ok := existingFilterChains[chainName]; ok {
3. writeBytesLine(proxier.filterChains, chain)
4. } else {
writeLine(proxier.filterChains,
5. utiliptables.MakeChainLine(chainName))
6. }
7. }
for _, chainName := range []utiliptables.Chain{kubeServicesChain,
8. kubeNodePortsChain, kubePostroutingChain, KubeMarkMasqChain} {
9. if chain, ok := existingNATChains[chainName]; ok {
10. writeBytesLine(proxier.natChains, chain)
11. } else {
12. writeLine(proxier.natChains, utiliptables.MakeChainLine(chainName))
13. }
14. }

写入 SNAT 地址伪装规则,在 POSTROUTING 阶段对地址进行 MASQUERADE 处理,原始请求源 IP


将被丢失,被请求 pod 的应用看到为 NodeIP 或 CNI 设备 IP(bridge/vxlan设备):

1. masqRule := []string{
2. ......
3. }
4. if proxier.iptables.HasRandomFully() {

本文档使用 书栈网 · BookStack.CN 构建 - 309 -


kube-proxy iptables 模式源码分析

5. masqRule = append(masqRule, "--random-fully")


6. } else {
7.
8. }
9. writeLine(proxier.natRules, masqRule...)
10.
11. writeLine(proxier.natRules, []string{
12. ......
13. }...)

为每个 service 创建规则,创建 KUBE-SVC-xxx 和 KUBE-XLB-xxx 链、创建 service


portal 规则、为 clusterIP 创建规则:

1. for svcName, svc := range proxier.serviceMap {


2. svcInfo, ok := svc.(*serviceInfo)
3.
4. ......
5. if hasEndpoints {
6. ......
7. }
8.
9. svcXlbChain := svcInfo.serviceLBChainName
10. if svcInfo.OnlyNodeLocalEndpoints() {
11. ......
12. }
13.
14. if hasEndpoints {
15. ......
16. } else {
17. ......
18. }

若服务使用了 externalIP,创建对应的规则:

1. for _, externalIP := range svcInfo.ExternalIPStrings() {


2. if local, err := utilproxy.IsLocalIP(externalIP); err != nil {
3. ......
4. if proxier.portsMap[lp] != nil {
5. ......
6. } else {
7. ......
8. }

本文档使用 书栈网 · BookStack.CN 构建 - 310 -


kube-proxy iptables 模式源码分析

9. }
10. if hasEndpoints {
11. ......
12. } else {
13. ......
14. }
15. }

若服务使用了 ingress,创建对应的规则:

1. for _, ingress := range svcInfo.LoadBalancerIPStrings() {


2. if ingress != "" {
3. if hasEndpoints {
4. ......
5. if !svcInfo.OnlyNodeLocalEndpoints() {
6. ......
7. }
8.
9. if len(svcInfo.LoadBalancerSourceRanges()) == 0 {
10. ......
11. } else {
12. ......
13. }
14. ......
15.
16. } else {
17. ......
18. }
19. }
20. }

若使用了 nodePort,创建对应的规则:

1. if svcInfo.NodePort() != 0 {
addresses, err :=
utilproxy.GetNodeAddresses(proxier.nodePortAddresses,
2. proxier.networkInterfacer)
3.
4. lps := make([]utilproxy.LocalPort, 0)
5. for address := range addresses {
6. ......
7. lps = append(lps, lp)

本文档使用 书栈网 · BookStack.CN 构建 - 311 -


kube-proxy iptables 模式源码分析

8. }
9.
10. for _, lp := range lps {
11. if proxier.portsMap[lp] != nil {
12.
13. } else if svcInfo.Protocol() != v1.ProtocolSCTP {
14. socket, err := proxier.portMapper.OpenLocalPort(&lp)
15. ......
16. if lp.Protocol == "udp" {
17. ......
18. }
19. replacementPortsMap[lp] = socket
20. }
21. }
22. if hasEndpoints {
23. ......
24. } else {
25. ......
26. }
27. }

为 endpoint 生成规则链 KUBE-SEP-XXX:

1. endpoints = endpoints[:0]
2. endpointChains = endpointChains[:0]
3. var endpointChain utiliptables.Chain
4. for _, ep := range proxier.endpointsMap[svcName] {
5. epInfo, ok := ep.(*endpointsInfo)
6. ......
if chain, ok :=
7. existingNATChains[utiliptables.Chain(endpointChain)]; ok {
8. writeBytesLine(proxier.natChains, chain)
9. } else {
writeLine(proxier.natChains,
10. utiliptables.MakeChainLine(endpointChain))
11. }
12. activeNATChains[endpointChain] = true
13. }

如果创建 service 时指定了 SessionAffinity 为 clientIP 则使用 recent 创建保持会话连


接的规则:

本文档使用 书栈网 · BookStack.CN 构建 - 312 -


kube-proxy iptables 模式源码分析

1. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
2. for _, endpointChain := range endpointChains {
3. ......
4. }
5. }

写入负载均衡和 DNAT 规则,对于 endpoints 中的 pod 使用随机访问负载均衡策略。

在 iptables 规则中加入该 service 对应的自定义链“KUBE-SVC-xxx”,如果该服务对应的


endpoints 大于等于2,则添加负载均衡规则;
针对非本地 Node 上的 pod,需进行 DNAT,将请求的目标地址设置成候选的 pod 的 IP 后
进行路由,KUBE-MARK-MASQ 将重设(伪装)源地址;

1. for i, endpointChain := range endpointChains {


2. ......
3. if svcInfo.OnlyNodeLocalEndpoints() && endpoints[i].IsLocal {
4. ......
5. }
6. ......
7. epIP := endpoints[i].IP()
8. if epIP == "" {
9. ......
10. }
11. ......
12. args = append(args, "-j", string(endpointChain))
13. writeLine(proxier.natRules, args...)
14.
15. ......
16. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
17. ......
18. }
19. ......
20. writeLine(proxier.natRules, args...)
21. }

若启用了 clusterCIDR 则生成对应的规则链:

1. if len(proxier.clusterCIDR) > 0 {
2. ......
3. writeLine(proxier.natRules, args...)
4. }

本文档使用 书栈网 · BookStack.CN 构建 - 313 -


kube-proxy iptables 模式源码分析

为本机的 pod 开启会话保持:

1. args = append(args[:0], "-A", string(svcXlbChain))


2. writeLine(proxier.natRules, ......)
3.
4. numLocalEndpoints := len(localEndpointChains)
5. if numLocalEndpoints == 0 {
6. ......
7. writeLine(proxier.natRules, args...)
8. } else {
9. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
10. for _, endpointChain := range localEndpointChains {
11. ......
12. }
13. }
14. ......
15. for i, endpointChain := range localEndpointChains {
16. ......
17. args = append(args, "-j", string(endpointChain))
18. writeLine(proxier.natRules, args...)
19. }
20. }
21. }

删除不存在服务的自定义链,KUBE-SVC-xxx、KUBE-SEP-xxx、KUBE-FW-xxx、KUBE-XLB-xxx:

1. for chain := range existingNATChains {


2. if !activeNATChains[chain] {
3. ......
if !strings.HasPrefix(chainString, "KUBE-SVC-") &&
!strings.HasPrefix(chainString, "KUBE-SEP-") && !strings.HasPrefix(chainString,
4. "KUBE-FW-") && ! strings.HasPrefix(chainString, "KUBE-XLB-") {
5. ......
6. continue
7. }
8.
9. writeBytesLine(proxier.natChains, existingNATChains[chain])
10. writeLine(proxier.natRules, "-X", chainString)
11. }
12. }

在 KUBE-SERVICES 链最后添加 nodePort 规则:

本文档使用 书栈网 · BookStack.CN 构建 - 314 -


kube-proxy iptables 模式源码分析

addresses, err := utilproxy.GetNodeAddresses(proxier.nodePortAddresses,


1. proxier.networkInterfacer)
2. if err != nil {
3. ......
4. } else {
5. for address := range addresses {
6. if utilproxy.IsZeroCIDR(address) {
7. ......
8. }
if isIPv6 && !utilnet.IsIPv6String(address) || !isIPv6 &&
9. utilnet.IsIPv6String(address) {
10. ......
11. }
12. .....
13. writeLine(proxier.natRules, args...)
14. }
15. }

为 INVALID 状态的包添加规则,为 KUBE-FORWARD 链添加对应的规则:

1. writeLine(proxier.filterRules,
2. ......
3. )
4.
5. writeLine(proxier.filterRules,
6. ......
7. )
8.
9. if len(proxier.clusterCIDR) != 0 {
10. writeLine(proxier.filterRules,
11. ......
12. )
13. writeLine(proxier.filterRules,
14. ......
15. )
16. }

在结尾添加标志:

1. writeLine(proxier.filterRules, "COMMIT")
2. writeLine(proxier.natRules, "COMMIT")

本文档使用 书栈网 · BookStack.CN 构建 - 315 -


kube-proxy iptables 模式源码分析

使用 iptables-restore 同步规则:

1. proxier.iptablesData.Reset()
2. proxier.iptablesData.Write(proxier.filterChains.Bytes())
3. proxier.iptablesData.Write(proxier.filterRules.Bytes())
4. proxier.iptablesData.Write(proxier.natChains.Bytes())
5. proxier.iptablesData.Write(proxier.natRules.Bytes())
6.
err = proxier.iptables.RestoreAll(proxier.iptablesData.Bytes(),
7. utiliptables.NoFlushTables, utiliptables.RestoreCounters)
8. if err != nil {
9. ......
10. }

以上就是对 kube-proxy iptables 代理模式核心源码的一个走读。

总结
本文主要讲了 kube-proxy iptables 模式的实现,可以看到其中的 iptables 规则是相当复杂
的,在实际环境中尽量根据已有服务再来梳理整个 iptables 规则链就比较清楚了,笔者对于
iptables 的知识也是现学的,文中如有不当之处望指正。上面分析完了整个 iptables 模式的功
能,但是 iptable 存在一些性能问题,比如有规则线性匹配时延、规则更新时延、可扩展性差等,为
了解决这些问题于是有了 ipvs 模式,在下篇文章中会继续介绍 ipvs 模式的实现。

参考:

https://www.jianshu.com/p/a978af8e5dd8

https://blog.csdn.net/ebay/article/details/52798074

https://blog.csdn.net/horsefoot/article/details/51249161

https://rootdeep.github.io/posts/kube-proxy-code-analysis/

https://www.cnblogs.com/charlieroro/p/9588019.html

本文档使用 书栈网 · BookStack.CN 构建 - 316 -


kube-proxy ipvs 模式源码分析

前几篇文章已经分析了 service 的原理以及 kube-proxy iptables 模式的原理与实现,本篇文


章会继续分析 kube-proxy ipvs 模式的原理与实现。

ipvs
ipvs (IP Virtual Server) 是基于 Netfilter 的,作为 linux 内核的一部分实现了传输层
负载均衡,ipvs 集成在LVS(Linux Virtual Server)中,它在主机中运行,并在真实服务器集群
前充当负载均衡器。ipvs 可以将对 TCP/UDP 服务的请求转发给后端的真实服务器,因此 ipvs 天
然支持 Kubernetes Service。ipvs 也包含了多种不同的负载均衡算法,例如轮询、最短期望延
迟、最少连接以及各种哈希方法等,ipvs 的设计就是用来为大规模服务进行负载均衡的。

ipvs 的负载均衡方式

ipvs 有三种负载均衡方式,分别为:

NAT
TUN
DR

关于三种模式的原理可以参考:LVS 配置小结。

上面的负载均衡方式中只有 NAT 模式可以进行端口映射,因此 kubernetes 中 ipvs 的实现使用


了 NAT 模式,用来将 service IP 和 service port 映射到后端的 container ip 和
container port。

NAT 模式下的工作流程如下所示:

1. +--------+
2. | Client |
3. +--------+
4. (CIP) <-- Client's IP address
5. |
6. |
7. { internet }
8. |
9. |
10. (VIP) <-- Virtual IP address
11. +----------+
12. | Director |
13. +----------+
14. (PIP) <-- (Director's Private IP address)
15. |
16. |

本文档使用 书栈网 · BookStack.CN 构建 - 317 -


kube-proxy ipvs 模式源码分析

17. (RIP) <-- Real (server's) IP address


18. +-------------+
19. | Real server |
20. +-------------+

其具体流程为:当用户发起一个请求时,请求从 VIP 接口流入,此时数据源地址是 CIP,目标地址是


VIP,当接收到请求后拆掉 mac 地址封装后看到目标 IP 地址就是自己,按照正常流程会通过
INPUT 转入用户空间,但此时工作在 INPUT 链上的 ipvs 会强行将数据转到 POSTROUTING 链
上,并根据相应的负载均衡算法选择后端具体的服务器,再通过 DNAT 转发给 Real server,此时
源地址 CIP,目标地址变成了 RIP。

ipvs 与 iptables 的区别与联系

区别:

底层数据结构:iptables 使用链表,ipvs 使用哈希表


负载均衡算法:iptables 只支持随机、轮询两种负载均衡算法而 ipvs 支持的多达 8 种;
操作工具:iptables 需要使用 iptables 命令行工作来定义规则,ipvs 需要使用
ipvsadm 来定义规则。

此外 ipvs 还支持 realserver 运行状况检查、连接重试、端口映射、会话保持等功能。

联系:

ipvs 和 iptables 都是基于 netfilter内核模块,两者都是在内核中的五个钩子函数处工作,下


图是 ipvs 所工作的几个钩子函数:

关于 kube-proxy iptables 与 ipvs 模式的区别,更多详细信息可以查看官方文档:


https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/README.md。

本文档使用 书栈网 · BookStack.CN 构建 - 318 -


kube-proxy ipvs 模式源码分析

ipset

IP sets 是 Linux 内核中的一个框架,可以由 ipset 命令进行管理。根据不同的类型,IP set


可以以某种方式保存 IP地址、网络、(TCP/UDP)端口号、MAC地址、接口名或它们的组合,并且能够
快速匹配。

根据官网的介绍,若有以下使用场景:

在保存了多个 IP 地址或端口号的 iptables 规则集合中想使用哈希查找;


根据 IP 地址或端口动态更新 iptables 规则时希望在性能上无损;
在使用 iptables 工具创建一个基于 IP 地址和端口的复杂规则时觉得非常繁琐;

此时,使用 ipset 工具可能是你最好的选择。

ipset 是 iptables 的一种扩展,在 iptables 中可以使用 -m set 启用 ipset 模块,具体


来说,ipvs 使用 ipset 来存储需要 NAT 或 masquared 时的 ip 和端口列表。在数据包过滤过
程中,首先遍历 iptables 规则,在定义了使用 ipset 的条件下会跳转到 ipset 列表中进行匹
配。

kube-proxy ipvs 模式
kube-proxy 的 ipvs 模式是在 2015 年由 k8s 社区的大佬 thockin 提出的(Try kube-
proxy via ipvs instead of iptables or userspace),在 2017 年由华为云团队实现的
(Implement IPVS-based in-cluster service load balancing)。前面的文章已经提到
了,在 kubernetes v1.8 中已经引入了 ipvs 模式。

kube-proxy 在 ipvs 模式下自定义了八条链,分别为 KUBE-SERVICES、KUBE-FIREWALL、


KUBE-POSTROUTING、KUBE-MARK-MASQ、KUBE-NODE-PORT、KUBE-MARK-DROP、KUBE-
FORWARD、KUBE-LOAD-BALANCER ,如下所示:

NAT 表:

本文档使用 书栈网 · BookStack.CN 构建 - 319 -


kube-proxy ipvs 模式源码分析

Filter 表:

此外,由于 linux 内核原生的 ipvs 模式只支持 DNAT,不支持 SNAT,所以,在以下几种场景中


ipvs 仍需要依赖 iptables 规则:

1、kube-proxy 启动时指定 –-masquerade-all=true 参数,即集群中所有经过 kube-


proxy 的包都做一次 SNAT;
2、kube-proxy 启动时指定 --cluster-cidr= 参数;
3、对于 Load Balancer 类型的 service,用于配置白名单;
4、对于 NodePort 类型的 service,用于配置 MASQUERADE;
5、对于 externalIPs 类型的 service;

本文档使用 书栈网 · BookStack.CN 构建 - 320 -


kube-proxy ipvs 模式源码分析

但对于 ipvs 模式的 kube-proxy,无论有多少 pod/service,iptables 的规则数都是固定


的。

ipvs 模式的启用

1、首先要加载 IPVS 所需要的 kernel module

1. $ modprobe -- ip_vs
2. $ modprobe -- ip_vs_rr
3. $ modprobe -- ip_vs_wrr
4. $ modprobe -- ip_vs_sh
5. $ modprobe -- nf_conntrack_ipv4
6. $ cut -f1 -d " " /proc/modules | grep -e ip_vs -e nf_conntrack_ipv4

2、在启动 kube-proxy 时,指定 proxy-mode 参数

1. --proxy-mode=ipvs

(如果要使用其他负载均衡算法,可以指定 --ipvs-scheduler= 参数,默认为 rr)

当创建 ClusterIP type 的 service 时,IPVS proxier 会执行以下三个操作:

确保本机已创建 dummy 网卡,默认为 kube-ipvs0。为什么要创建 dummy 网卡?因为


ipvs netfilter 的 DNAT 钩子挂载在 INPUT 链上,当访问 ClusterIP 时,将
ClusterIP 绑定在 dummy 网卡上为了让内核识别该 IP 就是本机 IP,进而进入 INPUT
链,然后通过钩子函数 ip_vs_in 转发到 POSTROUTING 链;
将 ClusterIP 绑定到 dummy 网卡;
为每个 ClusterIP 创建 IPVS virtual servers 和 real server,分别对应 service
和 endpoints;

例如下面的示例:

1. // kube-ipvs0 dummy 网卡
2. $ ip addr
3. ......
4. 4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
5. link/ether de:be:c0:73:bc:c7 brd ff:ff:ff:ff:ff:ff
6. inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0
7. valid_lft forever preferred_lft forever
8. inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
9. valid_lft forever preferred_lft forever
10. inet 10.97.4.140/32 brd 10.97.4.140 scope global kube-ipvs0

本文档使用 书栈网 · BookStack.CN 构建 - 321 -


kube-proxy ipvs 模式源码分析

11. valid_lft forever preferred_lft forever


12. ......
13.
14.
15. // 10.97.4.140 为 CLUSTER-IP 挂载在 kube-ipvs0 上
16. $ kubectl get svc
17. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
18. tenant-service ClusterIP 10.97.4.140 <none> 7000/TCP 23s
19.
20. // 10.97.4.140 后端的 realserver 分别为 10.244.1.2 和 10.244.1.3
21. $ ipvsadm -L -n
22. IP Virtual Server version 1.2.1 (size=4096)
23. Prot LocalAddress:Port Scheduler Flags
24. -> RemoteAddress:Port Forward Weight ActiveConn InActConn
25. TCP 10.97.4.140:7000 rr
26. -> 10.244.1.2:7000 Masq 1 0 0
27. -> 10.244.1.3:7000 Masq 1 0 0

ipvs 模式下数据包的流向

clusterIP 访问方式

PREROUTING --> KUBE-SERVICES --> KUBE-CLUSTER-IP --> INPUT --> KUBE-FIREWALL --


1. > POSTROUTING

首先进入 PREROUTING 链
从 PREROUTING 链会转到 KUBE-SERVICES 链,10.244.0.0/16 为 ClusterIP 网段
在 KUBE-SERVICES 链打标记
从 KUBE-SERVICES 链再进入到 KUBE-CLUSTER-IP 链
KUBE-CLUSTER-IP 为 ipset 集合,在此处会进行 DNAT
然后会进入 INPUT 链
从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记
在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到
POSTROUTING 链

-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-


1. SERVICES
2.
-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service
cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP
3. dst,dst -j KUBE-MARK-MASQ
4.

本文档使用 书栈网 · BookStack.CN 构建 - 322 -


kube-proxy ipvs 模式源码分析

5. // 执行完 PREROUTING 规则,数据打上0x4000/0x4000的标记


6. -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
7.
8. -A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT

KUBE-CLUSTER-IP 为 ipset 列表:

1. # ipset list | grep -A 20 KUBE-CLUSTER-IP


2. Name: KUBE-CLUSTER-IP
3. Type: hash:ip,port
4. Revision: 5
5. Header: family inet hashsize 1024 maxelem 65536
6. Size in memory: 352
7. References: 2
8. Members:
9. 10.96.0.10,17:53
10. 10.96.0.10,6:53
11. 10.96.0.1,6:443
12. 10.96.0.10,6:9153

然后会进入 INPUT:

1. -A INPUT -j KUBE-FIREWALL
2.
-A KUBE-FIREWALL -m comment --comment "kubernetes firewall for dropping marked
3. packets" -m mark --mark 0x8000/0x8000 -j DROP

如果进来的数据带有 0x8000/0x8000 标记则丢弃,若有 0x4000/0x4000 标记则正常执行:

-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-


1. POSTROUTING
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring
2. SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

nodePort 方式

PREROUTING --> KUBE-SERVICES --> KUBE-NODE-PORT --> INPUT --> KUBE-FIREWALL -->
1. POSTROUTING

首先进入 PREROUTING 链
从 PREROUTING 链会转到 KUBE-SERVICES 链

本文档使用 书栈网 · BookStack.CN 构建 - 323 -


kube-proxy ipvs 模式源码分析

在 KUBE-SERVICES 链打标记
从 KUBE-SERVICES 链再进入到 KUBE-NODE-PORT 链
KUBE-NODE-PORT 为 ipset 集合,在此处会进行 DNAT
然后会进入 INPUT 链
从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记
在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到
POSTROUTING 链

-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-


1. SERVICES
2.
-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service
cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP
3. dst,dst -j KUBE-MARK-MASQ
4.
5. -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
6.
7. -A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT

KUBE-NODE-PORT 对应的 ipset 列表:

1. # ipset list | grep -B 10 KUBE-NODE-PORT


2. Name: KUBE-NODE-PORT-TCP
3. Type: bitmap:port
4. Revision: 3
5. Header: range 0-65535
6. Size in memory: 8268
7. References: 0
8. Members:

流入 INPUT 后与 ClusterIP 的访问方式相同。

kube-proxy ipvs 源码分析

kubernetes 版本:v1.16

在前面的文章中已经介绍过 ipvs 的初始化了,下面直接看其核心方法:proxier.syncRunner。

1. func NewProxier(......) {
2. ......
proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner",
3. proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)

本文档使用 书栈网 · BookStack.CN 构建 - 324 -


kube-proxy ipvs 模式源码分析

4. ......
5. }

proxier.syncRunner() 执行流程:

通过 iptables-save 获取现有的 Filter 和 NAT 表存在的链数据


创建自定义链与规则
创建 Dummy 接口和 ipset 默认列表
为每个服务生成 ipvs 规则
对 serviceMap 内的每个服务进行遍历处理,对不同的服务类型
(clusterip/nodePort/externalIPs/load-balancer)进行不同的处理(ipset 列
表/vip/ipvs 后端服务器)
根据 endpoint 列表,更新 KUBE-LOOP-BACK 的 ipset 列表
若为 clusterIP 类型更新对应的 ipset 列表 KUBE-CLUSTER-IP
若为 externalIPs 类型更新对应的 ipset 列表 KUBE-EXTERNAL-IP
若为 load-balancer 类型更新对应的 ipset 列表 KUBE-LOAD-BALANCER、KUBE-LOAD-
BALANCER-LOCAL、KUBE-LOAD-BALANCER-FW、KUBE-LOAD-BALANCER-SOURCE-CIDR、
KUBE-LOAD-BALANCER-SOURCE-IP
若为 NodePort 类型更新对应的 ipset 列表 KUBE-NODE-PORT-TCP、KUBE-NODE-PORT-
LOCAL-TCP、KUBE-NODE-PORT-LOCAL-SCTP-HASH、KUBE-NODE-PORT-LOCAL-UDP、
KUBE-NODE-PORT-SCTP-HASH、KUBE-NODE-PORT-UDP
同步 ipset 记录
刷新 iptables 规则

1. func (proxier *Proxier) syncProxyRules() {


2. proxier.mu.Lock()
3. defer proxier.mu.Unlock()
4.
5.
serviceUpdateResult := proxy.UpdateServiceMap(proxier.serviceMap,
6. proxier.serviceChanges)
endpointUpdateResult :=
7. proxier.endpointsMap.Update(proxier.endpointsChanges)
8.
9. staleServices := serviceUpdateResult.UDPStaleClusterIP
10. // 合并 service 列表
11. for _, svcPortName := range endpointUpdateResult.StaleServiceNames {
if svcInfo, ok := proxier.serviceMap[svcPortName]; ok && svcInfo != nil
12. && svcInfo.Protocol() == v1.ProtocolUDP {
13. staleServices.Insert(svcInfo.ClusterIP().String())
14. for _, extIP := range svcInfo.ExternalIPStrings() {

本文档使用 书栈网 · BookStack.CN 构建 - 325 -


kube-proxy ipvs 模式源码分析

15. staleServices.Insert(extIP)
16. }
17. }
18. }
19. ......

读取系统 iptables 到内存,创建自定义链以及 iptables 规则,创建 dummy interface


kube-ipvs0,创建默认的 ipset 规则。

1. proxier.natChains.Reset()
2. proxier.natRules.Reset()
3. proxier.filterChains.Reset()
4. proxier.filterRules.Reset()
5.
6. writeLine(proxier.filterChains, "*filter")
7. writeLine(proxier.natChains, "*nat")
8.
9. // 创建kubernetes的表连接链数据
10. proxier.createAndLinkeKubeChain()
11.
12. // 创建 dummy interface kube-ipvs0
13. _, err := proxier.netlinkHandle.EnsureDummyDevice(DefaultDummyDevice)
14. if err != nil {
15. ......
16. return
17. }
18.
19. // 创建默认的 ipset 规则
20. for _, set := range proxier.ipsetList {
21. if err := ensureIPSet(set); err != nil {
22. return
23. }
24. set.resetEntries()
25. }

对每一个服务创建 ipvs 规则。根据 endpoint 列表,更新 KUBE-LOOP-BACK 的 ipset 列表。

1. for svcName, svc := range proxier.serviceMap {


2. svcInfo, ok := svc.(*serviceInfo)
3. if !ok {
4. ......
5. }

本文档使用 书栈网 · BookStack.CN 构建 - 326 -


kube-proxy ipvs 模式源码分析

6.
7. for _, e := range proxier.endpointsMap[svcName] {
8. ep, ok := e.(*proxy.BaseEndpointInfo)
9. if !ok {
10. klog.Errorf("Failed to cast BaseEndpointInfo %q", e.String())
11. continue
12. }
13. ......
14.
if valid :=
15. proxier.ipsetList[kubeLoopBackIPSet].validateEntry(entry); !valid {
16. ......
17. }

18. proxier.ipsetList[kubeLoopBackIPSet].activeEntries.Insert(entry.String())
19. }

对于 clusterIP 类型更新对应的 ipset 列表 KUBE-CLUSTER-IP。

if valid := proxier.ipsetList[kubeClusterIPSet].validateEntry(entry);
1. !valid {
2. ......
3. }

4. proxier.ipsetList[kubeClusterIPSet].activeEntries.Insert(entry.String())
5. ......
6. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
7. ......
8. }
9. // 绑定 ClusterIP to dummy interface
10. if err := proxier.syncService(svcNameString, serv, true); err == nil {
11. // 同步 endpoints 信息
12. if err := proxier.syncEndpoint(svcName, false, serv); err != nil {
13. ......
14. }
15. } else {
16. ......
17. }

为 externalIP 创建 ipvs 规则。

1. for _, externalIP := range svcInfo.ExternalIPStrings() {

本文档使用 书栈网 · BookStack.CN 构建 - 327 -


kube-proxy ipvs 模式源码分析

2. if local, err := utilproxy.IsLocalIP(externalIP); err != nil {


3. ......
4. } else if local && (svcInfo.Protocol() != v1.ProtocolSCTP) {
5. ......
6. if proxier.portsMap[lp] != nil {
7. ......
8. } else {
9. socket, err := proxier.portMapper.OpenLocalPort(&lp)
10. if err != nil {
11. ......
12. }
13. replacementPortsMap[lp] = socket
14. }
15. }
16. ......
if valid :=
17. proxier.ipsetList[kubeExternalIPSet].validateEntry(entry); !valid {
18. ......
19. }

20. proxier.ipsetList[kubeExternalIPSet].activeEntries.Insert(entry.String())
21.
22. ......
23. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
24. ......
25. }
if err := proxier.syncService(svcNameString, serv, true); err ==
26. nil {
27. ......
if err := proxier.syncEndpoint(svcName, false, serv); err !=
28. nil {
29. ......
30. }
31. } else {
32. ......
33. }
34. }

为 load-balancer类型创建 ipvs 规则。

1. for _, ingress := range svcInfo.LoadBalancerIPStrings() {


2. if ingress != "" {
3. ......

本文档使用 书栈网 · BookStack.CN 构建 - 328 -


kube-proxy ipvs 模式源码分析

if valid :=
4. proxier.ipsetList[kubeLoadBalancerSet].validateEntry(entry); !valid {
5. ......
6. }

7. proxier.ipsetList[kubeLoadBalancerSet].activeEntries.Insert(entry.String())
8.
9. if svcInfo.OnlyNodeLocalEndpoints() {
10. ......
11. }
12. if len(svcInfo.LoadBalancerSourceRanges()) != 0 {
13. ......
14. for _, src := range svcInfo.LoadBalancerSourceRanges() {
15. ......
16. }
17. ......
18. }
19. ......
if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP
20. {
21. ......
22. }
if err := proxier.syncService(svcNameString, serv, true); err
23. == nil {
24. ......
if err := proxier.syncEndpoint(svcName,
25. svcInfo.OnlyNodeLocalEndpoints(), serv); err != nil {
26. ......
27. }
28. } else {
29. ......
30. }
31. }
32. }

为 nodePort 类型创建 ipvs 规则。

1. if svcInfo.NodePort() != 0 {
2. ......
3.
4. var lps []utilproxy.LocalPort
5. for _, address := range nodeAddresses {
6. ......

本文档使用 书栈网 · BookStack.CN 构建 - 329 -


kube-proxy ipvs 模式源码分析

7. lps = append(lps, lp)


8. }
9. for _, lp := range lps {
10. if proxier.portsMap[lp] != nil {
11. ......
12. } else if svcInfo.Protocol() != v1.ProtocolSCTP {
13. socket, err := proxier.portMapper.OpenLocalPort(&lp)
14. if err != nil {
15. ......
16. }
17. if lp.Protocol == "udp" {
18. ......
19. }
20. }
21. }
22. switch protocol {
23. case "tcp":
24. ......
25. case "udp":
26. ......
27. case "sctp":
28. ......
29. default:
30. ......
31. }
32. if nodePortSet != nil {
33. for _, entry := range entries {
34. ......
35. nodePortSet.activeEntries.Insert(entry.String())
36. }
37. }
38.
39. if svcInfo.OnlyNodeLocalEndpoints() {
40. var nodePortLocalSet *IPSet
41. switch protocol {
42. case "tcp":
nodePortLocalSet =
43. proxier.ipsetList[kubeNodePortLocalSetTCP]
44. case "udp":
nodePortLocalSet =
45. proxier.ipsetList[kubeNodePortLocalSetUDP]
46. case "sctp":

本文档使用 书栈网 · BookStack.CN 构建 - 330 -


kube-proxy ipvs 模式源码分析

nodePortLocalSet =
47. proxier.ipsetList[kubeNodePortLocalSetSCTP]
48. default:
49. ......
50. }
51. if nodePortLocalSet != nil {
52. entryInvalidErr := false
53. for _, entry := range entries {
54. ......
55. nodePortLocalSet.activeEntries.Insert(entry.String())
56. }
57. ......
58. }
59. }
60. for _, nodeIP := range nodeIPs {
61. ......
if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP
62. {
63. ......
64. }
if err := proxier.syncService(svcNameString, serv, false); err
65. == nil {
if err := proxier.syncEndpoint(svcName,
66. svcInfo.OnlyNodeLocalEndpoints(), serv); err != nil {
67. ......
68. }
69. } else {
70. ......
71. }
72. }
73. }
74. }

同步 ipset 记录,清理 conntrack。

1. for _, set := range proxier.ipsetList {


2. set.syncIPSetEntries()
3. }
4.
5. proxier.writeIptablesRules()
6.
7. proxier.iptablesData.Reset()
8. proxier.iptablesData.Write(proxier.natChains.Bytes())

本文档使用 书栈网 · BookStack.CN 构建 - 331 -


kube-proxy ipvs 模式源码分析

9. proxier.iptablesData.Write(proxier.natRules.Bytes())
10. proxier.iptablesData.Write(proxier.filterChains.Bytes())
11. proxier.iptablesData.Write(proxier.filterRules.Bytes())
12.
err = proxier.iptables.RestoreAll(proxier.iptablesData.Bytes(),
13. utiliptables.NoFlushTables, utiliptables.RestoreCounters)
14. if err != nil {
15. ......
16. }
17. ......
18. proxier.deleteEndpointConnections(endpointUpdateResult.StaleEndpoints)
19. }

总结
本文主要讲述了 kube-proxy ipvs 模式的原理与实现,iptables 模式与 ipvs 模式下在源码实
现上有许多相似之处,但二者原理不同,理解了原理分析代码则更加容易,笔者对于 ipvs 的知识也
是现学的,文中如有不当之处望指正。虽然 ipvs 的性能要比 iptables 更好,但社区中已有相关
的文章指出 BPF(Berkeley Packet Filter) 比 ipvs 的性能更好,且 BPF 将要取代
iptables,至于下一步如何发展,让我们拭目以待。

参考:

http://www.austintek.com/LVS/LVS-HOWTO/HOWTO/LVS-HOWTO.filter_rules.html

https://bestsamina.github.io/posts/2018-10-19-ipvs-based-kube-proxy-4-
scaled-k8s-lb/

https://www.bookstack.cn/read/k8s-source-code-analysis/core-kube-proxy-
ipvs.md

https://blog.51cto.com/goome/2369150

https://xigang.github.io/2019/07/21/kubernetes-service/

https://segmentfault.com/a/1190000016333317

https://cilium.io/blog/2018/04/17/why-is-the-kernel-community-replacing-
iptables/

本文档使用 书栈网 · BookStack.CN 构建 - 332 -


kubelet 架构浅析

一、概要
kubelet 是运行在每个节点上的主要的“节点代理”,每个节点都会启动 kubelet进程,用来处理
Master 节点下发到本节点的任务,按照 PodSpec 描述来管理Pod 和其中的容器(PodSpec 是用
来描述一个 pod 的 YAML 或者 JSON 对象)。

kubelet 通过各种机制(主要通过 apiserver )获取一组 PodSpec 并保证在这些 PodSpec 中


描述的容器健康运行。

二、kubelet 的主要功能
1、kubelet 默认监听四个端口,分别为 10250 、10255、10248、4194。

LISTEN 0 128 *:10250 *:*


1. users:(("kubelet",pid=48500,fd=28))
LISTEN 0 128 *:10255 *:*
2. users:(("kubelet",pid=48500,fd=26))
LISTEN 0 128 *:4194 *:*
3. users:(("kubelet",pid=48500,fd=13))
LISTEN 0 128 127.0.0.1:10248 *:*
4. users:(("kubelet",pid=48500,fd=23))

10250(kubelet API):kubelet server 与 apiserver 通信的端口,定期请求


apiserver 获取自己所应当处理的任务,通过该端口可以访问获取 node 资源以及状态。

10248(健康检查端口):通过访问该端口可以判断 kubelet 是否正常工作, 通过 kubelet


的启动参数 --healthz-port 和 --healthz-bind-address 来指定监听的地址和端口。

1. $ curl http://127.0.0.1:10248/healthz
2. ok

4194(cAdvisor 监听):kublet 通过该端口可以获取到该节点的环境信息以及 node 上运


行的容器状态等内容,访问 http://localhost:4194 可以看到 cAdvisor 的管理界面,通
过 kubelet 的启动参数 --cadvisor-port 可以指定启动的端口。

1. $ curl http://127.0.0.1:4194/metrics

10255 (readonly API):提供了 pod 和 node 的信息,接口以只读形式暴露出去,访问


该端口不需要认证和鉴权。

本文档使用 书栈网 · BookStack.CN 构建 - 333 -


kubelet 架构浅析

1. // 获取 pod 的接口,与 apiserver 的


2. // http://127.0.0.1:8080/api/v1/pods?fieldSelector=spec.nodeName= 接口类似
3. $ curl http://127.0.0.1:10255/pods
4.
5. // 节点信息接口,提供磁盘、网络、CPU、内存等信息
6. $ curl http://127.0.0.1:10255/spec/

2、kubelet 主要功能:

pod 管理:kubelet 定期从所监听的数据源获取节点上 pod/container 的期望状态(运行


什么容器、运行的副本数量、网络或者存储如何配置等等),并调用对应的容器平台接口达到这
个状态。

容器健康检查:kubelet 创建了容器之后还要查看容器是否正常运行,如果容器运行出错,就要
根据 pod 设置的重启策略进行处理。

容器监控:kubelet 会监控所在节点的资源使用情况,并定时向 master 报告,资源使用数据


都是通过 cAdvisor 获取的。知道整个集群所有节点的资源情况,对于 pod 的调度和正常运
行至关重要。

三、kubelet 组件中的模块

本文档使用 书栈网 · BookStack.CN 构建 - 334 -


kubelet 架构浅析

上图展示了 kubelet 组件中的模块以及模块间的划分。

1、PLEG(Pod Lifecycle Event Generator) PLEG 是 kubelet 的核心模块,PLEG 会


一直调用 container runtime 获取本节点 containers/sandboxes 的信息,并与自身维
护的 pods cache 信息进行对比,生成对应的 PodLifecycleEvent,然后输出到
eventChannel 中,通过 eventChannel 发送到 kubelet syncLoop 进行消费,然后由
kubelet syncPod 来触发 pod 同步处理过程,最终达到用户的期望状态。

2、cAdvisor cAdvisor(https://github.com/google/cadvisor)是 google 开发


的容器监控工具,集成在 kubelet 中,起到收集本节点和容器的监控信息,大部分公司对容器
的监控数据都是从 cAdvisor 中获取的 ,cAvisor 模块对外提供了 interface 接口,该
接口也被 imageManager,OOMWatcher,containerManager 等所使用。

本文档使用 书栈网 · BookStack.CN 构建 - 335 -


kubelet 架构浅析

3、OOMWatcher 系统 OOM 的监听器,会与 cadvisor 模块之间建立 SystemOOM,通过


Watch方式从 cadvisor 那里收到的 OOM 信号,并产生相关事件。

4、probeManager probeManager 依赖于


statusManager,livenessManager,containerRefManager,会定时去监控 pod 中容器
的健康状况,当前支持两种类型的探针:livenessProbe 和readinessProbe。
livenessProbe:用于判断容器是否存活,如果探测失败,kubelet 会 kill 掉该容器,并
根据容器的重启策略做相应的处理。 readinessProbe:用于判断容器是否启动完成,将探测
成功的容器加入到该 pod 所在 service 的 endpoints 中,反之则移除。
readinessProbe 和 livenessProbe 有三种实现方式:http、tcp 以及 cmd。

5、statusManager statusManager 负责维护状态信息,并把 pod 状态更新到


apiserver,但是它并不负责监控 pod 状态的变化,而是提供对应的接口供其他组件调用,比
如 probeManager。

6、containerRefManager 容器引用的管理,相对简单的Manager,用来报告容器的创建,失
败等事件,通过定义 map 来实现了 containerID 与 v1.ObjectReferece 容器引用的映
射。

7、evictionManager 当节点的内存、磁盘或 inode 等资源不足时,达到了配置的 evict


策略, node 会变为 pressure 状态,此时 kubelet 会按照 qosClass 顺序来驱赶
pod,以此来保证节点的稳定性。可以通过配置 kubelet 启动参数 --eviction-hard= 来
决定 evict 的策略值。

8、imageGC imageGC 负责 node 节点的镜像回收,当本地的存放镜像的本地磁盘空间达到某


阈值的时候,会触发镜像的回收,删除掉不被 pod 所使用的镜像,回收镜像的阈值可以通过
kubelet 的启动参数 --image-gc-high-threshold 和 --image-gc-low-threshold
来设置。

9、containerGC containerGC 负责清理 node 节点上已消亡的 container,具体的 GC


操作由runtime 来实现。

10、imageManager 调用 kubecontainer 提供的


PullImage/GetImageRef/ListImages/RemoveImage/ImageStates 方法来保证pod 运
行所需要的镜像。

11、volumeManager 负责 node 节点上 pod 所使用 volume 的管理,volume 与 pod


的生命周期关联,负责 pod 创建删除过程中 volume 的 mount/umount/attach/detach
流程,kubernetes 采用 volume Plugins 的方式,实现存储卷的挂载等操作,内置几十种
存储插件。

12、containerManager 负责 node 节点上运行的容器的 cgroup 配置信息,kubelet 启


动参数如果指定 --cgroups-per-qos 的时候,kubelet 会启动 goroutine 来周期性的

本文档使用 书栈网 · BookStack.CN 构建 - 336 -


kubelet 架构浅析

更新 pod 的 cgroup 信息,维护其正确性,该参数默认为 true ,实现了 pod 的


Guaranteed/BestEffort/Burstable 三种级别的 Qos。

13、runtimeManager containerRuntime 负责 kubelet 与不同的 runtime 实现进行


对接,实现对于底层 container 的操作,初始化之后得到的 runtime 实例将会被之前描述
的组件所使用。可以通过 kubelet 的启动参数 --container-runtime 来定义是使用
docker 还是 rkt,默认是 docker 。

14、podManager podManager 提供了接口来存储和访问 pod 的信息,维持 static pod


和 mirror pods 的关系,podManager 会被
statusManager/volumeManager/runtimeManager 所调用,podManager 的接口处理流
程里面会调用 secretManager 以及 configMapManager。

在 v1.12 中,kubelet 组件有18个 manager:

1. certificateManager
2. cgroupManager
3. containerManager
4. cpuManager
5. nodeContainerManager
6. configmapManager
7. containerReferenceManager
8. evictionManager
9. nvidiaGpuManager
10. imageGCManager
11. kuberuntimeManager
12. hostportManager
13. podManager
14. proberManager
15. secretManager
16. statusManager
17. volumeManager
18. tokenManager

其中比较重要的模块后面会进行一一分析。

参考:

微软资深工程师详解 K8S 容器运行时

kubernetes 简介: kubelet 和 pod

Kubelet 组件解析

本文档使用 书栈网 · BookStack.CN 构建 - 337 -


kubelet 启动流程分析

本来这篇文章会继续讲述 kubelet 中的主要模块,但由于网友反馈能不能先从 kubelet 的启动流


程开始,kubelet 的启动流程在很久之前基于 v1.12 写过一篇文章,对比了 v1.16 中的启动流程
变化不大,但之前的文章写的比较简洁,本文会重新分析 kubelet 的启动流程。

Kubelet 启动流程

kubernetes 版本:v1.16

kubelet 的启动比较复杂,首先还是把 kubelet 的启动流程图放在此处,便于在后文中清楚各种调


用的流程:

本文档使用 书栈网 · BookStack.CN 构建 - 338 -


kubelet 启动流程分析

NewKubeletCommand

本文档使用 书栈网 · BookStack.CN 构建 - 339 -


kubelet 启动流程分析

首先从 kubelet 的 main 函数开始,其中调用的 NewKubeletCommand 方法主要负责获取配


置文件中的参数,校验参数以及为参数设置默认值。主要逻辑为:

1、解析命令行参数;
2、为 kubelet 初始化 feature gates 参数;
3、加载 kubelet 配置文件;
4、校验配置文件中的参数;
5、检查 kubelet 是否启用动态配置功能;
6、初始化 kubeletDeps,kubeletDeps 包含 kubelet 运行所必须的配置,是为了实现
dependency injection,其目的是为了把 kubelet 依赖的组件对象作为参数传进来,这样
可以控制 kubelet 的行为;
7、调用 Run 方法;

k8s.io/kubernetes/cmd/kubelet/app/server.go:111

1. func NewKubeletCommand() *cobra.Command {


2. cleanFlagSet := pflag.NewFlagSet(componentKubelet, pflag.ContinueOnError)
3. cleanFlagSet.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
4.
5. // 1、kubelet配置分两部分:
// KubeletFlag: 指那些不允许在 kubelet 运行时进行修改的配置集,或者不能在集群中各个
6. Nodes 之间共享的配置集。
7. // KubeletConfiguration: 指可以在集群中各个Nodes之间共享的配置集,可以进行动态配置。
8. kubeletFlags := options.NewKubeletFlags()
9. kubeletConfig, err := options.NewKubeletConfiguration()
10. if err != nil {
11. klog.Fatal(err)
12. }
13.
14. cmd := &cobra.Command{
15. Use: componentKubelet,
16. DisableFlagParsing: true,
17. ......
18. Run: func(cmd *cobra.Command, args []string) {
19. // 2、解析命令行参数
20. if err := cleanFlagSet.Parse(args); err != nil {
21. cmd.Usage()
22. klog.Fatal(err)
23. }
24. ......
25.
26. verflag.PrintAndExitIfRequested()

本文档使用 书栈网 · BookStack.CN 构建 - 340 -


kubelet 启动流程分析

27. utilflag.PrintFlags(cleanFlagSet)
28.
29. // 3、初始化 feature gates 配置
if err :=
utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates);
30. err != nil {
31. klog.Fatal(err)
32. }
33.
34. if err := options.ValidateKubeletFlags(kubeletFlags); err != nil {
35. klog.Fatal(err)
36. }
37.
if kubeletFlags.ContainerRuntime == "remote" &&
38. 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
39. remote runtime instead")
40. }
41.
42. // 4、加载 kubelet 配置文件
if configFile := kubeletFlags.KubeletConfigFile; len(configFile) >
43. 0 {
44. kubeletConfig, err = loadConfigFile(configFile)
45. ......
46. }
47. // 5、校验配置文件中的参数
if err :=
kubeletconfigvalidation.ValidateKubeletConfiguration(kubeletConfig); err != nil
48. {
49. klog.Fatal(err)
50. }
51.
52. // 6、检查 kubelet 是否启用动态配置功能
53. var kubeletConfigController *dynamickubeletconfig.Controller
if dynamicConfigDir := kubeletFlags.DynamicConfigDir.Value();
54. len(dynamicConfigDir) > 0 {
var dynamicKubeletConfig
55. *kubeletconfiginternal.KubeletConfiguration
dynamicKubeletConfig, kubeletConfigController, err =
56. BootstrapKubeletConfigController(dynamicConfigDir,
func(kc *kubeletconfiginternal.KubeletConfiguration) error
57. {
58. return kubeletConfigFlagPrecedence(kc, args)

本文档使用 书栈网 · BookStack.CN 构建 - 341 -


kubelet 启动流程分析

59. })
60. if err != nil {
61. klog.Fatal(err)
62. }
63. if dynamicKubeletConfig != nil {
64. kubeletConfig = dynamicKubeletConfig
if err :=
utilfeature.DefaultMutableFeatureGate.SetFromMap(kubeletConfig.FeatureGates);
65. err != nil {
66. klog.Fatal(err)
67. }
68. }
69. }
70. kubeletServer := &options.KubeletServer{
71. KubeletFlags: *kubeletFlags,
72. KubeletConfiguration: *kubeletConfig,
73. }
74. // 7、初始化 kubeletDeps
75. kubeletDeps, err := UnsecuredDependencies(kubeletServer)
76. if err != nil {
77. klog.Fatal(err)
78. }
79.
80. kubeletDeps.KubeletConfigController = kubeletConfigController
81. stopCh := genericapiserver.SetupSignalHandler()
82. if kubeletServer.KubeletFlags.ExperimentalDockershim {
if err := RunDockershim(&kubeletServer.KubeletFlags,
83. kubeletConfig, stopCh); err != nil {
84. klog.Fatal(err)
85. }
86. return
87. }
88.
89. // 8、调用 Run 方法
90. if err := Run(kubeletServer, kubeletDeps, stopCh); err != nil {
91. klog.Fatal(err)
92. }
93. },
94. }
95. kubeletFlags.AddFlags(cleanFlagSet)
96. options.AddKubeletConfigFlags(cleanFlagSet, kubeletConfig)
97. options.AddGlobalFlags(cleanFlagSet)
98. ......

本文档使用 书栈网 · BookStack.CN 构建 - 342 -


kubelet 启动流程分析

99.
100. return cmd
101. }

Run

该方法中仅仅调用 run 方法执行后面的启动逻辑。

k8s.io/kubernetes/cmd/kubelet/app/server.go:408

func Run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-


1. chan struct{}) error {
2. if err := initForOS(s.KubeletFlags.WindowsService); err != nil {
3. return fmt.Errorf("failed OS init: %v", err)
4. }
5. if err := run(s, kubeDeps, stopCh); err != nil {
6. return fmt.Errorf("failed to run Kubelet: %v", err)
7. }
8. return nil
9. }

run

run 方法中主要是为 kubelet 的启动做一些基本的配置及检查工作,主要逻辑为:

1、为 kubelet 设置默认的 FeatureGates,kubelet 所有的 FeatureGates 可以通过


命令参数查看,k8s 中处于 Alpha 状态的 FeatureGates 在组件启动时默认关闭,处于
Beta 和 GA 状态的默认开启;
2、校验 kubelet 的参数;
3、尝试获取 kubelet 的 lock file ,需要在 kubelet 启动时指定 --exit-on-lock-
contention 和 --lock-file ,该功能处于 Alpha 版本默认为关闭状态;
4、将当前的配置文件注册到 http server /configz URL 中;
5、检查 kubelet 启动模式是否为 standalone 模式,此模式下不会和 apiserver 交互,
主要用于 kubelet 的调试;
6、初始化 kubeDeps,kubeDeps 中包含 kubelet 的一些依赖,主要有
KubeClient 、 EventClient 、 HeartbeatClient 、 Auth 、 cadvisor 、 ContainerMa
nager ;
7、检查是否以 root 用户启动;
8、为进程设置 oom 分数,默认为 -999,分数范围为 [-1000, 1000],越小越不容易被
kill 掉;
9、调用 RunKubelet 方法;
10、检查 kubelet 是否启动了动态配置功能;

本文档使用 书栈网 · BookStack.CN 构建 - 343 -


kubelet 启动流程分析

11、启动 Healthz http server;


12、如果使用 systemd 启动,通知 systemd kubelet 已经启动;

k8s.io/kubernetes/cmd/kubelet/app/server.go:472

func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-


1. chan struct{}) (err error) {
2. // 1、为 kubelet 设置默认的 FeatureGates
err =
3. utilfeature.DefaultMutableFeatureGate.SetFromMap(s.KubeletConfiguration.FeatureGates
4. if err != nil {
5. return err
6. }
7. // 2、校验 kubelet 的参数
8. if err := options.ValidateKubeletServer(s); err != nil {
9. return err
10. }
11.
12. // 3、尝试获取 kubelet 的 lock file
13. if s.ExitOnLockContention && s.LockFilePath == "" {
return errors.New("cannot exit on lock file contention: no lock file
14. specified")
15. }
16. done := make(chan struct{})
17. if s.LockFilePath != "" {
18. klog.Infof("acquiring file lock on %q", s.LockFilePath)
19. if err := flock.Acquire(s.LockFilePath); err != nil {
return fmt.Errorf("unable to acquire file lock on %q: %v",
20. s.LockFilePath, err)
21. }
22. if s.ExitOnLockContention {
23. klog.Infof("watching for inotify events for: %v", s.LockFilePath)
if err := watchForLockfileContention(s.LockFilePath, done); err !=
24. nil {
25. return err
26. }
27. }
28. }
29. // 4、将当前的配置文件注册到 http server /configz URL 中;
30. err = initConfigz(&s.KubeletConfiguration)
31. if err != nil {
klog.Errorf("unable to register KubeletConfiguration with configz,
32. error: %v", err)

本文档使用 书栈网 · BookStack.CN 构建 - 344 -


kubelet 启动流程分析

33. }
34.
35. // 5、判断是否为 standalone 模式
36. standaloneMode := true
37. if len(s.KubeConfig) > 0 {
38. standaloneMode = false
39. }
40.
41. // 6、初始化 kubeDeps
42. if kubeDeps == nil {
43. kubeDeps, err = UnsecuredDependencies(s)
44. if err != nil {
45. return err
46. }
47. }
48. if kubeDeps.Cloud == nil {
49. if !cloudprovider.IsExternal(s.CloudProvider) {
cloud, err := cloudprovider.InitCloudProvider(s.CloudProvider,
50. s.CloudConfigFile)
51. if err != nil {
52. return err
53. }
54. ......
55. kubeDeps.Cloud = cloud
56. }
57. }
58.
59. hostName, err := nodeutil.GetHostname(s.HostnameOverride)
60. if err != nil {
61. return err
62. }
63. nodeName, err := getNodeName(kubeDeps.Cloud, hostName)
64. if err != nil {
65. return err
66. }
67. // 7、如果是 standalone 模式将所有 client 设置为 nil
68. switch {
69. case standaloneMode:
70. kubeDeps.KubeClient = nil
71. kubeDeps.EventClient = nil
72. kubeDeps.HeartbeatClient = nil
73.

本文档使用 书栈网 · BookStack.CN 构建 - 345 -


kubelet 启动流程分析

74. // 8、为 kubeDeps 初始化 KubeClient、EventClient、HeartbeatClient 模块


case kubeDeps.KubeClient == nil, kubeDeps.EventClient == nil,
75. kubeDeps.HeartbeatClient == nil:
clientConfig, closeAllConns, err := buildKubeletClientConfig(s,
76. nodeName)
77. if err != nil {
78. return err
79. }
80. if closeAllConns == nil {
return errors.New("closeAllConns must be a valid function other
81. than nil")
82. }
83. kubeDeps.OnHeartbeatFailure = closeAllConns
84.
85. kubeDeps.KubeClient, err = clientset.NewForConfig(clientConfig)
86. if err != nil {
87. return fmt.Errorf("failed to initialize kubelet client: %v", err)
88. }
89.
90. eventClientConfig := *clientConfig
91. eventClientConfig.QPS = float32(s.EventRecordQPS)
92. eventClientConfig.Burst = int(s.EventBurst)
93. kubeDeps.EventClient, err = v1core.NewForConfig(&eventClientConfig)
94. if err != nil {
return fmt.Errorf("failed to initialize kubelet event client: %v",
95. err)
96. }
97.
98. heartbeatClientConfig := *clientConfig
heartbeatClientConfig.Timeout =
99. s.KubeletConfiguration.NodeStatusUpdateFrequency.Duration
100.
101. if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
leaseTimeout :=
102. time.Duration(s.KubeletConfiguration.NodeLeaseDurationSeconds) * time.Second
103. if heartbeatClientConfig.Timeout > leaseTimeout {
104. heartbeatClientConfig.Timeout = leaseTimeout
105. }
106. }
107. heartbeatClientConfig.QPS = float32(-1)
kubeDeps.HeartbeatClient, err =
108. clientset.NewForConfig(&heartbeatClientConfig)
109. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 346 -


kubelet 启动流程分析

return fmt.Errorf("failed to initialize kubelet heartbeat client:


110. %v", err)
111. }
112. }
113. // 9、初始化 auth 模块
114. if kubeDeps.Auth == nil {
auth, err := BuildAuth(nodeName, kubeDeps.KubeClient,
115. s.KubeletConfiguration)
116. if err != nil {
117. return err
118. }
119. kubeDeps.Auth = auth
120. }
121.
122. var cgroupRoots []string
123.
124. // 10、设置 cgroupRoot
cgroupRoots = append(cgroupRoots, cm.NodeAllocatableRoot(s.CgroupRoot,
125. s.CgroupDriver))
126. kubeletCgroup, err := cm.GetKubeletContainer(s.KubeletCgroups)
127. if err != nil {
128. } else if kubeletCgroup != "" {
129. cgroupRoots = append(cgroupRoots, kubeletCgroup)
130. }
131.
runtimeCgroup, err := cm.GetRuntimeContainer(s.ContainerRuntime,
132. s.RuntimeCgroups)
133. if err != nil {
134. } else if runtimeCgroup != "" {
135. cgroupRoots = append(cgroupRoots, runtimeCgroup)
136. }
137. if s.SystemCgroups != "" {
138. cgroupRoots = append(cgroupRoots, s.SystemCgroups)
139. }
140.
141. // 11、初始化 cadvisor
142. if kubeDeps.CAdvisorInterface == nil {
imageFsInfoProvider :=
143. cadvisor.NewImageFsInfoProvider(s.ContainerRuntime, s.RemoteRuntimeEndpoint)
kubeDeps.CAdvisorInterface, err = cadvisor.New(imageFsInfoProvider,
s.RootDirectory, cgroupRoots, cadvisor.UsingLegacyCadvisorStats(s.
144. ContainerRuntime, s.RemoteRuntimeEndpoint))
145. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 347 -


kubelet 启动流程分析

146. return err


147. }
148. }
149.
150. makeEventRecorder(kubeDeps, nodeName)
151.
152. // 12、初始化 ContainerManager
153. if kubeDeps.ContainerManager == nil {
154. if s.CgroupsPerQOS && s.CgroupRoot == "" {
155. s.CgroupRoot = "/"
156. }
157. kubeReserved, err := parseResourceList(s.KubeReserved)
158. if err != nil {
159. return err
160. }
161. systemReserved, err := parseResourceList(s.SystemReserved)
162. if err != nil {
163. return err
164. }
165. var hardEvictionThresholds []evictionapi.Threshold
166. if !s.ExperimentalNodeAllocatableIgnoreEvictionThreshold {
hardEvictionThresholds, err =
167. eviction.ParseThresholdConfig([]string{}, s.EvictionHard, nil, nil, nil)
168. if err != nil {
169. return err
170. }
171. }
172. experimentalQOSReserved, err := cm.ParseQOSReserved(s.QOSReserved)
173. if err != nil {
174. return err
175. }
176.
devicePluginEnabled :=
177. utilfeature.DefaultFeatureGate.Enabled(features.DevicePlugins)
178. kubeDeps.ContainerManager, err = cm.NewContainerManager(
179. kubeDeps.Mounter,
180. kubeDeps.CAdvisorInterface,
181. cm.NodeConfig{
182. ......
183. },
184. s.FailSwapOn,
185. devicePluginEnabled,
186. kubeDeps.Recorder)

本文档使用 书栈网 · BookStack.CN 构建 - 348 -


kubelet 启动流程分析

187. if err != nil {


188. return err
189. }
190. }
191.
192. // 13、检查是否以 root 权限启动
193. if err := checkPermissions(); err != nil {
194. klog.Error(err)
195. }
196.
197. utilruntime.ReallyCrash = s.ReallyCrashForTesting
198.
199. // 14、为 kubelet 进程设置 oom 分数
200. oomAdjuster := kubeDeps.OOMAdjuster
201. if err := oomAdjuster.ApplyOOMScoreAdj(0, int(s.OOMScoreAdj)); err != nil {
202. klog.Warning(err)
203. }
204.
205. // 15、调用 RunKubelet 方法执行后续的启动操作
206. if err := RunKubelet(s, kubeDeps, s.RunOnce); err != nil {
207. return err
208. }
209.
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicKubeletConfig) &&
210. len(s.DynamicConfigDir.Value()) > 0 &&
kubeDeps.KubeletConfigController != nil && !standaloneMode &&
211. !s.RunOnce {
if err :=
kubeDeps.KubeletConfigController.StartSync(kubeDeps.KubeClient,
212. kubeDeps.EventClient, string(nodeName)); err != nil {
213. return err
214. }
215. }
216.
217. // 16、启动 Healthz http server
218. if s.HealthzPort > 0 {
219. mux := http.NewServeMux()
220. healthz.InstallHandler(mux)
221. go wait.Until(func() {
err := http.ListenAndServe(net.JoinHostPort(s.HealthzBindAddress,
222. strconv.Itoa(int(s.HealthzPort))), mux)
223. if err != nil {
224. klog.Errorf("Starting healthz server failed: %v", err)

本文档使用 书栈网 · BookStack.CN 构建 - 349 -


kubelet 启动流程分析

225. }
226. }, 5*time.Second, wait.NeverStop)
227. }
228.
229. if s.RunOnce {
230. return nil
231. }
232.
233. // 17、向 systemd 发送启动信号
234. go daemon.SdNotify(false, "READY=1")
235.
236. select {
237. case <-done:
238. break
239. case <-stopCh:
240. break
241. }
242. return nil
243. }

RunKubelet

RunKubelet 中主要调用了 createAndInitKubelet 方法执行 kubelet 组件的初始化,然


后调用 startKubelet 启动 kubelet 中的组件。

k8s.io/kubernetes/cmd/kubelet/app/server.go:989

func RunKubelet(kubeServer *options.KubeletServer, kubeDeps


1. *kubelet.Dependencies, runOnce bool) error {
2. hostname, err := nodeutil.GetHostname(kubeServer.HostnameOverride)
3. if err != nil {
4. return err
5. }
6. nodeName, err := getNodeName(kubeDeps.Cloud, hostname)
7. if err != nil {
8. return err
9. }
10. makeEventRecorder(kubeDeps, nodeName)
11.
12. // 1、默认启动特权模式
13. capabilities.Initialize(capabilities.Capabilities{
14. AllowPrivileged: true,

本文档使用 书栈网 · BookStack.CN 构建 - 350 -


kubelet 启动流程分析

15. })
16.
17. credentialprovider.SetPreferredDockercfgPath(kubeServer.RootDirectory)
18.
19. if kubeDeps.OSInterface == nil {
20. kubeDeps.OSInterface = kubecontainer.RealOS{}
21. }
22.
23. // 2、调用 createAndInitKubelet
24. k, err := createAndInitKubelet(&kubeServer.KubeletConfiguration,
25. ......
26. kubeServer.NodeStatusMaxImages)
27. if err != nil {
28. return fmt.Errorf("failed to create kubelet: %v", err)
29. }
30.
31. if kubeDeps.PodConfig == nil {
return fmt.Errorf("failed to create kubelet, pod source config was
32. nil")
33. }
34. podCfg := kubeDeps.PodConfig
35.
36. rlimit.RlimitNumFiles(uint64(kubeServer.MaxOpenFiles))
37.
38. if runOnce {
39. if _, err := k.RunOnce(podCfg.Updates()); err != nil {
40. return fmt.Errorf("runonce failed: %v", err)
41. }
42. klog.Info("Started kubelet as runonce")
43. } else {
44. // 3、调用 startKubelet
startKubelet(k, podCfg, &kubeServer.KubeletConfiguration, kubeDeps,
45. kubeServer.EnableCAdvisorJSONEndpoints, kubeServer.EnableServer)
46. klog.Info("Started kubelet")
47. }
48. return nil
49. }

createAndInitKubelet

createAndInitKubelet 中主要调用了三个方法来完成 kubelet 的初始化:

kubelet.NewMainKubelet :实例化 kubelet 对象,并对 kubelet 依赖的所有模块进行初

本文档使用 书栈网 · BookStack.CN 构建 - 351 -


kubelet 启动流程分析

始化;
k.BirthCry :向 apiserver 发送一条 kubelet 启动了的 event;
k.StartGarbageCollection :启动垃圾回收服务,回收 container 和 images;

k8s.io/kubernetes/cmd/kubelet/app/server.go:1089

1. func createAndInitKubelet(......) {
2. k, err = kubelet.NewMainKubelet(
3. ......
4. )
5. if err != nil {
6. return nil, err
7. }
8.
9. k.BirthCry()
10.
11. k.StartGarbageCollection()
12.
13. return k, nil
14. }

kubelet.NewMainKubelet

NewMainKubelet 是初始化 kubelet 的一个方法,主要逻辑为:

1、初始化 PodConfig 即监听 pod 元数据的来源(file,http,apiserver),将不同


source 的 pod configuration 合并到一个结构中;
2、初始化 containerGCPolicy、imageGCPolicy、evictionConfig 配置;
3、启动 serviceInformer 和 nodeInformer;
4、初始化 containerRefManager 、 oomWatcher ;
5、初始化 kubelet 对象;
6、初始化 secretManager 、 configMapManager ;
7、初始化 livenessManager 、 podManager 、 statusManager 、 resourceAnalyzer ;
8、调用 kuberuntime.NewKubeGenericRuntimeManager 初始化 containerRuntime ;
9、初始化 pleg ;
10、初始化
containerGC 、 containerDeletor 、 imageManager 、 containerLogManager ;
11、初始化
serverCertificateManager 、 probeManager 、 tokenManager 、 volumePluginMgr 、
pluginManager 、 volumeManager ;
12、初始化 workQueue 、 podWorkers 、 evictionManager ;
13、最后注册相关模块的 handler;

本文档使用 书栈网 · BookStack.CN 构建 - 352 -


kubelet 启动流程分析

NewMainKubelet 中对 kubelet 依赖的所有模块进行了初始化,每个模块对应的功能在上篇文


章“kubelet 架构浅析”有介绍,至于每个模块初始化的流程以及功能会在后面的文章中进行详细分
析。

k8s.io/kubernetes/pkg/kubelet/kubelet.go:335

1. func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,) {


2. if rootDirectory == "" {
3. return nil, fmt.Errorf("invalid root directory %q", rootDirectory)
4. }
5. if kubeCfg.SyncFrequency.Duration <= 0 {
return nil, fmt.Errorf("invalid sync frequency %d",
6. kubeCfg.SyncFrequency.Duration)
7. }
8.
9. if kubeCfg.MakeIPTablesUtilChains {
10. ......
11. }
12.
13. hostname, err := nodeutil.GetHostname(hostnameOverride)
14. if err != nil {
15. return nil, err
16. }
17.
18. nodeName := types.NodeName(hostname)
19. if kubeDeps.Cloud != nil {
20. ......
21. }
22.
23. // 1、初始化 PodConfig
24. if kubeDeps.PodConfig == nil {
25. var err error
kubeDeps.PodConfig, err = makePodSourceConfig(kubeCfg, kubeDeps,
26. nodeName, bootstrapCheckpointPath)
27. if err != nil {
28. return nil, err
29. }
30. }
31.
32. // 2、初始化 containerGCPolicy、imageGCPolicy、evictionConfig
33. containerGCPolicy := kubecontainer.ContainerGCPolicy{
34. MinAge: minimumGCAge.Duration,
35. MaxPerPodContainer: int(maxPerPodContainerCount),

本文档使用 书栈网 · BookStack.CN 构建 - 353 -


kubelet 启动流程分析

36. MaxContainers: int(maxContainerCount),


37. }
38. daemonEndpoints := &v1.NodeDaemonEndpoints{
39. KubeletEndpoint: v1.DaemonEndpoint{Port: kubeCfg.Port},
40. }
41.
42. imageGCPolicy := images.ImageGCPolicy{
43. MinAge: kubeCfg.ImageMinimumGCAge.Duration,
44. HighThresholdPercent: int(kubeCfg.ImageGCHighThresholdPercent),
45. LowThresholdPercent: int(kubeCfg.ImageGCLowThresholdPercent),
46. }
47.
48. enforceNodeAllocatable := kubeCfg.EnforceNodeAllocatable
49. if experimentalNodeAllocatableIgnoreEvictionThreshold {
50. enforceNodeAllocatable = []string{}
51. }
thresholds, err := eviction.ParseThresholdConfig(enforceNodeAllocatable,
kubeCfg.EvictionHard, kubeCfg.EvictionSoft, kubeCfg.
52. EvictionSoftGracePeriod, kubeCfg.EvictionMinimumReclaim)
53. if err != nil {
54. return nil, err
55. }
56. evictionConfig := eviction.Config{
PressureTransitionPeriod:
57. kubeCfg.EvictionPressureTransitionPeriod.Duration,
58. MaxPodGracePeriodSeconds: int64(kubeCfg.EvictionMaxPodGracePeriod),
59. Thresholds: thresholds,
60. KernelMemcgNotification: experimentalKernelMemcgNotification,
61. PodCgroupRoot: kubeDeps.ContainerManager.GetPodCgroupRoot(),
62. }
63. // 3、启动 serviceInformer 和 nodeInformer
serviceIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc,
64. cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
65. if kubeDeps.KubeClient != nil {
serviceLW :=
cache.NewListWatchFromClient(kubeDeps.KubeClient.CoreV1().RESTClient(),
66. "services", metav1.NamespaceAll, fields.Everything())
67. r := cache.NewReflector(serviceLW, &v1.Service{}, serviceIndexer, 0)
68. go r.Run(wait.NeverStop)
69. }
70. serviceLister := corelisters.NewServiceLister(serviceIndexer)
71.

本文档使用 书栈网 · BookStack.CN 构建 - 354 -


kubelet 启动流程分析

nodeIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc,
72. cache.Indexers{})
73. if kubeDeps.KubeClient != nil {
fieldSelector := fields.Set{api.ObjectNameField:
74. string(nodeName)}.AsSelector()
nodeLW :=
cache.NewListWatchFromClient(kubeDeps.KubeClient.CoreV1().RESTClient(),
75. "nodes", metav1.NamespaceAll, fieldSelector)
76. r := cache.NewReflector(nodeLW, &v1.Node{}, nodeIndexer, 0)
77. go r.Run(wait.NeverStop)
78. }
nodeInfo := &CachedNodeInfo{NodeLister:
79. corelisters.NewNodeLister(nodeIndexer)}
80.
81. ......
82.
83. // 4、初始化 containerRefManager、oomWatcher
84. containerRefManager := kubecontainer.NewRefManager()
85.
86. oomWatcher := oomwatcher.NewWatcher(kubeDeps.Recorder)
87. clusterDNS := make([]net.IP, 0, len(kubeCfg.ClusterDNS))
88. for _, ipEntry := range kubeCfg.ClusterDNS {
89. ip := net.ParseIP(ipEntry)
90. if ip == nil {
91. klog.Warningf("Invalid clusterDNS ip '%q'", ipEntry)
92. } else {
93. clusterDNS = append(clusterDNS, ip)
94. }
95. }
96. httpClient := &http.Client{}
97. parsedNodeIP := net.ParseIP(nodeIP)
98. protocol := utilipt.ProtocolIpv4
99. if parsedNodeIP != nil && parsedNodeIP.To4() == nil {
100. protocol = utilipt.ProtocolIpv6
101. }
102.
103. // 5、初始化 kubelet 对象
104. klet := &Kubelet{......}
105.
106. if klet.cloud != nil {
klet.cloudResourceSyncManager =
cloudresource.NewSyncManager(klet.cloud, nodeName,
107. klet.nodeStatusUpdateFrequency)

本文档使用 书栈网 · BookStack.CN 构建 - 355 -


kubelet 启动流程分析

108. }
109.
110. // 6、初始化 secretManager、configMapManager
111. var secretManager secret.Manager
112. var configMapManager configmap.Manager
113. switch kubeCfg.ConfigMapAndSecretChangeDetectionStrategy {
114. case kubeletconfiginternal.WatchChangeDetectionStrategy:
115. secretManager = secret.NewWatchingSecretManager(kubeDeps.KubeClient)
configMapManager =
116. configmap.NewWatchingConfigMapManager(kubeDeps.KubeClient)
117. case kubeletconfiginternal.TTLCacheChangeDetectionStrategy:
118. secretManager = secret.NewCachingSecretManager(
kubeDeps.KubeClient,
119. manager.GetObjectTTLFromNodeFunc(klet.GetNode))
120. configMapManager = configmap.NewCachingConfigMapManager(
kubeDeps.KubeClient,
121. manager.GetObjectTTLFromNodeFunc(klet.GetNode))
122. case kubeletconfiginternal.GetChangeDetectionStrategy:
123. secretManager = secret.NewSimpleSecretManager(kubeDeps.KubeClient)
configMapManager =
124. configmap.NewSimpleConfigMapManager(kubeDeps.KubeClient)
125. default:
return nil, fmt.Errorf("unknown configmap and secret manager mode: %v",
126. kubeCfg.ConfigMapAndSecretChangeDetectionStrategy)
127. }
128.
129. klet.secretManager = secretManager
130. klet.configMapManager = configMapManager
131. if klet.experimentalHostUserNamespaceDefaulting {
132. klog.Infof("Experimental host user namespace defaulting is enabled.")
133. }
134.
135. machineInfo, err := klet.cadvisor.MachineInfo()
136. if err != nil {
137. return nil, err
138. }
139. klet.machineInfo = machineInfo
140.
141. imageBackOff := flowcontrol.NewBackOff(backOffPeriod, MaxContainerBackOff)
142.
143. // 7、初始化 livenessManager、podManager、statusManager、resourceAnalyzer
144. klet.livenessManager = proberesults.NewManager()
145.

本文档使用 书栈网 · BookStack.CN 构建 - 356 -


kubelet 启动流程分析

146. klet.podCache = kubecontainer.NewCache()


147. var checkpointManager checkpointmanager.CheckpointManager
148. if bootstrapCheckpointPath != "" {
checkpointManager, err =
149. checkpointmanager.NewCheckpointManager(bootstrapCheckpointPath)
150. if err != nil {
return nil, fmt.Errorf("failed to initialize checkpoint manager:
151. %+v", err)
152. }
153. }
154.
klet.podManager =
kubepod.NewBasicPodManager(kubepod.NewBasicMirrorClient(klet.kubeClient),
155. secretManager, configMapManager, checkpointManager)
156.
klet.statusManager = status.NewManager(klet.kubeClient, klet.podManager,
157. klet)
158.
159. if remoteRuntimeEndpoint != "" {
160. if remoteImageEndpoint == "" {
161. remoteImageEndpoint = remoteRuntimeEndpoint
162. }
163. }
164.
165. pluginSettings := dockershim.NetworkPluginSettings{......}
166.
klet.resourceAnalyzer = serverstats.NewResourceAnalyzer(klet,
167. kubeCfg.VolumeStatsAggPeriod.Duration)
168.
169. var legacyLogProvider kuberuntime.LegacyLogProvider
170.
171. // 8、调用 kuberuntime.NewKubeGenericRuntimeManager 初始化 containerRuntime
172. switch containerRuntime {
173. case kubetypes.DockerContainerRuntime:
174. streamingConfig := getStreamingConfig(kubeCfg, kubeDeps, crOptions)
ds, err := dockershim.NewDockerService(kubeDeps.DockerClientConfig,
175. crOptions.PodSandboxImage, streamingConfig,
&pluginSettings, runtimeCgroups, kubeCfg.CgroupDriver,
176. crOptions.DockershimRootDirectory, !crOptions.RedirectContainerStreaming)
177. if err != nil {
178. return nil, err
179. }
180. if crOptions.RedirectContainerStreaming {

本文档使用 书栈网 · BookStack.CN 构建 - 357 -


kubelet 启动流程分析

181. klet.criHandler = ds
182. }
183.
184. server := dockerremote.NewDockerServer(remoteRuntimeEndpoint, ds)
185. if err := server.Start(); err != nil {
186. return nil, err
187. }
188.
189. supported, err := ds.IsCRISupportedLogDriver()
190. if err != nil {
191. return nil, err
192. }
193. if !supported {
194. klet.dockerLegacyService = ds
195. legacyLogProvider = ds
196. }
197. case kubetypes.RemoteContainerRuntime:
198. break
199. default:
200. return nil, fmt.Errorf("unsupported CRI runtime: %q", containerRuntime)
201. }
runtimeService, imageService, err :=
getRuntimeAndImageServices(remoteRuntimeEndpoint, remoteImageEndpoint,
202. kubeCfg.RuntimeRequestTimeout)
203. if err != nil {
204. return nil, err
205. }
206. klet.runtimeService = runtimeService
if utilfeature.DefaultFeatureGate.Enabled(features.RuntimeClass) &&
207. kubeDeps.KubeClient != nil {
208. klet.runtimeClassManager = runtimeclass.NewManager(kubeDeps.KubeClient)
209. }
210.
211. runtime, err := kuberuntime.NewKubeGenericRuntimeManager(......)
212. if err != nil {
213. return nil, err
214. }
215. klet.containerRuntime = runtime
216. klet.streamingRuntime = runtime
217. klet.runner = runtime
218.
219. runtimeCache, err := kubecontainer.NewRuntimeCache(klet.containerRuntime)
220. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 358 -


kubelet 启动流程分析

221. return nil, err


222. }
223. klet.runtimeCache = runtimeCache
224.
if cadvisor.UsingLegacyCadvisorStats(containerRuntime,
225. remoteRuntimeEndpoint) {
226. klet.StatsProvider = stats.NewCadvisorStatsProvider(......)
227. } else {
228. klet.StatsProvider = stats.NewCRIStatsProvider(......)
229. }
230. // 9、初始化 pleg
klet.pleg = pleg.NewGenericPLEG(klet.containerRuntime, plegChannelCapacity,
231. plegRelistPeriod, klet.podCache, clock.RealClock{})
232. klet.runtimeState = newRuntimeState(maxWaitForContainerRuntime)
233. klet.runtimeState.addHealthCheck("PLEG", klet.pleg.Healthy)
234. if _, err := klet.updatePodCIDR(kubeCfg.PodCIDR); err != nil {
235. klog.Errorf("Pod CIDR update failed %v", err)
236. }
237.
// 10、初始化 containerGC、containerDeletor、imageManager、
238. containerLogManager
containerGC, err := kubecontainer.NewContainerGC(klet.containerRuntime,
239. containerGCPolicy, klet.sourcesReady)
240. if err != nil {
241. return nil, err
242. }
243. klet.containerGC = containerGC
klet.containerDeletor = newPodContainerDeletor(klet.containerRuntime,
244. integer.IntMax(containerGCPolicy.MaxPerPodContainer, minDeadContainerInPod))
245.
imageManager, err := images.NewImageGCManager(klet.containerRuntime,
klet.StatsProvider, kubeDeps.Recorder, nodeRef, imageGCPolicy, crOptions.
246. PodSandboxImage)
247. if err != nil {
248. return nil, fmt.Errorf("failed to initialize image manager: %v", err)
249. }
250. klet.imageManager = imageManager
251.
if containerRuntime == kubetypes.RemoteContainerRuntime &&
252. utilfeature.DefaultFeatureGate.Enabled(features.CRIContainerLogRotation) {
253. containerLogManager, err := logs.NewContainerLogManager(
254. klet.runtimeService,
255. kubeCfg.ContainerLogMaxSize,

本文档使用 书栈网 · BookStack.CN 构建 - 359 -


kubelet 启动流程分析

256. int(kubeCfg.ContainerLogMaxFiles),
257. )
258. if err != nil {
return nil, fmt.Errorf("failed to initialize container log manager:
259. %v", err)
260. }
261. klet.containerLogManager = containerLogManager
262. } else {
263. klet.containerLogManager = logs.NewStubContainerLogManager()
264. }
// 11、初始化 serverCertificateManager、probeManager、tokenManager、
265. volumePluginMgr、pluginManager、volumeManager
if kubeCfg.ServerTLSBootstrap && kubeDeps.TLSOptions != nil &&
utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletServerCertificate)
266. {
klet.serverCertificateManager, err =
kubeletcertificate.NewKubeletServerCertificateManager(klet.kubeClient, kubeCfg,
267. klet.nodeName, klet. getLastObservedNodeAddresses, certDirectory)
268. if err != nil {
return nil, fmt.Errorf("failed to initialize certificate manager:
269. %v", err)
270. }
kubeDeps.TLSOptions.Config.GetCertificate = func(*tls.ClientHelloInfo)
271. (*tls.Certificate, error) {
272. cert := klet.serverCertificateManager.Current()
273. if cert == nil {
return nil, fmt.Errorf("no serving certificate available for
274. the kubelet")
275. }
276. return cert, nil
277. }
278. }
279.
280. klet.probeManager = prober.NewManager(......)
281. tokenManager := token.NewManager(kubeDeps.KubeClient)
282.
283. klet.volumePluginMgr, err =
NewInitializedVolumePluginMgr(klet, secretManager, configMapManager,
284. tokenManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber)
285. if err != nil {
286. return nil, err
287. }
288. klet.pluginManager = pluginmanager.NewPluginManager(

本文档使用 书栈网 · BookStack.CN 构建 - 360 -


kubelet 启动流程分析

289. klet.getPluginsRegistrationDir(), /* sockDir */


290. klet.getPluginsDir(), /* deprecatedSockDir */
291. kubeDeps.Recorder,
292. )
293.
294. if len(experimentalMounterPath) != 0 {
295. experimentalCheckNodeCapabilitiesBeforeMount = false

296. klet.dnsConfigurer.SetupDNSinContainerizedMounter(experimentalMounterPath)
297. }
298. klet.volumeManager = volumemanager.NewVolumeManager(......)
299.
300. // 12、初始化 workQueue、podWorkers、evictionManager
301. klet.reasonCache = NewReasonCache()
302. klet.workQueue = queue.NewBasicWorkQueue(klet.clock)
klet.podWorkers = newPodWorkers(klet.syncPod, kubeDeps.Recorder,
303. klet.workQueue, klet.resyncInterval, backOffPeriod, klet.podCache)
304.
305. klet.backOff = flowcontrol.NewBackOff(backOffPeriod, MaxContainerBackOff)
klet.podKillingCh = make(chan *kubecontainer.PodPair,
306. podKillingChannelCapacity)
307.
evictionManager, evictionAdmitHandler :=
eviction.NewManager(klet.resourceAnalyzer, evictionConfig,
killPodNow(klet.podWorkers, kubeDeps.Recorder),
klet.podManager.GetMirrorPodByPod, klet.imageManager, klet.containerGC,
308. kubeDeps.Recorder, nodeRef, klet.clock)
309.
310. klet.evictionManager = evictionManager
311. klet.admitHandlers.AddPodAdmitHandler(evictionAdmitHandler)
312. if utilfeature.DefaultFeatureGate.Enabled(features.Sysctls) {
runtimeSupport, err :=
313. sysctl.NewRuntimeAdmitHandler(klet.containerRuntime)
314. if err != nil {
315. return nil, err
316. }
317.
safeAndUnsafeSysctls := append(sysctlwhitelist.SafeSysctlWhitelist(),
318. allowedUnsafeSysctls...)
319. sysctlsWhitelist, err := sysctl.NewWhitelist(safeAndUnsafeSysctls)
320. if err != nil {
321. return nil, err
322. }

本文档使用 书栈网 · BookStack.CN 构建 - 361 -


kubelet 启动流程分析

323. klet.admitHandlers.AddPodAdmitHandler(runtimeSupport)
324. klet.admitHandlers.AddPodAdmitHandler(sysctlsWhitelist)
325. }
326.
327. // 13、为 pod 注册相关模块的 handler
activeDeadlineHandler, err := newActiveDeadlineHandler(klet.statusManager,
328. kubeDeps.Recorder, klet.clock)
329. if err != nil {
330. return nil, err
331. }
332. klet.AddPodSyncLoopHandler(activeDeadlineHandler)
333. klet.AddPodSyncHandler(activeDeadlineHandler)
334. if utilfeature.DefaultFeatureGate.Enabled(features.TopologyManager) {

335. klet.admitHandlers.AddPodAdmitHandler(klet.containerManager.GetTopologyPodAdmitHandler
336. }
criticalPodAdmissionHandler :=
preemption.NewCriticalPodAdmissionHandler(klet.GetActivePods,
337. killPodNow(klet.podWorkers, kubeDeps.Recorder),kubeDeps.Recorder)

klet.admitHandlers.AddPodAdmitHandler(lifecycle.NewPredicateAdmitHandler(klet.getNodeAn
338. criticalPodAdmissionHandler, klet.containerManager.UpdatePluginResources))
339. for _, opt := range kubeDeps.Options {
340. opt(klet)
341. }
342.
343. klet.appArmorValidator = apparmor.NewValidator(containerRuntime)

344. klet.softAdmitHandlers.AddPodAdmitHandler(lifecycle.NewAppArmorAdmitHandler(klet.

345. klet.softAdmitHandlers.AddPodAdmitHandler(lifecycle.NewNoNewPrivsAdmitHandler(klet
346.
347. if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
klet.nodeLeaseController = nodelease.NewController(klet.clock,
klet.heartbeatClient, string(klet.nodeName), kubeCfg.NodeLeaseDurationSeconds,
348. klet.onRepeatedHeartbeatFailure)
349. }
350.

351. klet.softAdmitHandlers.AddPodAdmitHandler(lifecycle.NewProcMountAdmitHandler(klet
352.
353. klet.kubeletConfiguration = *kubeCfg
354.
355. klet.setNodeStatusFuncs = klet.defaultNodeStatusFuncs()

本文档使用 书栈网 · BookStack.CN 构建 - 362 -


kubelet 启动流程分析

356.
357. return klet, nil
358. }

startKubelet

在 startKubelet 中通过调用 k.Run 来启动 kubelet 中的所有模块以及主流程,然后启动


kubelet 所需要的 http server,在 v1.16 中,kubelet 默认仅启动健康检查端口 10248 和
kubelet server 的端口 10250。

k8s.io/kubernetes/cmd/kubelet/app/server.go:1070

func startKubelet(k kubelet.Bootstrap, podCfg *config.PodConfig, kubeCfg


*kubeletconfiginternal.KubeletConfiguration, kubeDeps *kubelet.Dependencies,
1. enableCAdvisorJSONEndpoints, enableServer bool) {
2. // start the kubelet
3. go wait.Until(func() {
4. k.Run(podCfg.Updates())
5. }, 0, wait.NeverStop)
6.
7. // start the kubelet server
8. if enableServer {
go k.ListenAndServe(net.ParseIP(kubeCfg.Address), uint(kubeCfg.Port),
kubeDeps.TLSOptions, kubeDeps.Auth, enableCAdvisorJSONEndpoints, kubeCfg.
9. EnableDebuggingHandlers, kubeCfg.EnableContentionProfiling)
10.
11. }
12. if kubeCfg.ReadOnlyPort > 0 {
go k.ListenAndServeReadOnly(net.ParseIP(kubeCfg.Address),
13. uint(kubeCfg.ReadOnlyPort), enableCAdvisorJSONEndpoints)
14. }
15. if utilfeature.DefaultFeatureGate.Enabled(features.KubeletPodResources) {
16. go k.ListenAndServePodResources()
17. }
18. }

至此,kubelet 对象以及其依赖模块在上面的几个方法中已经初始化完成了,除了单独启动了 gc 模
块外其余的模块以及主逻辑最后都会在 Run 方法启动, Run 方法的主要逻辑在下文中会进行
解释,此处总结一下 kubelet 启动逻辑中的调用关系如下所示:

1. |--> NewMainKubelet

本文档使用 书栈网 · BookStack.CN 构建 - 363 -


kubelet 启动流程分析

2. |
|--> createAndInitKubelet
3. --|--> BirthCry
|
4. |
|--> RunKubelet --|
5. |--> StartGarbageCollection
6. | |
| |--> startKubelet -->
7. k.Run
8. |
9. NewKubeletCommand --> Run --> run --|--> http.ListenAndServe
10. |
11. |--> daemon.SdNotify

本文档使用 书栈网 · BookStack.CN 构建 - 364 -


kubelet 启动流程分析

Run
Run 方法是启动 kubelet 的核心方法,其中会启动 kubelet 的依赖模块以及主循环逻辑,该
方法的主要逻辑为:

1、注册 logServer;
2、判断是否需要启动 cloud provider sync manager;
3、调用 kl.initializeModules 首先启动不依赖 container runtime 的一些模块;
4、启动 volume manager ;
5、执行 kl.syncNodeStatus 定时同步 Node 状态;
6、调用 kl.fastStatusUpdateOnce 更新容器运行时启动时间以及执行首次状态同步;
7、判断是否启用 NodeLease 机制;
8、执行 kl.updateRuntimeUp 定时更新 Runtime 状态;
9、执行 kl.syncNetworkUtil 定时同步 iptables 规则;
10、执行 kl.podKiller 定时清理异常 pod,当 pod 没有被 podworker 正确处理的时
候,启动一个goroutine 负责 kill 掉 pod;
11、启动 statusManager ;
12、启动 probeManager ;
13、启动 runtimeClassManager ;
14、启动 pleg ;
15、调用 kl.syncLoop 监听 pod 变化;

在 Run 方法中主要调用了两个方法 kl.initializeModules 和


kl.fastStatusUpdateOnce 来完成启动前的一些初始化,在初始化完所有的模块后会启动主循环。

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1398

1. func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {


2. // 1、注册 logServer
3. if kl.logServer == nil {
kl.logServer = http.StripPrefix("/logs/",
4. http.FileServer(http.Dir("/var/log/")))
5. }
6. if kl.kubeClient == nil {
klog.Warning("No api server defined - no node status update will be
7. sent.")
8. }
9.
10. // 2、判断是否需要启动 cloud provider sync manager
11. if kl.cloudResourceSyncManager != nil {
12. go kl.cloudResourceSyncManager.Run(wait.NeverStop)

本文档使用 书栈网 · BookStack.CN 构建 - 365 -


kubelet 启动流程分析

13. }
14.
15. // 3、调用 kl.initializeModules 首先启动不依赖 container runtime 的一些模块
16. if err := kl.initializeModules(); err != nil {
kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning,
17. events.KubeletSetupFailed, err.Error())
18. klog.Fatal(err)
19. }
20.
21. // 4、启动 volume manager
22. go kl.volumeManager.Run(kl.sourcesReady, wait.NeverStop)
23.
24. if kl.kubeClient != nil {
25. // 5、执行 kl.syncNodeStatus 定时同步 Node 状态
go wait.Until(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency,
26. wait.NeverStop)
27.
28. // 6、调用 kl.fastStatusUpdateOnce 更新容器运行时启动时间以及执行首次状态同步
29. go kl.fastStatusUpdateOnce()
30.
31. // 7、判断是否启用 NodeLease 机制
32. if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
33. go kl.nodeLeaseController.Run(wait.NeverStop)
34. }
35. }
36.
37. // 8、执行 kl.updateRuntimeUp 定时更新 Runtime 状态
38. go wait.Until(kl.updateRuntimeUp, 5*time.Second, wait.NeverStop)
39.
40. // 9、执行 kl.syncNetworkUtil 定时同步 iptables 规则
41. if kl.makeIPTablesUtilChains {
42. go wait.Until(kl.syncNetworkUtil, 1*time.Minute, wait.NeverStop)
43. }
44.
45. // 10、执行 kl.podKiller 定时清理异常 pod
46. go wait.Until(kl.podKiller, 1*time.Second, wait.NeverStop)
47.
48. // 11、启动 statusManager、probeManager、runtimeClassManager
49. kl.statusManager.Start()
50. kl.probeManager.Start()
51.
52. if kl.runtimeClassManager != nil {
53. kl.runtimeClassManager.Start(wait.NeverStop)

本文档使用 书栈网 · BookStack.CN 构建 - 366 -


kubelet 启动流程分析

54. }
55.
56. // 12、启动 pleg
57. kl.pleg.Start()
58.
59. // 13、调用 kl.syncLoop 监听 pod 变化
60. kl.syncLoop(updates, kl)
61. }

initializeModules

initializeModules 中启动的模块是不依赖于 container runtime 的,并且不依赖于尚未初


始化的模块,其主要逻辑为:

1、调用 kl.setupDataDirs 创建 kubelet 所需要的文件目录;


2、创建 ContainerLogsDir /var/log/containers ;
3、启动 imageManager ,image gc 的功能已经在 RunKubelet 中启动了,此处主要是监
控 image 的变化;
4、启动 certificateManager ,负责证书更新;
5、启动 oomWatcher ,监听 oom 并记录事件;
6、启动 resourceAnalyzer ;

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1319

1. func (kl *Kubelet) initializeModules() error {


2. metrics.Register(
3. kl.runtimeCache,
4. collectors.NewVolumeStatsCollector(kl),
5. collectors.NewLogMetricsCollector(kl.StatsProvider.ListPodStats),
6. )
7. metrics.SetNodeName(kl.nodeName)
8. servermetrics.Register()
9.
10. // 1、创建文件目录
11. if err := kl.setupDataDirs(); err != nil {
12. return err
13. }
14.
15. // 2、创建 ContainerLogsDir
16. if _, err := os.Stat(ContainerLogsDir); err != nil {
17. if err := kl.os.MkdirAll(ContainerLogsDir, 0755); err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 367 -


kubelet 启动流程分析

klog.Errorf("Failed to create directory %q: %v", ContainerLogsDir,


18. err)
19. }
20. }
21.
22. // 3、启动 imageManager
23. kl.imageManager.Start()
24.
25. // 4、启动 certificate manager
26. if kl.serverCertificateManager != nil {
27. kl.serverCertificateManager.Start()
28. }
29. // 5、启动 oomWatcher.
30. if err := kl.oomWatcher.Start(kl.nodeRef); err != nil {
31. return fmt.Errorf("failed to start OOM watcher %v", err)
32. }
33.
34. // 6、启动 resource analyzer
35. kl.resourceAnalyzer.Start()
36.
37. return nil
38. }

fastStatusUpdateOnce

fastStatusUpdateOnce 会不断尝试更新 pod CIDR,一旦更新成功会立即执


行 updateRuntimeUp 和 syncNodeStatus 来进行运行时的更新和节点状态更新。此方法只在
kubelet 启动时执行一次,目的是为了通过更新 pod CIDR,减少节点达到 ready 状态的时延,尽
可能快的进行 runtime update 和 node status update。

k8s.io/kubernetes/pkg/kubelet/kubelet.go:2262

1. func (kl *Kubelet) fastStatusUpdateOnce() {


2. for {
3. time.Sleep(100 * time.Millisecond)
4. node, err := kl.GetNode()
5. if err != nil {
6. klog.Errorf(err.Error())
7. continue
8. }
9. if len(node.Spec.PodCIDRs) != 0 {
10. podCIDRs := strings.Join(node.Spec.PodCIDRs, ",")
11. if _, err := kl.updatePodCIDR(podCIDRs); err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 368 -


kubelet 启动流程分析

12. klog.Errorf("Pod CIDR update to %v failed %v", podCIDRs, err)


13. continue
14. }
15. kl.updateRuntimeUp()
16. kl.syncNodeStatus()
17. return
18. }
19. }
20. }

updateRuntimeUp

updateRuntimeUp 方法在容器运行时首次启动过程中初始化运行时依赖的模块,并在 kubelet


的 runtimeState 中更新容器运行时的启动时间。 updateRuntimeUp 方法首先检查 network
以及 runtime 是否处于 ready 状态,如果 network 以及 runtime 都处于 ready 状态,然
后调用 initializeRuntimeDependentModules 初始化 runtime 的依赖模块,包括
cadvisor 、 containerManager 、 evictionManager 、 containerLogManager 、 plugi
nManage 等。

k8s.io/kubernetes/pkg/kubelet/kubelet.go:2168

1. func (kl *Kubelet) updateRuntimeUp() {


2. kl.updateRuntimeMux.Lock()
3. defer kl.updateRuntimeMux.Unlock()
4.
5. // 1、获取 containerRuntime Status
6. s, err := kl.containerRuntime.Status()
7. if err != nil {
8. klog.Errorf("Container runtime sanity check failed: %v", err)
9. return
10. }
11. if s == nil {
12. klog.Errorf("Container runtime status is nil")
13. return
14. }
15.
16. // 2、检查 network 和 runtime 是否处于 ready 状态
17. networkReady := s.GetRuntimeCondition(kubecontainer.NetworkReady)
18. if networkReady == nil || !networkReady.Status {
kl.runtimeState.setNetworkState(fmt.Errorf("runtime network not ready:
19. %v", networkReady))
20. } else {
21. kl.runtimeState.setNetworkState(nil)

本文档使用 书栈网 · BookStack.CN 构建 - 369 -


kubelet 启动流程分析

22. }
23.
24. runtimeReady := s.GetRuntimeCondition(kubecontainer.RuntimeReady)
25. if runtimeReady == nil || !runtimeReady.Status {
26. kl.runtimeState.setRuntimeState(err)
27. return
28. }
29. kl.runtimeState.setRuntimeState(nil)
30. // 3、调用 kl.initializeRuntimeDependentModules 启动依赖模块
31. kl.oneTimeInitializer.Do(kl.initializeRuntimeDependentModules)
32. kl.runtimeState.setRuntimeSync(kl.clock.Now())
33. }

initializeRuntimeDependentModules

该方法的主要逻辑为:

1、启动 cadvisor ;
2、获取 CgroupStats;
3、启动 containerManager 、 evictionManager 、 containerLogManager ;
4、将 CSI Driver 和 Device Manager 注册到 pluginManager ,然后启动
pluginManager ;

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1361

1. func (kl *Kubelet) initializeRuntimeDependentModules() {


2. // 1、启动 cadvisor
3. if err := kl.cadvisor.Start(); err != nil {
4. ......
5. }
6.
7. // 2、获取 CgroupStats
8. kl.StatsProvider.GetCgroupStats("/", true)
9.
10. node, err := kl.getNodeAnyWay()
11. if err != nil {
12. klog.Fatalf("Kubelet failed to get node info: %v", err)
13. }
14.
15. // 3、启动 containerManager、evictionManager、containerLogManager
if err := kl.containerManager.Start(node, kl.GetActivePods,
16. kl.sourcesReady, kl.statusManager, kl.runtimeService); err != nil {
17. klog.Fatalf("Failed to start ContainerManager %v", err)

本文档使用 书栈网 · BookStack.CN 构建 - 370 -


kubelet 启动流程分析

18. }
19.
kl.evictionManager.Start(kl.StatsProvider, kl.GetActivePods,
20. kl.podResourcesAreReclaimed, evictionMonitoringPeriod)
21.
22. kl.containerLogManager.Start()
23.
kl.pluginManager.AddHandler(pluginwatcherapi.CSIPlugin,
24. plugincache.PluginHandler(csi.PluginHandler))
25.
kl.pluginManager.AddHandler(pluginwatcherapi.DevicePlugin,
26. kl.containerManager.GetPluginRegistrationHandler())
27. // 4、启动 pluginManager
28. go kl.pluginManager.Run(kl.sourcesReady, wait.NeverStop)
29. }

小结

在 Run 方法中可以看到,会直接调用 kl.syncNodeStatus 和 kl.updateRuntimeUp ,但


在 kl.fastStatusUpdateOnce 中也调用了这两个方法,而在 kl.fastStatusUpdateOnce 中
仅执行一次,在 Run 方法中会定期执行。在 kl.fastStatusUpdateOnce 中调用的目的就是当
kubelet 首次启动时尽可能快的进行 runtime update 和 node status update,减少节点达
到 ready 状态的时延。而在 kl.updateRuntimeUp 中调用的初始化 runtime 依赖模块的方法
kl.initializeRuntimeDependentModules 通过 sync.Once 调用仅仅会被执行一次。

syncLoop

syncLoop 是 kubelet 的主循环方法,它从不同的管道(file,http,apiserver)监听 pod


的变化,并把它们汇聚起来。当有新的变化发生时,它会调用对应的函数,保证 pod 处于期望的状
态。

syncLoop 中首先定义了一个 syncTicker 和 housekeepingTicker ,即使没有需要更新


的 pod 配置,kubelet 也会定时去做同步和清理 pod 的工作。然后在 for 循环中一直调用
syncLoopIteration ,如果在每次循环过程中出现错误时,kubelet 会记录到 runtimeState
中,遇到错误就等待 5 秒中继续循环。

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1821

func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler


1. SyncHandler) {
2. syncTicker := time.NewTicker(time.Second)
3. defer syncTicker.Stop()
4. housekeepingTicker := time.NewTicker(housekeepingPeriod)

本文档使用 书栈网 · BookStack.CN 构建 - 371 -


kubelet 启动流程分析

5. defer housekeepingTicker.Stop()
6. plegCh := kl.pleg.Watch()
7. const (
8. base = 100 * time.Millisecond
9. max = 5 * time.Second
10. factor = 2
11. )
12. duration := base
13. for {
14. if err := kl.runtimeState.runtimeErrors(); err != nil {
15. time.Sleep(duration)
duration = time.Duration(math.Min(float64(max),
16. factor*float64(duration)))
17. continue
18. }
19. duration = base
20. kl.syncLoopMonitor.Store(kl.clock.Now())
if !kl.syncLoopIteration(updates, handler, syncTicker.C,
21. housekeepingTicker.C, plegCh) {
22. break
23. }
24. kl.syncLoopMonitor.Store(kl.clock.Now())
25. }
26. }

syncLoopIteration

syncLoopIteration 方法会监听多个 channel,当发现任何一个 channel 有数据就交给


handler 去处理,在 handler 中通过调用 dispatchWork 分发任务。它会从以下几个
channel 中获取消息:

1、configCh:该信息源由 kubeDeps 对象中的 PodConfig 子模块提供,该模块将同时


watch 3 个不同来源的 pod 信息的变化(file,http,apiserver),一旦某个来源的
pod 信息发生了更新(创建/更新/删除),这个 channel 中就会出现被更新的 pod 信息和
更新的具体操作;
2、syncCh:定时器,每隔一秒去同步最新保存的 pod 状态;
3、houseKeepingCh:housekeeping 事件的通道,做 pod 清理工作;
4、plegCh:该信息源由 kubelet 对象中的 pleg 子模块提供,该模块主要用于周期性地向
container runtime 查询当前所有容器的状态,如果状态发生变化,则这个 channel 产生
事件;
5、liveness Manager:健康检查模块发现某个 pod 异常时,kubelet 将根据 pod 的
restartPolicy 自动执行正确的操作;

本文档使用 书栈网 · BookStack.CN 构建 - 372 -


kubelet 启动流程分析

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1888

1. func (kl *Kubelet) syncLoopIteration(......) bool {


2. select {
3. case u, open := <-configCh:
4. if !open {
5. return false
6. }
7.
8. switch u.Op {
9. case kubetypes.ADD:
10. handler.HandlePodAdditions(u.Pods)
11. case kubetypes.UPDATE:
12. handler.HandlePodUpdates(u.Pods)
13. case kubetypes.REMOVE:
14. handler.HandlePodRemoves(u.Pods)
15. case kubetypes.RECONCILE:
16. handler.HandlePodReconcile(u.Pods)
17. case kubetypes.DELETE:
18. handler.HandlePodUpdates(u.Pods)
19. case kubetypes.RESTORE:
20. handler.HandlePodAdditions(u.Pods)
21. case kubetypes.SET:
22. }
23.
24. if u.Op != kubetypes.RESTORE {
25. kl.sourcesReady.AddSource(u.Source)
26. }
27. case e := <-plegCh:
28. if isSyncPodWorthy(e) {
29. if pod, ok := kl.podManager.GetPodByUID(e.ID); ok {
klog.V(2).Infof("SyncLoop (PLEG): %q, event: %#v",
30. format.Pod(pod), e)
31. handler.HandlePodSyncs([]*v1.Pod{pod})
32. } else {
klog.V(4).Infof("SyncLoop (PLEG): ignore irrelevant event:
33. %#v", e)
34. }
35. }
36.
37. if e.Type == pleg.ContainerDied {
38. if containerID, ok := e.Data.(string); ok {
39. kl.cleanUpContainersInPod(e.ID, containerID)

本文档使用 书栈网 · BookStack.CN 构建 - 373 -


kubelet 启动流程分析

40. }
41. }
42. case <-syncCh:
43. podsToSync := kl.getPodsToSync()
44. if len(podsToSync) == 0 {
45. break
46. }
47. handler.HandlePodSyncs(podsToSync)
48. case update := <-kl.livenessManager.Updates():
49. if update.Result == proberesults.Failure {
50. pod, ok := kl.podManager.GetPodByUID(update.PodUID)
51. if !ok {
52. break
53. }
54. handler.HandlePodSyncs([]*v1.Pod{pod})
55. }
56. case <-housekeepingCh:
57. if !kl.sourcesReady.AllReady() {
klog.V(4).Infof("SyncLoop (housekeeping, skipped): sources aren't
58. ready yet.")
59. } else {
60. if err := handler.HandlePodCleanups(); err != nil {
61. klog.Errorf("Failed cleaning pods: %v", err)
62. }
63. }
64. }
65. return true
66. }

最后再总结一下启动 kubelet 以及其依赖模块 Run 方法中的调用流程:

1. |--> kl.cloudResourceSyncManager.Run
2. |
3. | |--> kl.setupDataDirs
4. | |--> kl.imageManager.Start
5. Run --|--> kl.initializeModules ---|--> kl.serverCertificateManager.Start
6. | |--> kl.oomWatcher.Start
7. | |--> kl.resourceAnalyzer.Start
8. |
9. |--> kl.volumeManager.Run
| |-->
10. kl.containerRuntime.Status

本文档使用 书栈网 · BookStack.CN 构建 - 374 -


kubelet 启动流程分析

11. |--> kl.syncNodeStatus |


| |--> kl.updateRuntimeUp --|
12. |--> kl.cadvisor.Start
| | |
13. |
|--> kl.fastStatusUpdateOnce --| |-->
14. kl.initializeRuntimeDependentModules --|--> kl.containerManager.Start
| |
15. |
| |--> kl.syncNodeStatus
16. |--> kl.evictionManager.Start
|
17. |
|--> kl.updateRuntimeUp
18. |--> kl.containerLogManager.Start
|
19. |
|--> kl.syncNetworkUtil
20. |--> kl.pluginManager.Run
21. |
22. |--> kl.podKiller
23. |
24. |--> kl.statusManager.Start
25. |
26. |--> kl.probeManager.Start
27. |
28. |--> kl.runtimeClassManager.Start
29. |
30. |--> kl.pleg.Start
31. |
32. |--> kl.syncLoop --> kl.syncLoopIteration

总结
本文主要介绍了 kubelet 的启动流程,可以看到 kubelet 启动流程中的环节非常多,kubelet
中也包含了非常多的模块,后续在分享 kubelet 源码的文章中会先以 Run 方法中启动的所有模
块为主,各个击破。

本文档使用 书栈网 · BookStack.CN 构建 - 375 -


kubelet 创建 pod 的流程

上篇文章介绍了 kubelet 的启动流程,本篇文章主要介绍 kubelet 创建 pod 的流程。

kubernetes 版本: v1.12

kubelet 的工作核心就是在围绕着不同的生产者生产出来的不同的有关 pod 的消息来调用相应的消


费者(不同的子模块)完成不同的行为(创建和删除 pod 等),即图中的控制循环(SyncLoop),通
过不同的事件驱动这个控制循环运行。

本文仅分析新建 pod 的流程,当一个 pod 完成调度,与一个 node 绑定起来之后,这个 pod 就


会触发 kubelet 在循环控制里注册的 handler,上图中的 HandlePods 部分。此时,通过检查
pod 在 kubelet 内存中的状态,kubelet 就能判断出这是一个新调度过来的 pod,从而触发
Handler 里的 ADD 事件对应的逻辑处理。然后 kubelet 会为这个 pod 生成对应的
podStatus,接着检查 pod 所声明的 volume 是不是准备好了,然后调用下层的容器运行时。如果
是 update 事件的话,kubelet 就会根据 pod 对象具体的变更情况,调用下层的容器运行时进行
容器的重建。

kubelet 创建 pod 的流程

本文档使用 书栈网 · BookStack.CN 构建 - 376 -


kubelet 创建 pod 的流程

1、kubelet 的控制循环(syncLoop)
syncLoop 中首先定义了一个 syncTicker 和 housekeepingTicker,即使没有需要更新的 pod
配置,kubelet 也会定时去做同步和清理 pod 的工作。然后在 for 循环中一直调用
syncLoopIteration,如果在每次循环过程中出现比较严重的错误,kubelet 会记录到
runtimeState 中,遇到错误就等待 5 秒中继续循环。

func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler


1. SyncHandler) {
2. glog.Info("Starting kubelet main sync loop.")
3.
4. // syncTicker 每秒检测一次是否有需要同步的 pod workers
5. syncTicker := time.NewTicker(time.Second)
6. defer syncTicker.Stop()
7. // 每两秒检测一次是否有需要清理的 pod

本文档使用 书栈网 · BookStack.CN 构建 - 377 -


kubelet 创建 pod 的流程

8. housekeepingTicker := time.NewTicker(housekeepingPeriod)
9. defer housekeepingTicker.Stop()
10. // pod 的生命周期变化
11. plegCh := kl.pleg.Watch()
12. const (
13. base = 100 * time.Millisecond
14. max = 5 * time.Second
15. factor = 2
16. )
17. duration := base
18. for {
19. if rs := kl.runtimeState.runtimeErrors(); len(rs) != 0 {
20. time.Sleep(duration)
duration = time.Duration(math.Min(float64(max),
21. factor*float64(duration)))
22. continue
23. }
24. ...
25.
26. kl.syncLoopMonitor.Store(kl.clock.Now())
27. // 第二个参数为 SyncHandler 类型,SyncHandler 是一个 interface,
28. // 在该文件开头处定义
if !kl.syncLoopIteration(updates, handler, syncTicker.C,
29. housekeepingTicker.C, plegCh) {
30. break
31. }
32. kl.syncLoopMonitor.Store(kl.clock.Now())
33. }
34. }

2、监听 pod 变化(syncLoopIteration)


syncLoopIteration 这个方法就会对多个管道进行遍历,发现任何一个管道有消息就交给
handler 去处理。它会从以下管道中获取消息:

configCh:该信息源由 kubeDeps 对象中的 PodConfig 子模块提供,该模块将同时


watch 3 个不同来源的 pod 信息的变化(file,http,apiserver),一旦某个来源的
pod 信息发生了更新(创建/更新/删除),这个 channel 中就会出现被更新的 pod 信息和
更新的具体操作。
syncCh:定时器管道,每隔一秒去同步最新保存的 pod 状态
houseKeepingCh:housekeeping 事件的管道,做 pod 清理工作
plegCh:该信息源由 kubelet 对象中的 pleg 子模块提供,该模块主要用于周期性地向

本文档使用 书栈网 · BookStack.CN 构建 - 378 -


kubelet 创建 pod 的流程

container runtime 查询当前所有容器的状态,如果状态发生变化,则这个 channel 产生


事件。
livenessManager.Updates():健康检查发现某个 pod 不可用,kubelet 将根据 Pod 的
restartPolicy 自动执行正确的操作

func (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate,


1. handler SyncHandler,
syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan
2. *pleg.PodLifecycleEvent) bool {
3. select {
4. case u, open := <-configCh:
5. if !open {
6. glog.Errorf("Update channel is closed. Exiting the sync loop.")
7. return false
8. }
9.
10. switch u.Op {
11. case kubetypes.ADD:
12. ...
13. case kubetypes.UPDATE:
14. ...
15. case kubetypes.REMOVE:
16. ...
17. case kubetypes.RECONCILE:
18. ...
19. case kubetypes.DELETE:
20. ...
21. case kubetypes.RESTORE:
22. ...
23. case kubetypes.SET:
24. ...
25. }
26. ...
27. case e := <-plegCh:
28. ...
29. case <-syncCh:
30. ...
31. case update := <-kl.livenessManager.Updates():
32. ...
33. case <-housekeepingCh:
34. ...
35. }

本文档使用 书栈网 · BookStack.CN 构建 - 379 -


kubelet 创建 pod 的流程

36. return true


37. }

3、处理新增 pod(HandlePodAddtions)
对于事件中的每个 pod,执行以下操作:

1、把所有的 pod 按照创建日期进行排序,保证最先创建的 pod 会最先被处理


2、把它加入到 podManager 中,podManager 子模块负责管理这台机器上的 pod 的信息,
pod 和 mirrorPod 之间的对应关系等等。所有被管理的 pod 都要出现在里面,如果
podManager 中找不到某个 pod,就认为这个 pod 被删除了
3、如果是 mirror pod 调用其单独的方法
4、验证 pod 是否能在该节点运行,如果不可以直接拒绝
5、通过 dispatchWork 把创建 pod 的工作下发给 podWorkers 子模块做异步处理
6、在 probeManager 中添加 pod,如果 pod 中定义了 readiness 和 liveness 健康
检查,启动 goroutine 定期进行检测

1. func (kl *Kubelet) HandlePodAdditions(pods []*v1.Pod) {


2. start := kl.clock.Now()
3. // 对所有 pod 按照日期排序,保证最先创建的 pod 优先被处理
4. sort.Sort(sliceutils.PodsByCreationTime(pods))
5. for _, pod := range pods {
6. if kl.dnsConfigurer != nil && kl.dnsConfigurer.ResolverConfig != "" {
7. kl.dnsConfigurer.CheckLimitsForResolvConf()
8. }
9. existingPods := kl.podManager.GetPods()
10. // 把 pod 加入到 podManager 中
11. kl.podManager.AddPod(pod)
12.
13. // 判断是否是 mirror pod(即 static pod)
14. if kubepod.IsMirrorPod(pod) {
15. kl.handleMirrorPod(pod, start)
16. continue
17. }
18.
19. if !kl.podIsTerminated(pod) {
20. activePods := kl.filterOutTerminatedPods(existingPods)
21. // 通过 canAdmitPod 方法校验Pod能否在该计算节点创建(如:磁盘空间)
22. // Check if we can admit the pod; if not, reject it.
23. if ok, reason, message := kl.canAdmitPod(activePods, pod); !ok {
24. kl.rejectPod(pod, reason, message)
25. continue

本文档使用 书栈网 · BookStack.CN 构建 - 380 -


kubelet 创建 pod 的流程

26. }
27. }
28.
29. mirrorPod, _ := kl.podManager.GetMirrorPodByPod(pod)
// 通过 dispatchWork 分发 pod 做异步处理,dispatchWork 主要工作就是把接收到的
30. 参数封装成 UpdatePodOptions,调用 UpdatePod 方法.
31. kl.dispatchWork(pod, kubetypes.SyncPodCreate, mirrorPod, start)
// 在 probeManager 中添加 pod,如果 pod 中定义了 readiness 和 liveness 健康
32. 检查,启动 goroutine 定期进行检测
33. kl.probeManager.AddPod(pod)
34. }
35. }

static pod 是由 kubelet 直接管理的,k8s apiserver 并不会感知到 static pod 的存在,当然也


不会和任何一个 rs 关联上,完全是由 kubelet 进程来监管,并在它异常时负责重启。Kubelet 会通过
apiserver 为每一个 static pod 创建一个对应的 mirror pod,如此以来就可以可以通过 kubectl 命
令查看对应的 pod,并且可以通过 kubectl logs 命令直接查看到static pod 的日志信息。

4、下发任务(dispatchWork)
dispatchWorker 的主要作用是把某个对 Pod 的操作(创建/更新/删除)下发给 podWorkers。

func (kl *Kubelet) dispatchWork(pod *v1.Pod, syncType kubetypes.SyncPodType,


1. mirrorPod *v1.Pod, start time.Time) {
2. if kl.podIsTerminated(pod) {
3. if pod.DeletionTimestamp != nil {
4. kl.statusManager.TerminatePod(pod)
5. }
6. return
7. }
8. // 落实在 podWorkers 中
9. kl.podWorkers.UpdatePod(&UpdatePodOptions{
10. Pod: pod,
11. MirrorPod: mirrorPod,
12. UpdateType: syncType,
13. OnCompleteFunc: func(err error) {
14. if err != nil {

15. metrics.PodWorkerLatency.WithLabelValues(syncType.String()).Observe(metrics.SinceInMicr
16. }
17. },
18. })

本文档使用 书栈网 · BookStack.CN 构建 - 381 -


kubelet 创建 pod 的流程

19. if syncType == kubetypes.SyncPodCreate {

20. metrics.ContainersPerPodCount.Observe(float64(len(pod.Spec.Containers)))
21. }
22. }

5、更新事件的 channel(UpdatePod)
podWorkers 子模块主要的作用就是处理针对每一个的 Pod 的更新事件,比如 Pod 的创建,删
除,更新。而 podWorkers 采取的基本思路是:为每一个 Pod 都单独创建一个 goroutine 和更
新事件的 channel,goroutine 会阻塞式的等待 channel 中的事件,并且对获取的事件进行处
理。而 podWorkers 对象自身则主要负责对更新事件进行下发。

1. func (p *podWorkers) UpdatePod(options *UpdatePodOptions) {


2. pod := options.Pod
3. uid := pod.UID
4. var podUpdates chan UpdatePodOptions
5. var exists bool
6.
7. p.podLock.Lock()
8. defer p.podLock.Unlock()
9.
10. // 如果当前 pod 还没有启动过 goroutine ,则启动 goroutine,并且创建 channel
11. if podUpdates, exists = p.podUpdates[uid]; !exists {
12. // 创建 channel
13. podUpdates = make(chan UpdatePodOptions, 1)
14. p.podUpdates[uid] = podUpdates
15.
16. // 启动 goroutine
17. go func() {
18. defer runtime.HandleCrash()
19. p.managePodLoop(podUpdates)
20. }()
21. }
22. // 下发更新事件
23. if !p.isWorking[pod.UID] {
24. p.isWorking[pod.UID] = true
25. podUpdates <- *options
26. } else {
27. update, found := p.lastUndeliveredWorkUpdate[pod.UID]
28. if !found || update.UpdateType != kubetypes.SyncPodKill {

本文档使用 书栈网 · BookStack.CN 构建 - 382 -


kubelet 创建 pod 的流程

29. p.lastUndeliveredWorkUpdate[pod.UID] = *options


30. }
31. }
32. }

6、调用 syncPodFn 方法同步 pod(managePodLoop)


managePodLoop 调用 syncPodFn 方法去同步 pod,syncPodFn 实际上就是
kubelet.SyncPod。在完成这次 sync 动作之后,会调用 wrapUp 函数,这个函数将会做几件事
情:

将这个 pod 信息插入 kubelet 的 workQueue 队列中,等待下一次周期性的对这个 pod


的状态进行 sync
将在这次 sync 期间堆积的没有能够来得及处理的最近一次 update 操作加入 goroutine
的事件 channel 中,立即处理。

1. func (p *podWorkers) managePodLoop(podUpdates <-chan UpdatePodOptions) {


2. var lastSyncTime time.Time
3. for update := range podUpdates {
4. err := func() error {
5. podUID := update.Pod.UID
6. status, err := p.podCache.GetNewerThan(podUID, lastSyncTime)
7. if err != nil {
8. ...
9. }
10. err = p.syncPodFn(syncPodOptions{
11. mirrorPod: update.MirrorPod,
12. pod: update.Pod,
13. podStatus: status,
14. killPodOptions: update.KillPodOptions,
15. updateType: update.UpdateType,
16. })
17. lastSyncTime = time.Now()
18. return err
19. }()
20. if update.OnCompleteFunc != nil {
21. update.OnCompleteFunc(err)
22. }
23. if err != nil {
24. ...
25. }
26. p.wrapUp(update.Pod.UID, err)

本文档使用 书栈网 · BookStack.CN 构建 - 383 -


kubelet 创建 pod 的流程

27. }
28. }

7、完成创建容器前的准备工作(SyncPod)
在这个方法中,主要完成以下几件事情:

如果是删除 pod,立即执行并返回
同步 podStatus 到 kubelet.statusManager
检查 pod 是否能运行在本节点,主要是权限检查(是否能使用主机网络模式,是否可以以
privileged 权限运行等)。如果没有权限,就删除本地旧的 pod 并返回错误信息
创建 containerManagar 对象,并且创建 pod level cgroup,更新 Qos level
cgroup
如果是 static Pod,就创建或者更新对应的 mirrorPod
创建 pod 的数据目录,存放 volume 和 plugin 信息,如果定义了 pv,等待所有的
volume mount 完成(volumeManager 会在后台做这些事情),如果有 image secrets,
去 apiserver 获取对应的 secrets 数据
然后调用 kubelet.volumeManager 组件,等待它将 pod 所需要的所有外挂的 volume 都
准备好。
调用 container runtime 的 SyncPod 方法,去实现真正的容器创建逻辑

这里所有的事情都和具体的容器没有关系,可以看到该方法是创建 pod 实体(即容器)之前需要完成


的准备工作。

1. func (kl *Kubelet) syncPod(o syncPodOptions) error {


2. // pull out the required options
3. pod := o.pod
4. mirrorPod := o.mirrorPod
5. podStatus := o.podStatus
6. updateType := o.updateType
7.
8. // 是否为 删除 pod
9. if updateType == kubetypes.SyncPodKill {
10. ...
11. }
12. ...
13. // 检查 pod 是否能运行在本节点
14. runnable := kl.canRunPod(pod)
15. if !runnable.Admit {
16. ...
17. }

本文档使用 书栈网 · BookStack.CN 构建 - 384 -


kubelet 创建 pod 的流程

18.
19. // 更新 pod 状态
20. kl.statusManager.SetPodStatus(pod, apiPodStatus)
21.
22. // 如果 pod 非 running 状态则直接 kill 掉
if !runnable.Admit || pod.DeletionTimestamp != nil || apiPodStatus.Phase ==
23. v1.PodFailed {
24. ...
25. }
26.
27. // 加载网络插件
if rs := kl.runtimeState.networkErrors(); len(rs) != 0 &&
28. !kubecontainer.IsHostNetworkPod(pod) {
29. ...
30. }
31.
32. pcm := kl.containerManager.NewPodContainerManager()
33. if !kl.podIsTerminated(pod) {
34. ...
35. // 创建并更新 pod 的 cgroups
36. if !(podKilled && pod.Spec.RestartPolicy == v1.RestartPolicyNever) {
37. if !pcm.Exists(pod) {
38. ...
39. }
40. }
41. }
42.
43. // 为 static pod 创建对应的 mirror pod
44. if kubepod.IsStaticPod(pod) {
45. ...
46. }
47.
48. // 创建数据目录
49. if err := kl.makePodDataDirs(pod); err != nil {
50. ...
51. }
52.
53. // 挂载 volume
54. if !kl.podIsTerminated(pod) {
55. if err := kl.volumeManager.WaitForAttachAndMount(pod); err != nil {
56. ...
57. }
58. }

本文档使用 书栈网 · BookStack.CN 构建 - 385 -


kubelet 创建 pod 的流程

59.
60. // 获取 secret 信息
61. pullSecrets := kl.getPullSecretsForPod(pod)
62.
63. // 调用 containerRuntime 的 SyncPod 方法开始创建容器
result := kl.containerRuntime.SyncPod(pod, apiPodStatus, podStatus,
64. pullSecrets, kl.backOff)
65. kl.reasonCache.Update(pod.UID, result)
66. if err := result.Error(); err != nil {
67. ...
68. }
69.
70. return nil
71. }

8、创建容器
containerRuntime(pkg/kubelet/kuberuntime)子模块的 SyncPod 函数才是真正完成 pod
内容器实体的创建。 syncPod 主要执行以下几个操作:

1、计算 sandbox 和 container 是否发生变化


2、创建 sandbox 容器
3、启动 init 容器
4、启动业务容器

initContainers 可以有多个,多个 container 严格按照顺序启动,只有当前一个 container


退出了以后,才开始启动下一个 container。

func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, _ v1.PodStatus,


podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff
1. *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) {
2. // 1、计算 sandbox 和 container 是否发生变化
3. podContainerChanges := m.computePodActions(pod, podStatus)
4. if podContainerChanges.CreateSandbox {
5. ref, err := ref.GetReference(legacyscheme.Scheme, pod)
6. if err != nil {
glog.Errorf("Couldn't make a ref to pod %q: '%v'", format.Pod(pod),
7. err)
8. }
9. ...
10. }
11.

本文档使用 书栈网 · BookStack.CN 构建 - 386 -


kubelet 创建 pod 的流程

12. // 2、kill 掉 sandbox 已经改变的 pod


13. if podContainerChanges.KillPod {
14. ...
15. } else {
16. // 3、kill 掉非 running 状态的 containers
17. ...
for containerID, containerInfo := range
18. podContainerChanges.ContainersToKill {
19. ...
if err := m.killContainer(pod, containerID, containerInfo.name,
20. containerInfo.message, nil); err != nil {
21. ...
22. }
23. }
24. }
25.
26. m.pruneInitContainersBeforeStart(pod, podStatus)
27. podIP := ""
28. if podStatus != nil {
29. podIP = podStatus.IP
30. }
31.
32. // 4、创建 sandbox
33. podSandboxID := podContainerChanges.SandboxID
34. if podContainerChanges.CreateSandbox {
podSandboxID, msg, err = m.createPodSandbox(pod,
35. podContainerChanges.Attempt)
36. if err != nil {
37. ...
38. }
39. ...
podSandboxStatus, err :=
40. m.runtimeService.PodSandboxStatus(podSandboxID)
41. if err != nil {
42. ...
43. }
// 如果 pod 网络是 host 模式,容器也相同;其他情况下,容器会使用 None 网络模式,
44. 让 kubelet 的网络插件自己进行网络配置
45. if !kubecontainer.IsHostNetworkPod(pod) {
podIP = m.determinePodSandboxIP(pod.Namespace, pod.Name,
46. podSandboxStatus)
glog.V(4).Infof("Determined the ip %q for pod %q after sandbox
47. changed", podIP, format.Pod(pod))

本文档使用 书栈网 · BookStack.CN 构建 - 387 -


kubelet 创建 pod 的流程

48. }
49. }
50.
configPodSandboxResult :=
51. kubecontainer.NewSyncResult(kubecontainer.ConfigPodSandbox, podSandboxID)
52. result.AddSyncResult(configPodSandboxResult)
53. // 获取 PodSandbox 的配置(如:metadata,clusterDNS,容器的端口映射等)
podSandboxConfig, err := m.generatePodSandboxConfig(pod,
54. podContainerChanges.Attempt)
55. ...
56.
57. // 5、启动 init container
if container := podContainerChanges.NextInitContainerToStart; container !=
58. nil {
59. ...
if msg, err := m.startContainer(podSandboxID, podSandboxConfig,
container, pod, podStatus, pullSecrets, podIP,
60. kubecontainer.ContainerTypeInit); err != nil {
61. ...
62. }
63. }
64.
65. // 6、启动业务容器
66. for _, idx := range podContainerChanges.ContainersToStart {
67. ...
if msg, err := m.startContainer(podSandboxID, podSandboxConfig,
container, pod, podStatus, pullSecrets, podIP,
68. kubecontainer.ContainerTypeRegular); err != nil {
69. ...
70. }
71. }
72.
73. return
74. }

9、启动容器
最终由 startContainer 完成容器的启动,其主要有以下几个步骤:

1、拉取镜像
2、生成业务容器的配置信息
3、调用 docker api 创建容器
4、启动容器

本文档使用 书栈网 · BookStack.CN 构建 - 388 -


kubelet 创建 pod 的流程

5、执行 post start hook

func (m *kubeGenericRuntimeManager) startContainer(podSandboxID string,


podSandboxConfig *runtimeapi.PodSandboxConfig, container *v1.Container, pod
*v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, podIP
1. string, containerType kubecontainer.ContainerType) (string, error) {
// 1、检查业务镜像是否存在,不存在则到 Docker Registry 或是 Private Registry 拉取
2. 镜像。
imageRef, msg, err := m.imagePuller.EnsureImageExists(pod, container,
3. pullSecrets)
4. if err != nil {
5. ...
6. }
7.
8. ref, err := kubecontainer.GenerateContainerRef(pod, container)
9. if err != nil {
10. ...
11. }
12.
13. // 设置 RestartCount
14. restartCount := 0
15. containerStatus := podStatus.FindContainerStatusByName(container.Name)
16. if containerStatus != nil {
17. restartCount = containerStatus.RestartCount + 1
18. }
19.
20. // 2、生成业务容器的配置信息
containerConfig, cleanupAction, err := m.generateContainerConfig(container,
21. pod, restartCount, podIP, imageRef, containerType)
22. if cleanupAction != nil {
23. defer cleanupAction()
24. }
25. ...
26.
27. // 3、通过 client.CreateContainer 调用 docker api 创建业务容器
containerID, err := m.runtimeService.CreateContainer(podSandboxID,
28. containerConfig, podSandboxConfig)
29. if err != nil {
30. ...
31. }
32. err = m.internalLifecycle.PreStartContainer(pod, container, containerID)
33. if err != nil {
34. ...

本文档使用 书栈网 · BookStack.CN 构建 - 389 -


kubelet 创建 pod 的流程

35. }
36. ...
37.
38. // 3、启动业务容器
39. err = m.runtimeService.StartContainer(containerID)
40. if err != nil {
41. ...
42. }
43.
44. containerMeta := containerConfig.GetMetadata()
45. sandboxMeta := podSandboxConfig.GetMetadata()
legacySymlink := legacyLogSymlink(containerID, containerMeta.Name,
46. sandboxMeta.Name,
47. sandboxMeta.Namespace)
containerLog := filepath.Join(podSandboxConfig.LogDirectory,
48. containerConfig.LogPath)
49. if _, err := m.osInterface.Stat(containerLog); !os.IsNotExist(err) {
if err := m.osInterface.Symlink(containerLog, legacySymlink); err !=
50. nil {
glog.Errorf("Failed to create legacy symbolic link %q to container
51. %q log %q: %v",
52. legacySymlink, containerID, containerLog, err)
53. }
54. }
55.
56. // 4、执行 post start hook
57. if container.Lifecycle != nil && container.Lifecycle.PostStart != nil {
58. kubeContainerID := kubecontainer.ContainerID{
59. Type: m.runtimeName,
60. ID: containerID,
61. }
62. // runner.Run 这个方法的主要作用就是在业务容器起来的时候,
63. // 首先会执行一个 container hook(PostStart 和 PreStop),做一些预处理工作。
64. // 只有 container hook 执行成功才会运行具体的业务服务,否则容器异常。
msg, handlerErr := m.runner.Run(kubeContainerID, pod, container,
65. container.Lifecycle.PostStart)
66. if handlerErr != nil {
67. ...
68. }
69. }
70.
71. return "", nil
72. }

本文档使用 书栈网 · BookStack.CN 构建 - 390 -


kubelet 创建 pod 的流程

总结
本文主要讲述了 kubelet 从监听到容器调度至本节点再到创建容器的一个过程,kubelet 最终调用
docker api 来创建容器的。结合上篇文章,可以看出 kubelet 从启动到创建 pod 的一个清晰过
程。

参考:

k8s源码分析-kubelet

Kubelet源码分析(一):启动流程分析

kubelet 源码分析:pod 新建流程

kubelet创建Pod流程解析

Kubelet: Pod Lifecycle Event Generator (PLEG) Design- proposals

本文档使用 书栈网 · BookStack.CN 构建 - 391 -


kubelet 状态上报的方式

分布式系统中服务端会通过心跳机制确认客户端是否存活,在 k8s 中,kubelet 也会定时上报心跳


到 apiserver,以此判断该 node 是否存活,若 node 超过一定时间没有上报心跳,其状态会被置
为 NotReady,宿主上容器的状态也会被置为 Nodelost 或者 Unknown 状态。kubelet 自身会
定期更新状态到 apiserver,通过参数 --node-status-update-frequency 指定上报频率,默
认是 10s 上报一次,kubelet 不止上报心跳信息还会上报自身的一些数据信息。

一、kubelet 上报哪些状态
在 k8s 中,一个 node 的状态包含以下几个信息:

Addresses
Condition
Capacity
Info

1、Addresses

主要包含以下几个字段:

HostName:Hostname 。可以通过 kubelet 的 --hostname-override 参数进行覆盖。


ExternalIP:通常是可以外部路由的 node IP 地址(从集群外可访问)。
InternalIP:通常是仅可在集群内部路由的 node IP 地址。

2、Condition

conditions 字段描述了所有 Running nodes 的状态。

本文档使用 书栈网 · BookStack.CN 构建 - 392 -


kubelet 状态上报的方式

3、Capacity

描述 node 上的可用资源:CPU、内存和可以调度到该 node 上的最大 pod 数量。

4、Info

本文档使用 书栈网 · BookStack.CN 构建 - 393 -


kubelet 状态上报的方式

描述 node 的一些通用信息,例如内核版本、Kubernetes 版本(kubelet 和 kube-proxy 版


本)、Docker 版本 (如果使用了)和系统版本,这些信息由 kubelet 从 node 上获取到。

使用 kubectl get node xxx -o yaml 可以看到 node 所有的状态的信息,其中 status 中的


信息都是 kubelet 需要上报的,所以 kubelet 不止上报心跳信息还上报节点信息、节点 OOD 信
息、内存磁盘压力状态、节点监控状态、是否调度等。

二、kubelet 状态异常时的影响
如果一个 node 处于非 Ready 状态超过 pod-eviction-timeout 的值(默认为 5 分钟,在
kube-controller-manager 中定义),在 v1.5 之前的版本中 kube-controller-manager
会 force delete pod 然后调度该宿主上的 pods 到其他宿主,在 v1.5 之后的版本中,
kube-controller-manager 不会 force delete pod ,pod 会一直处于 Terminating
或 Unknown 状态直到 node 被从 master 中删除或 kubelet 状态变为 Ready。在 node
NotReady 期间,Daemonset 的 Pod 状态变为 Nodelost,Deployment、Statefulset 和
Static Pod 的状态先变为 NodeLost,然后马上变为 Unknown。Deployment 的 pod 会
recreate,Static Pod 和 Statefulset 的 Pod 会一直处于 Unknown 状态。

当 kubelet 变为 Ready 状态时,Daemonset的pod不会recreate,旧pod状态直接变为


Running,Deployment的则是将kubelet进程停止的Node删除,Statefulset的Pod会重新
recreate,Staic Pod 会被删除。

三、kubelet 状态上报的实现
kubelet 有两种上报状态的方式,第一种定期向 apiserver 发送心跳消息,简单理解就是启动一个
goroutine 然后定期向 APIServer 发送消息。

第二中被称为 NodeLease,在 v1.13 之前的版本中,节点的心跳只有 NodeStatus,从 v1.13


开始,NodeLease feature 作为 alpha 特性引入。当启用 NodeLease feature 时,每个节点
在“kube-node-lease”名称空间中都有一个关联的“Lease”对象,该对象由节点定期更新,
NodeStatus 和 NodeLease 都被视为来自节点的心跳。NodeLease 会频繁更新,而只有在
NodeStatus 发生改变或者超过了一定时间(默认值为1分钟,node-monitor-grace-period 的默
认值为 40s),才会将 NodeStatus 上报给 master。由于 NodeLease 比 NodeStatus 更轻量

本文档使用 书栈网 · BookStack.CN 构建 - 394 -


kubelet 状态上报的方式

级,该特性在集群规模扩展性和性能上有明显提升。本文主要分析第一种上报方式的实现。

kubernetes 版本 :v1.13

kubelet 上报状态的代码大部分在 kubernetes/pkg/kubelet/kubelet_node_status.go 中实


现。状态上报的功能是在 kubernetes/pkg/kubelet/kubelet.go#Run 方法以 goroutine 形式
中启动的,kubelet 中多个重要的功能都是在该方法中启动的。

kubernetes/pkg/kubelet/kubelet.go#Run

1. func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {


2. // ...
3. if kl.kubeClient != nil {
// Start syncing node status immediately, this may set up things the
4. runtime needs to run.
go wait.Until(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency,
5. wait.NeverStop)
6. go kl.fastStatusUpdateOnce()
7.
8. // 一种新的状态上报方式
9. // start syncing lease
10. if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
11. go kl.nodeLeaseController.Run(wait.NeverStop)
12. }
13. }
14. // ...
15. }

kl.syncNodeStatus 便是上报状态的,此处 kl.nodeStatusUpdateFrequency 使用的是默认


设置的 10s,也就是说节点间同步状态的函数 kl.syncNodeStatus 每 10s 执行一次。

syncNodeStatus 是状态上报的入口函数,其后所调用的多个函数也都是在同一个文件中实现的。

kubernetes/pkg/kubelet/kubelet_node_status.go#syncNodeStatus

1. func (kl *Kubelet) syncNodeStatus() {


2. kl.syncNodeStatusMux.Lock()
3. defer kl.syncNodeStatusMux.Unlock()
4.
5. if kl.kubeClient == nil || kl.heartbeatClient == nil {
6. return
7. }
8.
9. // 是否为注册节点

本文档使用 书栈网 · BookStack.CN 构建 - 395 -


kubelet 状态上报的方式

10. if kl.registerNode {
11. // This will exit immediately if it doesn't need to do anything.
12. kl.registerWithAPIServer()
13. }
14. if err := kl.updateNodeStatus(); err != nil {
15. klog.Errorf("Unable to update node status: %v", err)
16. }
17. }

syncNodeStatus 调用 updateNodeStatus, 然后又调用 tryUpdateNodeStatus 来进行上报


操作,而最终调用的是 setNodeStatus。这里还进行了同步状态判断,如果是注册节点,则执行
registerWithAPIServer,否则,执行 updateNodeStatus。

updateNodeStatus 主要是调用 tryUpdateNodeStatus 进行后续的操作,该函数中定义了状态


上报重试的次数,nodeStatusUpdateRetry 默认定义为 5 次。

kubernetes/pkg/kubelet/kubelet_node_status.go#updateNodeStatus

1. func (kl *Kubelet) updateNodeStatus() error {


2. klog.V(5).Infof("Updating node status")
3. for i := 0; i < nodeStatusUpdateRetry; i++ {
4. if err := kl.tryUpdateNodeStatus(i); err != nil {
5. if i > 0 && kl.onRepeatedHeartbeatFailure != nil {
6. kl.onRepeatedHeartbeatFailure()
7. }
8. klog.Errorf("Error updating node status, will retry: %v", err)
9. } else {
10. return nil
11. }
12. }
13. return fmt.Errorf("update node status exceeds retry count")
14. }

tryUpdateNodeStatus 是主要的上报逻辑,先给 node 设置状态,然后上报 node 的状态到


master。

kubernetes/pkg/kubelet/kubelet_node_status.go#tryUpdateNodeStatus

1. func (kl *Kubelet) tryUpdateNodeStatus(tryNumber int) error {


2. opts := metav1.GetOptions{}
3. if tryNumber == 0 {
4. util.FromApiserverCache(&opts)
5. }

本文档使用 书栈网 · BookStack.CN 构建 - 396 -


kubelet 状态上报的方式

6.
7. // 获取 node 信息
node, err := kl.heartbeatClient.CoreV1().Nodes().Get(string(kl.nodeName),
8. opts)
9. if err != nil {
10. return fmt.Errorf("error getting node %q: %v", kl.nodeName, err)
11. }
12.
13. originalNode := node.DeepCopy()
14. if originalNode == nil {
15. return fmt.Errorf("nil %q node object", kl.nodeName)
16. }
17.
18. podCIDRChanged := false
19. if node.Spec.PodCIDR != "" {
if podCIDRChanged, err = kl.updatePodCIDR(node.Spec.PodCIDR); err !=
20. nil {
21. klog.Errorf(err.Error())
22. }
23. }
24.
25. // 设置 node 状态
26. kl.setNodeStatus(node)
27.
28. now := kl.clock.Now()
if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) &&
29. now.Before(kl.lastStatusReportTime.Add(kl.nodeStatusReportFrequency)) {
if !podCIDRChanged && !nodeStatusHasChanged(&originalNode.Status,
30. &node.Status) {

31. kl.volumeManager.MarkVolumesAsReportedInUse(node.Status.VolumesInUse)
32. return nil
33. }
34. }
35.
36. // 更新 node 信息到 master
37. // Patch the current status on the API server
updatedNode, _, err :=
nodeutil.PatchNodeStatus(kl.heartbeatClient.CoreV1(),
38. types.NodeName(kl.nodeName), originalNode, node)
39. if err != nil {
40. return err
41. }

本文档使用 书栈网 · BookStack.CN 构建 - 397 -


kubelet 状态上报的方式

42. kl.lastStatusReportTime = now


43. kl.setLastObservedNodeAddresses(updatedNode.Status.Addresses)
// If update finishes successfully, mark the volumeInUse as reportedInUse
44. to indicate
45. // those volumes are already updated in the node's status

46. kl.volumeManager.MarkVolumesAsReportedInUse(updatedNode.Status.VolumesInUse)
47. return nil
48. }

tryUpdateNodeStatus 中调用 setNodeStatus 设置 node 的状态。setNodeStatus 会获取


一次 node 的所有状态,然后会将 kubelet 中保存的所有状态改为最新的值,也就是会重置 node
status 中的所有字段。

kubernetes/pkg/kubelet/kubelet_node_status.go#setNodeStatus

1. func (kl *Kubelet) setNodeStatus(node *v1.Node) {


2. for i, f := range kl.setNodeStatusFuncs {
3. klog.V(5).Infof("Setting node status at position %v", i)
4. if err := f(node); err != nil {
5. klog.Warningf("Failed to set some node status fields: %s", err)
6. }
7. }
8. }

setNodeStatus 通过 setNodeStatusFuncs 方法覆盖 node 结构体中所有的字段,


setNodeStatusFuncs 是在

NewMainKubelet(pkg/kubelet/kubelet.go) 中初始化的。

kubernetes/pkg/kubelet/kubelet.go#NewMainKubelet

1. func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,


2. // ...
3. // Generating the status funcs should be the last thing we do,
4. klet.setNodeStatusFuncs = klet.defaultNodeStatusFuncs()
5.
6. return klet, nil
7. }

defaultNodeStatusFuncs 是生成状态的函数,通过获取 node 的所有状态指标后使用工厂函数生


成状态

本文档使用 书栈网 · BookStack.CN 构建 - 398 -


kubelet 状态上报的方式

kubernetes/pkg/kubelet/kubelet_node_status.go#defaultNodeStatusFuncs

1. func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error {


2. // if cloud is not nil, we expect the cloud resource sync manager to exist
3. var nodeAddressesFunc func() ([]v1.NodeAddress, error)
4. if kl.cloud != nil {
5. nodeAddressesFunc = kl.cloudResourceSyncManager.NodeAddresses
6. }
7. var validateHostFunc func() error
8. if kl.appArmorValidator != nil {
9. validateHostFunc = kl.appArmorValidator.ValidateHost
10. }
11. var setters []func(n *v1.Node) error
12. setters = append(setters,
nodestatus.NodeAddress(kl.nodeIP, kl.nodeIPValidator, kl.hostname,
13. kl.hostnameOverridden, kl.externalCloudProvider, kl.cloud, nodeAddressesFunc),
nodestatus.MachineInfo(string(kl.nodeName), kl.maxPods, kl.podsPerCore,
14. kl.GetCachedMachineInfo, kl.containerManager.GetCapacity,
kl.containerManager.GetDevicePluginResourceCapacity,
15. kl.containerManager.GetNodeAllocatableReservation, kl.recordEvent),
nodestatus.VersionInfo(kl.cadvisor.VersionInfo,
16. kl.containerRuntime.Type, kl.containerRuntime.Version),
17. nodestatus.DaemonEndpoints(kl.daemonEndpoints),
nodestatus.Images(kl.nodeStatusMaxImages,
18. kl.imageManager.GetImageList),
19. nodestatus.GoRuntime(),
20. )
21. if utilfeature.DefaultFeatureGate.Enabled(features.AttachVolumeLimit) {
setters = append(setters,
22. nodestatus.VolumeLimits(kl.volumePluginMgr.ListVolumePluginWithLimits))
23. }
24. setters = append(setters,
nodestatus.MemoryPressureCondition(kl.clock.Now,
25. kl.evictionManager.IsUnderMemoryPressure, kl.recordNodeStatusEvent),
nodestatus.DiskPressureCondition(kl.clock.Now,
26. kl.evictionManager.IsUnderDiskPressure, kl.recordNodeStatusEvent),
nodestatus.PIDPressureCondition(kl.clock.Now,
27. kl.evictionManager.IsUnderPIDPressure, kl.recordNodeStatusEvent),
nodestatus.ReadyCondition(kl.clock.Now, kl.runtimeState.runtimeErrors,
kl.runtimeState.networkErrors, kl.runtimeState.storageErrors, validateHostFunc,
28. kl.containerManager. Status, kl.recordNodeStatusEvent),
nodestatus.VolumesInUse(kl.volumeManager.ReconcilerStatesHasBeenSynced,
29. kl.volumeManager.GetVolumesInUse),
30. nodestatus.RemoveOutOfDiskCondition(),

本文档使用 书栈网 · BookStack.CN 构建 - 399 -


kubelet 状态上报的方式

// TODO(mtaufen): I decided not to move this setter for now, since all
31. it does is send an event
// and record state back to the Kubelet runtime object. In the future,
32. I'd like to isolate
// these side-effects by decoupling the decisions to send events and
33. partial status recording
34. // from the Node setters.
35. kl.recordNodeSchedulableEvent,
36. )
37. return setters
38. }

defaultNodeStatusFuncs 可以看到 node 上报的所有信息,主要有


MemoryPressureCondition、DiskPressureCondition、PIDPressureCondition、
ReadyCondition 等。每一种 nodestatus 都返回一个 setters,所有 setters 的定义在
pkg/kubelet/nodestatus/setters.go 文件中。

对于二次开发而言,如果我们需要 APIServer 掌握更多的 Node 信息,可以在此处添加自定义函


数,例如,上报磁盘信息等。

tryUpdateNodeStatus 中最后调用 PatchNodeStatus 上报 node 的状态到 master。

kubernetes/pkg/util/node/node.go#PatchNodeStatus

1. // PatchNodeStatus patches node status.


func PatchNodeStatus(c v1core.CoreV1Interface, nodeName types.NodeName, oldNode
2. *v1.Node, newNode *v1.Node) (*v1.Node, []byte, error) {
3. // 计算 patch
patchBytes, err := preparePatchBytesforNodeStatus(nodeName, oldNode,
4. newNode)
5. if err != nil {
6. return nil, nil, err
7. }
8.
updatedNode, err := c.Nodes().Patch(string(nodeName),
9. types.StrategicMergePatchType, patchBytes, "status")
10. if err != nil {
return nil, nil, fmt.Errorf("failed to patch status %q for node %q:
11. %v", patchBytes, nodeName, err)
12. }
13. return updatedNode, patchBytes, nil
14. }

本文档使用 书栈网 · BookStack.CN 构建 - 400 -


kubelet 状态上报的方式

在 PatchNodeStatus 会调用已注册的那些方法将状态把状态发给 APIServer。

四、总结
本文主要讲述了 kubelet 上报状态的方式及其实现,node 状态上报的方式目前有两种,本文仅分析
了第一种状态上报的方式。在大规模集群中由于节点数量比较多,所有 node 都频繁报状态对 etcd
会有一定的压力,当 node 与 master 通信时由于网络导致心跳上报失败也会影响 node 的状态,
为了避免类似问题的出现才有 NodeLease 方式,对于该功能的实现后文会继续进行分析。

参考:

https://www.qikqiak.com/post/kubelet-sync-node-status/

https://www.jianshu.com/p/054450557818

https://blog.csdn.net/shida_csdn/article/details/84286058

https://kubernetes.io/docs/concepts/architecture/nodes/

本文档使用 书栈网 · BookStack.CN 构建 - 401 -


kubelet 中事件处理机制

当集群中的 node 或 pod 异常时,大部分用户会使用 kubectl 查看对应的 events,那么


events 是从何而来的?其实 k8s 中的各个组件会将运行时产生的各种事件汇报到 apiserver,对
于 k8s 中的可描述资源,使用 kubectl describe 都可以看到其相关的 events,那 k8s 中又
有哪几个组件都上报 events 呢?

只要在 k8s.io/kubernetes/cmd 目录下暴力搜索一下就能知道哪些组件会产生 events:

1. $ grep -R -n -i "EventRecorder" .

可以看出,controller-manage、kube-proxy、kube-scheduler、kubelet 都使用了
EventRecorder,本文只讲述 kubelet 中对 Events 的使用。

1、Events 的定义

events 在 k8s.io/api/core/v1/types.go 中进行定义,结构体如下所示:

1. type Event struct {


2. metav1.TypeMeta `json:",inline"`
3. metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
InvolvedObject ObjectReference `json:"involvedObject"
4. protobuf:"bytes,2,opt,name=involvedObject"`
5. Reason string `json:"reason,omitempty" protobuf:"bytes,3,opt,name=reason"`
Message string `json:"message,omitempty"
6. protobuf:"bytes,4,opt,name=message"`
Source EventSource `json:"source,omitempty"
7. protobuf:"bytes,5,opt,name=source"`
FirstTimestamp metav1.Time `json:"firstTimestamp,omitempty"
8. protobuf:"bytes,6,opt,name=firstTimestamp"`
LastTimestamp metav1.Time `json:"lastTimestamp,omitempty"
9. protobuf:"bytes,7,opt,name=lastTimestamp"`
10. Count int32 `json:"count,omitempty" protobuf:"varint,8,opt,name=count"`
11. Type string `json:"type,omitempty" protobuf:"bytes,9,opt,name=type"`
EventTime metav1.MicroTime `json:"eventTime,omitempty"
12. protobuf:"bytes,10,opt,name=eventTime"`
Series *EventSeries `json:"series,omitempty"
13. protobuf:"bytes,11,opt,name=series"`
14. Action string `json:"action,omitempty" protobuf:"bytes,12,opt,name=action"`
Related *ObjectReference `json:"related,omitempty"
15. protobuf:"bytes,13,opt,name=related"`
ReportingController string `json:"reportingComponent"
16. protobuf:"bytes,14,opt,name=reportingComponent"`
ReportingInstance string `json:"reportingInstance"
17. protobuf:"bytes,15,opt,name=reportingInstance"`

本文档使用 书栈网 · BookStack.CN 构建 - 402 -


kubelet 中事件处理机制

ReportingInstance string `json:"reportingInstance"


18. protobuf:"bytes,15,opt,name=reportingInstance"`
19. }

其中 InvolvedObject 代表和事件关联的对象,source 代表事件源,使用 kubectl 看到的事件


一般包含 Type、Reason、Age、From、Message 几个字段。

k8s 中 events 目前只有两种类型:”Normal” 和 “Warning”:

2、EventBroadcaster 的初始化

events 的整个生命周期都与 EventBroadcaster 有关,kubelet 中对 EventBroadcaster


的初始化在 k8s.io/kubernetes/cmd/kubelet/app/server.go 中:

func RunKubelet(kubeServer *options.KubeletServer, kubeDeps


1. *kubelet.Dependencies, runOnce bool) error {
2. ...
3. // event 初始化
4. makeEventRecorder(kubeDeps, nodeName)
5. ...
6. }
7.
8.
func makeEventRecorder(kubeDeps *kubelet.Dependencies, nodeName types.NodeName)
9. {
10. if kubeDeps.Recorder != nil {
11. return
12. }
13. // 初始化 EventBroadcaster
14. eventBroadcaster := record.NewBroadcaster()
15. // 初始化 EventRecorder
kubeDeps.Recorder = eventBroadcaster.NewRecorder(legacyscheme.Scheme,
16. v1.EventSource{Component: componentKubelet, Host: string(nodeName)})
17. // 记录 events 到本地日志
18. eventBroadcaster.StartLogging(glog.V(3).Infof)
19. if kubeDeps.EventClient != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 403 -


kubelet 中事件处理机制

20. glog.V(4).Infof("Sending events to api server.")


21. // 上报 events 到 apiserver
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface:
22. kubeDeps.EventClient.Events("")})
23. } else {
glog.Warning("No api server defined - no events will be sent to API
24. server.")
25. }
26. }

Kubelet 在启动的时候会初始化一个 EventBroadcaster,它主要是对接收到的 events 做一些


后续的处理(保存、上报等),EventBroadcaster 也会被 kubelet 中的其他模块使用,以下是相
关的定义,对 events 生成和处理的函数都定义在 k8s.io/client-go/tools/record/event.go
中:

1. type eventBroadcasterImpl struct {


2. *watch.Broadcaster
3. sleepDuration time.Duration
4. }
5.
// EventBroadcaster knows how to receive events and send them to any EventSink,
6. watcher, or log.
7. type EventBroadcaster interface {
8. StartEventWatcher(eventHandler func(*v1.Event)) watch.Interface
9.
10. StartRecordingToSink(sink EventSink) watch.Interface
11.
12. StartLogging(logf func(format string, args ...interface{})) watch.Interface
13.
14. NewRecorder(scheme *runtime.Scheme, source v1.EventSource) EventRecorder
15. }

EventBroadcaster 是个接口类型,该接口有以下四个方法:

StartEventWatcher() : EventBroadcaster 中的核心方法,接收各模块产生的


events,参数为一个处理 events 的函数,用户可以使用 StartEventWatcher() 接收
events 然后使用自定义的 handle 进行处理
StartRecordingToSink() : 调用 StartEventWatcher() 接收 events,并将收到的
events 发送到 apiserver
StartLogging() :也是调用 StartEventWatcher() 接收 events,然后保存 events
到日志
NewRecorder() :会创建一个指定 EventSource 的 EventRecorder,EventSource 指

本文档使用 书栈网 · BookStack.CN 构建 - 404 -


kubelet 中事件处理机制

明了哪个节点的哪个组件

eventBroadcasterImpl 是 eventBroadcaster 实际的对象,初始化 EventBroadcaster 对


象的时候会初始化一个 Broadcaster,Broadcaster 会启动一个 goroutine 接收各组件产生的
events 并广播到每一个 watcher。

1. func NewBroadcaster() EventBroadcaster {


return &eventBroadcasterImpl{watch.NewBroadcaster(maxQueuedEvents,
2. watch.DropIfChannelFull), defaultSleepDuration}
3. }

可以看到,kubelet 在初始化完 EventBroadcaster 后会调用 StartRecordingToSink() 和


StartLogging() 两个方法,StartRecordingToSink() 处理函数会将收到的 events 进行缓
存、过滤、聚合而后发送到 apiserver,StartLogging() 仅将 events 保存到 kubelet 的日
志中。

3、Events 的生成

从初始化 EventBroadcaster 的代码中可以看到 kubelet 在初始化完 EventBroadcaster 后


紧接着初始化了 EventRecorder,并将已经初始化的 Broadcaster 对象作为参数传给了
EventRecorder,至此,EventBroadcaster、EventRecorder、Broadcaster 三个对象产生了
关联。EventRecorder 的主要功能是生成指定格式的 events,以下是相关的定义:

1. type recorderImpl struct {


2. scheme *runtime.Scheme
3. source v1.EventSource
4. *watch.Broadcaster
5. clock clock.Clock
6. }
7.
8. type EventRecorder interface {
9. Event(object runtime.Object, eventtype, reason, message string)
10.
Eventf(object runtime.Object, eventtype, reason, messageFmt string, args
11. ...interface{})
12.
PastEventf(object runtime.Object, timestamp metav1.Time, eventtype, reason,
13. messageFmt string, args ...interface{})
14.
AnnotatedEventf(object runtime.Object, annotations map[string]string,
15. eventtype, reason, messageFmt string, args ...interface{})
16. }

本文档使用 书栈网 · BookStack.CN 构建 - 405 -


kubelet 中事件处理机制

EventRecorder 中包含的几个方法都是产生指定格式的 events,Event() 和 Eventf() 的功能


类似 fmt.Println() 和 fmt.Printf(),kubelet 中的各个模块会调用 EventRecorder 生
成 events。recorderImpl 是 EventRecorder 实际的对象。EventRecorder 的每个方法会调
用 generateEvent,在 generateEvent 中初始化 events 。

以下是生成 events 的函数:

func (recorder *recorderImpl) generateEvent(object runtime.Object, annotations


1. map[string]string, timestamp metav1.Time, eventtype, reason, message string) {
2. ref, err := ref.GetReference(recorder.scheme, object)
3. if err != nil {
glog.Errorf("Could not construct reference to: '%#v' due to: '%v'. Will not
4. report event: '%v' '%v' '%v'", object, err, eventtype, reason, message)
5. return
6. }
7.
8. if !validateEventType(eventtype) {
9. glog.Errorf("Unsupported event type: '%v'", eventtype)
10. return
11. }
12.
13. event := recorder.makeEvent(ref, annotations, eventtype, reason, message)
14. event.Source = recorder.source
15.
16. go func() {
17. // NOTE: events should be a non-blocking operation
18. defer utilruntime.HandleCrash()
19. // 发送事件
20. recorder.Action(watch.Added, event)
21. }()
22. }
23.
func (recorder *recorderImpl) makeEvent(ref *v1.ObjectReference, annotations
24. map[string]string, eventtype, reason, message string) *v1.Event {
25. t := metav1.Time{Time: recorder.clock.Now()}
26. namespace := ref.Namespace
27. if namespace == "" {
28. namespace = metav1.NamespaceDefault
29. }
30. return &v1.Event{
31. ObjectMeta: metav1.ObjectMeta{
32. Name: fmt.Sprintf("%v.%x", ref.Name, t.UnixNano()),
33. Namespace: namespace,

本文档使用 书栈网 · BookStack.CN 构建 - 406 -


kubelet 中事件处理机制

34. Annotations: annotations,


35. },
36. InvolvedObject: *ref,
37. Reason: reason,
38. Message: message,
39. FirstTimestamp: t,
40. LastTimestamp: t,
41. Count: 1,
42. Type: eventtype,
43. }
44. }

初始化完 events 后会调用 recorder.Action() 将 events 发送到 Broadcaster 的事件接


收队列中, Action() 是 Broadcaster 中的方法。

以下是 Action() 方法的实现:

1. func (m *Broadcaster) Action(action EventType, obj runtime.Object) {


2. m.incoming <- Event{action, obj}
3. }

4、Events 的广播

上面已经说了,EventBroadcaster 初始化时会初始化一个 Broadcaster,Broadcaster 的作


用就是接收所有的 events 并进行广播,Broadcaster 的实现在
k8s.io/apimachinery/pkg/watch/mux.go 中,Broadcaster 初始化完成后会在后台启动一个
goroutine,然后接收所有从 EventRecorder 发送过来的 events,Broadcaster 中有一个
map 会保存每一个注册的 watcher, 接着将 events 广播给所有的 watcher,每个 watcher
都有一个接收消息的 channel,watcher 可以通过它的 ResultChan() 方法从 channel 中读取
数据进行消费。

以下是 Broadcaster 广播 events 的实现:

1. func (m *Broadcaster) loop() {


2. for event := range m.incoming {
3. if event.Type == internalRunFunctionMarker {
4. event.Object.(functionFakeRuntimeObject)()
5. continue
6. }
7. m.distribute(event)
8. }
9. m.closeAll()

本文档使用 书栈网 · BookStack.CN 构建 - 407 -


kubelet 中事件处理机制

10. m.distributing.Done()
11. }
12.
13. // distribute sends event to all watchers. Blocking.
14. func (m *Broadcaster) distribute(event Event) {
15. m.lock.Lock()
16. defer m.lock.Unlock()
17. if m.fullChannelBehavior == DropIfChannelFull {
18. for _, w := range m.watchers {
19. select {
20. case w.result <- event:
21. case <-w.stopped:
22. default: // Don't block if the event can't be queued.
23. }
24. }
25. } else {
26. for _, w := range m.watchers {
27. select {
28. case w.result <- event:
29. case <-w.stopped:
30. }
31. }
32. }
33. }

5、Events 的处理

那么 watcher 是从何而来呢?每一个要处理 events 的 client 都需要初始化一个 watcher,


处理 events 的方法是在 EventBroadcaster 中定义的,以下是 EventBroadcaster 中对
events 处理的三个函数:

func (eventBroadcaster *eventBroadcasterImpl) StartEventWatcher(eventHandler


1. func(*v1.Event)) watch.Interface {
2. watcher := eventBroadcaster.Watch()
3. go func() {
4. defer utilruntime.HandleCrash()
5. for watchEvent := range watcher.ResultChan() {
6. event, ok := watchEvent.Object.(*v1.Event)
7. if !ok {
8. // This is all local, so there's no reason this should
9. // ever happen.
10. continue
11. }

本文档使用 书栈网 · BookStack.CN 构建 - 408 -


kubelet 中事件处理机制

12. eventHandler(event)
13. }
14. }()
15. return watcher
16. }

StartEventWatcher() 首先实例化一个 watcher,每个 watcher 都会被塞入到 Broadcaster


的 watcher 列表中,watcher 从 Broadcaster 提供的 channel 中读取 events,然后再调
用 eventHandler 进行处理,StartLogging() 和 StartRecordingToSink() 都是对
StartEventWatcher() 的封装,都会传入自己的处理函数。

func (eventBroadcaster *eventBroadcasterImpl) StartLogging(logf func(format


1. string, args ...interface{})) watch.Interface {
2. return eventBroadcaster.StartEventWatcher(
3. func(e *v1.Event) {
logf("Event(%#v): type: '%v' reason: '%v' %v", e.InvolvedObject, e.Type,
4. e.Reason, e.Message)
5. })
6. }

StartLogging() 传入的 eventHandler 仅将 events 保存到日志中。

func (eventBroadcaster *eventBroadcasterImpl) StartRecordingToSink(sink


1. EventSink) watch.Interface {
2. // The default math/rand package functions aren't thread safe, so create a
3. // new Rand object for each StartRecording call.
4. randGen := rand.New(rand.NewSource(time.Now().UnixNano()))
5. eventCorrelator := NewEventCorrelator(clock.RealClock{})
6. return eventBroadcaster.StartEventWatcher(
7. func(event *v1.Event) {
recordToSink(sink, event, eventCorrelator, randGen,
8. eventBroadcaster.sleepDuration)
9. })
10. }
11.
func recordToSink(sink EventSink, event *v1.Event, eventCorrelator
12. *EventCorrelator, randGen *rand.Rand, sleepDuration time.Duration) {
13. eventCopy := *event
14. event = &eventCopy
15. result, err := eventCorrelator.EventCorrelate(event)
16. if err != nil {
17. utilruntime.HandleError(err)

本文档使用 书栈网 · BookStack.CN 构建 - 409 -


kubelet 中事件处理机制

18. }
19. if result.Skip {
20. return
21. }
22. tries := 0
23. for {
if recordEvent(sink, result.Event, result.Patch, result.Event.Count > 1,
24. eventCorrelator) {
25. break
26. }
27. tries++
28. if tries >= maxTriesPerEvent {
29. glog.Errorf("Unable to write event '%#v' (retry limit exceeded!)", event)
30. break
31. }
32. // 第一次重试增加随机性,防止 apiserver 重启的时候所有的事件都在同一时间发送事件
33. if tries == 1 {
34. time.Sleep(time.Duration(float64(sleepDuration) * randGen.Float64()))
35. } else {
36. time.Sleep(sleepDuration)
37. }
38. }
39. }

StartRecordingToSink() 方法先根据当前时间生成一个随机数发生器 randGen,增加随机数是


为了在重试时增加随机性,防止 apiserver 重启的时候所有的事件都在同一时间发送事件,接着实
例化一个EventCorrelator,EventCorrelator 会对事件做一些预处理的工作,其中包括过滤、
聚合、缓存等操作,具体代码不做详细分析,最后将 recordToSink() 函数作为处理函数,
recordToSink() 会将处理后的 events 发送到 apiserver,这是 StartEventWatcher() 的
整个工作流程。

6、Events 简单实现

了解完 events 的整个处理流程后,可以参考其实现方式写一个 demo,要实现一个完整的 events


需要包含以下几个功能:

1、事件的产生
2、事件的发送
3、事件广播
4、事件缓存
5、事件过滤和聚合

本文档使用 书栈网 · BookStack.CN 构建 - 410 -


kubelet 中事件处理机制

1. package main
2.
3. import (
4. "fmt"
5. "sync"
6. "time"
7. )
8.
9. // watcher queue
10. const queueLength = int64(1)
11.
12. // Events xxx
13. type Events struct {
14. Reason string
15. Message string
16. Source string
17. Type string
18. Count int64
19. Timestamp time.Time
20. }
21.
22. // EventBroadcaster xxx
23. type EventBroadcaster interface {
24. Event(etype, reason, message string)
25. StartLogging() Interface
26. Stop()
27. }
28.
29. // eventBroadcaster xxx
30. type eventBroadcasterImpl struct {
31. *Broadcaster
32. }
33.
34. func NewEventBroadcaster() EventBroadcaster {
35. return &eventBroadcasterImpl{NewBroadcaster(queueLength)}
36. }
37.
38. func (eventBroadcaster *eventBroadcasterImpl) Stop() {
39. eventBroadcaster.Shutdown()
40. }
41.
42. // generate event

本文档使用 书栈网 · BookStack.CN 构建 - 411 -


kubelet 中事件处理机制

func (eventBroadcaster *eventBroadcasterImpl) Event(etype, reason, message


43. string) {
44. events := &Events{Type: etype, Reason: reason, Message: message}
45. // send event to broadcast
46. eventBroadcaster.Action(events)
47. }
48.
49. // 仅实现 StartLogging() 的功能,将日志打印
50. func (eventBroadcaster *eventBroadcasterImpl) StartLogging() Interface {
51. // register a watcher
52. watcher := eventBroadcaster.Watch()
53. go func() {
54. for watchEvent := range watcher.ResultChan() {
55. fmt.Printf("%v\n", watchEvent)
56. }
57. }()
58.
59. go func() {
60. time.Sleep(time.Second * 4)
61. watcher.Stop()
62. }()
63.
64. return watcher
65. }
66.
67. // --------------------
68. // Broadcaster 定义与实现
69. // 接收 events channel 的长度
70. const incomingQueuLength = 100
71.
72. type Broadcaster struct {
73. lock sync.Mutex
74. incoming chan Events
75. watchers map[int64]*broadcasterWatcher
76. watchersQueue int64
77. watchQueueLength int64
78. distributing sync.WaitGroup
79. }
80.
81. func NewBroadcaster(queueLength int64) *Broadcaster {
82. m := &Broadcaster{
83. incoming: make(chan Events, incomingQueuLength),

本文档使用 书栈网 · BookStack.CN 构建 - 412 -


kubelet 中事件处理机制

84. watchers: map[int64]*broadcasterWatcher{},


85. watchQueueLength: queueLength,
86. }
87. m.distributing.Add(1)
88. // 后台启动一个 goroutine 广播 events
89. go m.loop()
90. return m
91. }
92.
93. // Broadcaster 接收所产生的 events
94. func (m *Broadcaster) Action(event *Events) {
95. m.incoming <- *event
96. }
97.
98. // 广播 events 到每个 watcher
99. func (m *Broadcaster) loop() {
100. // 从 incoming channel 中读取所接收到的 events
101. for event := range m.incoming {
102. // 发送 events 到每一个 watcher
103. for _, w := range m.watchers {
104. select {
105. case w.result <- event:
106. case <-w.stopped:
107. default:
108. }
109. }
110. }
111. m.closeAll()
112. m.distributing.Done()
113. }
114.
115. func (m *Broadcaster) Shutdown() {
116. close(m.incoming)
117. m.distributing.Wait()
118. }
119.
120. func (m *Broadcaster) closeAll() {
121. // TODO
122. m.lock.Lock()
123. defer m.lock.Unlock()
124. for _, w := range m.watchers {
125. close(w.result)

本文档使用 书栈网 · BookStack.CN 构建 - 413 -


kubelet 中事件处理机制

126. }
127. m.watchers = map[int64]*broadcasterWatcher{}
128. }
129.
130. func (m *Broadcaster) stopWatching(id int64) {
131. m.lock.Lock()
132. defer m.lock.Unlock()
133. w, ok := m.watchers[id]
134. if !ok {
135. return
136. }
137. delete(m.watchers, id)
138. close(w.result)
139. }
140.
141. // 调用 Watch()方法注册一个 watcher
142. func (m *Broadcaster) Watch() Interface {
143. watcher := &broadcasterWatcher{
144. result: make(chan Events, incomingQueuLength),
145. stopped: make(chan struct{}),
146. id: m.watchQueueLength,
147. m: m,
148. }
149. m.watchers[m.watchersQueue] = watcher
150. m.watchQueueLength++
151. return watcher
152. }
153.
154. // watcher 实现
155. type Interface interface {
156. Stop()
157. ResultChan() <-chan Events
158. }
159.
160. type broadcasterWatcher struct {
161. result chan Events
162. stopped chan struct{}
163. stop sync.Once
164. id int64
165. m *Broadcaster
166. }
167.

本文档使用 书栈网 · BookStack.CN 构建 - 414 -


kubelet 中事件处理机制

168. // 每个 watcher 通过该方法读取 channel 中广播的 events


169. func (b *broadcasterWatcher) ResultChan() <-chan Events {
170. return b.result
171. }
172.
173. func (b *broadcasterWatcher) Stop() {
174. b.stop.Do(func() {
175. close(b.stopped)
176. b.m.stopWatching(b.id)
177. })
178. }
179.
180. // --------------------
181.
182. func main() {
183. eventBroadcast := NewEventBroadcaster()
184.
185. var wg sync.WaitGroup
186. wg.Add(1)
187. // producer event
188. go func() {
189. defer wg.Done()
190. time.Sleep(time.Second)
191. eventBroadcast.Event("add", "test", "1")
192. time.Sleep(time.Second * 2)
193. eventBroadcast.Event("add", "test", "2")
194. time.Sleep(time.Second * 3)
195. eventBroadcast.Event("add", "test", "3")
196. //eventBroadcast.Stop()
197. }()
198.
199. eventBroadcast.StartLogging()
200. wg.Wait()
201. }

此处仅简单实现,将 EventRecorder 处理 events 的功能直接放在了 EventBroadcaster 中


实现,对 events 的处理方法仅实现了 StartLogging(),Broadcaster 中的部分功能是直接复
制 k8s 中的代码,有一定的精简,其实现值得学习,此处对 EventCorrelator 并没有进行实现。

代码请参考:https://github.com/gosoon/k8s-learning-notes/tree/master/k8s-
package/events

7、总结

本文档使用 书栈网 · BookStack.CN 构建 - 415 -


kubelet 中事件处理机制

本文讲述了 k8s 中 events 从产生到展示的一个完整过程,最后也实现了一个简单的 demo,在此


将 kubelet 对 events 的整个处理过程再梳理下,其中主要有三个对象 EventBroadcaster、
EventRecorder、Broadcaster:

1、kubelet 首先会初始化 EventBroadcaster 对象,同时会初始化一个 Broadcaster


对象。
2、kubelet 通过 EventBroadcaster 对象的 NewRecorder() 方法初始化
EventRecorder 对象,EventRecorder 对象提供的几个方法会生成 events 并通过
Action() 方法发送 events 到 Broadcaster 的 channel 队列中。
3、Broadcaster 的作用就是接收所有的 events 并进行广播,Broadcaster 初始化后会在
后台启动一个 goroutine,然后接收所有从 EventRecorder 发来的 events。
4、EventBroadcaster 对 events 有三个处理方法:StartEventWatcher()、
StartRecordingToSink()、StartLogging(),StartEventWatcher() 是其中的核心方
法,会初始化一个 watcher 注册到 Broadcaster,其余两个处理函数对
StartEventWatcher() 进行了封装,并实现了自己的处理函数。
5、 Broadcaster 中有一个 map 会保存每一个注册的 watcher,其会将所有的 events
广播给每一个 watcher,每个 watcher 通过它的 ResultChan() 方法从 channel 接收
events。
6、kubelet 会使用 StartRecordingToSink() 和 StartLogging() 对 events 进行
处理,StartRecordingToSink() 处理函数收到 events 后会进行缓存、过滤、聚合而后发
送到 apiserver,apiserver 会将 events 保存到 etcd 中,使用 kubectl 或其他客户
端可以查看。StartLogging() 仅将 events 保存到 kubelet 的日志中。

本文档使用 书栈网 · BookStack.CN 构建 - 416 -


kubelet statusManager 源码分析

本篇文章没有接上篇继续更新 kube-controller-manager,kube-controller-manager 的源
码阅读笔记也会继续更新,笔者会同时阅读多个组件的源码,阅读笔记也会按组件进行交叉更新,交叉
更新的目的一是为了加深印象避免阅读完后又很快忘记,二是某些代码的功能难以理解,避免死磕,但
整体目标是将每个组件的核心代码阅读完。

在前面的文章中已经介绍过 kubelet 的架构以及启动流程,本章会继续介绍 kubelet 中的核心功


能,kubelet 中包含数十个 manager 以及对 CNI、CRI、CSI 的调用。每个 manager 的功能各
不相同,manager 之间也会有依赖关系,本文会介绍比较简单的 statusManager。

statusManager 源码分析

kubernetes 版本:v1.16

statusManager 的主要功能是将 pod 状态信息同步到 apiserver,statusManage 并不会主动


监控 pod 的状态,而是提供接口供其他 manager 进行调用。

statusManager 的初始化

kubelet 在启动流程时会在 NewMainKubelet 方法中初始化其核心组件,包括各种 manager。

k8s.io/kubernetes/pkg/kubelet/kubelet.go:335

1. func NewMainKubelet() (*Kubelet, error) {


2. ......
3. // statusManager 的初始化
klet.statusManager = status.NewManager(klet.kubeClient, klet.podManager,
4. klet)
5. ......
6. }

NewManager 是用来初始化 statusManager 对象的,其中参数的功能如下所示:

kubeClient:用于和 apiserver 交互;


podManager:负责内存中 pod 的维护;
podStatuses:statusManager 的 cache,保存 pod 与状态的对应关系;
podStatusesChannel:当其他组件调用 statusManager 更新 pod 状态时,会将 pod 的
状态信息发送到podStatusesChannel 中;
apiStatusVersions:维护最新的 pod status 版本号,每更新一次会加1;
podDeletionSafety:删除 pod 的接口;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:118

本文档使用 书栈网 · BookStack.CN 构建 - 417 -


kubelet statusManager 源码分析

func NewManager(kubeClient clientset.Interface, podManager kubepod.Manager,


1. podDeletionSafety PodDeletionSafetyProvider) Manager {
2. return &manager{
3. kubeClient: kubeClient,
4. podManager: podManager,
5. podStatuses: make(map[types.UID]versionedPodStatus),
6. podStatusChannel: make(chan podStatusSyncRequest, 1000),
7. apiStatusVersions: make(map[kubetypes.MirrorPodUID]uint64),
8. podDeletionSafety: podDeletionSafety,
9. }
10. }

在初始化完成后,kubelet 会在 Run 方法中会以 goroutine 的方式启动 statusManager。

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1398

1. func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {


2. ......
3. kl.statusManager.Start()
4. ......
5. }

statusManager 的代码主要在 k8s.io/kubernetes/pkg/kubelet/status/ 目录中,其对外暴


露的接口有以下几个:

1. type Manager interface {


2. // 一个 interface 用来暴露给其他组件获取 pod status 的
3. PodStatusProvider
4.
5. // 启动 statusManager 的方法
6. Start()
7.
8. // 设置 pod 的状态并会触发一个状态同步操作
9. SetPodStatus(pod *v1.Pod, status v1.PodStatus)
10.
// 设置 pod .status.containerStatuses 中 container 是否为 ready 状态并触发状态
11. 同步操作
SetContainerReadiness(podUID types.UID, containerID
12. kubecontainer.ContainerID, ready bool)
13.
// 设置 pod .status.containerStatuses 中 container 是否为 started 状态并触发状
14. 态同步操作

本文档使用 书栈网 · BookStack.CN 构建 - 418 -


kubelet statusManager 源码分析

SetContainerStartup(podUID types.UID, containerID


15. kubecontainer.ContainerID, started bool)
16.
// 将 pod .status.containerStatuses 和 .status.initContainerStatuses 中
17. container 的 state 置为 Terminated 状态并触发状态同步操作
18. TerminatePod(pod *v1.Pod)
19.
20. // 从 statusManager 缓存 podStatuses 中删除对应的 pod
21. RemoveOrphanedStatuses(podUIDs map[types.UID]bool)
22. }

pod 对应的 status 字段如下所示:

1. status:
2. conditions:
3. ......
4. containerStatuses:
- containerID:
5. containerd://64e9d88459b38e90c2a4b4d87db5acd180c820c855a55aabe38e4e11b9b83576
6. image: docker.io/library/nginx:1.9
imageID:
7. sha256:f568d3158b1e871b713cb33aca5a9377bc21a1f644addf41368393d28c35e894
8. lastState: {}
9. name: nginx-pod
10. ready: true
11. restartCount: 0
12. started: true
13. state:
14. running:
15. startedAt: "2019-12-15T16:13:29Z"
16. podIP: 10.15.225.15
17. ......

然后继续看 statusManager 的启动方法 start , 其主要逻辑为:

1、设置定时器, syncPeriod 默认为 10s;


2、启动 wait.Forever goroutine 同步 pod 的状态,有两种同步方式,第一种是当监听
到某个 pod 状态改变时会调用 m.syncPod 进行同步,第二种是当触发定时器时调用
m.syncBatch 进行批量同步;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:147

1. func (m *manager) Start() {

本文档使用 书栈网 · BookStack.CN 构建 - 419 -


kubelet statusManager 源码分析

2. // 1、检查 kubeClient 是否被初始化


3. if m.kubeClient == nil {
4. klog.Infof("Kubernetes client is nil, not starting status manager.")
5. return
6. }
7.
8. // 2、设置定时器
9. syncTicker := time.Tick(syncPeriod)
10.
11. go wait.Forever(func() {
12. select {
13. // 3、监听 m.podStatusChannel channel,当接收到数据时触发同步操作
14. case syncRequest := <-m.podStatusChannel:
15. ......
16. m.syncPod(syncRequest.podUID, syncRequest.status)
17. // 4、定时同步
18. case <-syncTicker:
19. m.syncBatch()
20. }
21. }, 0)
22. }

syncPod

syncPod 是用来同步 pod 最新状态至 apiserver 的方法,主要逻辑为:

1、调用 m.needsUpdate 判断是否需要同步状态,若 apiStatusVersions 中的 status


版本号小于当前接收到的 status 版本号或者 apistatusVersions 中不存在该 status 版
本号则需要同步,若不需要同步则继续检查 pod 是否处于删除状态,若处于删除状态调用
m.podDeletionSafety.PodResourcesAreReclaimed 将 pod 完全删除;
2、从 apiserver 获取 pod 的 oldStatus;
3、检查 pod oldStatus 与 currentStatus 的 uid 是否相等,若不相等则说明 pod
被重建过;
4、调用 statusutil.PatchPodStatus 同步 pod 最新的 status 至 apiserver,并将返
回的 pod 作为 newPod;
5、检查 newPod 是否处于 terminated 状态,若处于 terminated 状态则调用
apiserver 接口进行删除并从 cache 中清除,删除后 pod 会进行重建;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:514

1. func (m *manager) syncPod(uid types.UID, status versionedPodStatus) {


2. // 1、判断是否需要同步状态

本文档使用 书栈网 · BookStack.CN 构建 - 420 -


kubelet statusManager 源码分析

3. if !m.needsUpdate(uid, status) {
4. klog.V(1).Infof("Status for pod %q is up-to-date; skipping", uid)
5. return
6. }
7.
8. // 2、获取 pod 的 oldStatus
pod, err :=
m.kubeClient.CoreV1().Pods(status.podNamespace).Get(status.podName,
9. metav1.GetOptions{})
10. if errors.IsNotFound(err) {
11. return
12. }
13. if err != nil {
14. return
15. }
16.
17. translatedUID := m.podManager.TranslatePodUID(pod.UID)
18. // 3、检查 pod UID 是否已经改变
if len(translatedUID) > 0 && translatedUID != kubetypes.ResolvedPodUID(uid)
19. {
20. return
21. }
22.
23. // 4、同步 pod 最新的 status 至 apiserver
24. oldStatus := pod.Status.DeepCopy()
newPod, patchBytes, err := statusutil.PatchPodStatus(m.kubeClient,
25. pod.Namespace, pod.Name, *oldStatus, mergePodStatus(*oldStatus, status.status))
26. if err != nil {
27. return
28. }
29. pod = newPod
30.
31. m.apiStatusVersions[kubetypes.MirrorPodUID(pod.UID)] = status.version
32.
// 5、若 newPod 处于 terminated 状态则调用 apiserver 删除该 pod,删除后 pod 会重
33. 建
34. if m.canBeDeleted(pod, status.status) {
35. deleteOptions := metav1.NewDeleteOptions(0)
deleteOptions.Preconditions =
36. metav1.NewUIDPreconditions(string(pod.UID))
err = m.kubeClient.CoreV1().Pods(pod.Namespace).Delete(pod.Name,
37. deleteOptions)
38. if err != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 421 -


kubelet statusManager 源码分析

39. return
40. }
41. // 6、从 cache 中清除
42. m.deletePodStatus(uid)
43. }
44. }

needsUpdate

needsUpdate 方法主要是检查 pod 的状态是否需要更新,以及检查当 pod 处于 terminated


状态时保证 pod 被完全删除。

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:570

1. func (m *manager) needsUpdate(uid types.UID, status versionedPodStatus) bool {


2. latest, ok := m.apiStatusVersions[kubetypes.MirrorPodUID(uid)]
3. if !ok || latest < status.version {
4. return true
5. }
6. pod, ok := m.podManager.GetPodByUID(uid)
7. if !ok {
8. return false
9. }
10. return m.canBeDeleted(pod, status.status)
11. }
12.
13. func (m *manager) canBeDeleted(pod *v1.Pod, status v1.PodStatus) bool {
14. if pod.DeletionTimestamp == nil || kubepod.IsMirrorPod(pod) {
15. return false
16. }
17. // 此处说明 pod 已经处于删除状态了
18. return m.podDeletionSafety.PodResourcesAreReclaimed(pod, status)
19. }

PodResourcesAreReclaimed

PodResourcesAreReclaimed 检查 pod 在 node 上占用的所有资源是否已经被回收,其主要逻


辑为:

1、检查 pod 中的所有 container 是否都处于非 running 状态;


2、从 podCache 中获取 podStatus,通过 podStatus 检查 pod 中的 container 是否
已被完全删除;
3、检查 pod 的 volume 是否被清理;

本文档使用 书栈网 · BookStack.CN 构建 - 422 -


kubelet statusManager 源码分析

4、检查 pod 的 cgroup 是否被清理;


5、若以上几个检查项都通过说明在 kubelet 端 pod 已被完全删除;

k8s.io/kubernetes/pkg/kubelet/kubelet_pods.go:900

func (kl *Kubelet) PodResourcesAreReclaimed(pod *v1.Pod, status v1.PodStatus)


1. bool {
2. // 1、检查 pod 中的所有 container 是否都处于非 running 状态
3. if !notRunning(status.ContainerStatuses) {
4. return false
5. }
6.
// 2、从 podCache 中获取 podStatus,通过 podStatus 检查 pod 中的 container 是否
7. 已被完全删除
8. runtimeStatus, err := kl.podCache.Get(pod.UID)
9. if err != nil {
10. return false
11. }
12. if len(runtimeStatus.ContainerStatuses) > 0 {
13. var statusStr string
14. for _, status := range runtimeStatus.ContainerStatuses {
15. statusStr += fmt.Sprintf("%+v ", *status)
16. }
17. return false
18. }
19.
20. // 3、检查 pod 的 volume 是否被清理
21. if kl.podVolumesExist(pod.UID) && !kl.keepTerminatedPodVolumes {
22. return false
23. }
24.
25. // 4、检查 pod 的 cgroup 是否被清理
26. if kl.kubeletConfiguration.CgroupsPerQOS {
27. pcm := kl.containerManager.NewPodContainerManager()
28. if pcm.Exists(pod) {
29. return false
30. }
31. }
32. return true
33. }

syncBatch

本文档使用 书栈网 · BookStack.CN 构建 - 423 -


kubelet statusManager 源码分析

syncBatch 是定期将 statusManager 缓存 podStatuses 中的数据同步到 apiserver 的


方法,主要逻辑为:

1、调用 m.podManager.GetUIDTranslations 从 podManager 中获取 mirrorPod uid


与 staticPod uid 的对应关系;
2、从 apiStatusVersions 中清理已经不存在的 pod,遍历 apiStatusVersions,检查
podStatuses 以及 mirrorToPod 中是否存在该对应的 pod,若不存在则从
apiStatusVersions 中删除;
3、遍历 podStatuses,首先调用 needsUpdate 检查 pod 的状态是否与
apiStatusVersions 中的一致,然后调用 needsReconcile 检查 pod 的状态是否与
podManager 中的一致,若不一致则将需要同步的 pod 加入到 updatedStatuses 列表中;
4、遍历 updatedStatuses 列表,调用 m.syncPod 方法同步状态;

syncBatch 主要是将 statusManage cache 中的数据与 apiStatusVersions 和


podManager 中的数据进行对比是否一致,若不一致则以 statusManage cache 中的数据为准同
步至 apiserver。

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:469

1. func (m *manager) syncBatch() {


2. var updatedStatuses []podStatusSyncRequest
3. // 1、获取 mirrorPod 与 staticPod 的对应关系
4. podToMirror, mirrorToPod := m.podManager.GetUIDTranslations()
5. func() {
6. m.podStatusesLock.RLock()
7. defer m.podStatusesLock.RUnlock()
8.
9. // 2、从 apiStatusVersions 中清理已经不存在的 pod
10. for uid := range m.apiStatusVersions {
11. _, hasPod := m.podStatuses[types.UID(uid)]
12. _, hasMirror := mirrorToPod[uid]
13. if !hasPod && !hasMirror {
14. delete(m.apiStatusVersions, uid)
15. }
16. }
17.
18. // 3、遍历 podStatuses,将需要同步状态的 pod 加入到 updatedStatuses 列表中
19. for uid, status := range m.podStatuses {
20. syncedUID := kubetypes.MirrorPodUID(uid)
if mirrorUID, ok := podToMirror[kubetypes.ResolvedPodUID(uid)]; ok
21. {
22. if mirrorUID == "" {

本文档使用 书栈网 · BookStack.CN 构建 - 424 -


kubelet statusManager 源码分析

23. continue
24. }
25. syncedUID = mirrorUID
26. }
27. if m.needsUpdate(types.UID(syncedUID), status) {
updatedStatuses = append(updatedStatuses,
28. podStatusSyncRequest{uid, status})
29. } else if m.needsReconcile(uid, status.status) {
30. delete(m.apiStatusVersions, syncedUID)
updatedStatuses = append(updatedStatuses,
31. podStatusSyncRequest{uid, status})
32. }
33. }
34. }()
35.
36. // 4、调用 m.syncPod 同步 pod 状态
37. for _, update := range updatedStatuses {
38. m.syncPod(update.podUID, update.status)
39. }
40. }

syncBatch 中主要调用了两个方法 needsUpdate 和 needsReconcile


, needsUpdate 在上文中已经介绍过了,下面介绍 needsReconcile 方法的主要逻辑。

needsReconcile

needsReconcile 对比当前 pod 的状态与 podManager 中的状态是否一致,podManager 中


保存了 node 上 pod 的 object,podManager 中的数据与 apiserver 是一致
的, needsReconcile 主要逻辑为:

1、通过 uid 从 podManager 中获取 pod 对象;


2、检查 pod 是否为 static pod,若为 static pod 则获取其对应的 mirrorPod;
3、格式化 pod status subResource;
4、检查 podManager 中的 status 与 statusManager cache 中的 status 是否一
致,若不一致则以 statusManager 为准进行同步;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:598

1. func (m *manager) needsReconcile(uid types.UID, status v1.PodStatus) bool {


2. // 1、从 podManager 中获取 pod 对象
3. pod, ok := m.podManager.GetPodByUID(uid)
4. if !ok {
5. return false

本文档使用 书栈网 · BookStack.CN 构建 - 425 -


kubelet statusManager 源码分析

6. }
7.
8. // 2、检查 pod 是否为 static pod,若为 static pod 则获取其对应的 mirrorPod
9. if kubetypes.IsStaticPod(pod) {
10. mirrorPod, ok := m.podManager.GetMirrorPodByPod(pod)
11. if !ok {
12. return false
13. }
14. pod = mirrorPod
15. }
16.
17. podStatus := pod.Status.DeepCopy()
18.
19. // 3、格式化 pod status subResource
20. normalizeStatus(pod, podStatus)
21.
22. // 4、检查 podManager 中的 status 与 statusManager cache 中的 status 是否一致
23. if isPodStatusByKubeletEqual(podStatus, &status) {
24. return false
25. }
26.
27. return true
28. }

以上就是 statusManager 同步 pod status 的主要逻辑,下面再了解一下 statusManager 对


其他组件暴露的方法。

SetPodStatus

SetPodStatus 是为 pod 设置 status subResource 并会触发同步操作,主要逻辑为:

1、检查 pod.Status.Conditions 中的类型是否为 kubelet 创建的,kubelet 会创建


ContainersReady 、 Initialized 、 Ready 、 PodScheduled 和 Unschedulable 五
种类型的 conditions;
2、调用 m.updateStatusInternal 触发更新操作;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:179

1. func (m *manager) SetPodStatus(pod *v1.Pod, status v1.PodStatus) {


2. m.podStatusesLock.Lock()
3. defer m.podStatusesLock.Unlock()
4.
5. for _, c := range pod.Status.Conditions {

本文档使用 书栈网 · BookStack.CN 构建 - 426 -


kubelet statusManager 源码分析

6. if !kubetypes.PodConditionByKubelet(c.Type) {
klog.Errorf("Kubelet is trying to update pod condition %q for pod
7. %q. "+
"But it is not owned by kubelet.", string(c.Type),
8. format.Pod(pod))
9. }
10. }
11.
12. status = *status.DeepCopy()
13.
14. m.updateStatusInternal(pod, status, pod.DeletionTimestamp != nil)
15. }

updateStatusInternal

statusManager 对外暴露的方法中触发状态同步的操作都是由 updateStatusInternal 完成


的, updateStatusInternal 会更新 statusManager 的 cache 并会将 newStatus 发送到
m.podStatusChannel 中,然后 statusManager 会调用 syncPod 方法同步到
apiserver。

1、从 cache 中获取 oldStatus;


2、检查 ContainerStatuses 和 InitContainerStatuses 是否合法;
3、为 status 设置
ContainersReady 、 PodReady 、 PodInitialized 、 PodScheduled conditions;
4、设置 status 的 StartTime ;
5、格式化 status;
6、将 newStatus 添加到 statusManager 的 cache podStatuses 中;
7、将 newStatus 发送到 m.podStatusChannel 中;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:362

func (m *manager) updateStatusInternal(pod *v1.Pod, status v1.PodStatus,


1. forceUpdate bool) bool {
2. var oldStatus v1.PodStatus
3. // 1、从 cache 中获取 oldStatus
4. cachedStatus, isCached := m.podStatuses[pod.UID]
5. if isCached {
6. oldStatus = cachedStatus.status
7. } else if mirrorPod, ok := m.podManager.GetMirrorPodByPod(pod); ok {
8. oldStatus = mirrorPod.Status
9. } else {
10. oldStatus = pod.Status
11. }

本文档使用 书栈网 · BookStack.CN 构建 - 427 -


kubelet statusManager 源码分析

12.
13. // 2、检查 ContainerStatuses 和 InitContainerStatuses 是否合法
if err := checkContainerStateTransition(oldStatus.ContainerStatuses,
14. status.ContainerStatuses, pod.Spec.RestartPolicy); err != nil {
15. return false
16. }
if err := checkContainerStateTransition(oldStatus.InitContainerStatuses,
17. status.InitContainerStatuses, pod.Spec.RestartPolicy); err != nil {
klog.Errorf("Status update on pod %v/%v aborted: %v", pod.Namespace,
18. pod.Name, err)
19. return false
20. }
21.
// 3、为 status 设置 ContainersReady、PodReady、PodInitialized、PodScheduled
22. conditions
23. updateLastTransitionTime(&status, &oldStatus, v1.ContainersReady)
24.
25. updateLastTransitionTime(&status, &oldStatus, v1.PodReady)
26.
27. updateLastTransitionTime(&status, &oldStatus, v1.PodInitialized)
28.
29. updateLastTransitionTime(&status, &oldStatus, v1.PodScheduled)
30.
31. // 4、设置 status 的 StartTime
32. if oldStatus.StartTime != nil && !oldStatus.StartTime.IsZero() {
33. status.StartTime = oldStatus.StartTime
34. } else if status.StartTime.IsZero() {
35. now := metav1.Now()
36. status.StartTime = &now
37. }
38.
39. // 5、格式化 status
40. normalizeStatus(pod, &status)
41.
if isCached && isPodStatusByKubeletEqual(&cachedStatus.status, &status) &&
42. !forceUpdate {
43. return false
44. }
45.
46. // 6、将 newStatus 添加到 statusManager 的 cache podStatuses 中
47. newStatus := versionedPodStatus{
48. status: status,
49. version: cachedStatus.version + 1,

本文档使用 书栈网 · BookStack.CN 构建 - 428 -


kubelet statusManager 源码分析

50. podName: pod.Name,


51. podNamespace: pod.Namespace,
52. }
53. m.podStatuses[pod.UID] = newStatus
54.
55. // 7、将 newStatus 发送到 m.podStatusChannel 中
56. select {
57. case m.podStatusChannel <- podStatusSyncRequest{pod.UID, newStatus}:
58. return true
59. default:
60. return false
61. }
62. }

SetPodStatus 方法主要会用在 kubelet 的主 syncLoop 中,并在 syncPod 方法中创建


pod 时使用。

SetContainerReadiness

SetContainerReadiness 方法会设置 pod status subResource 中 container 是否为


ready 状态,其主要逻辑为:

1、获取 pod 对象;


2、从 m.podStatuses 获取 oldStatus;
3、通过 containerID 从 pod 中获取 containerStatus;
4、若 container status 为 Ready 直接返回,此时该 container 状态无须更新;
5、添加 PodReadyCondition 和 ContainersReadyCondition;
6、调用 m.updateStatusInternal 触发同步操作;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:198

func (m *manager) SetContainerReadiness(podUID types.UID, containerID


1. kubecontainer.ContainerID, ready bool) {
2. m.podStatusesLock.Lock()
3. defer m.podStatusesLock.Unlock()
4.
5. // 1、获取 pod 对象
6. pod, ok := m.podManager.GetPodByUID(podUID)
7. if !ok {
8. return
9. }
10.
11. // 2、从 m.podStatuses 获取 oldStatus

本文档使用 书栈网 · BookStack.CN 构建 - 429 -


kubelet statusManager 源码分析

12. oldStatus, found := m.podStatuses[pod.UID]


13. if !found {
14. return
15. }
16.
17. // 3、通过 containerID 从 pod 中获取 containerStatus
containerStatus, _, ok := findContainerStatus(&oldStatus.status,
18. containerID.String())
19. if !ok {
20. return
21. }
22.
23. // 4、若 container status 为 Ready 直接返回,此时该 container 状态无须更新
24. if containerStatus.Ready == ready {
25. return
26. }
27.
28.
29. status := *oldStatus.status.DeepCopy()
30. containerStatus, _, _ = findContainerStatus(&status, containerID.String())
31. containerStatus.Ready = ready
32.
updateConditionFunc := func(conditionType v1.PodConditionType, condition
33. v1.PodCondition) {
34. conditionIndex := -1
35. for i, condition := range status.Conditions {
36. if condition.Type == conditionType {
37. conditionIndex = i
38. break
39. }
40. }
41. if conditionIndex != -1 {
42. status.Conditions[conditionIndex] = condition
43. } else {
44. status.Conditions = append(status.Conditions, condition)
45. }
46. }
47. // 5、添加 PodReadyCondition 和 ContainersReadyCondition
updateConditionFunc(v1.PodReady, GeneratePodReadyCondition(&pod.Spec,
48. status.Conditions, status.ContainerStatuses, status.Phase))
updateConditionFunc(v1.ContainersReady,
GenerateContainersReadyCondition(&pod.Spec, status.ContainerStatuses,
49. status.Phase))

本文档使用 书栈网 · BookStack.CN 构建 - 430 -


kubelet statusManager 源码分析

50. // 6、调用 m.updateStatusInternal 触发同步操作


51. m.updateStatusInternal(pod, status, false)
52. }

SetContainerReadiness 方法主要被用在 proberManager 中,关于 proberManager 的功能


会在后文说明。

SetContainerStartup

SetContainerStartup 方法会设置 pod status subResource 中 container 是否为


started 状态,主要逻辑为:

1、通过 podUID 从 podManager 中获取 pod 对象;


2、从 statusManager 中获取 pod 的 oldStatus;
3、检查要更新的 container 是否存在;
4、检查目标 container 的 started 状态是否已为期望值;
5、设置目标 container 的 started 状态;
6、调用 m.updateStatusInternal 触发同步操作;

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:255

func (m *manager) SetContainerStartup(podUID types.UID, containerID


1. kubecontainer.ContainerID, started bool) {
2. m.podStatusesLock.Lock()
3. defer m.podStatusesLock.Unlock()
4.
5. // 1、通过 podUID 从 podManager 中获取 pod 对象
6. pod, ok := m.podManager.GetPodByUID(podUID)
7. if !ok {
8. return
9. }
10.
11. // 2、从 statusManager 中获取 pod 的 oldStatus
12. oldStatus, found := m.podStatuses[pod.UID]
13. if !found {
14. return
15. }
16.
17. // 3、检查要更新的 container 是否存在
containerStatus, _, ok := findContainerStatus(&oldStatus.status,
18. containerID.String())
19. if !ok {

本文档使用 书栈网 · BookStack.CN 构建 - 431 -


kubelet statusManager 源码分析

klog.Warningf("Container startup changed for unknown container: %q -


20. %q",
21. format.Pod(pod), containerID.String())
22. return
23. }
24.
25. // 4、检查目标 container 的 started 状态是否已为期望值
26. if containerStatus.Started != nil && *containerStatus.Started == started {
27. return
28. }
29.
30. // 5、设置目标 container 的 started 状态
31. status := *oldStatus.status.DeepCopy()
32. containerStatus, _, _ = findContainerStatus(&status, containerID.String())
33. containerStatus.Started = &started
34.
35. // 6、触发同步操作
36. m.updateStatusInternal(pod, status, false)
37. }

SetContainerStartup 方法也是主要被用在 proberManager 中。

TerminatePod

TerminatePod 方法的主要逻辑是把 pod .status.containerStatuses 和


.status.initContainerStatuses 中 container 的 state 置为 Terminated 状态并触
发状态同步操作。

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:312

1. func (m *manager) TerminatePod(pod *v1.Pod) {


2. m.podStatusesLock.Lock()
3. defer m.podStatusesLock.Unlock()
4. oldStatus := &pod.Status
5. if cachedStatus, ok := m.podStatuses[pod.UID]; ok {
6. oldStatus = &cachedStatus.status
7. }
8. status := *oldStatus.DeepCopy()
9. for i := range status.ContainerStatuses {
10. status.ContainerStatuses[i].State = v1.ContainerState{
11. Terminated: &v1.ContainerStateTerminated{},
12. }
13. }

本文档使用 书栈网 · BookStack.CN 构建 - 432 -


kubelet statusManager 源码分析

14. for i := range status.InitContainerStatuses {


15. status.InitContainerStatuses[i].State = v1.ContainerState{
16. Terminated: &v1.ContainerStateTerminated{},
17. }
18. }
19. m.updateStatusInternal(pod, status, true)
20. }

TerminatePod 方法主要会用在 kubelet 的主 syncLoop 中。

RemoveOrphanedStatuses

RemoveOrphanedStatuses 的主要逻辑是从 statusManager 缓存 podStatuses 中删除对应


的 pod。

k8s.io/kubernetes/pkg/kubelet/status/status_manager.go:457

1. func (m *manager) RemoveOrphanedStatuses(podUIDs map[types.UID]bool) {


2. m.podStatusesLock.Lock()
3. defer m.podStatusesLock.Unlock()
4. for key := range m.podStatuses {
5. if _, ok := podUIDs[key]; !ok {
6. klog.V(5).Infof("Removing %q from status map.", key)
7. delete(m.podStatuses, key)
8. }
9. }
10. }

总结
本文主要介绍了 statusManager 的功能以及使用,其功能其实非常简单,当 pod 状态改变时
statusManager 会将状态同步到 apiserver,statusManager 也提供了多个接口供其他组件调
用,当其他组件需要改变 pod 的状态时会将 pod 的 status 信息发送到 statusManager 进行
同步。

本文档使用 书栈网 · BookStack.CN 构建 - 433 -


kubernetes 中 Qos 的设计与实现

kubernetes 中的 Qos
QoS(Quality of Service) 即服务质量,QoS 是一种控制机制,它提供了针对不同用户或者不同
数据流采用相应不同的优先级,或者是根据应用程序的要求,保证数据流的性能达到一定的水准。
kubernetes 中有三种 Qos,分别为:

1、 Guaranteed :pod 的 requests 与 limits 设定的值相等;


2、 Burstable :pod requests 小于 limits 的值且不为 0;
3、 BestEffort :pod 的 requests 与 limits 均为 0;

三者的优先级如下所示,依次递增:

1. BestEffort -> Burstable -> Guaranteed

不同 Qos 的本质区别

三种 Qos 在调度和底层表现上都不一样:

1、在调度时调度器只会根据 request 值进行调度;


2、二是当系统 OOM上时对于处理不同 OOMScore 的进程表现不同,OOMScore 是针对
memory 的,当宿主上 memory 不足时系统会优先 kill 掉 OOMScore 值低的进程,可以使
用 $ cat /proc/$PID/oom_score 查看进程的 OOMScore。OOMScore 的取值范围为
[-1000, 1000], Guaranteed pod 的默认值为 -998, Burstable pod 的值为
2~999, BestEffort pod 的值为 1000,也就是说当系统 OOM 时,首先会 kill 掉
BestEffort pod 的进程,若系统依然处于 OOM 状态,然后才会 kill 掉 Burstable
pod,最后是 Guaranteed pod;
3、三是 cgroup 的配置不同,kubelet 为会三种 Qos 分别创建对应的 QoS level
cgroups, Guaranteed Pod Qos 的 cgroup level 会直接创建在
RootCgroup/kubepods 下, Burstable Pod Qos 的创建在
RootCgroup/kubepods/burstable 下, BestEffort Pod Qos 的创建在
RootCgroup/kubepods/BestEffort 下,上文已经说了 root cgroup 可以通过 $ mount
| grep cgroup 看到,在 cgroup 的每个子系统下都会创建 Qos level cgroups, 此外在
对应的 QoS level cgroups 还会为 pod 创建 Pod level cgroups;

启用 Qos 和 Pod level cgroup


在 kubernetes 中为了限制容器资源的使用,避免容器之间争抢资源或者容器影响所在的宿主机,
kubelet 组件需要使用 cgroup 限制容器资源的使用量,cgroup 目前支持对进程多种资源的限
制,而 kubelet 只支持限制 cpu、memory、pids、hugetlb 几种资源,与此资源有关的几个参
数如下所示:

本文档使用 书栈网 · BookStack.CN 构建 - 434 -


kubernetes 中 Qos 的设计与实现

--cgroups-per-qos :启用后会为每个 pod 以及 pod 对应的 Qos 创建 cgroups 层级树,默


认启用;

--cgroup-root :指定 root cgroup,如果不指定默认为“”,若为默认值则直接使用 root


cgroup dir,在 node 上执行 $ mount | grep cgroup 可以看到 cgroup 所有子系统的挂载
点,这些挂载点就是 root cgroup;

--cpu-manager-policy :默认为 “none”,即默认不开启 ,支持使用 “static”,开启后可以支


持对 Guaranteed Pod 进行绑核操作,绑核的主要目的是为了高效使用 cpu cache 以及内存节
点;

--kube-reserved :为 kubernetes 系统组件设置预留资源值,可以设置 cpu、memory、


ephemeral-storage;

--kube-reserved-cgroup :指定 kube-reserved 的 cgroup dir name,默认为 “/kube-


reserved”;

--system-reserved :为非 kubernetes 组件设置预留资源值,可以设置 cpu、memory、


ephemeral-storage;

--system-reserved-cgroup :设置 system-reserved 的 cgroup dir name,默认为


“/system-reserved”;

--qos-reserved :Alpha feature,可以通过此参数为高优先级 pod 设置预留资源比例,目前


只支持预留 memory,使用前需要开启 QOSReserved feature gate;

当启用了 --cgroups-per-qos 后,kubelet 会为不同 Qos 创建对应的 level cgroups,在


Qos level cgroups 下也会为 pod 创建对应的 pod level cgroups,在 pod level
cgroups 下最终会为 container 创建对应的 level cgroups,从 Qos —> pod —>
container,层层限制每个 level cgroups 的资源使用量。

配置 cgroup driver

runtime 有两种 cgroup 驱动:一种是 systemd ,另外一种是 cgroupfs :

cgroupfs 比较好理解,比如说要限制内存是多少、要用 CPU share 为多少,其实直接把


pid 写入到对应cgroup task 文件中,然后把对应需要限制的资源也写入相应的 memory
cgroup 文件和 CPU 的 cgroup 文件就可以了;
另外一个是 systemd 的 cgroup 驱动,这个驱动是因为 systemd 本身可以提供一个
cgroup 管理方式。所以如果用 systemd 做 cgroup 驱动的话,所有的写 cgroup 操作都
必须通过 systemd 的接口来完成,不能手动更改 cgroup 的文件;

kubernetes 中默认 kubelet 的 cgroup 驱动就是 cgroupfs ,若要使用 systemd ,则必


须将 kubelet 以及 runtime 都需要配置为 systemd 驱动。

本文档使用 书栈网 · BookStack.CN 构建 - 435 -


kubernetes 中 Qos 的设计与实现

关于 cgroupfs 与 systemd driver 的区别可以参考 k8s 官方文档:container-


runtimes/#cgroup-drivers,或者 runc 中的实现
github.com/opencontainers/runc/libcontainer/cgroups。

kubernetes 中的 cgroup level


kubelet 启动后会在 root cgroup 下面创建一个叫做 kubepods 子 cgroup,kubelet 会
把本机的 allocatable 资源写入到 kubepods 下对应的 cgroup 文件中,比如
kubepods/cpu.share ,而这个 cgroup 下面也会存放节点上面所有 pod 的 cgroup,以此来达
到限制节点上所有 pod 资源的目的。在 kubepods cgroup 下面,kubernetes 会进一步再分
别创建两个 QoS level cgroup,名字分别叫做 burstable 和 besteffort ,这两个 QoS
level 的 cgroup 是作为各自 QoS 级别的所有 Pod 的父 cgroup 来存在的,在为 pod 创建
cgroup 时,首先在对应的 Qos cgroup 下创建 pod level cgroup,然后在 pod level
cgroup 继续创建对应的 container level cgroup,对于 Guaranteed Qos 对应的 pod
会直接在 kubepods 同级的 cgroup 中创建 pod cgroup。

目前 kubernetes 仅支持 cpu、memory、pids 、hugetlb 四个 cgroup 子系统。

当 kubernetes 在收到一个 pod 的资源申请信息后通过 kubelet 为 pod 分配资源,kubelet


基于 pod 申请的资源以及 pod 对应的 QoS 级别来通过 cgroup 机制最终为这个 pod 分配资源
的,针对每一种资源,它会做以下几件事情:

首先判断 pod 属于哪种 Qos,在对应的 Qos level cgroup 下对 pod 中的每一个容器在


cgroup 所有子系统下都创建一个 pod level cgroup 以及 container level cgroup,
并且 pod level cgroup 是 container level cgroup 的父 cgroup,Qos level
cgroup 在 kubelet 初始化时已经创建完成了;
然后根据 pod 的资源信息更新 QoS level cgroup 中的值;
最后会更新 kubepods level cgroup 中的值;

对于每一个 pod 设定的 requests 和 limits,kubernetes 都会转换为 cgroup 中的计算方


式,CPU 的转换方式如下所示:

cpu.shares = (cpu in millicores * 1024) / 1000


cpu.cfs_period_us = 100000 (i.e. 100ms)
cpu.cfs_quota_us = quota = (cpu in millicores * 100000) / 1000
memory.limit_in_bytes

CPU 最终都会转换为以微秒为单位,memory 会转换为以 bytes 为单位。

以下是 kubernetes 中的 cgroup level 的一个示例,此处仅展示 cpu、memory 对应的子


cgroup:

本文档使用 书栈网 · BookStack.CN 构建 - 436 -


kubernetes 中 Qos 的设计与实现

1. .
2. |-- blkio
3. |-- cpu -> cpu,cpuacct
4. |-- cpu,cpuacct
5. | |-- init.scope
6. | |-- kubepods
7. | | |-- besteffort
8. | | |-- burstable
9. | | `-- podd15c4b83-c250-4f1e-94ff-8a4bf31c6f25
10. | |-- system.slice
11. | `-- user.slice
12. |-- cpuacct -> cpu,cpuacct
13. |-- cpuset
14. | |-- kubepods
15. | | |-- besteffort
16. | | |-- burstable
17. | | `-- podd15c4b83-c250-4f1e-94ff-8a4bf31c6f25
18. |-- devices
19. |-- hugetlb
20. |-- memory
21. | |-- init.scope
22. | |-- kubepods
23. | | |-- besteffort
24. | | |-- burstable
25. | | `-- podd15c4b83-c250-4f1e-94ff-8a4bf31c6f25
26. | |-- system.slice
27. | | |-- -.mount
28. | `-- user.slice
29. |-- net_cls -> net_cls,net_prio
30. |-- net_cls,net_prio
31. |-- net_prio -> net_cls,net_prio
32. |-- perf_event
33. |-- pids
34. `-- systemd

本文档使用 书栈网 · BookStack.CN 构建 - 437 -


kubernetes 中 Qos 的设计与实现

例如,当创建资源如下所示的 pod:

1. spec:
2. containers:
3. - name: nginx
4. image: nginx:latest
5. imagePullPolicy: IfNotPresent
6. resources:
7. requests:
8. cpu: 250m
9. memory: 1Gi
10. limits:
11. cpu: 500m
12. memory: 2Gi

首先会根据 pod 的 Qos 该 pod 为 burstable 在其所属 Qos 下创建


ROOT/kubepods/burstable/pod<UID>/container<UID> 两个 cgroup level,然后会更新 pod
的父 cgroup 也就是 burstable/ cgroup 中的值,最后会更新 kubepods cgroup 中的
值,下面会针对每个 cgroup level 一一进行解释。

Container level cgroups

本文档使用 书栈网 · BookStack.CN 构建 - 438 -


kubernetes 中 Qos 的设计与实现

在 Container level cgroups 中,kubelet 会根据上述公式将 pod 中每个 container 的资


源转换为 cgroup 中的值并写入到对应的文件中。

1. /sys/fs/cgroup/cpu/kubepods/burstable/pod<UID>/container<UID>/cpu.shares = 256
/sys/fs/cgroup/cpu/kubepods/burstable/pod<UID>/container<UID>/cpu.cfs_quota_us
2. = 50000
/sys/fs/cgroup/memory/kubepods/burstable/pod<UID>/container<UID>/memory.limit_in_bytes
3. = 104857600

Pod level cgroups

在创建完 container level 的 cgroup 之后,kubelet 会为同属于某个 pod 的 containers


创建一个 pod level cgroup。为何要引入 pod level cgroup,主要是基于以下几点原因:

方便对 pod 内的容器资源进行统一的限制;


方便对 pod 使用的资源进行统一统计;

对于不同 Pod level cgroups 的设置方法如下所示:

Guaranteed Pod QoS

1. pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
2. pod<UID>/cpu.cfs_period_us = 100000
3. pod<UID>/cpu.cfs_quota_us = sum(pod.spec.containers.resources.limits[cpu])
pod<UID>/memory.limit_in_bytes =
4. sum(pod.spec.containers.resources.limits[memory])

Burstable Pod QoS

1. pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
2. pod<UID>/cpu.cfs_period_us = 100000
3. pod<UID>/cpu.cfs_quota_us = sum(pod.spec.containers.resources.limits[cpu])
pod<UID>/memory.limit_in_bytes =
4. sum(pod.spec.containers.resources.limits[memory])

BestEffort Pod QoS

1. pod<UID>/cpu.shares = 2
2. pod<UID>/cpu.cfs_quota_us = -1

cpu.shares 指定了 cpu 可以使用的下限,cpu 的上限通过使用 cpu.cfs_period_us +


cpu.cfs_quota_us 两个参数做动态绝对配额,两个参数的意义如下所示:

本文档使用 书栈网 · BookStack.CN 构建 - 439 -


kubernetes 中 Qos 的设计与实现

cpu.cfs_period_us:指 cpu 使用时间的周期统计;


cpu.cfs_quota_us:指周期内允许占用的 cpu 时间(指单核的时间, 多核则需要在设置时累
加) ;

container runtime 中 cpu.cfs_period_us 的值默认为 100000。若 kubelet 启用了 -


-cpu-manager-policy=static 时,对于 Guaranteed Qos,如果它的 request 是一个整数
的话,cgroup 会同时设置 cpuset.cpus 和 cpuset.mems 两个参数以此来对它进行绑核。

如果 pod 指定了 requests 和 limits,kubelet 会按以上的计算方式为 pod 设置资源限制,


如果没有指定 limit 的话,那么 cpu.cfs_quota_us 将会被设置为 -1,即没有限制。而如果
limit 和 request 都没有指定的话, cpu.shares 将会被指定为 2,这个是 cpu.shares
允许指定的最小数值了,可见针对这种 pod,kubernetes 只会给它分配最少的 cpu 资源。而对于
内存来说,如果没有 limit 的指定的话, memory.limit_in_bytes 将会被指定为一个非常大的
值,一般是 2^64 ,可见含义就是不对内存做出限制。

针对上面的例子,其 pod level cgroups 中的配置如下所示:

1. pod<UID>/cpu.shares = 102
2. pod<UID>/cpu.cfs_quota_us = 20000

QoS level cgroups

上文已经提到了 kubelet 会首先创建 kubepods cgroup,然后会在 kubepods cgroup 下面再


分别创建 burstable 和 besteffort 两个 QoS level cgroup,那么这两个 QoS level
cgroup 存在的目的是什么?为什么不为 guaranteed Qos 创建 cgroup level?

首先看一下三种 QoS level cgroups 的设置方法,对于 guaranteed Qos 因其直接使用 root


cgroup,此处只看另外两种的计算方式:

Burstable cgroup :

1. ROOT/burstable/cpu.shares = max(sum(Burstable pods cpu requests), 2)


2. ROOT/burstable/memory.limit_in_bytes =
Node.Allocatable - {(summation of memory requests of `Guaranteed` pods)*
3. (reservePercent / 100)}

BestEffort cgroup :

1. ROOT/besteffort/cpu.shares = 2
2. ROOT/besteffort/memory.limit_in_bytes =
Node.Allocatable - {(summation of memory requests of all `Guaranteed` and
3. `Burstable` pods)*(reservePercent / 100)}

本文档使用 书栈网 · BookStack.CN 构建 - 440 -


kubernetes 中 Qos 的设计与实现

首先第一个问题,所有 guaranteed 级别的 pod 的 cgroup 直接位于 kubepods 这个 cgroup


之下,和 burstable、besteffort QoS level cgroup 同级,主要原因在于 guaranteed 级
别的 pod 有明确的资源申请量(request)和资源限制量(limit),所以并不需要一个统一的 QoS
level 的 cgroup 进行管理或限制。

针对 burstable 和 besteffort 这两种类型的 pod,在默认情况下,kubernetes 则是希望能


尽可能地提升资源利用率,所以并不会对这两种 QoS 的 pod 的资源使用做限制。但在某些场景下我
们还是希望能够尽可能保证 guaranteed level pod 这种高 QoS 级别 pod 的资源,尤其是不可
压缩资源(如内存),不要被低 QoS 级别的 pod 抢占,导致高 QoS 级别的 pod 连它 request
的资源量的资源都无法得到满足,此时就可以使用 --qos-reserved 为高 Qos pod 进行预留资
源,举个例子,当前机器的 allocatable 内存资源量为 8G,当为这台机器的 kubelet 开启 -
-qos-reserved 参数后,并且设置为 memory=100%,如果此时创建了一个内存 request 为 1G
的 guaranteed level 的 pod,那么需要预留的资源就是 1G,此时这台机器上面的 burstable
QoS level cgroup 的 memory.limit_in_bytes 的值将会被设置为 7G,besteffort QoS
level cgroup 的 memory.limit_in_bytes 的值也会被设置为 7G。而如果此时又创建了一个
burstable level 的 pod,它的内存申请量为 2G,那么此时需要预留的资源为 3G,而
besteffort QoS level cgroup 的 memory.limit_in_bytes 的值也会被调整为 5G。

由上面的公式也可以看到,burstable 的 cgroup 需要为比他等级高的 guaranteed 级别的 pod


的内存资源做预留,而 besteffort 需要为 burstable 和 guaranteed 都要预留内存资源。

小结

kubelet 启动时首先会创建 root cgroups 以及为 Qos 创建对应的 level cgroups,然后当


pod 调度到节点上时,kubelet 也会为 pod 以及 pod 下的 container 创建对应的 level
cgroups。root cgroups 限制节点上所有 pod 的资源使用量,Qos level cgroups 限制不同
Qos 下 pod 的资源使用量,Pod level cgroups 限制一个 pod 下的资源使用量,Container
level cgroups 限制 pod 下 container 的资源使用量。

节点上 cgroup 层级树如下所示:

1. $ROOT
2. |
3. +- Pod1
4. | |
5. | +- Container1
6. | +- Container2
7. | ...
8. +- Pod2
9. | +- Container3
10. | ...
11. +- ...

本文档使用 书栈网 · BookStack.CN 构建 - 441 -


kubernetes 中 Qos 的设计与实现

12. |
13. +- burstable
14. | |
15. | +- Pod3
16. | | |
17. | | +- Container4
18. | | ...
19. | +- Pod4
20. | | +- Container5
21. | | ...
22. | +- ...
23. |
24. +- besteffort
25. | |
26. | +- Pod5
27. | | |
28. | | +- Container6
29. | | +- Container7
30. | | ...
31. | +- ...

QOSContainerManager 源码分析

kubernetes 版本:v1.16

qos 的具体实现是在 kubelet 中的 QOSContainerManager , QOSContainerManager 被包


含在 containerManager 模块中,kubelet 的 containerManager 模块中包含多个模块还
有, cgroupManager 、 containerManager 、 nodeContainerManager 、 podContainerMana
ger 、 topologyManager 、 deviceManager 、 cpuManager 等。

qosContainerManager 的初始化

首先看 QOSContainerManager 的初始化,因为 QOSContainerManager 包含在


containerManager 中,在初始化 containerManager 时也会初始化
QOSContainerManager 。

k8s.io/kubernetes/cmd/kubelet/app/server.go:471

func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-


1. chan struct{}) (err error) {
2. ......
3. kubeDeps.ContainerManager, err = cm.NewContainerManager(......)
4. ......

本文档使用 书栈网 · BookStack.CN 构建 - 442 -


kubernetes 中 Qos 的设计与实现

5. }

k8s.io/kubernetes/pkg/kubelet/cm/container_manager_linux.go:200

1. // 在 NewContainerManager 中会初始化 qosContainerManager


2. func NewContainerManager(......) (ContainerManager, error) {
3. ......
qosContainerManager, err := NewQOSContainerManager(subsystems, cgroupRoot,
4. nodeConfig, cgroupManager)
5. if err != nil {
6. return nil, err
7. }
8. ......
9. }

qosContainerManager 的启动

在调用 kl.containerManager.Start 启动 containerManager 时也会启动


qosContainerManager ,代码如下所示:

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1361

1. func (kl *Kubelet) initializeRuntimeDependentModules() {


2. ......
if err := kl.containerManager.Start(node, kl.GetActivePods,
3. kl.sourcesReady, kl.statusManager, kl.runtimeService); err != nil {
4. klog.Fatalf("Failed to start ContainerManager %v", err)
5. }
6. ......
7. }

cm.setupNode

cm.setupNode 是启动 qosContainerManager 的方法,其主要逻辑为:

1、检查 kubelet 依赖的内核参数是否配置正确;


2、若 CgroupsPerQOS 为 true,首先调用 cm.createNodeAllocatableCgroups 创建
root cgroup,然后调用 cm.qosContainerManager.Start 启动
qosContainerManager ;
3、调用 cm.enforceNodeAllocatableCgroups 计算 node 的 allocatable 资源并配置
到 root cgroup 中,然后判断是否启用了 SystemReserved 以及 KubeReserved 并配
置对应的 cgroup;
4、为系统组件配置对应的 cgroup 资源限制;

本文档使用 书栈网 · BookStack.CN 构建 - 443 -


kubernetes 中 Qos 的设计与实现

5、为系统进程配置 oom_score_adj;

k8s.io/kubernetes/pkg/kubelet/cm/container_manager_linux.go:568

1. func (cm *containerManagerImpl) Start(......) {


2. ......
3. if err := cm.setupNode(activePods); err != nil {
4. return err
5. }
6. }
7.
8. // 在 setupNode 中会启动 qosContainerManager
9. func (cm *containerManagerImpl) setupNode(activePods ActivePodsFunc) error {
10. f, err := validateSystemRequirements(cm.mountUtil)
11. if err != nil {
12. return err
13. }
14. if !f.cpuHardcapping {
15. cm.status.SoftRequirements = fmt.Errorf("CPU hardcapping unsupported")
16. }
17. b := KernelTunableModify
18. if cm.GetNodeConfig().ProtectKernelDefaults {
19. b = KernelTunableError
20. }
21. // 1、检查依赖的内核参数是否配置正确
22. if err := setupKernelTunables(b); err != nil {
23. return err
24. }
25.
26. if cm.NodeConfig.CgroupsPerQOS {
27. // 2、创建 root cgroup,即 kubepods dir
28. if err := cm.createNodeAllocatableCgroups(); err != nil {
29. return err
30. }
31. // 3、启动 qosContainerManager
err = cm.qosContainerManager.Start(cm.getNodeAllocatableAbsolute,
32. activePods)
33. if err != nil {
return fmt.Errorf("failed to initialize top level QOS containers:
34. %v", err)
35. }
36. }
37.

本文档使用 书栈网 · BookStack.CN 构建 - 444 -


kubernetes 中 Qos 的设计与实现

38. // 4、为 node 配置 cgroup 资源限制


39. if err := cm.enforceNodeAllocatableCgroups(); err != nil {
40. return err
41. }
42. if cm.ContainerRuntime == "docker" {
43. cm.periodicTasks = append(cm.periodicTasks, func() {
cont, err := getContainerNameForProcess(dockerProcessName,
44. dockerPidFile)
45. if err != nil {
46. klog.Error(err)
47. return
48. }
49. cm.Lock()
50. defer cm.Unlock()
51. cm.RuntimeCgroupsName = cont
52. })
53. }
54.
55. // 5、为系统组件配置对应的 cgroup 资源限制
56. if cm.SystemCgroupsName != "" {
57. if cm.SystemCgroupsName == "/" {
58. return fmt.Errorf("system container cannot be root (\"/\")")
59. }
60. cont := newSystemCgroups(cm.SystemCgroupsName)
61. cont.ensureStateFunc = func(manager *fs.Manager) error {
62. return ensureSystemCgroups("/", manager)
63. }
64. systemContainers = append(systemContainers, cont)
65. }
66.
67. // 6、为系统进程配置 oom_score_adj
68. if cm.KubeletCgroupsName != "" {
69. cont := newSystemCgroups(cm.KubeletCgroupsName)
70. allowAllDevices := true
71. manager := fs.Manager{
72. Cgroups: &configs.Cgroup{
73. Parent: "/",
74. Name: cm.KubeletCgroupsName,
75. Resources: &configs.Resources{
76. AllowAllDevices: &allowAllDevices,
77. },
78. },

本文档使用 书栈网 · BookStack.CN 构建 - 445 -


kubernetes 中 Qos 的设计与实现

79. }
80. cont.ensureStateFunc = func(_ *fs.Manager) error {
return ensureProcessInContainerWithOOMScore(os.Getpid(),
81. qos.KubeletOOMScoreAdj, &manager)
82. }
83. systemContainers = append(systemContainers, cont)
84. } else {
85. cm.periodicTasks = append(cm.periodicTasks, func() {
if err := ensureProcessInContainerWithOOMScore(os.Getpid(),
86. qos.KubeletOOMScoreAdj, nil); err != nil {
87. klog.Error(err)
88. return
89. }
90. cont, err := getContainer(os.Getpid())
91. if err != nil {
92. klog.Errorf("failed to find cgroups of kubelet - %v", err)
93. return
94. }
95. cm.Lock()
96. defer cm.Unlock()
97.
98. cm.KubeletCgroupsName = cont
99. })
100. }
101. cm.systemContainers = systemContainers
102. return nil
103. }

cm.qosContainerManager.Start

cm.qosContainerManager.Start 主要逻辑为:

1、检查 root cgroup 是否存在,root cgroup 会在启动 qosContainerManager 之前


创建;
2、为 Burstable 和 BestEffort 创建 Qos level cgroups 并设置默认值;
3、调用 m.UpdateCgroups 每分钟定期更新 cgroup 信息;

k8s.io/kubernetes/pkg/kubelet/cm/qos_container_manager_linux.go:80

func (m *qosContainerManagerImpl) Start(getNodeAllocatable func()


1. v1.ResourceList, activePods ActivePodsFunc) error {
2. cm := m.cgroupManager
3. rootContainer := m.cgroupRoot

本文档使用 书栈网 · BookStack.CN 构建 - 446 -


kubernetes 中 Qos 的设计与实现

4.
5. // 1、检查 root cgroup 是否存在
6. if !cm.Exists(rootContainer) {
7. return fmt.Errorf("root container %v doesn't exist", rootContainer)
8. }
9.
10. // 2、为 Qos 配置 Top level cgroups
11. qosClasses := map[v1.PodQOSClass]CgroupName{
v1.PodQOSBurstable: NewCgroupName(rootContainer,
12. strings.ToLower(string(v1.PodQOSBurstable))),
v1.PodQOSBestEffort: NewCgroupName(rootContainer,
13. strings.ToLower(string(v1.PodQOSBestEffort))),
14. }
15.
16. // 3、为 Qos 创建 top level cgroups
17. for qosClass, containerName := range qosClasses {
18. resourceParameters := &ResourceConfig{}
19. // 4、为 BestEffort QoS cpu.shares 设置默认值,默认为 2
20. if qosClass == v1.PodQOSBestEffort {
21. minShares := uint64(MinShares)
22. resourceParameters.CpuShares = &minShares
23. }
24.
25. containerConfig := &CgroupConfig{
26. Name: containerName,
27. ResourceParameters: resourceParameters,
28. }
29.
30. // 5、配置 huge page size
31. m.setHugePagesUnbounded(containerConfig)
32.
33. // 6、为 Qos 创建 cgroup 目录
34. if !cm.Exists(containerName) {
35. if err := cm.Create(containerConfig); err != nil {
36. ......
37. }
38. } else {
39. if err := cm.Update(containerConfig); err != nil {
40. ......
41. }
42. }
43. }
44. ......

本文档使用 书栈网 · BookStack.CN 构建 - 447 -


kubernetes 中 Qos 的设计与实现

45.
46. // 7、每分钟定期更新 cgroup 配置
47. go wait.Until(func() {
48. err := m.UpdateCgroups()
49. if err != nil {
klog.Warningf("[ContainerManager] Failed to reserve QoS requests:
50. %v", err)
51. }
52. }, periodicQOSCgroupUpdateInterval, wait.NeverStop)
53.
54. return nil
55. }

m.UpdateCgroups

m.UpdateCgroups 是用来更新 Qos level cgroup 中的值,其主要逻辑为:

1、调用 m.setCPUCgroupConfig 计算 node 上的 activePods 的资源以此来更新


bestEffort 和 burstable Qos level cgroup 的 cpu.shares 值, besteffort
的 cpu.shares 值默认为 2, burstable cpu.shares 的计算方式为:
max(sum(Burstable pods cpu requests)* 1024 /1000, 2);
2、调用 m.setHugePagesConfig 更新 huge pages;
3、检查是否启用了 --qos-reserved 参数,若启用了则调用 m.setMemoryReserve 计算每
个 Qos class 中需要设定的值然后调用 m.cgroupManager.Update 更新 cgroup 中的
值;
4、最后调用 m.cgroupManager.Update 更新 cgroup 中的值;

k8s.io/kubernetes/pkg/kubelet/cm/qos_container_manager_linux.go:269

1. func (m *qosContainerManagerImpl) UpdateCgroups() error {


2. m.Lock()
3. defer m.Unlock()
4.
5. qosConfigs := map[v1.PodQOSClass]*CgroupConfig{
6. v1.PodQOSBurstable: {
7. Name: m.qosContainersInfo.Burstable,
8. ResourceParameters: &ResourceConfig{},
9. },
10. v1.PodQOSBestEffort: {
11. Name: m.qosContainersInfo.BestEffort,
12. ResourceParameters: &ResourceConfig{},
13. },
14. }

本文档使用 书栈网 · BookStack.CN 构建 - 448 -


kubernetes 中 Qos 的设计与实现

15.
16. // 1、更新 bestEffort 和 burstable Qos level cgroup 的 cpu.shares 值
17. if err := m.setCPUCgroupConfig(qosConfigs); err != nil {
18. return err
19. }
20.
21. // 2、调用 m.setHugePagesConfig 更新 huge pages
22. if err := m.setHugePagesConfig(qosConfigs); err != nil {
23. return err
24. }
25.
26. // 3、设置资源预留
27. if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.QOSReserved) {
28. for resource, percentReserve := range m.qosReserved {
29. switch resource {
30. case v1.ResourceMemory:
31. m.setMemoryReserve(qosConfigs, percentReserve)
32. }
33. }
34.
35. updateSuccess := true
36. for _, config := range qosConfigs {
37. err := m.cgroupManager.Update(config)
38. if err != nil {
39. updateSuccess = false
40. }
41. }
42. if updateSuccess {
klog.V(4).Infof("[ContainerManager]: Updated QoS cgroup
43. configuration")
44. return nil
45. }
46.
47. for resource, percentReserve := range m.qosReserved {
48. switch resource {
49. case v1.ResourceMemory:
50. m.retrySetMemoryReserve(qosConfigs, percentReserve)
51. }
52. }
53. }
54.
55. // 4、更新 cgroup 中的值

本文档使用 书栈网 · BookStack.CN 构建 - 449 -


kubernetes 中 Qos 的设计与实现

56. for _, config := range qosConfigs {


57. err := m.cgroupManager.Update(config)
58. if err != nil {
59. return err
60. }
61. }
62.
63. return nil
64. }

m.cgroupManager.Update

m.cgroupManager.Update 方法主要是根据 cgroup 配置来更新 cgroup 中的值,其主要逻辑


为:

1、调用 m.buildCgroupPaths 创建对应的 cgroup 目录,在每个 cgroup 子系统下面都


有一个 kubelet 对应的 root cgroup 目录;
2、调用 setSupportedSubsystems 更新的 cgroup 子系统中的值;

k8s.io/kubernetes/pkg/kubelet/cm/cgroup_manager_linux.go:409

1. func (m *cgroupManagerImpl) Update(cgroupConfig *CgroupConfig) error {


2. ......
3. resourceConfig := cgroupConfig.ResourceParameters
4. resources := m.toResources(resourceConfig)
5.
6. cgroupPaths := m.buildCgroupPaths(cgroupConfig.Name)
7.
8. libcontainerCgroupConfig := &libcontainerconfigs.Cgroup{
9. Resources: resources,
10. Paths: cgroupPaths,
11. }
12.
13. if m.adapter.cgroupManagerType == libcontainerSystemd {
14. updateSystemdCgroupInfo(libcontainerCgroupConfig, cgroupConfig.Name)
15. } else {
16. libcontainerCgroupConfig.Path = cgroupConfig.Name.ToCgroupfs()
17. }
18.
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit)
&& cgroupConfig.ResourceParameters != nil && cgroupConfig.
19. ResourceParameters.PidsLimit != nil {

本文档使用 书栈网 · BookStack.CN 构建 - 450 -


kubernetes 中 Qos 的设计与实现

libcontainerCgroupConfig.PidsLimit =
20. *cgroupConfig.ResourceParameters.PidsLimit
21. }
22.
23. if err := setSupportedSubsystems(libcontainerCgroupConfig); err != nil {
return fmt.Errorf("failed to set supported cgroup subsystems for cgroup
24. %v: %v", cgroupConfig.Name, err)
25. }
26. return nil
27. }

setSupportedSubsystem

setSupportedSubsystems 首先通过 getSupportedSubsystems 获取 kubelet 支持哪些


cgroup 子系统,然后调用 sys.Set 设置对应子系统的值, sys.Set 是调用
runc/libcontainer 中的包进行设置的,其主要逻辑是在 cgroup 子系统对应的文件中写入值。

k8s.io/kubernetes/pkg/kubelet/cm/cgroup_manager_linux.go:345

1. func setSupportedSubsystems(cgroupConfig *libcontainerconfigs.Cgroup) error {


2. for sys, required := range getSupportedSubsystems() {
3. if _, ok := cgroupConfig.Paths[sys.Name()]; !ok {
4. if required {
return fmt.Errorf("failed to find subsystem mount for required
5. subsystem: %v", sys.Name())
6. }
7. ......
8. continue
9. }
if err := sys.Set(cgroupConfig.Paths[sys.Name()], cgroupConfig); err !=
10. nil {
return fmt.Errorf("failed to set config for supported subsystems :
11. %v", err)
12. }
13. }
14. return nil
15. }

例如为 cgroup 中 cpu 子系统设置值的方法如下所示:

1. func (s *CpuGroup) Set(path string, cgroup *configs.Cgroup) error {


2. if cgroup.Resources.CpuShares != 0 {

本文档使用 书栈网 · BookStack.CN 构建 - 451 -


kubernetes 中 Qos 的设计与实现

if err := writeFile(path, "cpu.shares",


3. strconv.FormatUint(cgroup.Resources.CpuShares, 10)); err != nil {
4. return err
5. }
6. }
7. if cgroup.Resources.CpuPeriod != 0 {
if err := writeFile(path, "cpu.cfs_period_us",
8. strconv.FormatUint(cgroup.Resources.CpuPeriod, 10)); err != nil {
9. return err
10. }
11. }
12. if cgroup.Resources.CpuQuota != 0 {
if err := writeFile(path, "cpu.cfs_quota_us",
13. strconv.FormatInt(cgroup.Resources.CpuQuota, 10)); err != nil {
14. return err
15. }
16. }
17. return s.SetRtSched(path, cgroup)
18. }

Pod Level Cgroup

Pod Level cgroup 是 kubelet 在创建 pod 时创建的,创建 pod 是在 kubelet 的


syncPod 方法中进行的,在 syncPod 方法中首先会调用
kl.containerManager.UpdateQOSCgroups 更新 Qos Level cgroup,然后调用
pcm.EnsureExists 创建 pod level cgroup。

1. func (kl *Kubelet) syncPod(o syncPodOptions) error {


2. ......
3. if !kl.podIsTerminated(pod) {
4. ......
if !(podKilled && pod.Spec.RestartPolicy == v1.RestartPolicyNever)
5. {
6. if !pcm.Exists(pod) {
if err := kl.containerManager.UpdateQOSCgroups(); err !=
7. nil {
8. ......
9. }
10. if err := pcm.EnsureExists(pod); err != nil {
11. ......
12. }
13. }
14. }

本文档使用 书栈网 · BookStack.CN 构建 - 452 -


kubernetes 中 Qos 的设计与实现

15. }
16. ......
17. }

EnsureExists 的主要逻辑是检查 pod 的 cgroup 是否存在,若不存在则调用


m.cgroupManager.Create 进行创建。

k8s.io/kubernetes/pkg/kubelet/cm/pod_container_manager_linux.go:79

1. func (m *podContainerManagerImpl) EnsureExists(pod *v1.Pod) error {


2. podContainerName, _ := m.GetPodContainerName(pod)
3.
4. alreadyExists := m.Exists(pod)
5. if !alreadyExists {
6. containerConfig := &CgroupConfig{
7. Name: podContainerName,
ResourceParameters: ResourceConfigForPod(pod, m.enforceCPULimits,
8. m.cpuCFSQuotaPeriod),
9. }
if
utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) &&
10. m.podPidsLimit > 0 {
11. containerConfig.ResourceParameters.PidsLimit = &m.podPidsLimit
12. }
13. if err := m.cgroupManager.Create(containerConfig); err != nil {
return fmt.Errorf("failed to create container for %v : %v",
14. podContainerName, err)
15. }
16. }
17. ......
18. return nil
19. }

Container Level Cgroup

Container Level Cgroup 是通过 runtime 进行创建的,若使用 runc 其会调用 runc 的


InitProcess.start 方法对 cgroup 资源组进行配置与应用。

k8s.io/kubernetes/vendor/github.com/opencontainers/runc/libcontainer/process_linux.g
o:282

1. func (p *initProcess) start() error {


2. ......

本文档使用 书栈网 · BookStack.CN 构建 - 453 -


kubernetes 中 Qos 的设计与实现

3.
4. // 调用 p.manager.Apply 为进程配置 cgroup
5. if err := p.manager.Apply(p.pid()); err != nil {
return newSystemErrorWithCause(err, "applying cgroup configuration for
6. process")
7. }
8. if p.intelRdtManager != nil {
9. if err := p.intelRdtManager.Apply(p.pid()); err != nil {
return newSystemErrorWithCause(err, "applying Intel RDT
10. configuration for process")
11. }
12. }
13. ......
14. }

总结
kubernetes 中有三种 Qos,分别为 Guaranteed、Burstable、BestEffort,三种 Qos 以
node 上 allocatable 资源量为基于为 pod 进行分配,并通过多个 level cgroup 进行层层限
制,对 cgroup 的配置都是通过调用 runc/libcontainer/cgroups/fs 中的方法进行资源更新
的。对于 Qos level cgroup,kubelet 会根据以下事件动态更新:

1、kubelet 服务启动时;
2、在创建 pod level cgroup 之前,即创建 pod 前;
3、在删除 pod level cgroup 后;
4、定期检测是否需要为 qos level cgroup 预留资源;

参考:

https://kubernetes.io/docs/setup/production-environment/container-
runtimes/#cgroup-drivers

https://zhuanlan.zhihu.com/p/38359775

https://github.com/kubernetes/community/blob/master/contributors/design-
proposals/node/pod-resource-management.md

https://github.com/cri-o/cri-o/issues/842

https://yq.aliyun.com/articles/737784?spm=a2c4e.11153940.0.0.577f6149mYFkTR

本文档使用 书栈网 · BookStack.CN 构建 - 454 -


kubelet 中垃圾回收机制的设计与实现

kubernetes 中的垃圾回收机制主要有两部分组成:

一是由 kube-controller-manager 中的 gc controller 自动回收 kubernetes 中被


删除的对象以及其依赖的对象;
二是在每个节点上需要回收已退出的容器以及当 node 上磁盘资源不足时回收已不再使用的容器
镜像;

本文主要分析 kubelet 中的垃圾回收机制,垃圾回收的主要目的是为了节约宿主上的资源,gc


controller 的回收机制可以参考以前的文章 garbage collector controller 源码分析。

kubelet 中与容器垃圾回收有关的主要有以下三个参数:

--maximum-dead-containers-per-container : 表示一个 pod 最多可以保存多少个已经停止


的容器,默认为1;(maxPerPodContainerCount)
--maximum-dead-containers :一个 node 上最多可以保留多少个已经停止的容器,默认为
-1,表示没有限制;
--minimum-container-ttl-duration :已经退出的容器可以存活的最小时间,默认为 0s;

与镜像回收有关的主要有以下三个参数:

--image-gc-high-threshold :当 kubelet 磁盘达到多少时,kubelet 开始回收镜像,默


认为 85% 开始回收,根目录以及数据盘;
--image-gc-low-threshold :回收镜像时当磁盘使用率减少至多少时停止回收,默认为
80%;
--minimum-image-ttl-duration :未使用的镜像在被回收前的最小存留时间,默认为 2m0s;

kubelet 中容器回收过程如下: pod 中的容器退出时间超过 --minimum-container-ttl-


duration 后会被标记为可回收,一个 pod 中最多可以保留 --maximum-dead-containers-per-
container 个已经停止的容器,一个 node 上最多可以保留 --maximum-dead-containers 个已停
止的容器。在回收容器时,kubelet 会按照容器的退出时间排序,最先回收退出时间最久的容器。需
要注意的是,kubelet 在回收时会将 pod 中的 container 与 sandboxes 分别进行回收,且在
回收容器后会将其对应的 log dir 也进行回收;

kubelet 中镜像回收过程如下: 当容器镜像挂载点文件系统的磁盘使用率大于 --image-gc-high-


threshold 时(containerRuntime 为 docker 时,镜像存放目录默认为
/var/lib/docker ),kubelet 开始删除节点中未使用的容器镜像,直到磁盘使用率降低至 --
image-gc-low-threshold 时停止镜像的垃圾回收。

kubelet GarbageCollect 源码分析

kubernetes 版本:v1.16

GarbageCollect 是在 kubelet 对象初始化完成后启动的,在 createAndInitKubelet 方法

本文档使用 书栈网 · BookStack.CN 构建 - 455 -


kubelet 中垃圾回收机制的设计与实现

中首先调用 kubelet.NewMainKubelet 初始化了 kubelet 对象,随后调用


k.StartGarbageCollection 启动了 GarbageCollect。

k8s.io/kubernetes/cmd/kubelet/app/server.go:1089

1. func createAndInitKubelet(......) {
2. k, err = kubelet.NewMainKubelet(
3. ......
4. )
5. if err != nil {
6. return nil, err
7. }
8.
9. k.BirthCry()
10.
11. k.StartGarbageCollection()
12.
13. return k, nil
14. }

k.StartGarbageCollection

在 kubelet 中镜像的生命周期和容器的生命周期是通过 imageManager 和 containerGC 管理


的。在 StartGarbageCollection 方法中会启动容器和镜像垃圾回收两个任务,其主要逻辑为:

1、启动 containerGC goroutine,ContainerGC 间隔时间默认为 1 分钟;


2、检查 --image-gc-high-threshold 参数的值,若为 100 则禁用 imageGC;
3、启动 imageGC goroutine,imageGC 间隔时间默认为 5 分钟;

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1270

1. func (kl *Kubelet) StartGarbageCollection() {


2. loggedContainerGCFailure := false
3.
4. // 1、启动容器垃圾回收服务
5. go wait.Until(func() {
6. if err := kl.containerGC.GarbageCollect(); err != nil {
7. loggedContainerGCFailure = true
8. } else {
9. var vLevel klog.Level = 4
10. if loggedContainerGCFailure {
11. vLevel = 1
12. loggedContainerGCFailure = false

本文档使用 书栈网 · BookStack.CN 构建 - 456 -


kubelet 中垃圾回收机制的设计与实现

13. }
14.
15. klog.V(vLevel).Infof("Container garbage collection succeeded")
16. }
17. }, ContainerGCPeriod, wait.NeverStop)
18.
19. // 2、检查 ImageGCHighThresholdPercent 参数的值
20. if kl.kubeletConfiguration.ImageGCHighThresholdPercent == 100 {
21. return
22. }
23.
24. // 3、启动镜像垃圾回收服务
25. prevImageGCFailed := false
26. go wait.Until(func() {
27. if err := kl.imageManager.GarbageCollect(); err != nil {
28. ......
29. prevImageGCFailed = true
30. } else {
31. var vLevel klog.Level = 4
32. if prevImageGCFailed {
33. vLevel = 1
34. prevImageGCFailed = false
35. }
36. }
37. }, ImageGCPeriod, wait.NeverStop)
38. }

kl.containerGC.GarbageCollect

kl.containerGC.GarbageCollect 调用的是 ContainerGC manager 中的方法,


ContainerGC 是在 NewMainKubelet 中初始化的,ContainerGC 在初始化时需要指定一个
runtime,该 runtime 即 ContainerRuntime,在 kubelet 中即
kubeGenericRuntimeManager,也是在 NewMainKubelet 中初始化的。

k8s.io/kubernetes/pkg/kubelet/kubelet.go

1. func NewMainKubelet(){
2. ......
// MinAge、MaxPerPodContainer、MaxContainers 分别上文章开头提到的与容器垃圾回收有
3. 关的
4. // 三个参数
5. containerGCPolicy := kubecontainer.ContainerGCPolicy{
6. MinAge: minimumGCAge.Duration,

本文档使用 书栈网 · BookStack.CN 构建 - 457 -


kubelet 中垃圾回收机制的设计与实现

7. MaxPerPodContainer: int(maxPerPodContainerCount),
8. MaxContainers: int(maxContainerCount),
9. }
10.
11. // 初始化 containerGC 模块
containerGC, err := kubecontainer.NewContainerGC(klet.containerRuntime,
12. containerGCPolicy, klet.sourcesReady)
13. if err != nil {
14. return nil, err
15. }
16. ......
17. }

以下是 ContainerGC 的初始化以及 GarbageCollect 的启动:

k8s.io/kubernetes/pkg/kubelet/container/container_gc.go:68

func NewContainerGC(runtime Runtime, policy ContainerGCPolicy,


1. sourcesReadyProvider SourcesReadyProvider) (ContainerGC, error) {
2. if policy.MinAge < 0 {
return nil, fmt.Errorf("invalid minimum garbage collection age: %v",
3. policy.MinAge)
4. }
5.
6. return &realContainerGC{
7. runtime: runtime,
8. policy: policy,
9. sourcesReadyProvider: sourcesReadyProvider,
10. }, nil
11. }
12.
13. func (cgc *realContainerGC) GarbageCollect() error {
return cgc.runtime.GarbageCollect(cgc.policy,
14. cgc.sourcesReadyProvider.AllReady(), false)
15. }

可以看到,ContainerGC 中的 GarbageCollect 最终是调用 runtime 中的 GarbageCollect


方法,runtime 即 kubeGenericRuntimeManager。

cgc.runtime.GarbageCollect

cgc.runtime.GarbageCollect 的实现是在 kubeGenericRuntimeManager 中,其主要逻辑


为:

本文档使用 书栈网 · BookStack.CN 构建 - 458 -


kubelet 中垃圾回收机制的设计与实现

1、回收 pod 中的 container;


2、回收 pod 中的 sandboxes;
3、回收 pod 以及 container 的 log dir;

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:378

func (cgc *containerGC) GarbageCollect(gcPolicy


kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictTerminatedPods
1. bool) error {
2. errors := []error{}
3. // 1、回收 pod 中的 container
if err := cgc.evictContainers(gcPolicy, allSourcesReady,
4. evictTerminatedPods); err != nil {
5. errors = append(errors, err)
6. }
7.
8. // 2、回收 pod 中的 sandboxes
9. if err := cgc.evictSandboxes(evictTerminatedPods); err != nil {
10. errors = append(errors, err)
11. }
12.
13. // 3、回收 pod 以及 container 的 log dir
14. if err := cgc.evictPodLogsDirectories(allSourcesReady); err != nil {
15. errors = append(errors, err)
16. }
17. return utilerrors.NewAggregate(errors)
18. }

cgc.evictContainers

在 cgc.evictContainers 方法中会回收所有可被回收的容器,其主要逻辑为:

1、首先调用 cgc.evictableContainers 获取可被回收的容器作为 evictUnits,可被回收


的容器指非 running 状态且创建时间超过 MinAge,evictUnits 数组中包含 pod 与
container 的对应关系;
2、回收 deleted 状态以及 terminated 状态的 pod,遍历 evictUnits,若 pod 是否
处于 deleted 或者 terminated 状态,则调用 cgc.removeOldestN 回收 pod 中的所有
容器。deleted 状态指 pod 已经被删除或者其 status.phase 为 failed 且其
status.reason 为 evicted 或者 pod.deletionTimestamp != nil 且 pod 中所有
容器的 status 为 terminated 或者 waiting 状态,terminated 状态指 pod 处于
Failed 或者 succeeded 状态;
3、对于非 deleted 或者 terminated 状态的 pod,调用
cgc.enforceMaxContainersPerEvictUnit 为其保留 MaxPerPodContainer 个已经退出的

本文档使用 书栈网 · BookStack.CN 构建 - 459 -


kubelet 中垃圾回收机制的设计与实现

容器,按照容器退出的时间进行排序优先删除退出时间最久的, MaxPerPodContainer 在上文


已经提过,表示一个 pod 最多可以保存多少个已经停止的容器,默认为1,可以使用 --
maximum-dead-containers-per-container 在启动时指定;
4、若 kubelet 启动时指定了 --maximum-dead-containers (默认为 -1 即不限制),即需
要为 node 保留退出的容器数,若 node 上保留已经停止的容器数超过 --maximum-dead-
containers ,首先计算需要为每个 pod 保留多少个已退出的容器保证其总数不超过 --
maximum-dead-containers 的值,若计算结果小于 1 则取 1,即至少保留一个,然后删除每
个 pod 中不需要保留的容器,此时若 node 上保留已经停止的容器数依然超过需要保留的最大
值,则将 evictUnits 中的容器按照退出时间进行排序删除退出时间最久的容器,使 node 上
保留已经停止的容器数满足 --maximum-dead-containers 值;

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:222

func (cgc *containerGC) evictContainers(gcPolicy


kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictTerminatedPods
1. bool) error {
2. // 1、获取可被回收的容器列表
3. evictUnits, err := cgc.evictableContainers(gcPolicy.MinAge)
4. if err != nil {
5. return err
6. }
7.
// 2、回收 Deleted 状态以及 Terminated 状态的 pod,此处 allSourcesReady 指
8. kubelet
9. // 支持的三种 podSource 是否都可用
10. if allSourcesReady {
11. for key, unit := range evictUnits {
if cgc.podStateProvider.IsPodDeleted(key.uid) ||
12. (cgc.podStateProvider.IsPodTerminated(key.uid) && evictTerminatedPods) {
13. cgc.removeOldestN(unit, len(unit))
14. delete(evictUnits, key)
15. }
16. }
17. }
18.
// 3、为非 Deleted 状态以及 Terminated 状态的 pod 保留 MaxPerPodContainer 个已经
19. 退出的容器
20. if gcPolicy.MaxPerPodContainer >= 0 {
cgc.enforceMaxContainersPerEvictUnit(evictUnits,
21. gcPolicy.MaxPerPodContainer)
22. }
23.

本文档使用 书栈网 · BookStack.CN 构建 - 460 -


kubelet 中垃圾回收机制的设计与实现

// 4、若 kubelet 启动时指定了 --maximum-dead-containers(默认为 -1 即不限制)参


24. 数,
25. // 此时需要为 node 保留退出的容器数不能超过 --maximum-dead-containers 个
if gcPolicy.MaxContainers >= 0 && evictUnits.NumContainers() >
26. gcPolicy.MaxContainers {
numContainersPerEvictUnit := gcPolicy.MaxContainers /
27. evictUnits.NumEvictUnits()
28. if numContainersPerEvictUnit < 1 {
29. numContainersPerEvictUnit = 1
30. }
cgc.enforceMaxContainersPerEvictUnit(evictUnits,
31. numContainersPerEvictUnit)
32.
33. numContainers := evictUnits.NumContainers()
34. if numContainers > gcPolicy.MaxContainers {
35. flattened := make([]containerGCInfo, 0, numContainers)
36. for key := range evictUnits {
37. flattened = append(flattened, evictUnits[key]...)
38. }
39. sort.Sort(byCreated(flattened))
40.
41. cgc.removeOldestN(flattened, numContainers-gcPolicy.MaxContainers)
42. }
43. }
44. return nil
45. }

cgc.evictSandboxes

cgc.evictSandboxes 方法会回收所有可回收的 sandboxes,其主要逻辑为:

1、首先获取 node 上所有的 containers 和 sandboxes;


2、构建 sandboxes 与 pod 的对应关系并将其保存在 sandboxesByPodUID 中;
3、对 sandboxesByPodUID 列表按创建时间进行排序;
4、若 sandboxes 所在的 pod 处于 deleted 状态,则删除该 pod 中所有的 sandboxes
否则只保留退出时间最短的一个 sandboxes,deleted 状态在上文 cgc.evictContainers
方法中已经解释过;

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:274

1. func (cgc *containerGC) evictSandboxes(evictTerminatedPods bool) error {


2. // 1、获取 node 上所有的 container
3. containers, err := cgc.manager.getKubeletContainers(true)

本文档使用 书栈网 · BookStack.CN 构建 - 461 -


kubelet 中垃圾回收机制的设计与实现

4. if err != nil {
5. return err
6. }
7. // 2、获取 node 上所有的 sandboxes
8. sandboxes, err := cgc.manager.getKubeletSandboxes(true)
9. if err != nil {
10. return err
11. }
12.
13. // 3、收集所有 container 的 PodSandboxId
14. sandboxIDs := sets.NewString()
15. for _, container := range containers {
16. sandboxIDs.Insert(container.PodSandboxId)
17. }
18.
19. // 4、构建 sandboxes 与 pod 的对应关系并将其保存在 sandboxesByPodUID 中
20. sandboxesByPod := make(sandboxesByPodUID)
21. for _, sandbox := range sandboxes {
22. podUID := types.UID(sandbox.Metadata.Uid)
23. sandboxInfo := sandboxGCInfo{
24. id: sandbox.Id,
25. createTime: time.Unix(0, sandbox.CreatedAt),
26. }
27.
28. if sandbox.State == runtimeapi.PodSandboxState_SANDBOX_READY {
29. sandboxInfo.active = true
30. }
31.
32. if sandboxIDs.Has(sandbox.Id) {
33. sandboxInfo.active = true
34. }
35.
36. sandboxesByPod[podUID] = append(sandboxesByPod[podUID], sandboxInfo)
37. }
38.
39. // 5、对 sandboxesByPod 进行排序
40. for uid := range sandboxesByPod {
41. sort.Sort(sandboxByCreated(sandboxesByPod[uid]))
42. }
43.
44. // 6、遍历 sandboxesByPod,若 sandboxes 所在的 pod 处于 deleted 状态,
45. // 则删除该 pod 中所有的 sandboxes 否则只保留退出时间最短的一个 sandboxes

本文档使用 书栈网 · BookStack.CN 构建 - 462 -


kubelet 中垃圾回收机制的设计与实现

46. for podUID, sandboxes := range sandboxesByPod {


if cgc.podStateProvider.IsPodDeleted(podUID) ||
47. (cgc.podStateProvider.IsPodTerminated(podUID) && evictTerminatedPods) {
48. cgc.removeOldestNSandboxes(sandboxes, len(sandboxes))
49. } else {
50. cgc.removeOldestNSandboxes(sandboxes, len(sandboxes)-1)
51. }
52. }
53. return nil
54. }

cgc.evictPodLogsDirectories

cgc.evictPodLogsDirectories 方法会回收所有可回收 pod 以及 container 的 log dir,


其主要逻辑为:

1、首先回收 deleted 状态 pod logs dir,遍历 pod logs dir


/var/log/pods , /var/log/pods 为 pod logs 的默认目录,pod logs dir 的格式为
/var/log/pods/NAMESPACE_NAME_UID ,解析 pod logs dir 获取 pod uid,判断 pod
是否处于 deleted 状态,若处于 deleted 状态则删除其 logs dir;

2、回收 deleted 状态 container logs 链接目录, /var/log/containers 为


container log 的默认目录,其会软链接到 pod 的 log dir 下,例如:

/var/log/containers/storage-provisioner_kube-system_storage-provisioner-acc8386e40
1. -> /var/log/pods/kube-system_storage-provisioner_b448e496-eb5d-4d71-b93f-ff7ff77d2

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:333

1. func (cgc *containerGC) evictPodLogsDirectories(allSourcesReady bool) error {


2. osInterface := cgc.manager.osInterface
3. // 1、回收 deleted 状态 pod logs dir
4. if allSourcesReady {
5. dirs, err := osInterface.ReadDir(podLogsRootDirectory)
6. if err != nil {
return fmt.Errorf("failed to read podLogsRootDirectory %q: %v",
7. podLogsRootDirectory, err)
8. }
9. for _, dir := range dirs {
10. name := dir.Name()
11. podUID := parsePodUIDFromLogsDirectory(name)
12. if !cgc.podStateProvider.IsPodDeleted(podUID) {

本文档使用 书栈网 · BookStack.CN 构建 - 463 -


kubelet 中垃圾回收机制的设计与实现

13. continue
14. }
err := osInterface.RemoveAll(filepath.Join(podLogsRootDirectory,
15. name))
16. if err != nil {
klog.Errorf("Failed to remove pod logs directory %q: %v", name,
17. err)
18. }
19. }
20. }
21. // 2、回收 deleted 状态 container logs 链接目录
logSymlinks, _ := osInterface.Glob(filepath.Join(legacyContainerLogsDir,
22. fmt.Sprintf("*.%s", legacyLogSuffix)))
23. for _, logSymlink := range logSymlinks {
24. if _, err := osInterface.Stat(logSymlink); os.IsNotExist(err) {
25. err := osInterface.Remove(logSymlink)
26. if err != nil {
klog.Errorf("Failed to remove container log dead symlink %q:
27. %v", logSymlink, err)
28. }
29. }
30. }
31. return nil
32. }

kl.imageManager.GarbageCollect

上面已经分析了容器回收的主要流程,下面会继续分析镜像回收的流
程, kl.imageManager.GarbageCollect 是镜像回收任务启动的方法,镜像回收流程是在
imageManager 中进行的,首先了解下 imageManager 的初始化,imageManager 也是在
NewMainKubelet 方法中进行初始化的。

k8s.io/kubernetes/pkg/kubelet/kubelet.go

1. func NewMainKubelet(){
2. ......
3. // 初始化时需要指定三个参数,三个参数已经在上文中提到过
4. imageGCPolicy := images.ImageGCPolicy{
5. MinAge: kubeCfg.ImageMinimumGCAge.Duration,
6. HighThresholdPercent: int(kubeCfg.ImageGCHighThresholdPercent),
7. LowThresholdPercent: int(kubeCfg.ImageGCLowThresholdPercent),
8. }
9. ......

本文档使用 书栈网 · BookStack.CN 构建 - 464 -


kubelet 中垃圾回收机制的设计与实现

imageManager, err := images.NewImageGCManager(klet.containerRuntime,


klet.StatsProvider, kubeDeps.Recorder, nodeRef, imageGCPolicy,
10. crOptions.PodSandboxImage)
11. if err != nil {
12. return nil, fmt.Errorf("failed to initialize image manager: %v", err)
13. }
14. klet.imageManager = imageManager
15. ......
16. }

kl.imageManager.GarbageCollect 方法的主要逻辑为:

1、首先调用 im.statsProvider.ImageFsStats 获取容器镜像存储目录挂载点文件系统的磁


盘信息;
2、获取挂载点的 available 和 capacity 信息并计算其使用率;
3、若使用率大于 HighThresholdPercent ,首先根据 LowThresholdPercent 值计算需要
释放的磁盘量,然后调用 im.freeSpace 释放未使用的 image 直到满足磁盘空闲率;

k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:269

1. func (im *realImageGCManager) GarbageCollect() error {


2. // 1、获取容器镜像存储目录挂载点文件系统的磁盘信息
3. fsStats, err := im.statsProvider.ImageFsStats()
4. if err != nil {
5. return err
6. }
7.
8. var capacity, available int64
9. if fsStats.CapacityBytes != nil {
10. capacity = int64(*fsStats.CapacityBytes)
11. }
12. if fsStats.AvailableBytes != nil {
13. available = int64(*fsStats.AvailableBytes)
14. }
15.
16. if available > capacity {
17. available = capacity
18. }
19.
20. if capacity == 0 {
21. err := goerrors.New("invalid capacity 0 on image filesystem")
im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning,
22. events.InvalidDiskCapacity, err.Error())

本文档使用 书栈网 · BookStack.CN 构建 - 465 -


kubelet 中垃圾回收机制的设计与实现

23. return err


24. }
25. // 2、若使用率大于 HighThresholdPercent,此时需要回收镜像
26. usagePercent := 100 - int(available*100/capacity)
27. if usagePercent >= im.policy.HighThresholdPercent {
28. // 3、计算需要释放的磁盘量
amountToFree := capacity*int64(100-im.policy.LowThresholdPercent)/100 -
29. available
30.
31. // 4、调用 im.freeSpace 回收未使用的镜像信息
32. freed, err := im.freeSpace(amountToFree, time.Now())
33. if err != nil {
34. return err
35. }
36.
37. if freed < amountToFree {
err := fmt.Errorf("failed to garbage collect required amount of
38. images. Wanted to free %d bytes, but freed %d bytes", amountToFree, freed)
im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning,
39. events.FreeDiskSpaceFailed, err.Error())
40. return err
41. }
42. }
43.
44. return nil
45. }

im.freeSpace

im.freeSpace 是回收未使用镜像的方法,其主要逻辑为:

1、首先调用 im.detectImages 获取已经使用的 images 列表作为 imagesInUse;


2、遍历 im.imageRecords 根据 imagesInUse 获取所有未使用的 images 信
息, im.imageRecords 记录 node 上所有 images 的信息;
3、根据使用时间对未使用的 images 列表进行排序;
4、遍历未使用的 images 列表然后调用 im.runtime.RemoveImage 删除镜像,直到回收完
所有未使用 images 或者满足空闲率;

k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:328

func (im *realImageGCManager) freeSpace(bytesToFree int64, freeTime time.Time)


1. (int64, error) {
2. // 1、获取已经使用的 images 列表

本文档使用 书栈网 · BookStack.CN 构建 - 466 -


kubelet 中垃圾回收机制的设计与实现

3. imagesInUse, err := im.detectImages(freeTime)


4. if err != nil {
5. return 0, err
6. }
7.
8. im.imageRecordsLock.Lock()
9. defer im.imageRecordsLock.Unlock()
10.
11. // 2、获取所有未使用的 images 信息
12. images := make([]evictionInfo, 0, len(im.imageRecords))
13. for image, record := range im.imageRecords {
14. if isImageUsed(image, imagesInUse) {
15. klog.V(5).Infof("Image ID %s is being used", image)
16. continue
17. }
18. images = append(images, evictionInfo{
19. id: image,
20. imageRecord: *record,
21. })
22. }
23. // 3、按镜像使用时间进行排序
24. sort.Sort(byLastUsedAndDetected(images))
25. // 4、回收未使用的镜像
26. var deletionErrors []error
27. spaceFreed := int64(0)
28. for _, image := range images {
29. if image.lastUsed.Equal(freeTime) || image.lastUsed.After(freeTime) {
30. continue
31. }
32.
33. if freeTime.Sub(image.firstDetected) < im.policy.MinAge {
34. continue
35. }
36.
37. // 5、调用 im.runtime.RemoveImage 删除镜像
38. err := im.runtime.RemoveImage(container.ImageSpec{Image: image.id})
39. if err != nil {
40. deletionErrors = append(deletionErrors, err)
41. continue
42. }
43. delete(im.imageRecords, image.id)
44. spaceFreed += image.size

本文档使用 书栈网 · BookStack.CN 构建 - 467 -


kubelet 中垃圾回收机制的设计与实现

45. if spaceFreed >= bytesToFree {


46. break
47. }
48. }
49.
50. if len(deletionErrors) > 0 {
return spaceFreed, fmt.Errorf("wanted to free %d bytes, but freed %d
bytes space with errors in image deletion: %v", bytesToFree, spaceFreed,
51. errors.NewAggregate(deletionErrors))
52. }
53. return spaceFreed, nil
54. }

总结
本文主要分析了 kubelet 中垃圾回收机制的实现,kubelet 中会定期回收 node 上已经退出的容
器已经当 node 磁盘资源不足时回收不再使用的镜像来释放磁盘资源,容器以及镜像回收策略主要是
通过 kubelet 中几个参数的阈值进行控制的。

本文档使用 书栈网 · BookStack.CN 构建 - 468 -

You might also like