# 容器生命周期

参考文档: Container Lifecycle Hooks

本文描述了 kubelet 管理的容器如何使用容器生命周期钩子执行指定的代码。

# 概述

绝大多数高级程序编程语言的框架(例如,Angular、Spring Framework、Vue 等)在组件的生命周期中提供 hook(钩子函数),例如 Vue 组件的 createdmountedbeforeDestroydestroyed, Java Web 应用中 ServeletContextListener 的 contextInitializedcontextDestroyed 等。Kubernetes 中,也为容器提供了对应的生命周期钩子函数,使得容器可以获知其所在运行环境对其进行管理的生命周期事件,以便容器可以响应该事件,并执行对应的代码。

# 容器钩子

Kubernetes中为容器提供了两个 hook(钩子函数):

  • PostStart

    此钩子函数在容器创建后将立刻执行。但是,并不能保证该钩子函数在容器的 ENTRYPOINT 之前执行。该钩子函数没有输入参数。

  • PreStop

    此钩子函数在容器被 terminate(终止)之前执行,例如:

    • 通过接口调用删除容器所在 Pod
    • 某些管理事件的发生:健康检查失败、资源紧缺等

    如果容器已经被关闭或者进入了 completed 状态,preStop 钩子函数的调用将失败。该函数的执行是同步的,即,kubernetes 将在该函数完成执行之后才删除容器。该钩子函数没有输入参数。

    更多内容请参考 Termination of Pods

# Hook handler的实现

容器只要实现并注册 hook handler 便可以使用钩子函数。Kubernetes 中,容器可以实现两种类型的 hook handler:

  • Exec - 在容器的名称空进和 cgroups 中执行一个指定的命令,例如 pre-stop.sh。该命令所消耗的 CPU、内存等资源,将计入容器可以使用的资源限制。
  • HTTP - 向容器的指定端口发送一个 HTTP 请求

# Hook handler的执行

当容器的生命周期事件发生时,Kubernetes 在容器中执行该钩子函数注册的 handler。

对于 Pod 而言,hook handler 的调用是同步的。即,如果是 PostStart hook,容器的 ENTRYPOINT 和 hook 是同时出发的,然而如果 hook 执行的时间过长或者挂起了,容器将不能进入到 Running 状态。

PreStop hook 的行为与此相似。如果 hook 在执行过程中挂起了,Pod phase 将停留在 Terminating 的状态,并且在 terminationGracePeriodSeconds 超时之后,Pod被删除。如果 PostStart 或者 PreStop hook 执行失败,则 Kubernetes 将 kill(杀掉)该容器。

用户应该使其 hook handler 越轻量级越好。例如,对于长时间运行的任务,在停止容器前,调用 PreStop 钩子函数,以保存当时的计算状态和数据。

# Hook触发的保证

Hook 将至少被触发一次,即,当指定事件 PostStartPreStop 发生时,hook 有可能被多次触发。hook handler 的实现需要保证即使多次触发,执行也不会出错。

通常来说,hook 实际值被触发一次。例如:如果 HTTP hook 的服务端已经停机,或者因为网络的问题不能接收到请求,请求将不会被再次发送。在极少数的情况下, 触发两次 hook 的事情会发生。例如,如果 kueblet 在触发 hook 的过程中重启了,该 hook 将在 Kubelet 重启后被再次触发。

# 调试 hook handler

Hook handler 的日志并没有在 Pod 的 events 中发布。如果 handler 因为某些原因失败了,kubernetes 将广播一个事件 PostStart hook 发送 FailedPreStopHook 事件。 可以执行命令 kubectl describe pod $(pod_name) 以查看这些事件,例如:

Events:
  FirstSeen  LastSeen  Count  From                                                   SubobjectPath          Type      Reason               Message
  ---------  --------  -----  ----                                                   -------------          --------  ------               -------
  1m         1m        1      {default-scheduler }                                                          Normal    Scheduled            Successfully assigned test-1730497541-cq1d2 to gke-test-cluster-default-pool-a07e5d30-siqd
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Pulling              pulling image "test:1.0"
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Created              Created container with docker id 5c6a256a2567; Security:[seccomp=unconfined]
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Pulled               Successfully pulled image "test:1.0"
  1m         1m        1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Started              Started container with docker id 5c6a256a2567
  38s        38s       1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Killing              Killing container with docker id 5c6a256a2567: PostStart handler: Error executing in Docker Container: 1
  37s        37s       1      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}  spec.containers{main}  Normal    Killing              Killing container with docker id 8df9fdfd7054: PostStart handler: Error executing in Docker Container: 1
  38s        37s       2      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}                         Warning   FailedSync           Error syncing pod, skipping: failed to "StartContainer" for "main" with RunContainerError: "PostStart handler: Error executing in Docker Container: 1"
  1m         22s       2      {kubelet gke-test-cluster-default-pool-a07e5d30-siqd}
1
2
3
4
5
6
7
8
9
10
11
12