05-K8 Pod:最小调度单元的使用进阶及实践
通过上一节课的学习,相信你已经知道了 Pod 是 Kubernetes 中原子化的部署单元,它可以包含一个或多个容器,而且容器之间可以共享网络、存储资源。在日常使用过程中,也应该尽量避免在一个 Pod 内运行多个不相关的容器,具体原因在上一节课中也已经详细阐述。
在实际生产使用的过程中,通过 kubectl 可以很方便地部署一个 Pod。但是 Pod 运行过程中还会出现一些意想不到的问题,比如:
-
Pod 里的某一个容器异常退出了怎么办?
-
有没有“健康检查”方便你知道业务的真实运行情况,比如容器运行正常,但是业务不工作了?
-
容器在启动或删除前后,如果需要做一些特殊处理怎么办?比如做一些清理工作。
-
如果容器所在节点宕机,重启后会对你的容器产生影响吗?
-
……
在这一课时中,我将通过一些示例一一解答你的这些问题,并告诉你 Pod 最佳的使用方法。
在了解 Pod 的高阶用法之前,我们先聊聊 Pod 的运行状态。
Pod 的运行状态
我们先回到上一节 04 课时中的例子:
apiVersion: v1 #指定当前描述文件遵循v1版本的Kubernetes API
kind: Pod #我们在描述一个pod
metadata:
name: twocontainers #指定pod的名称
namespace: default #指定当前描述的pod所在的命名空间
labels: #指定pod标签
app: twocontainers
annotations: #指定pod注释
version: v1
releasedBy: david
purpose: demo
spec:
containers:
- name: sise #容器的名称
image: quay.io/openshiftlabs/simpleservice:0.5.0 #创建容器所使用的镜像
ports:
- containerPort: 9876 #应用监听的端口
- name: shell #容器的名称
image: centos:7 #创建容器所使用的镜像
command: #容器启动命令
- "bin/bash"
- "-c"
- "sleep 10000"
我们通过 kubectl 创建 Pod 成功后,可以通过如下命令看到 Pod 的状态:
$ kubectl get pod twocontainers -o=jsonpath='{.status.phase}'
Pending
注:我们这里使用了 kubectl 命令行 JSONPATH 模板能力,你可以将这条命令当作一个 tip,在日常工作中使用。在本专栏的最后,我们也会单独讲一些 kubectl 的使用秘笈,在此不展开讲。
我们看到,这个时候 Pod 处于Pending
状态,具体的值来自 Pod 对象的status.phase
字段。
你也可以使用 kubectl get 命令来查看容器的状态:
$ kubectl get pod twocontainers
NAME READY STATUS RESTARTS AGE
twocontainers 0/2 ContainerCreating 0 13s
看到这里,你会发现这个地方显示的是ContainerCreating
,这和上面的Pending
不一致啊!先别急,我们来 describe 一下(这里我只截取跟 Pod 状态最相关的片段):
$ kubectl describe pod twocontainers
Name: twocontainers
Namespace: default
...
Status: Pending
IP:
IPs: <none>
Containers:
sise:
Container ID:
Image: quay.io/openshiftlabs/simpleservice:0.5.0
...
State: Waiting
Reason: ContainerCreating
Ready: False
Restart Count: 0
...
shell:
Container ID:
Image: centos:7
Image ID:
...
State: Waiting
Reason: ContainerCreating
Ready: False
...
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned default/twocontainers to node-1
Normal Pulling 3m57s kubelet, node-1 Pulling image "quay.io/openshiftlabs/simpleservice:0.5.0"
可以看到,这边 Status 依然是Pending
。其实这是 kubectl 在显示时做的转换,它会遍历容器的 State,如果容器的状态为Waiting
的话,就读取State.Reason
字段作为 Pod 的 Status。这个时候由于镜像在本地不存在,需要去镜像中心拉取。
一般来说,处于Pending
状态的 Pod,不外乎以下 2 个原因:
-
Pod 还未被调度;
-
Pod 内的容器镜像在待运行的节点上不存在,需要从镜像中心拉取。
等待镜像拉取结束,再来查看 Pod 的状态,已经变为Running
状态。
$ kubectl get pod twocontainers -o=jsonpath='{.status.phase}'
Running
$ kubectl describe pod twocontainers
Name: twocontainers
Namespace: default
...
Start Time: Wed, 26 Aug 2020 16:49:11 +0800
...
Status: Running
...
Containers:
sise:
Container ID: docker://4dc8244a19e6766b151b36d986b9b3661f3bf05260aedd2b76dd5f0fcd6e637f
Image: quay.io/openshiftlabs/simpleservice:0.5.0
Image ID: docker-pullable://quay.io/openshiftlabs/simpleservice@sha256:72bfe1acc54829c306dd6683fe28089d222cf50a2df9d10c4e9d32974a591673
...
State: Running
Started: Wed, 26 Aug 2020 17:00:52 +0800
Ready: True
...
shell:
Container ID: docker://1b6137b4cef60d0309412f5cdba7f0ff743ee03c1112112f6aadd78f9981bbaa
Image: centos:7
Image ID: docker-pullable://centos@sha256:19a79828ca2e505eaee0ff38c2f3fd9901f4826737295157cc5212b7a372cd2b
...
State: Running
Started: Wed, 26 Aug 2020 17:01:46 +0800
Ready: True
...
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
...
这个时候,就标志着 Pod 内的所有容器均被创建出来了,且至少有一个容器为在运行状态中。那么如果想知道 Pod 内所有的容器是否都在运行中呢?我们可以通过 kubectl get 来看到:
$ kubectl get pod twocontainers
NAME READY STATUS RESTARTS AGE
twocontainers 2/2 Running 0 2m
在这里,我们看到2/2
。前一个 2 表示目前正在运行的容器数量,后一个 2 表示定义的容器数量。当这两个数值相等的时候,就可以标识着 Pod 内所有容器均正常运行。
Pod 的 Status 除了上述的Pending
、Running
以外,官方还定义了下面这些状态:
-
Succeeded
来表示 Pod 内的所有容器均成功运行结束,即正常退出,退出码为 0; -
Failed
来表示 Pod 内的所有容器均运行终止,且至少有一个容器终止失败了,一般这种情况,都是由于容器运行异常退出,或者被系统终止掉了; -
Unknown
一般是由于 Node 失联导致的 Pod 状态无法获取到。
既然 Pod 内的容器会出现异常退出状态,那么有没有一些重启策略可以让 Kubelet 对容器进行重启呢?
Pod 的重启策略
Kubernetes 中定义了如下三种重启策略,可以通过spec.restartPolicy
字段在 Pod 定义中进行设置。
-
Always 表示一直重启,这也是默认的重启策略。Kubelet 会定期查询容器的状态,一旦某个容器处于退出状态,就对其执行重启操作;
-
OnFailure 表示只有在容器异常退出,即退出码不为 0 时,才会对其进行重启操作;
-
Never 表示从不重启;
注:在 Pod 中设置的重启策略适用于 Pod 内的所有容器。
虽然我们可以设置一些重启策略,确保容器异常退出时可以重启。但是对于运行中的容器,是不是就意味着容器内的服务正常了呢?
比如某些 Java 进程启动速度非常慢,在容器启动阶段其实是无法提供服务的,虽然这个时候该容器是处于运行状态。
再比如,有些服务的进程发生阻塞,导致无法对外提供服务,这个时候容器对外还是显示为运行态。
那么我们该如何解决此类问题呢?有没有一些方法,比如可以通过一些周期性的检查,来确保容器中运行的业务没有任何问题。
Pod 中的健康检查
为此,Kubernetes 中提供了一系列的健康检查,可以定制调用,来帮助解决类似的问题,我们称之为 Probe(探针)。
目前有如下三种 Probe:
-
livenessProbe可以用来探测容器是否真的在“运行”,即“探活”。如果检测失败的话,这个时候 kubelet 就会停掉该容器,容器的后续操作会受到其重启策略的影响。
-
readinessProbe常常用于指示容器是否可以对外提供正常的服务请求,即“就绪”,比如 nginx 容器在 reload 配置的时候无法对外提供 HTTP 服务。
-
startupProbe则可以用于判断容器是否已经启动好,就比如上面提到的容器启动慢的例子。我们可以通过参数,保证有足够长的时间来应对“超长”的启动时间。 如果检测失败的话,同livenessProbe的操作。这个 Probe 是在 1.16 版本才加入进来的,到 1.18 版本变为 beta。也就是说如果你的 Kubernetes 版本小于 1.18 的话,你需要在 kube-apiserver 的启动参数中,显式地在 feature gate 中开启这个功能。可以参考这个文档查看如何配置该参数。
如果某个 Probe 没有设置的话,我们默认其是成功的。
为了简化一些通用的处理逻辑,Kubernetes 也为这些 Probe 内置了如下三个 Handler:
-
ExecAction 可以在容器内执行 shell 脚本;
-
HTTPGetAction 方便对指定的端口和 IP 地址执行 HTTP Get 请求;
-
TCPSocketAction 可以对指定端口进行 TCP 检查;
在这里 Probe 还提供了其他配置字段,比如 failureThreshold (失败阈值)等,你可以到这个官方文档中查看更详细的解释。
注:对于每一种 Probe,Kubelet 只会执行其中一种 Handler。如果你定义了多个 Handler,则会按照 Exec、HTTPGet、TCPSocket 的优先级顺序,选择第一个定义的 Handler。
下面我们通过一个例子,来了解这三个 Probe 的工作流程。
apiVersion: v1
kind: Pod
metadata:
name: probe-demo
namespace: demo
spec:
containers:
- name: sise
image: quay.io/openshiftlabs/simpleservice:0.5.0
ports:
- containerPort: 9876
readinessProbe:
tcpSocket:
port: 9876
periodSeconds: 10
livenessProbe:
periodSeconds: 5
httpGet:
path: /health
port: 9876
startupProbe:
httpGet:
path: /health
port: 9876
failureThreshold: 3
periodSeconds: 2
在这个例子中,我们在命名空间 demo 下面创建了一个名为 probe-demo 的 Pod。在这个 Pod 里,我们配置了三种 Probe。在 Kubelet 创建好对应的容器以后,会先运行 startupProbe 中的配置,这里我们用 HTTP handler 每隔 2 秒钟通过 http://localhost:9876/health 来判断服务是不是启动好了。这里我们会尝试 3 次检测,如果 6 秒以后还未成功,那么这个容器就会被干掉。而是否重启,这就要看 Pod 定义的重启策略。
一旦容器通过了 startupProbe 后,Kubelet 会每隔 5 秒钟进行一次探活检测 (livenessProbe),每隔 10 秒进行一次就绪检测(readinessProbe)。
在平常使用中,建议你对全部服务同时设置 readiness 和 liveness 的健康检查。
有一点需要注意的是,通过 TCP 对端口进行检查,仅适用于端口已关闭或者进程停止的情况。因为即使服务异常,只要端口是打开状态,健康检查依然是通过的。
除了健康检查以外,我们有时候在容器退出前要做一些清理工作,比如利用 Nginx 自带的停止功能停掉进程,而不是强制杀掉该进程,这可以避免一些正在处理的请求中断。此时我们就需要一个 hook(钩子程序)来帮助我们达到这个目的了。
容器生命周期内的 hook
目前在 Kubernetes 中,有如下两种 hook。
-
PostStart 可以在容器启动之后就执行。但需要注意的是,此 hook 和容器里的 ENTRYPOINT 命令的执行顺序是不确定的。
-
PreStop 则在容器被终止之前被执行,是一种阻塞式的方式。执行完成后,Kubelet 才真正开始销毁容器。
同上面的 Probe 一样,hook 也有类似的 Handler:
-
Exec 用来执行 Shell 命令;
-
HTTPGet 可以执行 HTTP 请求。
我们来看个例子:
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
namespace: demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx:1.19
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
可以看出来,我们可以借助preStop
以优雅的方式停掉 Nginx 服务,从而避免强制停止容器,造成正在处理的请求无法响应。
init 容器
在 Kubernetes 中还有一种特殊的容器,即 init 容器。看名字就知道,这个容器工作在正常容器(为了方便区分,我们这里称为应用容器)启动之前,通常用来做一些初始化工作,比如环境检测、OSS 文件下载、工具安装,等等。
应用容器专注于业务处理,其他一些无关的初始化任务就可以放到 init 容器中。这种解耦有利于各自升级,也降低相互依赖。
一个 Pod 中允许有一个或多个 init 容器。init 容器和其他一般的容器非常像,其与众不同的特点主要有:
-
总是运行到完成,可以理解为一次性的任务,不可以运行常驻型任务,因为会 block 应用容器的启动运行;
-
顺序启动执行,下一个的 init 容器都必须在上一个运行成功后才可以启动;
-
禁止使用 readiness/liveness 探针,可以使用 Pod 定义的activeDeadlineSeconds,这其中包含了 Init Container 的启动时间;
-
禁止使用 lifecycle hook。
我们来看一个 Init 容器的例子:
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
namespace: demo
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox:1.31
command: [‘sh’, ‘-c’, ‘echo The app is running! && sleep 3600‘]
initContainers:
- name: init-myservice
image: busybox:1.31
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
- name: init-mydb
image: busybox:1.31
command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']
在 myapp-container 启动之前,它会依次启动 init-myservice、init-mydb,分别来检查依赖的服务是否可用。
写在最后
其实作为 Kubernetes 内部最核心的对象之一,Pod 承载了太多的功能。 为了增加可扩展、可配置性,Kubernetes 增加了各种 Probe、Hook 等,以此方便使用者进行接入配置。所以在一开始使用的时候,会觉得 Pod 中配置项太多。
但是不要害怕,这些配置项都是有一定目的的 。通过上面合理地归类和示例,可以很好地帮助你理解 Pod Spec 中的一些定义。
下一节课开始,我们就要学习如何部署高可用业务。如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。