#yyds干货盘点# Kubernetes 最小调度单元 Pod 的使用进阶及实践(05)

枕上诗书闲处好,门前风景雨来佳。这篇文章主要讲述#yyds干货盘点# Kubernetes 最小调度单元 Pod 的使用进阶及实践(05)相关的知识,希望能为你提供帮助。
相信你已经知道了 Pod 是 Kubernetes 中原子化的部署单元,它可以包含一个或多个容器,而且容器之间可以共享网络、存储资源。在日常使用过程中,也应该尽量避免在一个 Pod 内运行多个不相关的容器。
在实际生产使用的过程中,通过 kubectl 可以很方便地部署一个 Pod。但是 Pod 运行过程中还会出现一些意想不到的问题,比如:

  • Pod 里的某一个容器异常退出了怎么办?
  • 有没有“健康检查”方便你知道业务的真实运行情况,比如容器运行正常,但是业务不工作了?
  • 容器在启动或删除前后,如果需要做一些特殊处理怎么办?比如做一些清理工作。
  • 如果容器所在节点宕机,重启后会对你的容器产生影响吗?
  • ……
在这一节,我将通过一些示例一一解答你的这些问题,并告诉你 Pod 最佳的使用方法。
在了解 Pod 的高阶用法之前,我们先聊聊 Pod 的运行状态。
Pod 的运行状态我们先回到上一节中的例子:
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

我们看到,这个时候 Pod 处于Pending状态,具体的值来自 Pod 对象的status.phase字段。
你也可以使用 kubectl get 命令来查看容器的状态:
$ kubectl get pod twocontainers NAMEREADYSTATUSRESTARTSAGE twocontainers0/2ContainerCreating013s

看到这里,你会发现这个地方显示的是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: TypeReasonAgeFromMessage ------------------------- NormalScheduled< unknown> default-schedulerSuccessfully assigned default/twocontainers to node-1 NormalPulling3m57skubelet, node-1Pulling image "quay.io/openshiftlabs/simpleservice:0.5.0"

可以看到,这边 Status 依然是Pending。其实这是 kubectl 在显示时做的转换,它会遍历容器的 State,如果容器的状态为Waiting的话,就读取State.Reason字段作为 Pod 的 Status。这个时候由于镜像在本地不存在,需要去镜像中心拉取。
一般来说,处于Pending状态的 Pod,不外乎以下 2 个原因:
  1. Pod 还未被调度;
  2. 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 2021 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 2021 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 2021 17:01:46 +0800 Ready:True ... Conditions: TypeStatus InitializedTrue ReadyTrue ContainersReadyTrue PodScheduledTrue ...

这个时候,就标志着 Pod 内的所有容器均被创建出来了,且至少有一个容器为在运行状态中。那么如果想知道 Pod 内所有的容器是否都在运行中呢?我们可以通过 kubectl get 来看到:
$ kubectl get pod twocontainers NAMEREADYSTATUSRESTARTSAGE twocontainers2/2Running02m

在这里,我们看到2/2。前一个 2 表示目前正在运行的容器数量,后一个 2 表示定义的容器数量。当这两个数值相等的时候,就可以标识着 Pod 内所有容器均正常运行。
Pod 的 Status 除了上述的PendingRunning以外,官方还定义了下面这些状态:
  • Succeeded来表示 Pod 内的所有容器均成功运行结束,即正常退出,退出码为 0;
  • Failed来表示 Pod 内的所有容器均运行终止,且至少有一个容器终止失败了,一般这种情况,都是由于容器运行异常退出,或者被系统终止掉了;
  • Unknown一般是由于 Node 失联导致的 Pod 状态无法获取到。
既然 Pod 内的容器会出现异常退出状态,那么有没有一些重启策略可以让 Kubelet 对容器进行重启呢?
Pod 的重启策略Kubernetes 中定义了如下三种重启策略,可以通过spec.restartPolicy字段在 Pod 定义中进行设置。
  • Always 表示一直重启,这也是默认的重启策略。Kubelet 会定期查询容器的状态,一旦某个容器处于退出状态,就对其执行重启操作;
  • OnFailure 表示只有在容器异常退出,即退出码不为 0 时,才会对其进行重启操作;
  • Never 表示从不重启;
虽然我们可以设置一些重启策略,确保容器异常退出时可以重启。但是对于运行中的容器,是不是就意味着容器内的服务正常了呢?
比如某些 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 的工作流程。
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)。
在平常使用中,< u> 建议你对全部服务同时设置 readiness 和 liveness 的健康检查< /u> 。
有一点需要注意的是,通过 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 中的一些定义。
欢迎大家扫码关注,获取更多信息【#yyds干货盘点# Kubernetes 最小调度单元 Pod 的使用进阶及实践(05)】
#yyds干货盘点# Kubernetes 最小调度单元 Pod 的使用进阶及实践(05)

文章图片


    推荐阅读