Pod 是 Kubernetes 项目里面一个原子调度单位
例如:Kubernetes 把 它 类 比 为 一 个 操 作 系 统 , 比 如 说 Linux。针对于容器,可以类比为进程,就是 Linux 线 程。
由此而言: Pod 相当于进程组, 也就是 Linux 里的线程组
由于容器实际上是一个“单进程”模型,所以如果 你在容器里启动多个进程,只有一个可以作为 PID=1 的进程,而这时候, 如果这个 PID=1 的进程挂了,或者说失败退出了,那么其他三个进程就 会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资 源,这是一个非常不好的情况。
注意:Linux 容器的“单进程”模型,指的是容器的生命周期等 同于 PID=1 的进程(容器应用进程)的生命周期,而不是 说容器里不能创建多进程。当然,一般情况下,容器应用进 程并不具备进程管理能力,所以你通过 exec 或者 ssh 在 容器里创建的其他进程,一旦异常退出(比如 ssh 终止)是 很容易变成孤儿进程的。
在 kubernetes 里面,Pod 实际上正是 kubernetes 项目为你抽象出来 的一个可以类比为进程组
真正起来在物理上存在的东西,就是四个容器。这四个容器,或者说是多 个容器的组合就叫做 Pod
。并且还有一个概念一定要非常明确,Pod 是 Kubernetes 分配资源的一个单位,因为里面的容器要共享某些资源,所 以 Pod 也是 Kubernetes 的原子调度单位
为什么 Pod 必须是原子调度单位?
假如现在有两个容器,紧密协作的,所以它们应该被部署在一个 Pod 里面。具体来说,第一个容器叫做 App,就是业务容器,它会写日 志文件;第二个容器叫做 LogCollector,它会把刚刚 App 容器写的日志 文件转发到后端的 ElasticSearch 中。
两个容器的资源需求是这样的:App 容器需要 1G 内存,LogCollector 需要 0.5G 内存,而当前集群环境的可用内存是这样一个情况:Node_A: 1.25G 内存,Node_B:2G 内存
假如说现在没有 Pod 概念,就只有两个容器,这两个容器要紧密协作、 运行在一台机器上。可是,如果调度器先把 App 调度到了 Node_A 上 面,接下来会怎么样呢?这时你会发现:LogCollector 实际上是没办法调 度到 Node_A 上的,因为资源不够。其实此时整个应用本身就已经出问 题了,调度已经失败了,必须去重新调度。
什么叫做超亲密关系呢?
Pod,它们需要运行在同一台宿主机上,那这样就属 于亲密关系,调度器一定是可以帮助去做的。但是对于超亲密关系来说
, 有一个问题,即它必须通过 Pod 来解决。因为如果超亲密关系赋予不了, 那么整个 Pod 或者说是整个应用都无法启动
。
容器之间是被 Linux Namespace 和 cgroups 隔开
的,所以现 在实际要解决的是怎么去打破这个隔离,然后共享某些事情和某些信息。 这就是 Pod 的设计要解决的核心问题所在。
解法分为两个部分:网络和存储
共享网络 : Pod 里的多个容器怎么去共享网络
比如说现在有一个 Pod,其中包含了一个容器 A 和一个容器 B,它们 两个就要共享 Network Namespace。在 Kubernetes 里的解法是这样 的:它会在每个 Pod 里,额外起一个 Infra container 小容器来共享整 个 Pod 的 Network Namespace
。
Infra container 是一个非常小的镜像,大概 100~200KB 左右,是一个 汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。
所以说一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的。 即:它们看到的网络设备、IP 地址、Mac 地址等等,跟网络相关的信息, 其 实 全 是 一 份 , 这 一 份 都 来 自 于 Pod 第 一 次 创 建 的 这 个 Infra container。这就是 Pod 解决网络共享的一个解法。
在 Pod 里 面 , 一 定 有 一 个 IP 地 址 , 是 这 个 Pod 的 Network Namespace 对应的地址,也是这个 Infra container 的 IP 地址。所以 大家看到的都是一份,而其他所有网络资源,都是一个 Pod 一份,并且 被 Pod 中的所有容器共享。这就是 Pod 的网络实现方式。
由于需要有一个相当于说中间的容器存在,所以整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这也是为什么在 Kubernetes 里面,它是允许去单独更新 Pod 里的某一个镜像的,即: 做这个操作,整个 Pod 不会重建,也不会重启,这是非常重要的一个设 计。
共享存储 :Pod 怎么去共享存储?
比如说现在有两个容器,一个是 Nginx,另外一个是非常普通的容器,在 Nginx 里放一些文件,让我能通过 Nginx 访问到。所以它需要去 share 这个目录。我 share 文件或者是 share 目录在 Pod 里面是非常简单 的,实际上就是把 volume 变成了 Pod level
。然后所有容器,就是所有 同属于一个 Pod 的容器,他们共享所有的 volume。
第一种方式:可以把 WAR 包和 Tomcat 打包放进一个镜像
里面。但 是这样带来一个问题,就是现在这个镜像实际上揉进了两个东西。那 么接下来,无论是我要更新 WAR 包还是说我要更新 Tomcat,都要 重新做一个新的镜像
,这是比较麻烦的;
第二种方式:就是镜像里面只打包 Tomcat。它就是一个 Tomcat,但 是需要使用数据卷
的方式,比如说 hostPath,从宿主机上把 WAR 包 挂载
进我们 Tomcat 容器中,挂到我的 web APP 目录下面,这样把 这个容器启用起来之后,里面就能用了。
问题
例如:容器不在同一个宿主机启动
使容器不管是在 A 还是在 B 上, 都可以找到这个 WAR 包,找到这个数据。
需要一套分布式存储系统
且这个容器本身必须依赖于一套持久化的存储插件
这种方式叫做 Init Container
首先定义一个 Init Container, 它只做一件事情,就是把 WAR 包从镜像里拷贝到一个 Volume 里面, 它做完这个操作就退出了,所以 Init Container 会比用户容器先启动,并 且严格按照定义顺序来依次执行。
APP 目录,实际 上是一个 Volume。而我们前面提到,一个 Pod 里面的多个容器,它们 是可以共享 Volume 的,所以现在这个 Tomcat 容器,只是打包了一个 Tomcat 镜 像 。 但 在 启 动 的 时 候 , 要 声 明 使 用 APP 目 录 作 为 我 的 Volume,并且要把它们挂载在 Web APP 目录下面。
而这个时候,由于前面已经运行过了一个 Init Container,已经执行完拷 贝操作了,所以这个 Volume 里面已经存在了应用的 WAR 包:就是 sample.war,绝对已经存在这个 Volume 里面了。等到第二步执行启动 这个 Tomcat 容器的时候,去挂这个 Volume,一定能在里面找到前面 拷贝来的 sample.war。
这个 Pod 就是一个自包含的
,可以把这一个 Pod 在全世界任何一个 Kubernetes 上面都顺利启用起来
。不用担心没有分布 式存储、Volume 不是持久化的,它一定是可以公布的
。
所以这是一个通过组合两个不同角色的容器,并且按照这样一些像 Init Container 这样一种编排方式,统一的去打包这样一个应用,把它用 Pod 来去做的非常典型的一个例子。像这样的一个概念,在 Kubernetes 里面 就是一个非常经典的容器设计模式,叫做:“Sidecar
”。
什么是 Sidecar?就是说其实在 Pod 里面,可以定义一些专门的容器, 来执行主业务容器所需要的一些辅助工作
,比如我们前面举的例子,其实 就干了一个事儿,这个 Init Container,它就是一个 Sidecar,它只负责 把镜像里的 WAR 包拷贝到共享目录里面,以便被 Tomcat 能够用起 来。
例如:
SSH
需要干的一些事情,可以写脚本、一 些 前 置 的 条 件 , 其 实 都 可 以 通 过 像 Init Container 或 者 另 外 像 Sidecar 的方式去解决日志收集
,日志收集本身是一个进程, 是一个小容器,那么就可以把它打包进 Pod 里面去做这个收集工作;Debug 应用
,实际上现在 Debug 整 个 应 用 都 可 以 在 应 用 Pod 里 面 再 次 定 义 一 个 额 外 的 小 的 Container,它可以去 exec 应用 pod 的 namespace;监控其他容器的工作状态
,这也是它可以做的事情。不再需要去 SSH 登陆到容器里去看,只要把监控组件装到额外的小容器里面就可以了, 然后把它作为一个 Sidecar 启动起来,跟主业务容器进行协作,所以 同样业务监控也都可以通过 Sidecar 方式来去做。第二种用法Sidecar:代理容器
假如有个 Pod 需要访问一个外部系统
,或者一些外部服务,但是这 些外部系统是一个集群,那么这个时候如何通过一个统一的、简单的方式, 用一个 IP 地址,就把这些集群都访问到?
方法一:修改代码。 因为代码里记录了这些集群的地址;
方法二:一种解耦法,即通过 Sidecar 代理容器。Proxy也就是说对外暴露一个ip
第三种用法 适配器容器 Adapter
现在业务暴露出来的 API,比如说有个 API 的一个格式是 API A
,但是现在 有一个外部系统要去访问我的业务容器
,它只知道的一种格式是 API B
, 所以要做一个工作,就是把业务容器怎么想办法改掉,要去改业务代码。 但实际上,你可以通过一个 Adapter 帮你来做这层转换。
的关键还在于 Pod 之中的容器是通过 localhost 直接通信
Spec、 Status
两部分。描述期望
的状态,描述观测
到的状态。另一个部分元数据部分
。该部分主 要 包 括 了 用 来 识 别 资 源 的 标 签 : Labels
, 用 来 描 述 资 源 的 注 解 ; Annotations, 用来描述多个资源之间相互关系的 OwnerReference。这 些元数据在 K8s 运行中有非常重要的作用。
Key:Value
元数据,这里展示了几个常见的标签。打在了 Pod 对象上
,分别标识了对应的应用环境、发布的 成熟度和应用的版本。从应用标签的例子可以看到,标签的名字包括了一 个域名的前缀,用来描述打标签的系统和工具, 最后一个标签打在 Node 对象上,还在域名前增加了版本的标识 beta 字符串。筛选资源和组合资源
,可以使用类似于 SQL 查询 select, 来根据 Label 查询相关的资源。相等型
Selector 还可以包括多个相等条件,多个相等条件之间是逻辑”与“的关系。集合型
Selector,Selector 筛选所有 环境是 test 或者 gray 的 Pod。Annotations 另外一种重要的元数据是
annotations,一般是系统或者工具用来存储资 源的非标示性信息
,可以用来扩展资源的 spec/status 的描述
,
例子:
第一个例子,存储了阿里云负载器的证书 ID,我们可以看到 annotations 一 样 可 以 拥 有 域 名 的 前 缀 , 标 注 中 也 可 以 包 含 版 本 信 息 。 第 二 个 annotation 存储了 nginx 接入层的配置信息,我们可以看到 annotations 中包括“,”这样无法出现在 label 中的特殊字符。第三个 annotations 一 般可以在 kubectl apply 命令行操作后的资源中看到, annotation 值是 一个结构化的数据,实际上是一个 json 串,标记了上一次 kubectl 操 作的资源的 json 的描述。
Ownereference
最后一个元数据叫做 Ownereference,所谓所有者,一般 就是指集合类的资源
,比如说 Pod 集合,就有 replicaset、statefulset
, 集合类资源的控制器会创建对应的归属资源。
比如:replicaset 控制器在 操 作 中 会 创 建 Pod, 被 创 建 Pod 的 Ownereference 就 指 向 了 创 建 Pod 的 replicaset,Ownereference 使得用户可以方便地查找一个创建 资源的对象,另外,还可以用来实现级联删除的效果。
怎么样对 Pod 已有的 lable 进行修改
指定 Pod 名字,在环境再加 上它的一个值 test
kubectl label pods nginx1 env=test
如果想覆盖掉它的话,得额外再加上一个覆盖的选项
kubectl label pods nginx1 env=test —overwrite
最后查看一下
kubectl get pods —show-labels
怎样对Pod 去掉一个标签
env 后就不是等号了。只加上 label 名字,后面不加等号,改成用减号表示
kubectl label pods nginx tie-
kubectl get pods —show-labels
label Selector 进行匹配?
-l
这个选项来进 行指定lable Selector
kubectl get pods —show-labels -l env=test
多个相等的条件
需要指定的,用逗号
隔开
kubectl get pods —show-labels -l env=dev,tie=front
怎么样用集合型的 label Selector 来进行筛选
kubectl get pods —show-labels -l ’env in (dev,test)’
怎样对 Pod 增加一个注解
把 label 命令改成 annotate 命令
kubectl annotate pods nginx1 my-annotate=‘my annotate,ok’
最后查看一下元数据
kubectl get pods nging1 -o yaml | less
控制循环
在控制循环中包括了控制器
, 被控制的系统
,以及能够观测系统的传感器
外界通过修改资源 spec 来控制资源,控制器 比较资源 spec 和 status,从而计算一个 diff,diff 最后会用来决定执 行对系统进行什么样的控制操作,控制操作会使得系统产生新的输出,并 被传感器以资源 status 形式上报,控制器的各个组件将都会是独立自主 地运行,不断使系统向 spec 表示终态趋近。
Sensor
控制循环中逻辑的传感器
主要由 Reflector、Informer、Indexer
三个组 件构成。
Reflector
通过 List 和 Watch K8s server 来获取资源的数据。List 用 来在 Controller 重启以及 Watch 中断的情况下,进行系统资源的全量 更新;而 Watch 则在多次 List 之间进行增量的资源更新;Reflector 在 获取新的资源数据后,会在 Delta 队列中塞入一个包括资源对象信息本 身以及资源对象事件类型的 Delta 记录,Delta 队列中可以保证同一个 对象在队列中仅有一条记录,从而避免 Reflector 重新 List 和 Watch 的时候产生重复的记录。
Informer
组件不断地从 Delta 队列中弹出 delta 记录,然后把资源对象 交给 indexer,让 indexer 把资源记录在一个缓存中,缓存在默认设置 下是用资源的命名空间来做索引的,并且可以被 Controller Manager 或 多个 Controller 所共享。之后,再把这个事件交给事件的回调函数
控制循环中的控制器组件主要由事件处理函数以及 worker 组成,事件处 理函数之间会相互关注资源的新增、更新、删除的事件,并根据控制器的 逻辑去决定是否需要处理。对需要处理的事件,会把事件关联资源的命名 空间以及名字塞入一个工作队列中,并且由后续的 worker 池中的一个 Worker 来处理,工作队列会对存储的对象进行去重,从而避免多个 Woker 处理同一个资源的情况。 Worker 在处理资源对象时,一般需要用资源的名字来重新获得最新的资 源数据,用来创建或者更新资源对象,或者调用其他的外部服务,Worker 如果处理失败的时候,一般情况下会把资源的名字重新加入到工作队列 中,从而方便之后进行重试。
控制循环例子- 扩容
ReplicaSet 是 一 个 用 来 描 述 无 状 态 应 用 的 扩 缩 容 行 为 的 资 源 , ReplicaSet controler 通过监听 ReplicaSet 资源来维持应用希望的状态 数量,ReplicaSet 中通过 selector 来匹配所关联的 Pod,在这里考虑 ReplicaSet rsA 的,replicas 从 2 被改到 3 的场景。
Reflector
会 watch 到 ReplicaSet 和 Pod 两种资源的变化,为 什么我们还会 watch pod 资源的变化稍后会讲到。发现 ReplicaSet 发 生变化后,在 delta 队列中塞入了对象是 rsA,而且类型是更新的记录。
Informer
一方面把新的 ReplicaSet 更新到缓存中,并与 Namespace nsA 作为索引。另外一方面,调用 Update 的回调函数,ReplicaSet 控 制器发现 ReplicaSet 发生变化后会把字符串的 nsA/rsA 字符串塞入到 工作队列中,工作队列后的一个 Worker 从工作队列中取到了 nsA/rsA 这个字符串的 key,并且从缓存中取到了最新的 ReplicaSet 数据。
Worker
通过比较 ReplicaSet 中 spec 和 status 里的数值,发现需要 对这个 ReplicaSet 进行扩容,因此 ReplicaSet 的 Worker 创建了一个 Pod,这个 pod 中的 Ownereference 取向了 ReplicaSet rsA。
(1)客户端提交创建请求,可以通过API Server的Restful API,也可以使用kubectl命令行工具。支持的数据类型包括JSON和YAML。
(2)API Server处理用户请求,存储Pod数据到etcd。
(3)调度器通过API Server查看未绑定的Pod。尝试为Pod分配主机。
(4)过滤主机 (调度预选):调度器用一组规则过滤掉不符合要求的主机。比如Pod指定了所需要的资源量,那么可用资源比Pod需要的资源量少的主机会被过滤掉。
(5)主机打分(调度优选):对第一步筛选出的符合要求的主机进行打分,在主机打分阶段,调度器会考虑一些整体优化策略,比如把容一个Replication Controller的副本分布到不同的主机上,使用最低负载的主机等。
(6)选择主机:选择打分最高的主机,进行binding操作,结果存储到etcd中。
(7)kubelet根据调度结果执行Pod创建操作: 绑定成功后,scheduler会调用APIServer的API在etcd中创建一个boundpod对象,描述在一个工作节点上绑定运行的所有pod信息。运行在每个工作节点上的kubelet也会定期与etcd同步boundpod信息,一旦发现应该在该工作节点上运行的boundpod对象没有更新,则调用Docker API创建并启动pod内的容器。