细讲Pod的生命周期

1:Pod生命周期图

pod

2:Pod状态

首先我们需要了解的就是Pod的状态,因为Pod的状态可以直观的反映出当前我们的Pod具体的状态信息,也是我们分析拍错的必须知道的一个方式,我们可以通过kubectl explain pod.status命令来了解关于Pod状态的信息,Pod的状态定义在PodStatus对象中,其中有一个phase字段,下面是phase的可取值

1:Pending(挂起):Pod信息已经提交给了集群,但是还没有被调度器调度到合适的节点或者Pod里的镜像正在下载
2:Running(运行中):该Pod已经绑定到了一个节点上,Pod中所有容器都已经被创建,至少有一个容器正在运行,或者正处于启动或者重启状态
3:Successed(成功):Pod中所有容器都被成功终止,并且不再重启
4:Failed(失败):Pod中的所有容器都已经终止,并且至少有一个容器因为失败终止,也就是说,容器以非0的状态退出或者被系统终止
5:Unknow(未知):因为某些原因无法取得Pod的状态,通常因为Pod与所在主机通信失败导致

除此之外,PodStatus对象中还包含一个PodCondition的数组,里面包含如下属性:

1:lastProbeTime:最后一次探测Pod Condition的时间戳
2:lastTransitionTime:上次Condition从一种状态转换到另一种状态的时间
3:message:上次Condition状态转换的详细描述
4:reason:Condition最后一次转换的原因
5:satatus:Condition状态类型,可以为True,Flase和Unknow
6:type:Condition类型,包含如下:
	1:PodScheduled:Pod已经被调度到其他Node中
	2:PodHasNetwork:Alpha功能,表示Runtime已经创建并配置了网络
	3:ContainersReady:Pod 中的所有容器都是Ready状态
	4:Initialized:所有的init Container都已经完成
	5:Ready:Pod 能够处理请求,Service可以将流量转发到此Pod
	6:Unscheduleble:调度程序现在无法调度Pod,由于缺乏资源或者其他限制

3:Pod重启策略

我们可以通过restartPolicy字段来配置Pod中所有容器的重启策略,它的值可以为:Always,OnFailure和Never,默认为Always,restartPolicy指通过kubelet在同一个节点上重新启动容器,通过Kubelet重新启动的退出容器将以指数增加延迟(10s,20s,40s...)重新启动,上限为5分钟,并在执行10分钟后重置,不同类型的控制器可以控制Pod的重启策略:

1:Job:适用于一次性任务如批量计算,任务结束后Pod会被此类型控制器删除,Job的重启策略只能是OnFailure或者Never
2:ReplicaSet,Deployment:此类控制器希望Pod一直运行下去,它们的重启策略只能是Always
3:DaemonSet:每个节点上运行一个Pod,很明显此类控制器的重启策略也应该是Always

4:Pod初始化容器

了解了Pod的状态之后,我们就需要来看看最开始那张图,Pod中优先启动的Init Container,也就是我们常说的`初始化容器`,Init Container就是用来做初始化工作的容器,可以是一个或者多个,如果是多个的话,这些容器会按照定义的顺序依次执行,我们知道一个Pod里面的所有容器都是共享数据卷的(要主动声明)和Network Namespace等,所以Init Container里面产生的数据可以被主容器使用到,初始化容器是独立于主容器之外的,只有所有的初始化容器执行完之后,主容器才会被启动,那么初始化容器有哪儿些应用场景呢?

1:等待其他模块Ready:它可以解决服务之间的依赖问题,比如我们有个Web服务,该服务依赖于一个数据库服务,但是我们在启动这个Web服务之前并不能保证数据库服务已经就绪了,所以可能会出现一段时间Web服务连接数据库异常的情况,要解决这个问题我们就可以在Web服务的Pod中是使用一个Init Container,在这个初始化容器中去检查数据库是否已经准备好,如果准备好了初始化容器就会退出,然后Web服务就会启动,这个时候Web服务连接数据库就不会出现连接数据库异常的情况了

2:初始化配置:比如集群里面检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。
3:其他配置:比如将Pod注册到一个中央数据库,或者注册中心等	

5:Pod Hook

我们都知道Pod是Kubernetes的最小单元,而Pod是由容器组成的,所以在讨论Pod的生命周期的时候我们可以先来探讨一下容器的生命周期,实际上Kubernetes为我们提供了生命周期的钩子,就是我们说的Pod Hook,它是由kubelet发起的,当容器中的进程启动前或者容器启动中的进程终止之前运行,这是包含在容器的生命周期之中的,我们可以同时为Pod中的所有容器都配置Hook

Kubernetes为我们提供了两种钩子函数;

1:PostStart:Kubernetes在容器创建后立刻发送postStart事件,然而postStart处理函数的调用不保证早于容器入口点(entrypoint)的执行,postStart处理函数与容器的代码是异步执行的,但Kuberentes的容器管理逻辑会一直阻塞等待postStart处理函数执行完毕,只有postStart处理函数执行完毕,容器的状态才会变成Running

2:PreStop:这个钩子在容器终止之前立即被调用,它是阻塞的,所以它必须在删除容器的调用发出之前完成,主要用于优雅关闭应用程序,通知其他系统等操作。

我们应该让钩子函数尽量轻量,当然有些情况下,长时间运行命令是合理的,比如停止容器之前预先保存状态。

Kubernetes只有在Pod结束(Terminated)的时候才会发送preStop事件,这意味着在Pod完成(Complated)时,preStop的事件处理逻辑不会被触发。

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

1:Exec:用于执行一段特定的命令,不过要注意的是该命令消耗的资源会被计入容器
2:HTTP:对容器上的特定端点执行HTTP请求
3:tcpSocket:通过发送tcp请求进行探测
apiVersion: v1
kind: Pod
metadata:
  name: nginx-hook-poststart
spec:
  containers:
  - name: nginx
    image: nginx:latest
    lifecycle:
      postStart:
        exec: 
          command: ["/bin/sh", "-c", "echo Hello From The postStart handler > /usr/share/message"]
# 执行一下然后查看一下效果
[root@k-m-1 pod]# kubectl apply -f nginx-hook-poststart.yam 
pod/nginx-hook created

[root@k-m-1 pod]# kubectl exec -it nginx-hook-poststart /bin/cat /usr/share/message
...
Hello From The postStart handler

# 下面我们来看的就是一个preStop的钩子函数,我们同样也来创建一个Pod来优雅关闭Nginx
# TOP1
apiVersion: v1
kind: Pod
metadata:
  name: nginx-hook-prestop-1
spec:
  containers:
  - name: nginx
    image: nginx:latest
    lifecycle:
      preStop:
        exec:
          command: ["/usr/sbin/nginx", "-s", "quit"]
---
# TOP2
apiVersion: v1
kind: Pod
metadata:
  name: nginx-hook-prestop-2
spec:
  volumes:
  - name: message
    hostPath:
      path: /tmp
  containers:
  - name: nginx
    image: nginx:latest
    ports:
    - name: http
      containerPort: 80
    volumeMounts:
    - name: message
      mountPath: /usr/share
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "echo Hello From PreStop Handler > /usr/share/message"]
# 上面的两个Pod,一个是利用PreStop来进行优雅停止服务,另外一个是利用PreStop来做一些信息记录的事情,同样直接创建上面的Pod

[root@k-m-1 pod]# kubectl apply -f nginx-hook-prestop.yaml 
pod/nginx-hook-prestop-1 created
pod/nginx-hook-prestop-2 created

# 启动一个终端watch监听tmp目录
[root@k-m-1 pod]# watch ls /tmp
Every 2.0s: ls /tmp                                                                                                 k-m-1: Tue Jun 20 04:30:45 2023

systemd-private-b1d5e7c62ee84ff19cfc42ff6af8948c-chronyd.service-kNcCa1
systemd-private-b1d5e7c62ee84ff19cfc42ff6af8948c-dbus-broker.service-d6cvat
systemd-private-b1d5e7c62ee84ff19cfc42ff6af8948c-systemd-logind.service-krCnVw
vmware-root_821-4290232204

# 这个时候我们先删除 nginx-hook-prestop-1,而它用的是优雅退出的方式,我们来操作一下
[root@k-m-1 pod]# kubectl delete pod nginx-hook-prestop-1 
pod "nginx-hook-prestop-1" deleted

# 但是这个其实优雅退出是在Nginx中做的,我们也无法验证,所以我们只能去验证一下 nginx-hook-prestop-2了,然后去查看watch的监听,可以看到一个message的文件

Every 2.0s: ls /tmp                                                                                                 k-m-1: Tue Jun 20 04:33:06 2023

message
systemd-private-b1d5e7c62ee84ff19cfc42ff6af8948c-chronyd.service-kNcCa1
systemd-private-b1d5e7c62ee84ff19cfc42ff6af8948c-dbus-broker.service-d6cvat
systemd-private-b1d5e7c62ee84ff19cfc42ff6af8948c-systemd-logind.service-krCnVw
vmware-root_821-4290232204

[root@k-m-1 pod]# cat /tmp/message 
Hello From PreStop Handler

# 这里就可以看到,PreStop起作用了,这就是它的用处了。

# 另外Hook调用的日志没有暴露给Pod,如果处理程序由于某种原因失败,它将产生一个事件,对于PostStart,这是FailedPostStartHook事件,对于PreStop,是FailedPreStopHook事件,比如我们修改下面的资源清单,将postStart命令替换为badcommand并应用它
apiVersion: v1
kind: Pod
metadata:
  name: nginx-hook-poststart
spec:
  containers:
  - name: nginx
    image: nginx:latest
    lifecycle:
      postStart:
        exec: 
          command: ["/bin/sh", "-c", "Echo Hello From The postStart handler > /usr/share/message"]
      preStop:
        exec:
          command: ["/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"]
[root@k-m-1 pod]# kubectl apply -f nginx-hook-poststart.yaml 
pod/nginx-hook-poststart created
# 这是FailedPostStartHook
[root@k-m-1 pod]# kubectl describe pod nginx-hook-poststart
....
Events:
  Type     Reason               Age   From               Message
  ----     ------               ----  ----               -------
  Normal   Scheduled            4s    default-scheduler  Successfully assigned default/nginx-hook-poststart to k-m-1
  Normal   Pulling              3s    kubelet            Pulling image "nginx:latest"
  Normal   Pulled               1s    kubelet            Successfully pulled image "nginx:latest" in 2.463331245s
  Normal   Created              1s    kubelet            Created container nginx
  Normal   Started              1s    kubelet            Started container nginx
  Warning  FailedPostStartHook  1s    kubelet            Exec lifecycle hook ([/bin/sh -c Echo Hello From The postStart handler > /usr/share/message]) for Container "nginx" in Pod "nginx-hook-poststart_default(00f58429-538a-42b0-8bc8-0b406f09e7a8)" failed - error: command '/bin/sh -c Echo Hello From The postStart handler > /usr/share/message' exited with 127: /bin/sh: 1: Echo: not found
, message: "/bin/sh: 1: Echo: not found\n"
  Normal  Killing  1s  kubelet  FailedPostStartHook

# 至于PreStop这个Hook,我们也只能输出文件来检查了

6:Pod健康检查

现在在Pod的整个生命周期中,能影响Pod的只剩下健康检查了,在Kubernetes集群当中,我们可以通过配置liveness probe(存活探针),readiness probe(就绪探针),以及startup Probe(启动探针)来影响容器的生命周期

1:kubelet通过使用liveness probe来确定你的应用是否正常运行,通俗的说就是是否还活着,一般来说,如果程序一旦崩溃了,kubernetes就会立刻知道这个程序已经终止了,然后就会重启这个程序,而我们的liveness probe的目的就是来捕获到当前应用程序还没有终止,没有崩溃,如果出现这些情况,那么就重启处于该状态下的容器,使应用在存在bug的情况下依然能够继续运行下去

2:kubelet使用readiness probe来确定容器是否已经就绪可以接收流量了,这个探针通俗点就是说是否准备好了,现在可以开始工作了,只有当Pod中的容器都处于就绪状态的时候kubelet才会认定Pod处于就绪状态,因为一个Pod下面可能会有很多个容器,如果就绪探测失败,端点控制器将从于与Pod匹配的所有服务的端点列表中删除该Pod的IP地址,这样我们的流量就不会路由到这个Pod内了

3:kubelet使用startup probe来指示容器中的应用是否已经启动,如果提供了启动探针,则所有其他探针都会被禁用,直到该探针成功为止,如果启动探针探测失败,kubelet将杀死容器,而容器依其重启策略进行重启,如果容器没有提供启动探针,则默认状态为Success

问:何时使用存活探针?
答:如果容器中的进程能够在遇到的问题或不健康的情况下自行崩溃,则不一定需要存活态探针:kubelet将根据Pod的restartPolicy自动执行修复操作,如果你希望容器在探测失效时被杀死并重新启动,那么请指定一个存活态探针,并指定restartPolicy为Always或OnFailure

问:何时使用就绪探针?
答:如果要仅在探针成功时开始向Pod发送请求流量,那么就需要指定就绪探针,在这种情况下,就绪探针可能与存活态探针相同,就绪探针的存在意味着Pod将在启动阶段不接受任何数据,并且只有在探针探测成功后才开始收数据。

如果你希望容器能够自行进入维护状态,也可以指定一个就绪态探针,检查某个特定于就绪态的不同于存活态探针的端点,如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针,当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用,这可以帮助你避免将流量导向错误信息的Pod

如果你的容器需要在启动期间加载大型的数据,配置文件等操作,那么这个时候我们可以用启动探针(startup probe)

# 请注意:如果只是想在Pod被删除时能排空请求,则不一定需要使用就绪探针,在删除Pod时,Pod会自动将自身置于未就绪状态,无论就绪探针是否存在,等待Pod中的容器停止期间,Pod会一直处于未就绪状态

问:何时使用启动探针?
答:该探针在kubernetes 1.20版本才变成了稳定状态,对于所包含的容器需要较长时间才能启动就绪探针的Pod而言,启动探针是有用的,你不在需要配置一个较长的存活探针探测时间间隔,只需要设置一个独立的配置项,对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。

如果你的容器启动时间超出initialDelaySeconds + failureThreshold x periodSeconds的总值,你应该设置一个启动探针,对于存活探针所使用的同一段点执行检查,periodSeconds的默认值是10s,还应该将其failureThreshold设置得足够高,以便容器有充足的时间完成启动,并且避免更改存活态探针所使用的默认值,这一设置有助于减少死锁状况的发生


# 容器探测

probe是由kubelet对容器执行的定期诊断,要执行诊断,kubelet即可以在容器内执行代码,也可以发出一个网络请求,使用探针来检查容器有四种不同的方法,每个探针都必须定义为四种机制中的一种

1:exec:在容器内执行命令,如果命令退出时状态码为0,则认为判断成功
2:grpc:使用gRPC执行一个远程调用,目标应该实现gRPC健康检查,如果响应的状态为SERVING,则认为诊断成功,不过需要注意的是,gRPC探测是一个Alpha的属性,只有在启用了GRPCContaienrProbe特性时才能使用
3:httpGet:对容器的IP地址上的指定端口和路径执行HTTP GET请求,如果响应的状态码大于等于200且小于400,则诊断被认定为成功
4:tcpSocket:使用此配置kubelet将尝试在指定端口上打开容器的套接字,如果可以建立连接,容器被认定为是健康的,如果不能就认定后为是失败的,实际上就是检测端口的

每次探测都会获得以下三种结果:

1:Success(成功):容器通过了诊断
2:Failure(失败):容器未通过探测
3:Unknow (未知):诊断失败,因此不会采取任何行动

我们来演示下存活探针的使用方法,我们这里使用exec执行命令的方式来检测容器的存活
apiVersion: v1
kind: Pod
metadata:
  name: liveness-pod-exec
spec:
  containers:
  - name: liveness
    image: busybox:latest
    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        # 每隔多久去执行一次探针
      successThreshold: 1     # 至少探针执行多少次过后才会被认为是真正的成功
      failureThreshold: 3     # 执行失败多少次过后才会被认为是真正的失败
# 尝试部署
[root@k-m-1 pod]# kubectl apply -f liveness-pod-exec.yaml 
pod/liveness-pod-exec created

[root@k-m-1 pod]# kubectl describe pod liveness-pod-exec 
......
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  84s                default-scheduler  Successfully assigned default/liveness-pod-exec to k-m-1
  Normal   Pulled     81s                kubelet            Successfully pulled image "busybox:latest" in 2.550467835s
  Warning  Unhealthy  39s (x3 over 49s)  kubelet            Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
  Normal   Killing    39s                kubelet            Container liveness failed liveness probe, will be restarted
  Normal   Pulling    8s (x2 over 83s)   kubelet            Pulling image "busybox:latest"
  Normal   Created    6s (x2 over 81s)   kubelet            Created container liveness
  Normal   Started    6s (x2 over 81s)   kubelet            Started container liveness
  Normal   Pulled     6s                 kubelet            Successfully pulled image "busybox:latest" in 2.6779872s

# 当探测不到的时候它会尝试重启我们应用
[root@k-m-1 pod]# kubectl get pod
NAME                READY   STATUS    RESTARTS      AGE
liveness-pod-exec   1/1     Running   1 (19s ago)   94s

# 同样的,我们用HTTP GET请求来配置存活探针
apiVersion: v1
kind: Pod
metadata:
  name: liveness-pod-httpget
spec:
  containers:
  - name: nginx
    image: nginx:latest
    ports:
    - name: http
      containerPort: 80
    livenessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 5  # 容器启动多久后开始去执行当前探针
      periodSeconds: 5        # 每隔多久去执行一次探针
      successThreshold: 1     # 至少探针执行多少次过后才会被认为是真正的成功
      failureThreshold: 3     # 执行失败多少次过后才会被认为是真正的失败
# 部署看结果
[root@k-m-1 pod]# kubectl apply -f liveness-pod-httpget.yaml 
pod/liveness-pod-httpget created

# 可以看到启动了这么久容器都没事,然后我们看看关键字
[root@k-m-1 pod]# kubectl get pod
NAME                   READY   STATUS    RESTARTS   AGE
liveness-pod-httpget   1/1     Running   0          2m11s

[root@k-m-1 pod]# kubectl describe pod liveness-pod-httpget | grep Liveness
    Liveness:       http-get http://:80/ delay=5s timeout=1s period=5s #success=1 #failure=3

# 我们来看看探测的日志
[root@k-m-1 pod]# kubectl logs -f liveness-pod-httpget 
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2023/06/19 22:43:14 [notice] 1#1: using the "epoll" event method
2023/06/19 22:43:14 [notice] 1#1: nginx/1.25.1
2023/06/19 22:43:14 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) 
2023/06/19 22:43:14 [notice] 1#1: OS: Linux 5.14.0-165.el9.x86_64
2023/06/19 22:43:14 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2023/06/19 22:43:14 [notice] 1#1: start worker processes
2023/06/19 22:43:14 [notice] 1#1: start worker process 29
2023/06/19 22:43:14 [notice] 1#1: start worker process 30
2023/06/19 22:43:14 [notice] 1#1: start worker process 31
2023/06/19 22:43:14 [notice] 1#1: start worker process 32
10.0.0.11 - - [19/Jun/2023:22:43:21 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:22:43:26 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:22:43:31 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:22:43:36 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:22:43:41 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:22:43:46 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:22:43:51 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:22:43:56 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"

# 当然还有tcpSocket和HTTP的时候都是可以直接使用ports.name这个字段的名称来当作参数的,不过我们要注意的是,尽量避免使用TCP探测,因为TCP探测实际上就是Kubelet向指定端口发送TCP SVN握手包,当端口被监听内核就会直接响应ACK,探测就会成功,当程序死锁或者hang住的情况,这些并不影响端口监听,所以探测结果还是健康,流量打到表面健康但实际不健康的Pod上,就会无法处理请求,从而引发业务故障

有时候,会有一些现有的应用在启动时需要较长的初始化时间,前面我们提到了探针里面有一个initialDelaySeconds的属性,可以来配置第一次执行探针的等待时间,对于启动非常慢的应用这个参数非常有用,比如Jenkins,Gitlab这类应用,但是如何设置一个合适的的初始延迟时间呢?这个就和应用具体环境有关系了,所以这个值往往不是通用的,这样的话就可能导致一个问题,我们的资源清单在别的环境下可能就会健康检查失败了,这个时候可以使用startupProbe(启动探针),该探针推迟所有其他探针,直到Pod完成启动为止,使用方法和存活探针一样,技巧就是使用想用的命令来设置启动探针,针对HTTP或者TCP检测,可以通过将failureThreshold * periodSeconds参数设置为足够长的时间来应对糟糕的情况下启动时间
apiVersion: v1
kind: Pod
metadata:
  name: liveness-pod-httpget
spec:
  containers:
  - name: nginx
    image: nginx:latest
    ports:
    - name: http
      containerPort: 80
    livenessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 5
      successThreshold: 1
      failureThreshold: 3
    startupProbe:
      httpGet:
        path: /
        port: 80
      failureThreshold: 30    # 尽量设置大
      periodSeconds: 10
# 查看状态
[root@k-m-1 pod]# kubectl describe pod liveness-pod-httpget 
......
    Liveness:       http-get http://:80/ delay=5s timeout=1s period=5s #success=1 #failure=3
    Startup:        http-get http://:80/ delay=0s timeout=1s period=10s #success=1 #failure=30
......

# 探测日志
[root@k-m-1 pod]# kubectl logs -f liveness-pod-httpget 
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2023/06/19 23:06:52 [notice] 1#1: using the "epoll" event method
2023/06/19 23:06:52 [notice] 1#1: nginx/1.25.1
2023/06/19 23:06:52 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) 
2023/06/19 23:06:52 [notice] 1#1: OS: Linux 5.14.0-165.el9.x86_64
2023/06/19 23:06:52 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2023/06/19 23:06:52 [notice] 1#1: start worker processes
2023/06/19 23:06:52 [notice] 1#1: start worker process 29
2023/06/19 23:06:52 [notice] 1#1: start worker process 30
2023/06/19 23:06:52 [notice] 1#1: start worker process 31
2023/06/19 23:06:52 [notice] 1#1: start worker process 32
10.0.0.11 - - [19/Jun/2023:23:06:59 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:04 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:09 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:14 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:19 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:24 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:29 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:34 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:39 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:44 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:49 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"
10.0.0.11 - - [19/Jun/2023:23:07:54 +0000] "GET / HTTP/1.1" 200 615 "-" "kube-probe/1.25" "-"

我们上面配置表示我们的慢速容器最多可以有5分钟(30个检查 * 10秒 = 300s)来完成启动

有的时候,应用程序可能暂时无法对外提供服务,例如,应用程序可能需要启动期间加载大量的数据,在这种情况下,你不想杀死应用程序,也不想对外提供服务,那么这个时候我们可以使用readiness probe来检测和减轻这些情况,Pod中的容器可以报告自己还没有准备好,不能处理kubernetes服务发送过来的流量,存活探针和就绪探针如果同时使用的话就可以确保流量不会达到还未准备好的容器,准备好后,如果应用程序出现了错误,则会重新启动容器

就绪探针和存活探针的配置相似,唯一区别就是要使用readinessProbe字段,而不是livenessProbe字段
apiVersion: v1
kind: Pod
metadata:
  name: liveness-readiness-startup
spec:
  containers:
  - name: nginx
    image: nginx:latest
    ports:
    - name: http
      containerPort: 80
    livenessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 5
      successThreshold: 1
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 5
      successThreshold: 1
      failureThreshold: 3
    startupProbe:
      httpGet:
        path: /
        port: 80
      failureThreshold: 30
      periodSeconds: 10
[root@k-m-1 pod]# kubectl describe pod liveness-readiness-startup
......
    Liveness:       http-get http://:80/ delay=5s timeout=1s period=5s #success=1 #failure=3
    Readiness:      http-get http://:80/ delay=5s timeout=1s period=5s #success=1 #failure=3
    Startup:        http-get http://:80/ delay=0s timeout=1s period=10s #success=1 #failure=30
......

# 如果你的容器对外提供了服务,监听了端口,那么应该配置上就绪探针,就绪探针不通过就视为Pod不健康,然后会自动将不健康的Pod剔除,避免将业务流量转发给异常的Pod

另外除了上面添加initialDelaySeconds和periodSeconds属性之外,探针还可以配置如下几个参数:

1:timeoutSeconds:探测超时时间,默认1秒,最小1秒
2:successThreshold:探测失败后,最少连续探测成功多少次才会被认定为成功,默认是1,但是如果是liveness则必须是1,最小值1
3:failureThreshold:探测成功后,最少连续探测失败多少次才会被认定为失败,默认是3,最小值是1

7:Pod的终止

image

1:用户发出删除Pod的指令,Pod被删除,状态变更为Terminating从API层面来看就是Pod的metadata中的deletionTimestamp字段会被标记上删除时间

2:kube-proxy watch到了就开始更新转发规则,将Pod从Service的Endpoints列表中摘除掉,新的流量不再转发到该Pod

3:kubelet watch到了就开始销毁Pod
	1:如果Pod中有container配置了preStop Hook,则Pod被标记为Termination状态时以同步的方式被启动执行,若宽限期结束后preStop仍未执行结束,则会额外获得一个2秒的小宽限期
	2:发送SIGTERM信号给容器内的主进程以通知容器进程开始优雅停止
	3:等待Container 中的进程完全停止,如果在宽限期结束后还未停止,就发送SIGKILL信号将其强制杀死
	4:所有容器进程终止,清理Pod资源
	5:通知API Server Pod销毁完成,完成Pod删除
	
	
# 对于长连接类型的业务,比如游戏类应用,我们可以将terminationGracePeriodSeconds设置大一点,避免过载的被SIGKILL杀死,但是具体多长时间是不好预估的,所以最好在业务层面进行优化,比如Pod销毁时优雅终止逻辑里面主动通知下客户端,让客户端连接到新的后端,然后客户端来保证这两个连接的平滑切换,等旧Pod上所有客户端连接切换到了新的Pod上,才最终退出。
# 强制终止Pod

默认情况下,所有删除操作都会有30秒的宽限时间,kubectl delete 命令支持--grace-period=<seconds>选项,允许你重载默认值,设置为自己希望的期限值

将宽限时间强制设置为0意味着立即从APIServer删除Pod,如果Pod仍然处于运行于某个节点上,强制删除操作会触发kubelet立即执行清理操作。

# 必须在这只--grace-period=0的同时额外设置--force参数才能发起强制删除请求

执行强制删除操作时,APIServer不再等待来自kubelet的,关于Pod已经在原来运行的节点上终止执行的确认消息,APIServer直接删除Pod对象,这样新的与之同名的Pod即可以被创建,在节点侧,被设置为立即终止的Pod仍然会被在强制杀死之前获得一点点的宽限时间

对于已失败的Pod而言,对应的API对象仍然会保留在集群的API服务器上,直到用户或者控制器进程显式地将其删除,控制面组件会在Pod个数超出所配置的阈值,(根据kube-controller-manager的termainated-pod-gc-threshold设置)时删除已经终止的Pod(phase值设置为Succeeded或者Failed),这一行为避免随着时间不断创建和终止Pod而引起的资源泄露问题
# 业务代码处理SIGTERM信号

要实现优雅退出,我们需要业务代码得支持下优雅退出的逻辑,在业务代码里面处理下SIGTERM信号,一般主要逻辑就是"排水",即等待存量的任务或者连接完全结束,再退出进程,下面我们拿Go语言来实现一个优雅的退出代码示例

# 初始化一个Go的go.mod
PS D:\Codes\gin-web> go mod init github.com/gitlayzer/gin-web
go: creating new go.mod: module github.com/gitlayzer/gin-web
package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

func main() {
	sigs := make(chan os.Signal, 1)
	done := make(chan bool, 1)

	signal.Notify(sigs, syscall.SIGTERM)

	go func() {
		sig := <-sigs
		fmt.Println("Caught SIGTERM,SHUTTING DOWN", sig)
		done <- true
	}()

	fmt.Println("Starting Application")
	<-done
	fmt.Println("Application Stopped")
}
1:收不到SIGTERM信号怎么办?

上面我们写的是可以收到SIGTERM的情况,然后我们就可以执行停止逻辑以实现优雅退出了,在kubernetes环境中,业务发版时经常会对工作负载进行滚动更新,当旧版本Pod被删除时,K8S会对Pod中的各个容器的主进程发送SIGTERM信号,当达到退出宽容期限后进程还未完全停止的话,就会发送SIGKILL信号将其强制杀死,但是有些场景下Kubernetes环境中实际运行时,有时候可能发现在滚动更新时,我们业务的优雅停止逻辑并没有被执行,现象是在等了较长时间后,业务进程直接被SIGKILL强制杀死了。

这是什么原因造成的呢?通常情况下这都是因为容器启动入口使用了shell,比如使用了类似/bin/sh -c my-app这样的启动入口,或者使用了/entrypoint.sh这样的脚本文件作为入口,在脚本中再启动业务进程,比如下面的一个entrypoint.sh
#! /bin/bash

/web_server
这样可能会导致容器内的业务进程收不到SIGTERM信号,原因是:

1:容器主进程时Shell,业务进程是在shell中启动的,变成了shell进程的子进程了。
2:shell进程默认不会处理SIGTERM信号,自己不会退出,也不会将信号传递给子进程,所以就导致了业务进程不会触发停止逻辑。
3:当等到K8S优雅停止宽限时间(terminationGracePeriodSeconds,默认30秒),就只能发送SIGKILL强制杀死shell及其子进程了。

那么我们该如何让我们的业务进程收到SIGTERM信号呢?当然如果可以的话,尽量不要使用shell启动业务进程,这样当然最好了,此外我们还有其他解决方案。

1:使用exec启动

在shell中启动二进制的命令前加一个exec命令,即可让该二进制启动的进程代替当前shell进程,即让新启动的进程作为主进程
#! /bin/bash
...

exec /web_server
然后业务进程就可以正常接收所有的信号了,实现优雅的退出当然可就可以了。
2:多进程场景

通常我们一个容器只有一个进程,但有些时候我们不得不启动多个进程,比如从传统部署迁移到Kubernetes的过渡期间,使用了富容器,即单个容器中需要启动多个业务进程,这时候我们可以通过shell来启动,但却无法使用上面的exec方式来传递信号了,因为exec只能让一个进程替代当前shell成为主进程。

这个时候我们可以在shell中trap来捕获信号,当收到信号后触发回调函数来将信号通过kill命令传递给业务进程,来看看示例:
#! /bin/bash

/bin/app1 & pid1="$!"  # 启动第一个业务进程并记录pid
echo "app1 started with pid $pid1"

/bin/app2 & pid2="$!"  # 启动第二个业务进程并记录pid
echo "app2 started with pid $pid2"

handle_sigterm() {
  echo "[INFO] Received SIGTERM"
  kill -SIGTERM $pid1 $pid2    # 传递SIGTERM给业务进程
  wait $pid1 $pid2             # 等待所有业务进程完全终止
}

trap handle_sigterm SIGTERM    # 捕获SIGTERM信号并回调handle_sigterm函数

wait                           # 等待回调执行完毕,主进程再退出
3:使用init系统

前面的一种方案实际是用脚本写了一个极简的init系统(或supervisor)来管理所有子进程,只不过它的逻辑很简陋,仅仅简单的透传指定信号给子进程,其他社区有更完善的方案,dumb-init和tini都可以作为init进程,作为主进程(PID 1)在容器中启动,然后它再运行shell来执行我们指定的脚本(shell作为子进程),shell中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而能够完美的解决shell无法传递信号的问题,并且还有回收僵尸进程的能力,我们也比较推荐这种方式

下面我们来写一个Dockerfile作为示例
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y dumb-init
ADD start.sh /
ADD app1 /bin/app1
ENTRYPOINT ["dumb-init", "--"]
CMD ["start.sh"]
posted @ 2023-06-21 06:04  Layzer  阅读(222)  评论(0编辑  收藏  举报