05-K8 Pod:最小调度单元的使用进阶及实践

发布时间 2024-01-11 13:28:29作者: miao酱

通过上一节课的学习,相信你已经知道了 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 个原因:

  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 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 除了上述的PendingRunning以外,官方还定义了下面这些状态:

  • 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:

在这里 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 中的一些定义。

下一节课开始,我们就要学习如何部署高可用业务。如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。