前言
我们知道 Pod 是 Kubernetes 集群中的最⼩单元,⽽ Pod 是有容器组组成的,所以在讨论 Pod 的⽣命周期的时候我们可以先来讨论下容器的⽣命周期。
Pod Hook
Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终⽌之前运⾏,这是包含在容器的⽣命周期之中。我们可以同时为 Pod 中的所有容器都配置 hook。
Kubernetes 为我们提供了两种钩⼦函数:
- PostStart:这个钩⼦在容器创建后⽴即执⾏。但是,并不能保证钩⼦将在容器 ENTRYPOINT 之前运 ⾏,因为没有参数传递给处理程序。主要⽤于资源部署、环境准备等。不过需要注意的是如果钩 ⼦花费太⻓时间以⾄于不能运⾏或者挂起, 容器将不能达到 running 状态。
- PreStop:这个钩⼦在容器终⽌之前⽴即被调⽤。它是阻塞的,意味着它是同步的, 所以它必须 在删除容器的调⽤发出之前完成。主要⽤于优雅关闭应⽤程序、通知其他系统等。如果钩⼦在执 ⾏期间挂起, Pod阶段将停留在 running 状态并且永不会达到 failed 状态。
如果 PostStart 或者 PreStop 钩⼦失败, 它会杀死容器。所以我们应该让钩⼦函数尽可能的轻量。当 然有些情况下,⻓时间运⾏命令是合理的, ⽐如在停⽌容器之前预先保存状态。
另外我们有两种⽅式来实现上⾯的钩⼦函数:
- Exec - ⽤于执⾏⼀段特定的命令,不过要注意的是该命令消耗的资源会被计⼊容器。
- HTTP - 对容器上的特定的端点执⾏ HTTP 请求。
环境准备
以下示例中,定义了⼀个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 from the postStart handler > /usr/share/m
essage"]
优雅删除资源对象
当⽤户请求删除含有 pod 的资源对象时(如Deployment等),K8S 为了让应⽤程序优雅关闭(即让应⽤程序完成正在处理的请求后,再关闭软件),K8S提供两种信息通知:
- 默认:K8S 通知 node 执⾏
docker stop
命令,docker 会先向容器中 PID 为1的进程发送系统信号 SIGTERM ,然后等待容器中的应⽤程序终⽌执⾏,如果等待时间达到设定的超时时间,或者默认超时时间(30s),会继续发送 SIGKILL 的系统信号强⾏ kill 掉进程。 - 使⽤ pod ⽣命周期(利⽤ PreStop 回调函数),它执⾏在发送终⽌信号之前。
默认所有的优雅退出时间都在30秒内。kubectl delete
命令⽀持 --grace-period=<seconds>
选项,这 个选项允许⽤户⽤他们⾃⼰指定的值覆盖默认值。值’0’代表 强制删除 pod. 在 kubectl 1.5 及以上的版 本⾥,执⾏强制删除时必须同时指定 --force --grace-period=0
。
强制删除⼀个 pod 是从集群状态还有 etcd ⾥⽴刻删除这个 pod。 当 Pod 被强制删除时, api 服务器 不会等待来⾃ Pod 所在节点上的 kubelet 的确认信息:pod 已经被终⽌。在 API ⾥ pod 会被⽴刻删 除,在节点上, pods 被设置成⽴刻终⽌后,在强⾏杀掉前还会有⼀个很⼩的宽限期。
以下示例中,定义了⼀个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 from the preStop Handler > /usr/share/mes
sage']
volumes:
- name: message
hostPath:
path: /tmp
另外 Hook 调⽤的⽇志没有暴露个给 Pod 的 event,所以只能通过 describe
命令来获取,如果有错误 将可以看到 FailedPostStartHook
或 FailedPreStopHook
这样的 event。
健康检查
除了上⾯这两个钩⼦函数以外,还有⼀项配置会影响到容器的⽣命周期的,那就是健康检查的探针。
在 Kubernetes 集群当中,我们可以通过配置 liveness probe
(存活探针)和 readiness probe
(可读性探针)来影响容器的⽣存周期。
- kubelet 通过使⽤ liveness probe 来确定你的应⽤程序是否正在运⾏,通俗点将就是是否还活着。⼀般来 说,如果你的程序⼀旦崩溃了, Kubernetes 就会⽴刻知道这个程序已经终⽌了,然后就会重启这个程序。⽽我 们的 liveness probe 的⽬的就是来捕获到当前应⽤程序还没有终⽌,还没有崩溃,如果出现了这些情况,那么 就重启处于该状态下的容器,使应⽤程序在存在 bug 的情况下依然能够继续运⾏下去。
- kubelet 使⽤ readiness probe 来确定容器是否已经就绪可以接收流量过来了。这个探针通俗点讲就是说是 否准备好了,现在可以开始⼯作了。只有当 Pod 中的容器都处于就绪状态的时候 kubelet 才会认定该 Pod 处 于就绪状态,因为⼀个 Pod 下⾯可能会有多个容器。当然 Pod 如果处于⾮就绪状态,那么我们就会将他从我们 的⼯作队列(实际上就是我们后⾯需要重点学习的 Service)中移除出来,这样我们的流量就不会被路由到这个 Pod ⾥⾯来了。
和前⾯的钩⼦函数⼀样的,我们这两个探针的⽀持两种配置⽅式:
- exec:执⾏⼀段命令
- http:检测某个 http 请求
- tcpSocket:使⽤此配置, kubelet 将尝试在指定端⼝上打开容器的套接字。如果可以建⽴连接,容器被认为 是健康的,如果不能就认为是失败的。实际上就是检查端⼝
我们先来给⼤家演示下存活探针的使⽤⽅法,⾸先我们⽤ 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 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
我们这⾥需要⽤到⼀个新的属性: livenessProbe
,下⾯通过 exec 执⾏⼀段命令,其 中 periodSeconds 属性表示让 kubelet 每隔5秒执⾏⼀次存活探针,也就是每5秒执⾏⼀次上⾯的 cat /tmp/healthy
命令,如果命令执⾏成功了,将返回0,那么 kubelet 就会认为当前这个容器是存活的并且很监控,如果返回的是⾮0值,那么 kubelet 就会把该容器杀掉然后重启它。另外⼀个属性 initialDelaySeconds
表示在第⼀次执⾏探针的时候要等待5秒,这样能够确保我们的容器能够有⾜够的时间启动起来。
另外我们在容器启动的时候,执⾏了如下命令:
☁ ~ /bin/sh -c "touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600"
意思是说在容器最开始的30秒内有⼀个 /tmp/healthy
⽂件,在这30秒内执⾏ cat /tmp/healthy
命令 都会返回⼀个成功的返回码。30秒后,我们删除这个⽂件,现在执⾏ cat /tmp/healthy
是不是就会失败了,这个时候就会重启容器了。
我们来创建下该 Pod ,在30秒内,查看 Pod 的 Event :
☁ ~ kubectl describe pod liveness-exec
我们可以观察到容器是正常启动的,在隔⼀会⼉,⽐如40s后,再查看下 Pod 的 Event ,在最下⾯有⼀条信息显示 liveness probe 失败了,容器被删掉并重新创建。
然后通过 kubectl get pod liveness-exec
可以看到 RESTARTS 值加1了。
同样的,我们还可以使⽤ HTTP GET 请求来配置我们的存活探针,我们这⾥使⽤⼀个 liveness 镜像来验证演示下
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-http
spec:
containers:
- name: liveness
image: cnych/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秒钟。
通常来说,任何⼤于200⼩于400的返回码都会认定是成功的返回码。其他返回码都会被认为是失败的返回码。
我们可以来查看下上⾯的 healthz 的实现:
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
duration := time.Now().Sub(started)
if duration.Seconds() > 10 {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds())))
} else {
w.WriteHeader(200)
w.Write([]byte("ok"))
}
})
⼤概意思就是最开始前10s返回状态码200,10s过后就返回500的 status_code
了。所以当容器启动3 秒后, kubelet 开始执⾏健康检查。第⼀次健康监测会成功,因为是在10s之内,但是10秒后,健康 检查将失败,因为现在返回的是⼀个错误的状态码了,所以 kubelet 将会杀掉和重启容器。
同样的,我们来创建下该 Pod 测试下效果,10秒后,查看 Pod 的 event,确认 liveness probe
失败并重启了容器。
☁ ~ kubectl describe pod liveness-http
然后我们来通过端⼝的⽅式来配置存活探针,使⽤此配置, kubelet 将尝试在指定端⼝上打开容器的套接字。 如果可以建⽴连接,容器被认为是健康的,如果不能就认为是失败的。
apiVersion: v1
kind: Pod
metadata:
name: goproxy
labels:
app: goproxy
spec:
containers:
- name: goproxy
image: cnych/goproxy
ports:
- containerPort: 8080
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
我们可以看到,TCP 检查的配置与 HTTP 检查⾮常相似,只是将 httpGet 替换成了 tcpSocket 。
另外除了上⾯的 initialDelaySeconds
和 periodSeconds
属性外,探针还可以配置如下⼏个参数:
- timeoutSeconds:探测超时时间,默认1秒,最⼩1秒。
- successThreshold:探测失败后,最少连续探测成功多少次才被认定为成功。默认是 1,但是如果是
livene ss
则必须是 1。最⼩值是 1。 - failureThreshold:探测成功后,最少连续探测失败多少次才被认定为失败。默认是 3,最⼩值是 1。