理解 Pod 的状态
Pod phase
在 Pod 完整的生命周期中,存在着 5 种不同的阶段:
-
Pending
:创建 Pod 对象后的初始化阶段。会一直持续到 Pod 被分配给某个工作节点,镜像被拉取到本地并启动 -
Running
:Pod 中至少一个容器处于运行状态 -
Succeeded
:对于不打算无限期运行的 Pod,其容器部署完成后的状态 -
Failed
:对于不打算无限期运行的 Pod,其容器中至少有一个由于错误终止 -
Unknown
:由于 Kubelet 与 API Server 的通信中断,Pod 的状态未知。可能是工作节点挂掉或断网
从 kubia.yml
清单文件创建一个 Pod。
apiVersion: v1
kind: Pod
metadata:
name: kubia
spec:
containers:
- name: kubia
image: luksa/kubia:1.0
ports:
- containerPort: 8080
$ kubectl apply -f kubia.yml
查看 Pod 的 Phase
$ kubectl get po kubia -o yaml | grep phase
phase: Running
或者借助 jq
工具从 JSON 格式的输出中检索 phase 字段:
$ kubectl get po kubia -o json | jq .status.phase
"Running"
也可以使用 kubectl describe
命令:
$ kubectl describe po kubia | grep Status:
Status: Running
Pod conditions
Pod 的 conditions 用来表示某个 Pod 是否达到了特定的状态以及达到或未达到的原因。
与 phase 相反,一个 Pod 可以同时有几个 conditions。
-
PodScheduled
:表明 Pod 是否已经被安排给了某个工作节点 -
Initialized
:Pod 的初始化容器已经部署完成 -
ContainersReady
:Pod 中的所有容器都已经准备完毕 -
Ready
:Pod 自身已经准备好对其客户端提供服务
查看 Pod 的 conditions
$ kubectl describe po kubia | grep Conditions: -A5
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
kubectl describe
命令只会显示每个 condition 是 true
还是 false
,不会显示更详细的信息。
为了显示某个 condition 为 false 的具体原因,需要检索 Pod 的清单文件:
$ kubectl get po kubia -o json | jq .status.conditions
[
{
"lastProbeTime": null,
"lastTransitionTime": "2021-12-30T03:02:45Z",
"status": "True",
"type": "Initialized"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2021-12-30T03:02:46Z",
"status": "True",
"type": "Ready"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2021-12-30T03:02:46Z",
"status": "True",
"type": "ContainersReady"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2021-12-30T03:02:45Z",
"status": "True",
"type": "PodScheduled"
}
]
当 condition 为 false
时,上述输出中会包含 reason
和 message
字段来显示失败的具体原因和详细信息。
容器的 status
容器的 status 包含多个字段。其中 state
字段表示该容器当前的状态,restartCount
表示容器重启的频率,此外还有 containerID
、image
、imageID
等。
容器的 status 包含以下几种:
-
Waiting
:容器等待启动。reason
和message
字段会记录容器处于此状态的原因 -
Running
:容器已经创建,进程正在运行 -
Terminated
:容器中运行的进程已经终止。exitCode
字段会记录进程的退出码 -
Unknown
:容器的状态无法确定
比如修改 kubia.yml
清单文件中的 image
字段,故意改成 uksa/kubia:1.0
这样无效的地址,运行 kubectl apply -f kubia.yml
命令重新应用清单文件。
等待几分钟直到新的配置生效,查看容器的状态。
可以使用 kubectl describe
命令查看容器的状态:
$ kubectl describe po kubia | grep Containers: -A15
Containers:
kubia:
Container ID: docker://62fa208957d396c38f65305fd073d6b334dd8da22ab5beab196ca9bcf2f9ff91
Image: uksa/kubia:1.0
Image ID: docker-pullable://luksa/kubia@sha256:a961dc8f377916936fa963508726d77cf77dcead5c97de7e5361f0875ba3bef7
Port: 8080/TCP
Host Port: 0/TCP
State: Waiting
Reason: ImagePullBackOff
Last State: Terminated
Reason: Error
Exit Code: 137
Started: Fri, 31 Dec 2021 14:50:39 +0800
Finished: Fri, 31 Dec 2021 14:51:36 +0800
Ready: False
Restart Count: 0
或者使用 kubectl get po kubia -o json
命令:
$ kubectl get po kubia -o json | jq .status.containerStatuses
[
{
"containerID": "docker://62fa208957d396c38f65305fd073d6b334dd8da22ab5beab196ca9bcf2f9ff91",
"image": "luksa/kubia:1.0",
"imageID": "docker-pullable://luksa/kubia@sha256:a961dc8f377916936fa963508726d77cf77dcead5c97de7e5361f0875ba3bef7",
"lastState": {
"terminated": {
"containerID": "docker://62fa208957d396c38f65305fd073d6b334dd8da22ab5beab196ca9bcf2f9ff91",
"exitCode": 137,
"finishedAt": "2021-12-31T06:51:36Z",
"reason": "Error",
"startedAt": "2021-12-31T06:50:39Z"
}
},
"name": "kubia",
"ready": false,
"restartCount": 0,
"started": false,
"state": {
"waiting": {
"message": "Back-off pulling image \"uksa/kubia:1.0\"",
"reason": "ImagePullBackOff"
}
}
}
]
上面输出中的 state
字段都表明了容器当前的状态是 waiting
,还有 reason
和 message
字段表明处于此状态的原因:镜像拉取失败。
确保容器的健康状态
理解容器的自动重启机制
当一个 Pod 被分配给了某个工作节点,该工作节点上的 Kubelet 就会负责启动容器并保证该容器一直处于运行状态,只要该 Pod 对象一直存在没被移除。
如果容器中的主进程由于某些原因终止运行,Kubernetes 就会自动重启该容器。
创建如下内容的清单文件 kubia-ssl.yml
:
apiVersion: v1
kind: Pod
metadata:
name: kubia-ssl
spec:
containers:
- name: kubia
image: luksa/kubia:1.0
ports:
- name: http
containerPort: 8080
- name: envoy
image: luksa/kubia-ssl-proxy:1.0
ports:
- name: https
containerPort: 8443
- name: admin
containerPort: 9901
运行如下命令应用上述清单文件,并启用端口转发:
$ kubectl apply -f kubia-ssl.yml
pod/kubia-ssl created
$ kubectl port-forward kubia-ssl 8080 8443 9901
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443
Forwarding from 127.0.0.1:9901 -> 9901
Forwarding from [::1]:9901 -> 9901
待容器启动成功后,访问 localhost 的 8080、8443、9901 端口就等同于访问容器中 8080、8443、9901 端口上运行的服务。
打开一个新的命令行窗口运行 kubectl get pods -w
命令,实时监控容器的运行状态。
打开一个新的命令行窗口运行 kubectl get events -w
命令,实时监控触发的事件。
打开一个新的命令行窗口,尝试终止 Envoy 容器中运行的主进程。Envoy 容器 9901 端口上运行的服务刚好提供了一个管理接口,能够接收 HTTP POST 请求来终止进程:
$ curl -X POST http://localhost:9901/quitquitquit
OK
此时查看前两个窗口中的输出,负责监控容器状态的窗口输出如下:
$ kubectl get pods -w
NAME READY STATUS RESTARTS AGE
kubia-ssl 2/2 Running 0 11m
kubia-ssl 1/2 NotReady 0 12m
kubia-ssl 2/2 Running 1 (2s ago) 12m
最新的输出表明,在杀掉 Envoy 容器中的主进程后,Pod 的状态是先变成 NotReady
,之后就立即重启该容器,Pod 的状态稍后恢复成 Running
。
负责监控最新事件的窗口输出如下:
$ kubectl get events -w
LAST SEEN TYPE REASON OBJECT MESSAGE
11m Normal Scheduled pod/kubia-ssl Successfully assigned default/kubia-ssl to minikube
11m Normal Pulled pod/kubia-ssl Container image "luksa/kubia:1.0" already present on machine
11m Normal Created pod/kubia-ssl Created container kubia
11m Normal Started pod/kubia-ssl Started container kubia
11m Normal Pulled pod/kubia-ssl Container image "luksa/kubia-ssl-proxy:1.0" already present on machine
11m Normal Created pod/kubia-ssl Created container envoy
11m Normal Started pod/kubia-ssl Started container envoy
0s Normal Pulled pod/kubia-ssl Container image "luksa/kubia-ssl-proxy:1.0" already present on machine
0s Normal Created pod/kubia-ssl Created container envoy
0s Normal Started pod/kubia-ssl Started container envoy
最新的事件信息中包含了新的 envoy 容器启动的过程。其中有一个很重要的细节:Kubernetes 从来不会重启容器,而是直接丢弃停止的容器并创建一个新的。一般在 Kubernetes 中提及“重启”容器,实质上都指的是“重建”。
任何写入到容器文件系统中的数据,在容器重新创建后都会丢失。为了持久化这些数据,需要向 Pod 添加 Volume。
配置容器的重启策略
Kubernetes 支持 3 种容器重启策略:
-
Always
:默认配置。不管容器中主进程的退出码是多少,容器都会自动重启 -
OnFailure
:容器只会在退出码非 0 时重启 -
Never
:容器永不重启
容器重启时的延迟时间
第一次容器终止时,重启会立即触发。但容器第二次重启时会先等待 10s,这个等待时间会随着重启次数依次增加到 20、40、80、160s。再之后则一直保持在 5 分钟。
等待过程中容器会处于 Waiting
状态,reason
字段显示 CrashLoopBackOff
,message
字段显示需要等待的时间。
liveness probes
Kubernetes 会在容器的进程终止时重启容器,以保证应用的健康。但应用实际上有可能在进程不终止的情况下无响应,比如一个 Java 应用报出 OutOfMemoryError 错误,而 JVM 进程仍在运行中。
理想情况下,Kubernetes 需要能够检测到此类错误并重启容器。
当然,应用自身也可以捕获这类错误并令进程立即终止。但假如应用因为进入无限循环或死锁导致无响应,又或者应用本身无法检测到错误存在呢?
为了确保容器能够在这些复杂情况下重启,应该提供一种从外部检查应用状态的机制。
liveness probe 介绍
Kubernetes 可以通过配置 liveness probe 来检查某个应用是否能够正常响应,Pod 中的每个容器都可以分别配置 liveness probe。一旦应用无响应或有错误发生,容器就会被认为是不健康的并被终止掉。之后容器被 Kubernetes 重新启动。
Kubernetes 支持以下三种 probe 机制:
-
HTTP GET probe
:会发送 GET 请求到容器的 IP 地址、端口号和 URL 路径。如果 probe 收到正常的响应(2xx 或 3xx),该 probe 就被认定是成功的。如果服务返回了一个错误的响应码,或者没有在规定的时间内响应,则该 probe 被认定是失败的。 -
TCP Socket probe
:会尝试打开一个 TCP 连接到容器的特定端口。若连接成功,probe 就被认定是成功的;否则失败。 -
Exec probe
:会在容器内部执行一个命令并检查该命令的退出码。若退出码为 0,则 probe 被认定是成功的;否则失败。
HTTP GET liveness probe
创建如下内容的 kubia-liveness.yml
清单文件:
apiVersion: v1
kind: Pod
metadata:
name: kubia-liveness
spec:
containers:
- name: kubia
image: luksa/kubia:1.0
ports:
- name: http
containerPort: 8080
livenessProbe:
httpGet:
path: /
port: 8080
- name: envoy
image: luksa/kubia-ssl-proxy:1.0
ports:
- name: https
containerPort: 8443
- name: admin
containerPort: 9901
livenessProbe:
httpGet:
path: /ready
port: admin
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
其中 kubia 容器的 liveness probe 是最简单版本的 HTTP 应用的 probe。该 probe 只是向 8080 端口的 /
路径发送 HTTP GET,看容器是否仍然能够处理请求。当应用的响应码介于 200 到 399 之间时,该应用就被认为是健康的。
由于该 probe 没有配置其他选项,默认配置生效。第一次检查请求会在容器启动 10s 后发起,之后每隔 10s 发起新的请求。若应用没有在 1s 之内响应,则该次 probe 请求被认定是失败的。连续 3 次请求失败以后,容器就被认为是不健康的并被终止掉。
Envoy 容器的管理员接口提供了一个 /ready
入口,可以返回其健康状态,因此 envoy 容器的 liveness probe 的目标可以是容器的 admin
端口即 9901。
参数 initialDelaySeconds
表示容器启动后到发起第一个检测请求之间的等待时间,periodSeconds
表示两次连续的检测请求之间的时间间隔,timeoutSeconds
表示多长时间以后没有响应则认定此次检测失败,failureThreshold
则表示连续多少次检测失败以后才认定容器失效并重启。
观察 liveness probe 的效果
运行 kubectl apply
命令应用上述清单文件并通过 kubectl port-forward
命令启用端口转发,启动该 Pod 并令其能够被访问:
$ kubectl apply -f kubia-liveness.yml
pod/kubia-liveness created
$ kubectl port-forward kubia-liveness 8080 8443 9901
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443
Forwarding from 127.0.0.1:9901 -> 9901
Forwarding from [::1]:9901 -> 9901
Pod 启动成功以后,liveness probe 会在初始等待时间过后持续向 Pod 中的容器发起检测请求,其检测结果只会在容器的 log 中看到。
kubia 容器中的 Node.js 应用会在每次处理 HTTP 请求时向标准输出打印记录,这些请求也包括 liveness probe 的检测请求。因此可以打开一个新的命令行窗口,使用如下命令查看请求记录:
$ kubectl logs kubia-liveness -c kubia -f
Kubia server starting...
Local hostname is kubia-liveness
Listening on port 8080
Received request for / from ::ffff:172.17.0.1
Received request for / from ::ffff:172.17.0.1
Received request for / from ::ffff:172.17.0.1
Received request for / from ::ffff:172.17.0.1
Received request for / from ::ffff:172.17.0.1
...
envoy 容器的 liveness probe 被配置成向其管理员接口发送 HTTP 请求,这些请求被记录在 /var/log/envoy.admin.log
文件中。可以使用如下命令查看:
$ kubectl exec kubia-liveness -c envoy -- tail -f /var/log/envoy.admin.log
[2022-01-02T18:34:59.818Z] "GET /ready HTTP/1.1" 200 - 0 5 0 - "172.17.0.1" "kube-probe/1.22" "-" "172.17.0.3:9901" "-"
[2022-01-02T18:35:04.818Z] "GET /ready HTTP/1.1" 200 - 0 5 0 - "172.17.0.1" "kube-probe/1.22" "-" "172.17.0.3:9901" "-"
[2022-01-02T18:35:09.818Z] "GET /ready HTTP/1.1" 200 - 0 5 0 - "172.17.0.1" "kube-probe/1.22" "-" "172.17.0.3:9901" "-"
[2022-01-02T18:35:14.818Z] "GET /ready HTTP/1.1" 200 - 0 5 0 - "172.17.0.1" "kube-probe/1.22" "-" "172.17.0.3:9901" "-"
观察失败的 liveness probe
可以尝试手动令 liveness probe 的检测请求失败。先在一个新的窗口中运行 kubectl get events -w
命令,方便后续观察检测失败时输出的事件信息。
访问 Envoy 容器的管理员接口,手动配置其健康状态为 fail
:
$ curl -X POST localhost:9901/healthcheck/fail
OK
此时转到观察事件信息的命令行窗口,发现连续输出了 3 次 probe failed 503 错误,之后 envoy 容器开始重启:
kubectl get events -w
LAST SEEN TYPE REASON OBJECT MESSAGE
...
0s Warning Unhealthy pod/kubia-liveness Liveness probe failed: HTTP probe failed with statuscode: 503
0s Warning Unhealthy pod/kubia-liveness Liveness probe failed: HTTP probe failed with statuscode: 503
0s Warning Unhealthy pod/kubia-liveness Liveness probe failed: HTTP probe failed with statuscode: 503
0s Normal Killing pod/kubia-liveness Container envoy failed liveness probe, will be restarted
0s Normal Pulled pod/kubia-liveness Container image "luksa/kubia-ssl-proxy:1.0" already present on machine
0s Normal Created pod/kubia-liveness Created container envoy
0s Normal Started pod/kubia-liveness Started container envoy
exec & tcpSocket liveness probe
添加 tcpSocket liveness probe
对于接收非 HTTP 请求的应用,可以配置 tcpSocket liveness probe。
一个 tcpSocket liveness probe 的示例配置如下:
livenessProbe:
tcpSocket:
port: 1234
periodSeconds: 2
failureThreshold: 1
该 probe 会检查容器的 1234 端口是否打开,每隔 2s 检查一次,一次检查失败则认定该容器是不健康的并重启它。
exec liveness probe
不接受 TCP 连接的应用可以配置一条命令去检测其状态。
下面的示例配置会每隔 2s 运行 /usr/bin/healthcheck
命令,检测容器中的应用是否仍在运行:
livenessProbe:
exec:
command:
- /usr/bin/healthcheck
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 1
startup probe
默认的 liveness probe 配置会给应用 20 到 30s 的时间启动,如果应用需要更长的时间才能启动完成,容器会永远达不到 liveness probe 检测成功的状态,从而进入了无限重启的循环。
为了防止上述情况发生,可以增大 initialDelaySeconds
、periodSeconds
或 failureThreshold
的值,但也会有一定的副作用。periodSeconds * failureThreshold
的值越大,当应用不健康时重启的时间就越长。
Kubernetes 还提供了一种 startup probe。当容器配置了 startup probe 时,容器启动时只有 startup probe 会执行。startup probe 可以按照应用的启动时间配置,检测成功之后 Kubernetes 会切换到使用 liveness probe 检测。
比如 Node.js 应用需要 1 分钟以上的时间启动,当启动成功以后若应用状态不正常,则在 10s 以内重启。可以这样配置:
containers:
- name: kubia
image: luksa/kubia:1.0
ports:
- name: http
containerPort: 8080
startupProbe:
httpGet:
path: /
port: http
periodSeconds: 10
failureThreshold: 12
livenessProbe:
httpGet:
path: /
port: http
periodSeconds: 5
failureThreshold: 2
上面配置的效果如下图:
应用有 120s 的时间启动。Kubernetes 一开始每隔 10s 发起 startup probe 请求,最多尝试 12 次。
不同于 liveness probe,startup probe 失败是很正常的,只是说明应用还未成功启动。一旦某次 startup probe 检测返回成功状态,Kubernetes 就会立即切换到 liveness probe 模式,通常拥有更短的检测间隔,能够对未响应应用做出更快速的反应。
在容器启动或关闭时触发动作
可以向容器中添加 lifecycle hooks。Kubernetes 目前支持两种类型的钩子:
- Post-start hooks:在容器启动后执行
- Pre-stop hooks:在容器停止前执行
post-start hooks
post-start lifecycle hook 会在容器创建完成之后立即触发。可以使用 exec
类型的钩子在主进程启动的同时执行一个额外的程序,或者 httpGet
类型的钩子向容器中运行的应用发送 HTTP 请求,以完成初始化或预热操作。
假如你是应用的作者,类似的操作当然可以加入到应用本身的代码中。但对于一个已经存在的并非自己创建的应用,就有可能无法做到。post-start hook 提供了一种不需要修改应用或容器镜像的替代方案。
post-start hook 在容器中执行命令
apiVersion: v1
kind: Pod
metadata:
name: fortune-poststart
spec:
containers:
- name: nginx
image: nginx:alpine
lifecycle:
postStart:
exec:
command:
- sh
- -c
- "apk add fortune && fortune > /usr/share/nginx/html/quote"
ports:
- name: http
containerPort: 80
上述清单文件定义的 Pod 名为 fortune-poststart
,包含一个基于 nginx:alpine
镜像的容器,同时定义了一个 postStart
钩子。该钩子会在 Nginx 服务启动时执行以下命令:
sh -c "apk add fortune && fortune > /usr/share/nginx/html/quote"
postStart
这个名称其实有些误导作用,它并不是在主进程完全启动后才开始执行,而是在容器创建后,几乎和主进程同时执行。
$ kubectl apply -f fortune-poststart.yml
pod/fortune-poststart unchanged
$ kubectl port-forward fortune-poststart 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
打开一个新的命令行窗口使用 curl
命令测试效果:
$ curl localhost:8080/quote
The Official MBA Handbook on business cards:
Avoid overly pretentious job titles such as "Lord of the Realm,
Defender of the Faith, Emperor of India" or "Director of Corporate
Planning."
post-startup hook 对容器的影响
虽然 post-start hook 相对于容器的主进程以异步的方式执行,它还是会对容器产生两个方面的影响。
首先,在 post-start hook 的执行过程中容器会一直处于 Waiting
状态,原因显示为 ContainerCreating
,直到 hook 执行完毕。
其次,若 hook 绑定的命令无法执行或返回了一个非零的状态值,则整个容器会被重启。
在容器终止前执行命令
前面 fortune Pod 中的 Nginx 服务在收到 TERM
信号后会立即关闭所有打开的连接并终止进程,这并不是理想的操作,不会等待正在处理的客户端请求彻底完成。
可以使用 pre-stop hook 执行 nginx -s quit
命令舒缓地关闭 Nginx 服务。示例配置如下:
lifecycle:
preStop:
exec:
command:
- nginx
- -s
- quit
假如某个 pre-stop hook 执行失败,只会在 Pod 的 events 消息中显示一条 FailedPreStopHook
警告信息,并不影响容器继续被终止。
理解容器的生命周期
一个 Pod 的生命周期可以被分成如下三个阶段:
- 初始化阶段:Pod 的 init 容器从开始运行到启动完成
- 运行阶段:Pod 的普通容器从开始运行到启动完成
- 终止阶段:Pod 的所有容器被终止运行
初始化阶段
Pod 中的初始化容器会最先运行,按照 spec
的 initContainers
字段中定义的顺序。
第一个初始化容器的镜像被下载到工作节点并启动,完成后继续拉取第二个初始化容器的镜像,直到所有的初始化容器都成功运行。
若某个初始化容器因为某些错误启动失败,且其重启策略设置为 Always
或 OnFailure
,则该失败的容器自动重启。若其重启策略设置为 Never
,则 Pod 的状态显示为 Init:Error
,必须删除并重新创建 Pod 对象。
运行阶段
当所有的初始化容器成功运行后,Pod 的普通容器开始以并行的方式创建(需要注意的是,容器的 post-start hook 会阻塞下一个容器的创建)。
termination grace period
容器中的应用都有一个固定的关闭时间做缓冲用,定义在 spec
下的 terminationGracePeriodSeconds
字段中,默认是 30s。
该时间从 pre-stop hook 触发或收到 TERM
信号时开始计算,若时间过了进程仍在运行,应用会收到 KILL
信号被强制关闭。
终止阶段
Pod 的容器以并行的方式终止。对每个容器来说,pre-stop hook 触发,然后主进程接收 TERM
信号,如果应用关闭的时间超过了 terminationGracePeriodSeconds
,就发送 KILL
信号给容器的主进程。
在所有的容器都被终止以后,Pod 对象被删除。
可以在删除一个 Pod 时手动指定一个新的时间覆盖 terminationGracePeriodSeconds
的值,如:
kubectl delete po kubia-ssl --grace-period 10
Pod 完整生命周期图示
参考资料
Kubernetes in Action, Second Edition