| 导语 从这篇文章开始, 我们将详细介绍 K8S 的各个组件. 学习一项技术, 理论先行, 只有充分的了解了内在原理才能在日后的维护和调优方面有所思路.
一, 什么是 Pod
1. Pod 是什么
Pod 的介绍, 我们前面在第二篇文章里有个大概的解释. 你可以把它想象成一个 "豆荚" , 然后里面包着一组有关联关系的 "豆子"(容器).
一个豆荚里的豆子, 它们共同吸收着同一个养分, Pod 也是如此, 里面的容器共有着同一组资源. K8S 官方文档对 Pod 的描述是: A Pod is the basic building block of Kubernetes-the smallest and simplest unit in the Kubernetes object model that you create or deploy. A Pod represents processesrunning on your Cluster. 翻译成中文: Pod 是 K8S 的基本构建模块, 它是你在 K8S 集群里能创建或部署的最小和最简单的单元.
刚学习 K8S 的同学一般会认为容器 Docker 是最小单元, 其实不是, Pod 才是 K8S 里最小的单元. 其实, 你把 Pod 想成虚拟机, 然后容器想成虚拟机里面的应用你就明白很多了. 我们还是拿 K8S 跟 OpenStack 对比就明白了:
K8S | OpenStack |
---|---|
Pod | VM |
Docker | application 应用、进程 |
OpenStack 管理的 VM 可以说是 OpenStack 里的最小单元, 虚拟机我们知道有隔离性, 里面部署的应用只跑在虚拟机里面, 他们共享这个 VM 的 CPU,Mem, 网络, 存储资源. 那么, 同理 Pod 也是如此, Pod 里面的容器共享着 Pod 里面的 CPU,Mem, 网络和存储资源. 那么 Pod 是如何做到的呢? 我们接着看下面的知识点你就明白了.
2. Pod 的特点
集群里的最小单元
注意 K8S 集群的最小单元是 Pod, 而不是容器; K8S 直接管理的也是 Pod, 而不是容器.
Pod 里可以运行一个或多个容器
如果 Pod 里只运行一个容器, 那就是 "one-container-per-Pod" 模式. 当然你也可以把一组有关联关系的容器放在一个 Pod 里面, 这些容器共享着同一组网络命名空间和存储卷. 比如 K8S 官网文档里就举了一个例子, 把一个 web Server 的容器跟 File Puller 的容器放在一起(Web Server 对外提供 Web 服务, File Puller 负责内容存储和提供), 然后它们就形成了一个 Pod, 构成了一个统一的服务单元.
另外, 我们也可以想复杂点, 比如我们在传统运维里搭建的 LAMP 环境, 如果把它们容器化部署在 K8S 集群里, 那么是怎样个模式? IAAS 层里面部署 LAMP 环境, 如果是规模小的话, 用一个虚拟机就可以把 apache + MySQL + PHP 环境部署起来, 规模大的话多用几个虚拟机, 然后做分布式. 那么如果是 PAAS 层 Pod 容器化部署呢? 是否是 apache + MySQL + PHP 分别跑一个 docker, 然后打包放在一个 Pod 里, 然后规模大就多起几个 Pod?
Pod 里的容器共用相同的网络和存储资源
在 K8S 里, 每个 Pod 会被分配唯一的 IP 地址, 然后里面的容器都会共享着这个网络空间, 这个网络空间包含了 IP 地址和网络端口. Pod 容器内部通信用的是 localhost, 如果要跟外面通信, 就需要用到共享的那个 IP 和端口.
Pod 可以指定共享的存储 Volume, 然后 Pod 里的所有容器都有权限访问这个 Volume.Volume 是持久化数据用的, Pod 的重启都不会影响 Volume 里的数据.
Pod 里的容器共用相同的依赖关系
前面提到过有关联关系的容器可以放在一个 Pod 里, 那么这里的关联关系怎么理解? 通常, 我们会把有紧耦合的服务部署在一个 Pod 里, 比如 LAMP 应用栈. 这里做的目的就是能做到共同调度和协调管理. 所以, 没有关联的容器最好不要放一个 Pod 里, 没有规则的乱放, 你将无法体会到 K8S 的强大之处 -- 编排.
二, Pod 内部机制
1. Pod 实现原理
我们在学习容器的时候就了解到因为 Linux 提供了 Namespace 和 Cgroup 两种机制才让容器的出现提供了可能. Namespace 用于进程之间的隔离, Cgroup 用于控制进程资源的使用; Namespace 由 hostname,PID,filesystem,network,IPC 组成. 在 K8S 里, Pod 的生成也是基于 Namespace 和 Cgroup 的, 所以 Pod 内的架构合成, 我们可以用下面这张图画出来:
Pod 架构
那这些要素是通过什么机制组合在一起呢? 这里是通过一个叫 Pause(gcr.io/google_containers/pause-amd64)的容器完成的. K8S 在初始化一个 Pod 的时候会先启动一个叫 Pause 的容器, 然后再启动用户自定义的业务容器. 这个 Pause 容器我们认为它是一个 "根容器", 它主要有两方面作用:
扮演 PID 1 的角色, 处理僵尸进程
在 Pod 里为其他容器共享 Linux namespace 的基础
首先我们了解下 Linux 系统下 PID 为 1 的进程它的作用和意义. 在 Linux 里, PID 为 1 的进程, 叫超级进程, 也叫根进程, 它是系统的第一个进程, 是其他进程的父进程, 所有的进程都会被挂在这个进程下. 如果一个子进程的父进程退了, 那么这个子进程会被挂到 PID 1 下面.
其次, 我们知道容器本身就是一个进程. 在一个 Namespace 下, Pause 作为 PID 为 1 的进程存在于一个 Pod 里, 其他的业务容器都挂载这个 Pause 进程下面. 这样, 一个 Namespace 下的进程就会以 Pause 作为根, 呈树状的结构存在一个 Pod 下.
最后, Pause 还有个功能是负责处理僵尸进程. 僵尸进程: 一个进程使用 fork 函数创建子进程, 如果子进程退出, 而父进程并没有来得及调用 wait 或 waitpid 获取其子进程的状态信息, 那么这个子进程的描述符仍然保存在系统中, 其进程号会一直存在被占用(而系统的进程号是有限的), 这种进程称之为僵尸进程(Z 开头).
Pause 这个容器代码是用 C 写的 (代码见下), 其中 Pause 的代码里, 有个无限循环的 for(;;) 函数, 函数里面执行的是 pause( )函数, pause() 函数本身是在睡眠状态的, 直到被信号 (signal) 所中断. 因此, 正是因为这个机制, Pause 容器会一直等待 SIGCHLD 信号, 一旦有了 SIGCHLD 信号(进程终止或者停止时会发出这种信号),Pause 就会启动 sigreap 方法, sigreap 方法里就会调用 waitpid 获取其子进程的状态信息, 这样自然就不会在 Pod 里产生僵尸进程了.
- ## Pause 代码
- static void sigdown(int signo) {
- psignal(signo, "Shutting down, got signal");
- exit(0);
- }
- static void sigreap(int signo) {
- while (waitpid(-1, NULL, WNOHANG)> 0);
- }
- int main() {
- if (getpid() != 1)
- /* Not an error because pause sees use outside of infra containers. */
- fprintf(stderr, "Warning: pause should be the first process\n");
- if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
- return 1;
- if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
- return 2;
- if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
- .sa_flags = SA_NOCLDSTOP},
- NULL) < 0)
- return 3;
- ## 关注这下面的 for 循环代码
- for (;;)
- pause();
- fprintf(stderr, "Error: infinite loop terminated\n");
- return 42;
- }
2. Pod 的生命周期
这一点我们来分析下 Pod 的生命周期 lifecycle, 在 K8S 官方文档里, 分别通过 phase,Conditions,Container probes,Container States,Pod readiness gate,Restart policy,Pod lifetime 等几方面来讲述.
2.1 Pod 的状态值有哪些 - phase
Pod 的状态是 PodStatus 对象里的 phase 字段来表示的, 这个 phase 字段有以下一些值:
阶段名 | 描述 |
---|---|
Pending | K8S 集群里已经发起创建 Pod 请求,里面的 Pod 还没有容器。这个阶段一般是发生在 Pod 被调度之前或者 Pod 里的镜像正在下载。 |
Running | Pod 已经调度落地在了一个 Node 里,并且里面的容器已经创建好了。里面至少一个容器正在运行或者正在启动或者重新启动。 |
Succeeded | Pod 里面的所有容器成功运行,也没发生重启等行为。 |
Failed | Pod 里面的所有容器终止,至少有一个容器以失败方式终止。也就是说,这个容器要么已非 0 状态退出,要么被系统终止。 |
Unknown | 由于一些原因,Pod 的状态无法获取,通常是与 Pod 通信时有出错。 |
2.2 Pod Conditions
前面说到 PodStatus 对象里有 phase 字段, 那么 PodStatus 里也包含 Pod Conditions, 这是一个数组, 里面包含的属性有:
字段 | 描述 |
---|---|
lastProbeTime | 最后一次探测 Pod Condition 的时间戳。 |
lastTransitionTime | 上次 Condition 从一种状态转换到另一种状态的时间。 |
message | 上次 Condition 状态转换的详细描述。 |
reason | Condition 最后一次转换的原因。 |
status | Condition 状态类型,可以为 “True”, “False”, and “Unknown”. |
type | Condition 类型,包括以下方面: - PodScheduled(Pod 已经被调度到其他 node 里) - Ready(Pod 能够提供服务请求,可以被添加到所有可匹配服务的负载平衡池中) - Initialized(所有的 init containers 已经启动成功) - Unschedulable(调度程序现在无法调度 Pod,例如由于缺乏资源或其他限制;) - ContainersReady(Pod 里的所有容器都是 ready 状态) |
2.3 Container probes
probes 中文就是探针的意思, 所以 container probes 翻译成中文就是 "容器探针". 这是 K8S 的一种诊断容器状态的机制. 我们知道 Node 里会运行 kubelet 进程, 它有一个作用是收集容器的状态, 然后汇报给 master 节点."容器探针" 这种机制就是通过 kubelet 来实现的. 那么 kubelet 是怎么知道节点里容器状态信息的呢? 具体的主要是 kubelet 调用容器提供的 Handler(钩子)三种处理程序:
ExecAction : 在容器内执行指定的命令. 如果命令以状态代码 0 退出, 则认为诊断成功, 容器是健康的.
TCPSocketAction : 通过容器的 IP 地址和端口号执行 TCP 检查. 如果端口存在, 则认为诊断成功, 容器是健康的.
HTTPGetAction : 通过容器的 IP 地址和端口号及路径调用 HTTP GET 方法, 如果响应的状态码大于等于 200 且小于 400, 则认为容器状态健康.
每个 Container probes 都会获得三种结果:
成功: 容器通过了诊断.
失败: 容器未通过诊断.
未知: 诊断失败, 不应采取任何措施.
另外, kubelet 可以在运行的 Containers(容器)里, 有两种探针方式:
livenessProbe : 存活探针, 这个是为了表明容器是否正在运行, 服务是否正常. 如果 LivenessProbe 探测到容器不健康, 则 kubelet 会杀死 Container, 并且根据 Container 的重启策略来重启. 如果 Container 未提供 livenessProbe, 则默认状态为 Success .
readinessProbe : 就绪探针, 这个是为了表明 Container 是否已准备好提供服务(是否启动成功). 如果 readinessProbe 探测失败, 则 Container 的 Ready 将为 False, 控制器将此 Pod 的 Endpoint 从对应的 service 的 Endpoint 列表中移除, 从此不再将任何请求调度此 Pod 上, 直到下次探测成功. 如果 Container 未提供 readinessProbe, 则默认状态为 Success .
为什么会有这两种探针机制, 主要是 POD 的生命周期会受到很多环境条件的影响, 比如 POD 内部各个容器的状态, 容器依赖的上游或者周边服务的状态等等. 所以需要有一个机制来根据容器不同的状态来决定 POD 是否健康. 所以, liveness 和 readiness 探测就是为了解决这个问题产生的.
下面我们用两张动态图来介绍下这两种探针具体的工作方式(图网上借用, 侵删):
比如有个 Pod 通过 LivenessProbe 探测发现无法再提供服务了, 那么 LivenessProbe 会根据容器重启策略判断它是否重启, 策略通过后, 执行新 Pod 替代操作.
Liveness 探针机制工作动态图
有时候一些应用需要一段时间来预热和启动, 比如一个后端项目的启动需要先启动消息队列或者数据库等才能提供服务. 那么这样的情况, 使用就绪探针比较合适.
Readiness 探针机制工作动态图
那么在具体的生产环境实践中, 什么时候用 Liveness? 什么时候用 Readiness? 这里总结了一些经验, 可以参考下:
(1)Liveness 跟 Readiness 应该直接探测程序, 不要走间接拐弯的探测方式.
(2)Liveness 探测的程序里不要做任何其他逻辑, 它很简单就是探测服务是否运行正常. 如果主线程是正常的, 那就直接返回 200, 不是的话就返回 5xx. 如果有其他逻辑存在, 则探测程序会把握不准.
(3)Readiness 探测的程序里有相关的处理逻辑. Readiness 主要是探测判断容器是否已经准备好对外提供服务. 因此, 实现一些逻辑来检查目标程序后端所有依赖组件的可用性非常重要. 实现 Readiness 探测时, 需要清楚的知道所探测的程序依赖于哪些功能, 并且这些依赖的功能什么时候准备 OK. 例如, 如果应用程序需要先建立与数据库的连接然后才能提供服务, 那么在 "Readiness" 的处理程序中就必须检查是否已建立与数据库连接才能最终确认程序是否就绪.
(4)Readiness 不要嵌套使用. 也就是说某个程序已经使用了 Readiness 做探测, 那么外面不要再套一层 Readiness.
最后, Liveness 跟 Readiness YAML 配置语法上是一样的, 也就是说同样的 YAML 配置文件, 你把 Liveness 设置成 Readiness 即可使用.
2.4 Container States 容器状态
一旦 Pod 落地 Node 被创建了, kubelet 就会在 Pod 里创建容器了. 容器在 K8S 里有三种状态: Waiting,Running 和 Terminated. 如果你要检查容器的状态, 我们可以使用命令 kubectl describe pod [POD_NAME] , 这个命令会显示该 Pod 里每个容器的状态. 另外, K8S 在创建资源对象时, 可以使用 lifecycle 来管理容器在运行前和关闭前的一些动作. lifecycle 有两种回调函数:
PostStart: 容器创建成功后, 运行前的任务, 用于资源部署, 环境准备等.
PreStop: 在容器被终止前的任务, 用于优雅关闭应用程序, 通知其他系统等等.
Waiting: 这是容器的默认状态. 如果容器未处于 "正在运行" 或 "已终止" 状态, 则它就是 "Waiting" 状态. 处于 Waiting 状态的容器仍然可以运行其所需的操作, 如拉图像, 应用秘密等. 在这个状态下, Reason 字段将显示一些原因表示为什么会是 Waiting 状态.
- ...
- State: Waiting
- Reason: ErrImagePull
- ...
Running: 表示容器正在运行. 一旦容器进入 Running, 如果有 postStart 的话, 将会执行. 另外, Started 字段会显示容器启动的具体时间
- ...
- State: Running
- Started: Wed, 30 Jan 2019 16:46:38 +0530
- ...
Terminated: 表示容器已终止运行. 容器在成功完成执行或由于某种原因失败就出现此状态. 容器终止的原因, 退出代码以及容器的开始和结束时间都会一起显示出来(如下示例所示). 另外在容器进入 Terminated 之前, 如果有 preStop 则会执行.
- ...
- State: Terminated
- Reason: Completed
- Exit Code: 0
- Started: Wed, 30 Jan 2019 11:45:26 +0530
- Finished: Wed, 30 Jan 2019 11:45:26 +0530
- ...
2.5 Pod 生命周期控制方法
一般情况下, Pod 如果不被人为干预或被某个控制器删除, 它是不会消失的. 不过, 例外情况就是处于 "Succeeded" 或者 "Failed" 的 Pod, 如果处于此种状态超过一定的时间, 比如 terminated-pod-gc-threshold 的设定值, 就会被垃圾回收机制清除.
注: terminate-pod-gc-threshold 在 master 节点里, 它的作用是设置 gcTerminated 的阈值, 默认是 12500s.
三种类型的控制器控制 Pod 生命周期的方法:
Job: 适用于一次性任务如批量计算, 任务结束后 Pod 会被此类控制器清除. Job 的重启策略只能是 "OnFailure" 或者 "Never".
ReplicationController, ReplicaSet, or Deployment, 此类控制器希望 Pod 一直运行下去, 它们的 restart policy 只能是 "always".
DaemonSet: 每个 node 一个 Pod, 很明显此类控制器的 restart policy 应该是 "always".
3. Pod 资源使用机制
我们前面提到过 Pod 好比一个虚拟机, 虚拟机我们是能分配固定的 CPU,Mem,Disk, 网络资源的. 同理, Pod 也是如此, 那么 Pod 如何使用和控制这些分配的资源呢?
首先, 我们先了解下 CPU 资源的分配模式:
计算机里 CPU 的资源是按 "时间片" 的方式分配给请求的, 系统里的每一个操作都需要 CPU 的处理, 我们知道 CPU 的单位是 Hz,GHz(1Hz = 1/s, 即在单位时间内完成振动的次数, 1GHz = 1 000 000 000 Hz = 1 000 000 000 次 / s), 频率越大, 单位时间内完成的处理次数就越多. 所以, 哪个任务要是申请的 CPU 时间片越多, 那么它得到的 CPU 资源就越多.
其次, 我们再了解一些 Cgroup 里资源的换算单位:
CPU 换算单位
- 1 CPU = 1000 millicpu(1 Core = 1000m)
- 0.5 CPU = 500 millicpu (0.5 Core = 500m)
这里的 m 就是毫, 毫核的意思, K8S 集群中的每一个节点可以通过操作系统的命令来确认本节点的 CPU 内核数量, 然后将这个数量乘以 1000, 得到的就是节点总 CPU 总毫数. 比如一个节点有四核, 那么该节点的 CPU 总毫量为 4000m. 如果你要使用 0.5 core, 则你要求的是 4000*0.5 = 2000m.
K8S 里是通过以下两个参数来限制和请求 CPU 的资源的:
spec.containers[].resources.limits.CPU CPU 上限值, 可以短暂超过, 容器也不会被停止.
spec.containers[].resources.requests.CPU CPU 请求值, K8S 调度算法里的依据值, 可以超过.
这里需要明白的是, 如果 resources.requests.CPU 设置的值大于集群里每个 Node 的最大 CPU 核心数, 那么这个 Pod 将无法调度(很容易理解, 没有 Node 能满足它).
例子, 我们在 YAML 里定义一个容器 CPU 资源如下:
- resources:
- requests:
- memory: 50Mi
- CPU: 50m
- limits:
- memory: 100Mi
- CPU: 100m
这里, CPU 我们给的是 50m, 也就是 0.05core, 这 0.05 core 也就是占了 1 CPU 里的 5% 的资源时间.
另外, 我们还要知道 K8S CPU 资源这块, 它是一个可压缩性的资源. 如果容器达到了 CPU 设定值会开始限制, 容器性能会下降, 但是不会终止和退出.
最后我们了解下 MEM 这块的资源控制:
单位换算: 1 MiB = 1024 KiB , 这里注意的是 MiB ≠ MB,MB 是十进制单位, MiB 是二进制, 平时我们以为 MB 等于 1024KB, 其实 1MB=1000KB,1MiB 才等于 1024KiB. 中间带字母 i 的是国际电工协会 (IEC) 定的, 走 1024 乘积; KB,MB,GB 是国际单位制, 走 1000 乘积.
内存这块在 K8S 里一般用的是 Mi 单位, 当然你也可以使用 Ki,Gi 甚至 Pi, 看具体的业务需求和资源容量.
这里要注意的是, 内存这里不是可压缩性资源, 如果容器使用内存资源到达了上限, 那么会 OOM, 造成内存溢出, 容器就会终止和退出.
三, Pod 基本操作命令
说明 | 具体命令 |
---|---|
创建 | kubectl create -f xxx.yaml |
查询 | kubectl get pod PodName / kubectl describe pod PodName |
删除 | kubectl delete pod PodName |
更新 | kubectl replace /path/to/newPodName.yaml (当然也可以加 --force 强制替换) |
查看 logs 输出 | kubectl logs PodName |
命令这东西其实多半是要多用才能熟练, 很多敲着敲着你就会了. 根据本人个人经验, 计算机 IT 里的命令都离不开这些关键词: create,get,delete ... 当然, 还有万能的 --help.
来源: https://www.qcloud.com/developer/article/1443520