原文连接:kubectl 建立 Pod 背后到底发生了什么?node
想象一下,若是我想将 nginx 部署到 Kubernetes 集群,我可能会在终端中输入相似这样的命令:nginx
$ kubectl run --image=nginx --replicas=3
而后回车。几秒钟后,你就会看到三个 nginx pod 分布在全部的工做节点上。这一切就像变魔术同样,但你并不知道这一切的背后究竟发生了什么事情。git
Kubernetes 的神奇之处在于:它能够经过用户友好的 API 来处理跨基础架构的 deployments
,而背后的复杂性被隐藏在简单的抽象中。但为了充分理解它为咱们提供的价值,咱们须要理解它的内部原理。github
本指南将引导您理解从 client 到 Kubelet
的请求的完整生命周期,必要时会经过源代码来讲明背后发生了什么。web
这是一份能够在线修改的文档,若是你发现有什么能够改进或重写的,欢迎提供帮助!算法
当敲下回车键之后,kubectl
首先会执行一些客户端验证操做,以确保不合法的请求(例如,建立不支持的资源或使用格式错误的镜像名称)将会快速失败,也不会发送给 kube-apiserver
。经过减小没必要要的负载来提升系统性能。shell
验证经过以后, kubectl 开始将发送给 kube-apiserver 的 HTTP 请求进行封装。kube-apiserver
与 etcd 进行通讯,全部尝试访问或更改 Kubernetes 系统状态的请求都会经过 kube-apiserver 进行,kubectl 也不例外。kubectl 使用生成器(generators)来构造 HTTP 请求。生成器是一个用来处理序列化的抽象概念。json
经过 kubectl run
不只能够运行 deployment
,还能够经过指定参数 --generator
来部署其余多种资源类型。若是没有指定 --generator
参数的值,kubectl 将会自动判断资源的类型。api
例如,带有参数 --restart-policy=Always
的资源将被部署为 Deployment,而带有参数 --restart-policy=Never
的资源将被部署为 Pod。同时 kubectl 也会检查是否须要触发其余操做,例如记录命令(用来进行回滚或审计)。缓存
在 kubectl 判断出要建立一个 Deployment 后,它将使用 DeploymentV1Beta1
生成器从咱们提供的参数中生成一个运行时对象。
为了更容易地消除字段或者从新组织资源结构,Kubernetes 支持多个 API 版本,每一个版本都在不一样的 API 路径下,例如 /api/v1
或者 /apis/extensions/v1beta1
。不一样的 API 版本代表不一样的稳定性和支持级别,更详细的描述能够参考 Kubernetes API 概述。
API 组旨在对相似资源进行分类,以便使得 Kubernetes API 更容易扩展。API 的组名在 REST 路径或者序列化对象的 apiVersion
字段中指定。例如,Deployment 的 API 组名是 apps
,最新的 API 版本是 v1beta2
,这就是为何你要在 Deployment manifests 顶部输入 apiVersion: apps/v1beta2
。
kubectl 在生成运行时对象后,开始为它找到适当的 API 组和 API 版本,而后组装成一个版本化客户端,该客户端知道资源的各类 REST 语义。该阶段被称为版本协商,kubectl 会扫描 remote API
上的 /apis
路径来检索全部可能的 API 组。因为 kube-apiserver 在 /apis
路径上公开了 OpenAPI 格式的规范文档, 所以客户端很容易找到合适的 API。
为了提升性能,kubectl 将 OpenAPI 规范缓存到了 ~/.kube/cache
目录。若是你想了解 API 发现的过程,请尝试删除该目录并在运行 kubectl 命令时将 -v
参数的值设为最大值,而后你将会看到全部试图找到这些 API 版本的HTTP 请求。参考 kubectl 备忘单。
最后一步才是真正地发送 HTTP 请求。一旦请求发送以后得到成功的响应,kubectl 将会根据所需的输出格式打印 success message。
在发送 HTTP 请求以前还要进行客户端认证,这是以前没有提到的,如今能够来看一下。
为了可以成功发送请求,kubectl 须要先进行身份认证。用户凭证保存在 kubeconfig
文件中,kubectl 经过如下顺序来找到 kubeconfig 文件:
--kubeconfig
参数, kubectl 就使用 --kubeconfig 参数提供的 kubeconfig 文件。$KUBECONFIG
,则使用该环境变量提供的 kubeconfig 文件。$KUBECONFIG
都没有提供,kubectl 就使用默认的 kubeconfig 文件 $HOME/.kube/config
。解析完 kubeconfig 文件后,kubectl 会肯定当前要使用的上下文、当前指向的群集以及与当前用户关联的任何认证信息。若是用户提供了额外的参数(例如 --username),则优先使用这些参数覆盖 kubeconfig 中指定的值。一旦拿到这些信息以后, kubectl 就会把这些信息填充到将要发送的 HTTP 请求头中:
bearer tokens
在 HTTP 请求头 Authorization
中发送。OpenID
认证过程是由用户事先手动处理的,产生一个像 bearer token 同样被发送的 token。如今咱们的请求已经发送成功了,接下来将会发生什么?这时候就该 kube-apiserver
闪亮登场了!kube-apiserver 是客户端和系统组件用来保存和检索集群状态的主要接口。为了执行相应的功能,kube-apiserver 须要可以验证请求者是合法的,这个过程被称为认证。
那么 apiserver 如何对请求进行认证呢?当 kube-apiserver 第一次启动时,它会查看用户提供的全部 CLI 参数,并组合成一个合适的令牌列表。
举个例子:若是提供了 --client-ca-file
参数,则会将 x509 客户端证书认证添加到令牌列表中;若是提供了 --token-auth-file
参数,则会将 breaer token 添加到令牌列表中。
每次收到请求时,apiserver 都会经过令牌链进行认证,直到某一个认证成功为止:
--token-auth-file
参数提供的 token 文件是否存在。若是认证失败,则请求失败并返回相应的错误信息;若是验证成功,则将请求中的 Authorization
请求头删除,并将用户信息添加到其上下文中。这给后续的受权和准入控制器提供了访问以前创建的用户身份的能力。
OK,如今请求已经发送,而且 kube-apiserver 已经成功验证咱们是谁,终于解脱了!
然而事情并无结束,虽然咱们已经证实了咱们是合法的,但咱们有权执行此操做吗?毕竟身份和权限不是一回事。为了进行后续的操做,kube-apiserver 还要对用户进行受权。
kube-apiserver 处理受权的方式与处理身份验证的方式类似:经过 kube-apiserver 的启动参数 --authorization_mode
参数设置。它将组合一系列受权者,这些受权者将针对每一个传入的请求进行受权。若是全部受权者都拒绝该请求,则该请求会被禁止响应而且不会再继续响应。若是某个受权者批准了该请求,则请求继续。
kube-apiserver 目前支持如下几种受权方法:
rbac.authorization.k8s.io
API Group实现受权决策,容许管理员经过 Kubernetes API 动态配置策略。突破了以前所说的认证和受权两道关口以后,客户端的调用请求就可以获得 API Server 的真正响应了吗?答案是:不能!
从 kube-apiserver 的角度来看,它已经验证了咱们的身份而且赋予了相应的权限容许咱们继续,但对于 Kubernetes 而言,其余组件对于应不该该容许发生的事情仍是颇有意见的。因此这个请求还须要经过 Admission Controller
所控制的一个 准入控制链
的层层考验,官方标准的 “关卡” 有近十个之多,并且还能自定义扩展!
虽然受权的重点是回答用户是否有权限,但准入控制器会拦截请求以确保它符合集群的更普遍的指望和规则。它们是资源对象保存到 etcd
以前的最后一个堡垒,封装了一系列额外的检查以确保操做不会产生意外或负面结果。不一样于受权和认证只关心请求的用户和操做,准入控制还处理请求的内容,而且仅对建立、更新、删除或链接(如代理)等有效,而对读操做无效。
准入控制器的工做方式与受权者和验证者的工做方式相似,但有一点区别:与验证链和受权链不一样,若是某个准入控制器检查不经过,则整个链会中断,整个请求将当即被拒绝而且返回一个错误给终端用户。
准入控制器设计的重点在于提升可扩展性,某个控制器都做为一个插件存储在 plugin/pkg/admission
目录中,而且与某一个接口相匹配,最后被编译到 kube-apiserver 二进制文件中。
大部分准入控制器都比较容易理解,接下来着重介绍 SecurityContextDeny
、ResourceQuota
及 LimitRanger
这三个准入控制器。
ResourceQuota
一块儿实现了资源配额管理。LimitRange
一块儿实现资源配额管理。到如今为止,Kubernetes 已经对该客户端的调用请求进行了全面完全地审查,而且已经验证经过,运行它进入下一个环节。下一步 kube-apiserver 将对 HTTP 请求进行反序列化,而后利用获得的结果构建运行时对象(有点像 kubectl 生成器的逆过程),并保存到 etcd
中。下面咱们将这个过程分解一下。
当收到请求时,kube-apiserver 是如何知道它该怎么作的呢?事实上,在客户端发送调用请求以前就已经产生了一系列很是复杂的流程。咱们就从 kube-apiserver 二进制文件首次运行开始分析吧:
Kubernetes API
进行扩展的方式。generic apiserver
做为默认的 apiserver。POST
时,kube-apiserver 就会将请求转交给 资源建立处理器。如今 kube-apiserver 已经知道了全部的路由及其对应的 REST 路径,以便在请求匹配时知道调用哪些处理器和键值存储。多么机智的设计!如今假设客户端的 HTTP 请求已经被 kube-apiserver 收到了:
/apis
时);若是没有任何一个基于路径的处理器注册到该路径,请求就会被转交给 not found 处理器,最后返回 404
。createHandler
的注册路由!它有什么做用呢?首先它会解码 HTTP 请求并进行基本的验证,例如确保请求提供的 json 与 API 资源的版本相匹配。/
,你也能够自定义。storage provider
会执行 get
调用来确认该资源是否被成功建立。若是须要额外的清理工做,就会调用后期建立的处理器和装饰器。原来 apiserver 作了这么多的工做,之前居然没有发现呢!到目前为止,咱们建立的 Deployment
资源已经保存到了 etcd 中,但 apiserver 仍然看不到它。
在一个资源对象被持久化到数据存储以后,apiserver 还没法彻底看到或调度它,在此以前还要执行一系列Initializers。Initializers是一种与资源类型相关联的控制器,它会在资源对外可用以前执行某些逻辑。若是某个资源类型没有Initializers,就会跳过此初始化步骤当即使资源对外可见。
正如大佬的博客指出的那样,Initializers是一个强大的功能,由于它容许咱们执行通用引导操做。例如:
annotation
。volume
注入到特定命名空间的全部 Pod 中。Secret
中的密码小于 20 个字符,就组织其建立。initializerConfiguration
资源对象容许你声明某些资源类型应该运行哪些Initializers。若是你想每建立一个 Pod 时就运行一个自定义Initializers,你能够这样作:
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
name: custom-pod-initializer
initializers:
- name: podimage.example.com
rules:
- apiGroups:
- ""
apiVersions:
- v1
resources:
- pods
经过该配置建立资源对象 InitializerConfiguration
以后,就会在每一个 Pod 的 metadata.initializers.pending
字段中添加 custom-pod-initializer
字段。该初始化控制器会按期扫描新的 Pod,一旦在 Pod 的 pending
字段中检测到本身的名称,就会执行其逻辑,执行完逻辑以后就会将 pending
字段下的本身的名称删除。
只有在 pending
字段下的列表中的第一个Initializers能够对资源进行操做,当全部的Initializers执行完成,而且 pending
字段为空时,该对象就会被认为初始化成功。
你可能会注意到一个问题:若是 kube-apiserver 不能显示这些资源,那么用户级控制器是如何处理资源的呢?
为了解决这个问题,kube-apiserver 暴露了一个 ?includeUninitialized
查询参数,它会返回全部的资源对象(包括未初始化的)。
到了这个阶段,咱们的 Deployment 记录已经保存在 etcd 中,而且全部的初始化逻辑都执行完成,接下来的阶段将会涉及到该资源所依赖的拓扑结构。在 Kubernetes 中,Deployment 实际上只是一系列 Replicaset
的集合,而 Replicaset 是一系列 Pod
的集合。那么 Kubernetes 是如何从一个 HTTP 请求按照层级结构依次建立这些资源的呢?其实这些工做都是由 Kubernetes 内置的 Controller
(控制器) 来完成的。
Kubernetes 在整个系统中使用了大量的 Controller,Controller 是一个用于将系统状态从“当前状态”修正到“指望状态”的异步脚本。全部 Controller 都经过 kube-controller-manager
组件并行运行,每种 Controller 都负责一种具体的控制流程。首先介绍一下 Deployment Controller
:
将 Deployment 记录存储到 etcd 并初始化后,就能够经过 kube-apiserver 使其可见,而后 Deployment Controller
就会检测到它(它的工做就是负责监听 Deployment 记录的更改)。在咱们的例子中,控制器经过一个 Informer
注册一个建立事件的特定回调函数(更多信息参加下文)。
当 Deployment 第一次对外可见时,该 Controller 就会将该资源对象添加到内部工做队列,而后开始处理这个资源对象:
经过使用标签选择器查询 kube-apiserver 来检查该 Deployment 是否有与其关联的
ReplicaSet
或Pod
记录。
有趣的是,这个同步过程是状态不可知的,它核对新记录与核对已经存在的记录采用的是相同的方式。
在乎识到没有与其关联的 ReplicaSet
或 Pod
记录后,Deployment Controller 就会开始执行弹性伸缩流程:
建立 ReplicaSet 资源,为其分配一个标签选择器并将其版本号设置为 1。
ReplicaSet 的 PodSpec
字段从 Deployment 的 manifest 以及其余相关元数据中复制而来。有时 Deployment 记录在此以后也须要更新(例如,若是设置了 process deadline
)。
当完成以上步骤以后,该 Deployment 的 status
就会被更新,而后从新进入与以前相同的循环,等待 Deployment 与指望的状态相匹配。因为 Deployment Controller 只关心 ReplicaSet,所以须要经过 ReplicaSet Controller
来继续协调。
在前面的步骤中,Deployment Controller 建立了第一个 ReplicaSet,但仍然仍是没有 Pod,这时候就该 ReplicaSet Controller
登场了!ReplicaSet Controller 的工做是监视 ReplicaSets 及其相关资源(Pod)的生命周期。和大多数其余 Controller 同样,它经过触发某些事件的处理器来实现此目的。
当建立 ReplicaSet 时(由 Deployment Controller 建立),RS Controller 检查新 ReplicaSet 的状态,并检查当前状态与指望状态之间存在的误差,而后经过调整 Pod 的副本数来达到指望的状态。
Pod 的建立也是批量进行的,从 SlowStartInitialBatchSize
开始,而后在每次成功的迭代中以一种 slow start
操做加倍。这样作的目的是在大量 Pod 启动失败时(例如,因为资源配额),能够减轻 kube-apiserver 被大量没必要要的 HTTP 请求吞没的风险。若是建立失败,最好可以优雅地失败,而且对其余的系统组件形成的影响最小!
Kubernetes 经过 Owner References
(在子级资源的某个字段中引用其父级资源的 ID) 来构造严格的资源对象层级结构。这确保了一旦 Controller 管理的资源被删除(级联删除),子资源就会被垃圾收集器删除,同时还为父级资源提供了一种有效的方式来避免他们竞争同一个子级资源(想象两对父母都认为他们拥有同一个孩子的场景)。
Owner References 的另外一个好处是:它是有状态的。若是有任何 Controller 重启了,那么因为资源对象的拓扑关系与 Controller 无关,该操做不会影响到系统的稳定运行。这种对资源隔离的重视也体如今 Controller 自己的设计中:Controller 不能对本身没有明确拥有的资源进行操做,它们应该选择对资源的全部权,互不干涉,互不共享。
有时系统中也会出现孤儿(orphaned)资源,一般由如下两种途径产生:
当发生这种状况时,Controller 将会确保孤儿资源拥有新的 Owner
。多个父级资源能够相互竞争同一个孤儿资源,但只有一个会成功(其余父级资源会收到验证错误)。
你可能已经注意到,某些 Controller(例如 RBAC 受权器或 Deployment Controller)须要先检索集群状态而后才能正常运行。拿 RBAC 受权器举例,当请求进入时,受权器会将用户的初始状态缓存下来,而后用它来检索与 etcd 中的用户关联的全部 角色(Role
)和 角色绑定(RoleBinding
)。那么问题来了,Controller 是如何访问和修改这些资源对象的呢?事实上 Kubernetes 是经过 Informer
机制来解决这个问题的。
Infomer 是一种模式,它容许 Controller 查找缓存在本地内存中的数据(这份数据由 Informer 本身维护)并列出它们感兴趣的资源。
虽然 Informer 的设计很抽象,但它在内部实现了大量的对细节的处理逻辑(例如缓存),缓存很重要,由于它不但能够减小对 Kubenetes API 的直接调用,同时也能减小 Server 和 Controller 的大量重复性工做。经过使用 Informer,不一样的 Controller 之间以线程安全(Thread safety)的方式进行交互,而没必要担忧多个线程访问相同的资源时会产生冲突。
有关 Informer 的更多详细解析,请参考这篇文章:Kubernetes: Controllers, Informers, Reflectors and Stores
当全部的 Controller 正常运行后,etcd 中就会保存一个 Deployment、一个 ReplicaSet 和 三个 Pod 资源记录,而且能够经过 kube-apiserver 查看。然而,这些 Pod 资源如今还处于 Pending
状态,由于它们尚未被调度到集群中合适的 Node 上运行。这个问题最终要靠调度器(Scheduler)来解决。
Scheduler
做为一个独立的组件运行在集群控制平面上,工做方式与其余 Controller 相同:监听实际并将系统状态调整到指望的状态。具体来讲,Scheduler 的做用是将待调度的 Pod 按照特定的算法和调度策略绑定(Binding)到集群中某个合适的 Node 上,并将绑定信息写入 etcd 中(它会过滤其 PodSpec 中 NodeName
字段为空的 Pod),默认的调度算法的工做方式以下:
当 Scheduler 启动时,会注册一个默认的预选策略链,这些预选策略
会对备选节点进行评估,判断备选节点是否知足备选 Pod 的需求。例如,若是 PodSpec 字段限制了 CPU 和内存资源,那么当备选节点的资源容量不知足备选 Pod 的需求时,备选 Pod 就不会被调度到该节点上(资源容量=备选节点资源总量-节点中已存在 Pod 的全部容器的需求资源(CPU 和内存)的总和)
一旦筛选出符合要求的候选节点,就会采用优选策略
计算出每一个候选节点的积分,而后对这些候选节点进行排序,积分最高者胜出。例如,为了在整个系统中分摊工做负载,这些优选策略会从备选节点列表中选出资源消耗最小的节点。每一个节点经过优选策略时都会算出一个得分,计算各项得分,最终选出分值大的节点做为优选的结果。
一旦找到了合适的节点,Scheduler 就会建立一个 Binding
对象,该对象的 Name
和 Uid
与 Pod 相匹配,而且其 ObjectReference
字段包含所选节点的名称,而后经过 POST
请求发送给 apiserver。
当 kube-apiserver 接收到此 Binding 对象时,注册吧会将该对象反序列化并更新 Pod 资源中的如下字段:
NodeName
的值设置为 ObjectReference 中的 NodeName。PodScheduled
的 status
值设置为 True。能够经过 kubectl 来查看:$ kubectl get <PODNAME> -o go-template='{{range .status.conditions}}{{if eq .type "PodScheduled"}}{{.status}}{{end}}{{end}}'
一旦 Scheduler 将 Pod 调度到某个节点上,该节点的 Kubelet
就会接管该 Pod 并开始部署。
预选策略和优选策略均可以经过
--policy-config-file
参数来扩展,若是默认的调度器不知足要求,还能够部署自定义的调度器。若是podSpec.schedulerName
的值设置为其余的调度器,则 Kubernetes 会将该 Pod 的调度转交给那个调度器。
如今,全部的 Controller 都完成了工做,咱们来总结一下:
然而到目前为止,全部的状态变化仅仅只是针对保存在 etcd 中的资源记录,接下来的步骤涉及到运行在工做节点之间的 Pod 的分布情况,这是分布式系统(好比 Kubernetes)的关键因素。这些任务都是由 Kubelet
组件完成的,让咱们开始吧!
在 Kubernetes 集群中,每一个 Node 节点上都会启动一个 Kubelet 服务进程,该进程用于处理 Scheduler 下发到本节点的任务,管理 Pod 的生命周期,包括挂载卷、容器日志记录、垃圾回收以及其余与 Pod 相关的事件。
若是换一种思惟模式,你能够把 Kubelet 当成一种特殊的 Controller,它每隔 20 秒(能够自定义)向 kube-apiserver 经过 NodeName
获取自身 Node 上所要运行的 Pod 清单。一旦获取到了这个清单,它就会经过与本身的内部缓存进行比较来检测新增长的 Pod,若是有差别,就开始同步 Pod 列表。咱们来详细分析一下同步过程:
若是 Pod 正在建立, Kubelet 就会记录一些在 Prometheus
中用于追踪 Pod 启动延时的指标。
而后生成一个 PodStatus
对象,它表示 Pod 当前阶段的状态。Pod 的状态(Phase
) 是 Pod 在其生命周期中的最精简的概要,包括 Pending
,Running
,Succeeded
,Failed
和 Unkown
这几个值。状态的产生过程很是过程,因此颇有必要深刻了解一下背后的原理:
首先串行执行一系列 Pod 同步处理器(PodSyncHandlers
),每一个处理器检查检查 Pod 是否应该运行在该节点上。当全部的处理器都认为该 Pod 不该该运行在该节点上,则 Pod 的 Phase
值就会变成 PodFailed
,而且将该 Pod 从该节点上驱逐出去。例如当你建立一个 Job
时,若是 Pod 失败重试的时间超过了 spec.activeDeadlineSeconds
设置的值,就会将 Pod 从该节点驱逐出去。
接下来,Pod 的 Phase 值由 init 容器
和应用容器的状态共同来决定。由于目前容器尚未启动,容器被视为处于等待阶段,若是 Pod 中至少有一个容器处于等待阶段,则其 Phase
值为 Pending。
最后,Pod 的 Condition
字段由 Pod 内全部容器的状态决定。如今咱们的容器尚未被容器运行时建立,因此 `PodReady` 的状态被设置为 `False`。能够经过 kubectl 查看:
$ kubectl get <PODNAME> -o go-template='{{range .status.conditions}}{{if eq .type "Ready"}}{{.status}}{{end}}{{end}}'
生成 PodStatus 以后(Pod 中的 status
字段),Kubelet 就会将它发送到 Pod 的状态管理器,该管理器的任务是经过 apiserver 异步更新 etcd 中的记录。
接下来运行一系列准入处理器来确保该 Pod 是否具备相应的权限(包括强制执行 AppArmor
配置文件和 NO_NEW_PRIVS
),被准入控制器拒绝的 Pod 将一直保持 Pending
状态。
若是 Kubelet 启动时指定了 cgroups-per-qos
参数,Kubelet 就会为该 Pod 建立 cgroup
并进行相应的资源限制。这是为了更方便地对 Pod 进行服务质量(QoS)管理。
而后为 Pod 建立相应的目录,包括 Pod 的目录(/var/run/kubelet/pods/<podID>
),该 Pod 的卷目录(<podDir>/volumes
)和该 Pod 的插件目录(<podDir>/plugins
)。
卷管理器会挂载 Spec.Volumes
中定义的相关数据卷,而后等待是否挂载成功。根据挂载卷类型的不一样,某些 Pod 可能须要等待更长的时间(好比 NFS 卷)。
从 apiserver 中检索 Spec.ImagePullSecrets
中定义的全部 Secret
,而后将其注入到容器中。
最后经过容器运行时接口(Container Runtime Interface(CRI)
)开始启动容器(下面会详细描述)。
到了这个阶段,大量的初始化工做都已经完成,容器已经准备好开始启动了,而容器是由容器运行时(例如 Docker
和 Rkt
)启动的。
为了更容易扩展,Kubelet 从 1.5.0 开始经过容器运行时接口与容器运行时(Container Runtime)交互。简而言之,CRI 提供了 Kubelet 和特定的运行时之间的抽象接口,它们之间经过协议缓冲区(它像一个更快的 JSON)和 gRPC API(一种很是适合执行 Kubernetes 操做的 API)。这是一个很是酷的想法,经过使用 Kubelet 和运行时之间定义的契约关系,容器如何编排的具体实现细节已经变得可有可无。因为不须要修改 Kubernetes 的核心代码,开发者能够以最小的开销添加新的运行时。
很差意思有点跑题了,让咱们继续回到容器启动的阶段。第一次启动 Pod 时,Kubelet 会经过 Remote Procedure Command
(RPC) 协议调用 RunPodSandbox。sandbox
用于描述一组容器,例如在 Kubernetes 中它表示的是 Pod。sandbox
是一个很宽泛的概念,因此对于其余没有使用容器的运行时仍然是有意义的(好比在一个基于 hypervisor
的运行时中,sandbox 可能指的就是虚拟机)。
咱们的例子中使用的容器运行时是 Docker,建立 sandbox 时首先建立的是 pause
容器。pause 容器做为同一个 Pod 中全部其余容器的基础容器,它为 Pod 中的每一个业务容器提供了大量的 Pod 级别资源,这些资源都是 Linux 命名空间(包括网络命名空间,IPC 命名空间和 PID 命名空间)。
pause 容器提供了一种方法来管理全部这些命名空间并容许业务容器共享它们,在同一个网络命名空间中的好处是:同一个 Pod 中的容器可使用 localhost
来相互通讯。pause 容器的第二个功能与 PID 命名空间的工做方式相关,在 PID 命名空间中,进程之间造成一个树状结构,一旦某个子进程因为父进程的错误而变成了“孤儿进程”,其便会被 init
进程进行收养并最终回收资源。关于 pause 工做方式的详细信息能够参考:The Almighty Pause Container。
一旦建立好了 pause 容器,下面就会开始检查磁盘状态而后开始启动业务容器。
如今咱们的 Pod 已经有了基本的骨架:一个共享全部命名空间以容许业务容器在同一个 Pod 里进行通讯的 pause 容器。但如今还有一个问题,那就是容器的网络是如何创建的?
当 Kubelet 为 Pod 建立网络时,它会将建立网络的任务交给 CNI
插件。CNI 表示容器网络接口(Container Network Interface),和容器运行时的运行方式相似,它也是一种抽象,容许不一样的网络提供商为容器提供不一样的网络实现。经过将 json 配置文件(默认在 /etc/cni/net.d
路径下)中的数据传送到相关的 CNI 二进制文件(默认在 /opt/cni/bin
路径下)中,cni 插件能够给 pause 容器配置相关的网络,而后 Pod 中其余的容器都使用 pause 容器的网络。下面是一个简单的示例配置文件:
{
"cniVersion": "0.3.1",
"name": "bridge",
"type": "bridge",
"bridge": "cnio0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"ranges": [
[{"subnet": "${POD_CIDR}"}]
],
"routes": [{"dst": "0.0.0.0/0"}]
}
}
CNI 插件还会经过 CNI_ARGS
环境变量为 Pod 指定其余的元数据,包括 Pod 名称和命名空间。
下面的步骤因 CNI 插件而异,咱们以 bridge
插件举例:
该插件首先会在根网络命名空间(也就是宿主机的网络命名空间)中设置本地 Linux 网桥,以便为该主机上的全部容器提供网络服务。
而后它会将一个网络接口(veth
设备对的一端)插入到 pause 容器的网络命名空间中,并将另外一端链接到网桥上。你能够这样来理解 veth 设备对:它就像一根很长的管道,一端链接到容器,一端链接到根网络命名空间中,数据包就在管道中进行传播。
接下来 json 文件中指定的 IPAM
Plugin 会为 pause 容器的网络接口分配一个 IP 并设置相应的路由,如今 Pod 就有了本身的 IP。
IPAM Plugin 的工做方式和 CNI Plugin 相似:经过二进制文件调用并具备标准化的接口,每个 IPAM Plugin 都必需要肯定容器网络接口的 IP、子网以及网关和路由,并将信息返回给 CNI 插件。最多见的 IPAM Plugin 是 host-local
,它从预约义的一组地址池中为容器分配 IP 地址。它将地址池的信息以及分配信息保存在主机的文件系统中,从而确保了同一主机上每一个容器的 IP 地址的惟一性。
最后 Kubelet 会将集群内部的 DNS
服务器的 Cluster IP
地址传给 CNI 插件,而后 CNI 插件将它们写到容器的 /etc/resolv.conf
文件中。
一旦完成了上面的步骤,CNI 插件就会将操做的结果以 json 的格式返回给 Kubelet。
到目前为止,咱们已经描述了容器如何与宿主机进行通讯,但跨主机之间的容器如何通讯呢?
一般状况下使用 overlay
网络来进行跨主机容器通讯,这是一种动态同步多个主机间路由的方法。 其中最经常使用的 overlay 网络插件是 flannel
,flannel 具体的工做方式能够参考 CoreOS 的文档。
全部网络都配置完成后,接下来就开始真正启动业务容器了!
一旦 sanbox 完成初始化并处于 active
状态,Kubelet 就能够开始为其建立容器了。首先启动 PodSpec 中定义的 init 容器,而后再启动业务容器。具体过程以下:
ContainerConfig
数据结构(在其中定义了命令,镜像,标签,挂载卷,设备,环境变量等待),而后经过 protobufs
发送给 CRI 接口。对于 Docker 来讲,它会将这些信息反序列化并填充到本身的配置信息中,而后再发送给 Dockerd
守护进程。在这个过程当中,它会将一些元数据标签(例如容器类型,日志路径,dandbox ID 等待)添加到容器中。UpdateContainerResources
CRI 方法将容器分配给本节点上的 CPU 资源池。Hook
。Hook 的类型包括两种:Exec
(执行一段命令) 和 HTTP
(发送HTTP请求)。若是 PostStart Hook 启动的时间过长、挂起或者失败,容器将永远不会变成 running
状态。若是上面一切顺利,如今你的集群上应该会运行三个容器,全部的网络,数据卷和秘钥都被经过 CRI 接口添加到容器中并配置成功。
上文所述的建立 Pod 整个过程的流程图以下所示:
Kubelet 建立 Pod 的流程