Kubernetes - Pod 基本概念
1 Pod 的进程模型
Pod 是一组容器的集合,而为什么实现上需要多个容器?据传,因为开发 Borg 项目的工程师发现,部署的应用往往都是一个进程组,而不是一个进程。换个角度说,部署的业务往往有着多个不同的进程,并且往往需要部署到同一个机器上。
容器是 “单进程模型”,在云上代表的是一个进程。而 Pod 是 “进程组模型”,在云上代表着就是进程组。
“单进程模型” 不代表容器只有一个进程,而是指容器无法管理多进程的能力。
你可以将其理解为,Pod 实现了 Supervisor/goreman 这样的功能,将多个进程进行分组管理,这对于业务应用的部署是非常有用的。
所以 Kubernetes 是一个操作系统,管理着 Pod “进程组”。
1.1 Pause 容器
在创建 Pod 时,会先创建一个 pause 容器
,也称为 根容器
或者 infra 容器
。
pause 容器使用 k8s.gcr.io/pause 镜像,永远处于暂停,占用着很少的资源。
因为容器共享资源的特殊性。容器在需要共享 namespace 时,需要先创建一个 namespace,然后让其他的容器加入这个 namespace。
这带来的一个问题就是:如果容器 A 先启动,容器 B 加入。那么容器 A 异常重启后,是容器 A 加入容器 B,还是重新启动容器 B 再次加入容器 A。
很难选择,实现起来也很复杂,因为两个容器有着前后顺序的依赖性,而不是“平等的”。
Kubernetes 使用一个占用极少资源,非业务容器的 pause 容器首先启动。然后在其 pause 容器配置好 namespace。接着其他的业务容器加入到 pause 容器的 namespace。
因为 pause 容器占用极少资源,也没有任何逻辑,所以其他业务容器重启后,只需要加入 pause 容器的 namespace 即可,不需要担心 namespace 发生变化。
1.2 容器共享的资源
Pod 内容器默认共享 network uts ipc namespace,所以容器间的网络是相同的。
Pod 可以配置开启共享 PID namespace,使得容器之间可以看到对方的进程。
spec.shareProcessNamespace
为 true 来共享 pid namespace。Pod 通过 Volume 的抽象概念来进行共享存储,将其块设备或者目录提供到每个容器内的不同路径。也就是说,每个容器看到的目录路径或者块设备是独立的,但是都对应同一个宿主机的目录或块设备。
2 Dynamic Pod 与 Static Pod
2.1 Dynamic Pod
大多数情况,我们会通过 ReplicaSet 或者 Deployment 这样的副本控制器来创建 Pod,甚至通过编写 Pod 的定义来手动部署 Pod。这种情况下,Pod 是由 Kubernetes 控制面管理的,可以通过 APIServer 启停它。
这样的 Pod 也被称为 动态 Pod。
2.2 Static Pod
另一类是特殊的 Pod,被称为 静态 Pod。
静态 Pod 是由本地 kubelet 创建的,仅仅在 kubelet 的 Node 上运行。Kubernetes 控制面可以看见,但是无法管理它。kubelet 也不会其进行健康检查。
创建静态 Pod 有着两种方式:配置文件 和 HTTP 方式。
2.2.1 配置文件方式
在 kubelet 的配置文件中社会中 staticPodPath
为一个目录,kubelet 会扫描该目录,读取目录下的 .yaml 或 .json 文件,创建对应的 Pod。
你在 APIServer 可以看到该 Pod,但是如果你进行删除操作,该 Pod 会一直进入 Pending 状态,但是不会真正被停止。
删除该 Pod 只能通过删除目录下的对应 .yaml 文件。
2.2.2 HTTP 方式
设置 kubelet 的启动参数 --manifest-url
,kubelet 会定期从该 URL 地址下载文件,然后以 yaml 或 json 格式解析,然后创建对应的 Pod。
3 Pod 的生命周期
3.1 Pod phase
Pod 的 status.phase
属性表明了 Pod 的生命周期阶段:
|
|
Pod 生命周期包含如下阶段:
状态 | 含义 |
---|---|
Pending | Pod 被创建后到 Pod 内所有容器成功创建前,都属于 Pending 阶段,包括了下载镜像期间。 |
Running | Pod 内的所有容器被成功创建,至少一个容器还在运行(即使其他容器异常重启)。 |
Succeeded | Pod 内所有容器成功执行并退出(退出码为 0),并且 Pod 不再会被重新拉起。 |
Failed | Pod 内所有容器都已经停止,并且至少一个容器是异常退出的(退出码非 0)。 |
Unknown | 无法得知 Pod 的状态。大多数情况是 Node 失联导致。 |
你会发现通过 kubectl get pods
输出的 STATUS 可能并不是上述的 phase,而是 Pod 中容器状态的一个原因,即 status.containerStatuses[].state.<>.reason
,这是为了直观输出 Pod 的异常原因。
通过 kubectl get pods -o yaml
可以看到 status.phase
。
3.2 Container State
Kubernetes 会跟踪 Pod 下每个容器的状态,位于 status.containerStatuses.state
字段。
容器可能的状态有:
状态 | 含义 |
---|---|
Waiting | 等待运行。可能由于正在拉取 image,或者等待 init container 运行等情况。 |
Running | 容器正在运行中。 |
Terminated | 容器运行结束,无论正常退出还是异常退出。 |
通过 status.containerStatuses.state
你可以看到为啥容器处于该状态。
3.2.1 容器声明周期回调
Kubernetes 提供两个容器生命周期的回调:
-
postStart :在容器创建后立即异步执行,因此不保证在容器的 entrypoint 前执行。
但是 kubelet 会等待 postStart 后执行完,才将容器的状态变为 Running。
-
preStop :在 kubelet 发送容器停止信号前,执行的操作。
Note注意,preStop 仅仅在 kubelet 停止容器的情况下才会执行(因为这是 kubelet 执行的操作),而容器自己退出的情况下不会执行。
使用回调的示例如下:
|
|
可以看到,回调可以配置两种执行方式:
httpGet
:执行一次 HTTP GET 请求;exec
:执行命令行;
3.3 Pod Condition
Pod 的 status.conditions
表明了 Pod 的一些细节情况。
默认包含以下的 condition:
Conditon | 含义 |
---|---|
PodScheduled | Pod 是否已经被调度到某个节点。 |
Initialized | 所有 Init Container 是否成功执行。 |
ContainersReady | 所有容器是否是就绪的(运行中)。 |
Ready | Pod 是否能够提供服务,即能否加入到 Service 的 Endpoints 中。 Ready 状态受到 Readiness Probe 的控制。 |
|
|
可以看到每个 Condition 都包含了如下信息:
type
:condition 类型;status
:condition 是否正常,包括 True False Unknown;lastProbeTime
:上一次检查该 condition 的时间;lastTransitionTime
:上一次 condition status 变化的时间;reason
:上一次 condition status 变化的原因;message
:上一次 conditon status 变化的原因信息;
3.3.1 自定义 condition
除了默认的 condition 外,可以通过 spec.readinessGates
来自定义 condition。
自定义 condition 往往由一个 controller 控制,通过自定义 controller 来主动将自定义 condition 设置为 true。
自定义 condition 仅仅会影响到 Ready condition,只有所有自定义 condition 为 true 后,Ready condition 才能变为 true。
|
|
3.4 Pod 重启策略
通过 spec.restartPolicy
指定 Pod 的重启策略,其可能的值为:
- Always :容器退出时,kubelet 自动重启该容器;
- OnFailure :容器退出且退出码非 0 时,kubelet 自动重启容器;
- Never :任何容器提出,kubelet 都不重启容器;
kubelet 重启容器的时间间隔以指数的方式增长(1n、2n、4n …),最长延迟 5min。如果容器运行了 10min 没有退出,则间隔时间会被重置。
4 Pod 的停止
通常情况下,CRI 会先给每个 Container 的主进程发送一个 SIGTERM
信号(或者是 Image 定义的 STOPSIGNAL
信号),来尝试优雅停止 Pod。
一旦优雅停止超时,CRI 会向所有剩余的进程发送 SIGKILL
信号,之后 Pod 就会从 APIServer 上移除。
更加细节的停止过程如下:
-
APIServer 中 Pod 对象被更新,通过
deletionTimestamp
将其标记为删除中。这时,kubectl describe
展示 Pod 为Terminating
。 -
kubelet
看到 Pod 为删除中,开始本地的 Pod 关闭过程。 -
如果 Container 定义了
preStop
回调,kubelet
开始在容器中执行回调。如果preStop
执行时间超过了terminationGracePeriodSeconds
时间,那么kubelet
每次 2s 继续执行preStop
。 -
kubelet
调用 CRI 来给每个 Container 的进程 1 发送SIGTERM
信号。 -
在
kubelet
关闭 Pod 的同时,Controller 会将 Pod 从Endpoints
或者EndpointSlice
对象中移除。 -
如果停止时间超过
terminationGracePeriodSeconds
,那么kubelet
会向 Pod 中所有的容器发送SIGKILL
信号,包括pause
容器。 -
kubelet
触发将 Pod 从 APIServer 中强制删除的逻辑,将terminationGracePeriodSeconds
设置为 0(表明立马删除 Pod 对象)。 -
APIServer 删除 Pod 对象,任何 Client 都无法看到该 Pod。
4.1 强制删除
默认情况下,所有的删除操作都有 30s 的优雅删除等待时间。可以通过配置 Pod 的 terminationGracePeriodSeconds
来配置等待时间。
如果将 Pod 的 terminationGracePeriodSeconds
设置为 0,那么 APIServer 会立即删除该 Pod 对象,而不会等待 kubelet
来修改 Pod 对象。
通过 kubectl delete --grace-period=0 --force
可以执行强制删除 Pod。
4.2 Pod GC
在控制面运行着 Pod GC Controller,会在 Pod 个数超过配置的阈值时(配置的 terminated-pod-gc-threshold
参数),删除已经终止的 Pod。这些 Pod 的 Phase 为 Succeeded
或者 Failed
。
5 Probe
Probe 以旁路的方式,周期性地检测 Pod 容器的情况,检测失败是及时更新 Pod 的状态,使得上层感知后进行一些恢复操作。
目前包含三种探针:LivenessProbe、ReadinessProbe 和 StartupProbe。
5.1 LivenessProbe
LivenessProbe
用于周期性判断一个容器是否是存活的,如果 LivenessProbe 探测失败,那么 kubelet 将会 “杀掉” 该容器。
容器停止后,后续的操作是由 重启策略 控制的。
5.2 ReadinessProbe
ReadinessProbe
用于周期性判断一个容器是否是 Ready,从而影响 Pod 的 Ready condition 是否为 True。
只有 Pod Ready condition 为 True 时,Service 才可以将其 Pod 加入到 Endpoints,从而该 Pod 才能接受请求并处理。
同样,当 Pod Ready condition 从 True 变为 False 时,Service 会将其 Pod 从 Endpoints 移除。
5.3 StartupProbe
StartupProbe
仅仅在容器启动后会运行,并且成功后不会再次运行,直到容器重新启动。
StartupProbe 是为了解决有些容器的启动情况很慢的情况,这种情况只需要进行一次成功的探测,所以不适合 ReadyinessProbe 的周期性语义。
5.4 使用 Probe
三种探针都可以使用三种探测方式:
-
exec
:执行一个 shell 命令,退出码为 0 表明容器健康;1 2 3 4 5 6 7 8 9
spec: containers: - name: liveness image: k8s.gcr.io/liveness livenessProbe: exec: command: - cat - /tmp/healthy
-
tcpSocket
:连接 TCP 的一个 IP 与 Port,连接成功表明容器健康;1 2 3 4 5 6 7 8 9
spec: containers: - name: goproxy image: k8s.gcr.io/goproxy:0.1 ports: - containerPort: 8080 readinessProbe: tcpSocket: port: 8080
-
httpGet
:发送一个 HTTP Get 请求,状态码 [200, 400) 表明容器健康;1 2 3 4 5 6 7 8 9 10
spec: containers: - name: liveness livenessProbe: httpGet: path: /healthz port: 8080 httpHeaders: - name: Custom-Header value: Awesome
每种探测器也包含如下的配置参数,用以控制探测的行为:
initialDelaySeconds
:容器启动后多久开始进行探测。仅仅适用于 LivenessProbe 与 ReadinessProbe。periodSeconds
:执行探测的时间间隔。默认 10s。timeoutSeconds
:每次探测的超时时间。默认 1s。successThreshold
:探测失败后,经过几次探测成功后,才将其变为健康状态,默认值为 1。仅仅适用于 StartupProbe。failureThreshold
:连续探测失败多少次后,才将其容器视为不健康状态,默认值为 3。
6 Init Container
大多数应用在启动前都需要进行一些初始化的操作。Kubernetes 提供了 init container 来进行 Pod 的初始化操作。
init container
也是普通的容器,但是仅仅只运行一次就结束。多个 init container 的情况下,按照定义的顺序运行,并且只有前面的运行成功后才会运行后面的。
某个 init container 运行失败后,其 Pod 的启动流程会终止。而其后的操作就是受到 重启策略 的控制。
init container 在 spec.initContainers
中定义:
|
|
init container 与应用容器的区别如下:
- 启动顺序。init container 全部启动结束后,Kubernetes 才会初始化 Pod 各种信息,然后启动应用容器
- 资源限制,见:Resource。
- init container 不能设置 ReadinessProber。
- Pod 重新启动时,init container 将会重新运行。所以 init container 应该是幂等的。
7 Ephemeral Container
有时由于容器崩溃或者容器镜像不包含 debug 工具,使得通过 kubectl exec
方式不能很好的进行调试。v1.18 版本开始,新添加一个 kubectl debug
命令,用于创建 ephemeral container
来进行 debug。
具体使用方式可以参考:Ephemeral Container。