原来 k8s pod也有生命周期?

1,841 阅读10分钟

上一次已经给大家讲了如何使用 YAML 文件来创建 Pod,那么这一节我们来聊聊 Pod 的生命周期以及是如何做到的。

Pod 可以说是 Kubernetes 集群中的最小执行单元,而 Pod 底下是由容器组组成的,因此在讨论 Pod 的生命周期之前,我们先来了解下容器的生命周期。

pod 钩子函数

实际上 Kubernetes 为我们的容器提供了生命周期钩子的,比如我们常说的 Pod Hook,Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前,可以先运行钩子函数,是包含在容器的生命周期之中的。

Kubernetes 通常有两种钩子函数可以使用:

  • PostStart:这个钩子在容器创建后会被执行。通常用于一些环境准备和资源部署等。不过需要注意的是如果钩子执行太长时间会导致容器不能正常运行,以至于被挂起的状态。

  • PreStop:这个钩子在容器终止之前会被调用。它是阻塞的,意味着它是同步的, 所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。

如果 PostStart 或者 PreStop 钩子执行过程中失败, 它会杀死相应的容器。所以我们应该让钩子函数尽可能的轻量。

另外我们有两种方式来实现上面的钩子函数:

  • Exec 方式 - 通过执行一段特定的命令来实现,该命令消耗的资源会被计入容器。
  • HTTP 方式 - 对容器上特定的端点执行 HTTP 请求。

举例一:PostStart

以下示例中,定义了一个 Nginx Pod,其中设置了 PostStart 钩子函数,即在容器创建成功后,写入一句话到 /usr/share/message 文件中。

apiVersion: v1
kind: Pod
metadata:
  name: hook-demo1
spec:
  containers:
  - name: hook-demo1
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo hello postStart handler > /usr/share/message"]

举例二:优雅删除资源

当有请求需要删除含有 pod 的资源对象时(比如 Deployment 等),为了让应用程序可以优雅的关闭(即让正在处理的程序完成后,再关闭),K8S 提供两种信息通知来处理:

  • 默认情况:K8S 会通知 node 执行 docker stop 命令,然后 docker 会先向容器中 PID 为 1 的进程发送系统信号 SIGTERM,等待容器中的应用程序终止执行。如果等待时间达到设定的超时时间,或者默认超时时间(30s),则会继续发送 SIGKILL 的系统信号强行 kill 掉进程。
  • 使用 PreStop 钩子函数:在发送终止信号之前会执行该钩子函数进行关闭资源。

下面举个例子,定义了一个 Nginx Pod,其中设置了 PreStop 钩子函数,即在容器退出之前,优雅的关闭 Nginx:

apiVersion: v1
kind: Pod
metadata:
  name: hook-demo2
spec:
  containers:
  - name: hook-demo2
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]

---
apiVersion: v1
kind: Pod
metadata:
  name: hook-demo2
  labels:
    app: hook
spec:
  containers:
  - name: hook-demo2
    image: nginx
    ports:
    - name: webport
      containerPort: 80
    volumeMounts:
    - name: message
      mountPath: /usr/share/
    lifecycle:
      preStop:
        exec:
          command: ['/bin/sh', '-c', 'echo hello preStop Handler > /usr/share/message']

pod 健康检查

上面讲了 Pod 中容器的生命周期的两个钩子函数,PostStartPreStop,其中 PostStart 是在容器创建后立即执行的,而 PreStop 这个钩子函数则是在容器终止之前执行的。

除了上面这两个钩子函数以外,还有一项配置会影响到容器的生命周期的,那就是健康检查的探针。

在 Kubernetes 集群当中,我们可以通过配置 liveness probe(存活探针)和 readiness probe(就绪探针)来影响容器的生存周期。

exec 方式检测容器存活

下面先来看下存活探针的使用方法,首先我们用 exec 执行命令的方式来检测容器的存活,如下:

apiVersion: v1
kind: Pod
metadata:
  name: liveness-exec
  labels:
    test: liveness
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 10; rm -rf /tmp/healthy; sleep 500
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 6
      periodSeconds: 3

这里用到了 livenessProbe,然后通过 exec 执行一段命令,其中 periodSeconds 属性表示让 kubelet 每隔 3 秒执行一次存活探针,也就是每 3 秒执行一次上面的 cat /tmp/healthy 命令,如果命令执行成功了,将返回 0,那么 kubelet 就会认为当前这个容器是存活的并且是可监控,如果返回的是非 0 值,那么 kubelet 就会把该容器杀掉然后重启它。

再来看下 initialDelaySeconds 表示在第一次执行探针的时候要等待 6 秒,这样能够确保我们的容器能够有足够的时间启动起来。试想下,如果第一次执行探针等候的时间太短,就很有可能容器还没正常启动起来就进行存活探针检查,这样检查始终都是失败的,从而会不断的重启下去。所以设置一个合理的 initialDelaySeconds 就非常重要。

HTTP 方式配置存活探针

另外,我们可以使用 HTTP GET 请求来配置我们的存活探针,这里使用一个 liveness 镜像来验证下:

apiVersion: v1
kind: Pod
metadata:
labels:
  test: liveness
name: liveness-http
spec:
containers:
- name: liveness
  image: routeman/liveness
  args:
  - /server
  livenessProbe:
    httpGet:
      path: /healthz
      port: 8080
      httpHeaders:
      - name: X-Custom-Header
        value: Awesome
    initialDelaySeconds: 3
    periodSeconds: 3

同样的,根据 periodSeconds 可以知道 kubelet 需要每隔 3 秒执行一次 liveness probe,该探针将向容器中的 server 的 8080 端口发送一个 HTTP GET 请求。如果 server 的 /healthz 路径的 handler 返回一个成功的返回码,kubelet 就会认定该容器是健康存活的。

如果返回失败的返回码,kubelet 将杀掉该容器并重启。initialDelaySeconds 指定 kubelet 在该执行第一次探测之前需要等待 3 秒钟。

从上面的 YAML 文件我们可以看出 readiness probe 的配置跟 liveness probe 大同小异。不同是使用 readinessProbe 而不是 livenessProbe。

两者如果同时使用的话就可以确保流量不会到达还未准备好的容器,如果应用程序出现了错误,则会重新启动容器。

这就是 liveness probe(存活探针)和 readiness probe(就绪探针)的使用方法。

下面继续讲下 Pod 生命周期中的初始化容器

Init Container

前面学习了容器的健康检查的两个探针:liveness probe(存活探针)和 readiness probe(就绪探针)的使用方法,我们说在这两个探针是可以影响容器的生命周期的,包括我们之前提到的容器的两个钩子函数 PostStart 和 PreStop。

接下来我们今天要给大家介绍的是 Init Container(初始化容器)。

Init Container 就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行,只有所有的 Init Container 执行完后,主容器才会被启动。我们知道一个 Pod 里面的所有容器是共享数据卷和网络命名空间的,所以 Init Container 里面产生的数据可以被主容器使用到的。

是不是感觉 Init Container 和之前的钩子函数有点类似啊,只是是在容器执行前来做一些工作,是吧?从直观的角度看上去的话,初始化容器的确有点像 PreStart,但是钩子函数和我们的 Init Container 是处在不同的阶段的,我们可以通过下面的图来了解下:

初始化容器

从上面这张图我们可以直观的看到 PostStart 和 PreStop 包括 liveness 和 readiness 是属于主容器的生命周期范围内的,而 Init Container 是独立于主容器之外的,当然他们都属于 Pod 的生命周期范畴之内的,现在我们应该明白 Init Container 和钩子函数之类的区别了吧。

另外我们可以看到上面我们的 Pod 右边还有一个 infra 的容器,这是一个什么容器呢?我们可以在集群环境中去查看下人任意一个 Pod 对应的运行的 Docker 容器,我们可以发现每一个 Pod 下面都包含了一个 pause-amd64 的镜像,这个就是我们的 infra 镜像,我们知道 Pod 下面的所有容器是共享同一个网络命名空间的,这个镜像就是来做这个事情的,所以每一个 Pod 当中都会包含一个这个镜像。

我们说 Init Container 主要是来做初始化容器工作的,那么有哪些应用场景呢?

等待依赖模块 Ready

这个可以用来解决服务之间的依赖问题,比如我们有一个 Web 服务,该服务又依赖于另外一个数据库服务,但是在我们启动这个 Web 服务的时候我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内 Web 服务连接数据库异常。

要解决这个问题的话我们就可以在 Web 服务的 Pod 中使用一个 InitContainer,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们的主容器 Web 服务被启动起来,这个时候去连接数据库就不会有问题了。

做初始化配置

比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。

其它场景

如将 pod 注册到一个中央数据库、配置中心等。

服务依赖场景应用示例

下面以服务依赖的场景下初始化容器的使用方法,Pod 的定义方法:

apiVersion: v1
kind: Pod
metadata:
name: init-pod-demo
labels:
  app: init
spec:
containers:
- name: init-container
  image: busybox
  command: ['sh', '-c', 'echo app is running! && sleep 600']
initContainers:
- name: init-myservice
  image: busybox
  command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 3; done;']
- name: init-mydb
  image: busybox
  command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 3; done;']

Service 对应的 yaml 文件 内容如下:

kind: Service
apiVersion: v1
metadata:
name: myservice
spec:
ports:
- protocol: TCP
  port: 80
  targetPort: 6376
---
kind: Service
apiVersion: v1
metadata:
name: mydb
spec:
ports:
- protocol: TCP
  port: 80
  targetPort: 6377

可以先创建上面的 Pod,查看 Pod 的状态,然后再创建 Service,看下前后状态有什么不一样。

在 Pod 启动过程中,初始化容器会按顺序在网络和数据卷初始化之后启动。每个容器必须在下一个容器启动之前成功退出。

如果由于运行时或失败退出,导致容器启动失败,它会根据 Pod 的 restartPolicy 策略进行重试。 如果 Pod 的 restartPolicy 设置为 Always,Init 容器失败时会不断的重启。

总结

以上是对 pod 的理解,以及初始化容器的使用方法,到这里我们已经把 Pod 的整个生命周期中的几个主要部分讲完了。

  • 第一个是容器的两个钩子函数:PostStartPreStop
  • 还有就是容器健康检查的两个探针:liveness probereadiness probe
  • 最后就是 Init Container 了。