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

Pod 是 Kubernetes 中原子化的部署单元,它可以包含一个或多个容器,而且容器之间可以共享网络、存储资源。在日常使用过程中,也应该尽量避免在一个 Pod 内运行多个不相关的容器。

在实际生产使用的过程中,通过 kubectl 可以很方便地部署一个 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

注:我们这里使用了 kubectl 命令行 JSONPATH 模板能力,你可以将这条命令当作一个 tip,在日常工作中使用。

我们看到,这个时候 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 就会停掉该容器,容器的后续操作会受到其重启策略(https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy)的影响。

readinessProbe常常用于指示容器是否可以对外提供正常的服务请求,即“就绪”,比如 nginx 容器在 reload 配置的时候无法对外提供 HTTP 服务。

startupProbe则可以用于判断容器是否已经启动好,就比如上面提到的容器启动慢的例子。我们可以通过参数,保证有足够长的时间来应对“超长”的启动时间。 如果检测失败的话,同livenessProbe的操作。这个 Probe 是在 1.16 版本才加入进来的,到 1.18 版本变为 beta。也就是说如果你的 Kubernetes 版本小于 1.18 的话,你需要在 kube-apiserver 的启动参数中,显式地在 feature gate 中开启这个功能。可以参考这个文档(https://kubernetes.io/zh/docs/reference/command-line-tools-reference/feature-gates/)查看如何配置该参数。

如果某个 Probe 没有设置的话,我们默认其是成功的。

为了简化一些通用的处理逻辑,Kubernetes 也为这些 Probe 内置了如下三个 Handler:

ExecAction(https://kubernetes.io/docs/resources-reference/v1.7/#execaction-v1-core) 可以在容器内执行 shell 脚本;

HTTPGetAction(https://kubernetes.io/docs/resources-reference/v1.7/#httpgetaction-v1-core) 方便对指定的端口和 IP 地址执行 HTTP Get 请求;

TCPSocketAction(https://kubernetes.io/docs/resources-reference/v1.7/#tcpsocketaction-v1-core) 可以对指定端口进行 TCP 检查;

在这里 Probe 还提供了其他配置字段,比如 failureThreshold (失败阈值)等,你可以到这个官方文档(https://kubernetes.io/zh/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes)中查看更详细的解释。

注:对于每一种 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 中的一些定义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值