天行健,君子以自强不息;地势坤,君子以厚德载物;

Kubernetes--POD的启动流程及控制容器的启动顺序

POD的启动流程

整体流程

client向APIServer发送创建pod的请求:

  1. APIServer将pod信息存入etcd,通知Scheduler;
  2. Scheduler根据调度算法,为pod选择一个节点,然后向APIServer发送更新spec.nodeName;
  3. APIServer更新完毕,通知对应节点的kubelet;
  4. kubelet发现pod调度到本节点,创建并运行pod的容器;

《Kubernetes--POD的启动流程及控制容器的启动顺序》

APIServer处理pod启动

APIServer收到创建pod的请求后:

  • 首先,对client来源进行认证,实现方式是证书(cert)或token(sa);
  • 然后,对client进行鉴权,确认其是否有创建pod的权限,实现方法是rbac;
  • 然后,通过准入控制插件验证或修改资源请求,常用的准入控制插件有:LimitRanger/ResourceQuota/NamespaceLifecycle;
  • 最后,将pod资源存储到etcd;

《Kubernetes--POD的启动流程及控制容器的启动顺序》

Scheduler处理pod启动

Scheduler监听到APIServer创建pod的事件后:

  • 按照默认的调度算法(预选算法 优选算法),为pod选择一个可运行的节点nodeName;
  • 向APIServer发送更新pod的消息:pod.spec.nodeName;
  • APIServer更新pod,通知nodeName上的kubelet,pod被调度到了nodeName;

Kubelet处理pod启动

kubelet监听到APIServer创建pod的事件后,通知容器运行时dockerd拉取镜像,创建容器,启动容器。

《Kubernetes--POD的启动流程及控制容器的启动顺序》

pod中容器的启动过程:

  • InitC容器:
    • 多个initC顺序启动,前一个启动成功后才启动下一个;
    • 仅当最后一个initC执行完毕后,才会启动主容器;
    • 常用于进行初始化操作或等待依赖的服务已ok;
  • postStart钩子:
    • postStart与container的主进程并行执行
    • 在postStart执行完毕前,容器一直是waiting状态,pod一直是pending状态;
    • 若postStart运行失败,容器会被杀死;
  • startupProbe钩子:
    • v1.16版本后新增的探测方式;
    • 若配置了startupProbe,就会先禁止其他探测,直到成功为止;
  • readinessProbe探针:
    • 探测容器状态是否ready,准备好接收用户流量;
    • 探测成功后,将pod的endpoint添加到service;
  • livenessProbe探针:
    • 探测容器的健康状态,若探测失败,则按照重启策略进行重启;
  • containers:
    • 多个container之间是顺序启动的,参考源码

参考

  1. kubernetes in action
  2. container启动源码:https://github.com/kubernetes...

了解了启动顺序后,来讨论一下POD中容器的启动顺序问题

启动顺序离不开Pod 生命周期与postStart、preStop事件,先熟悉一下相关知识

Pod容器生命周期

《Kubernetes--POD的启动流程及控制容器的启动顺序》

Pause容器说明

每个Pod里运行着一个特殊的被称之为Pause的容器,其他容器则为业务容器,这些业务容器共享Pause容器的网络栈和Volume挂载卷,因此他们之间通信和数据交换更为高效。在设计时可以充分利用这一特性,将一组密切相关的服务进程放入同一个Pod中;同一个Pod里的容器之间仅需通过localhost就能互相通信。

kubernetes中的pause容器主要为每个业务容器提供以下功能:
PID命名空间:Pod中的不同应用程序可以看到其他应用程序的进程ID。

网络命名空间:Pod中的多个容器能够访问同一个IP和端口范围。

IPC命名空间:Pod中的多个容器能够使用SystemV IPC或POSIX消息队列进行通信。

UTS命名空间:Pod中的多个容器共享一个主机名;Volumes(共享存储卷)。

Pod中的各个容器可以访问在Pod级别定义的Volumes。

Init Container容器

Pod可以包含多个容器,应用运行在这些容器里面,同时 Pod 也可以有一个或多个先于应用容器启动的 Init 容器。

如果为一个 Pod 指定了多个 Init 容器,这些Init容器会按顺序逐个运行。每个 Init 容器都必须运行成功,下一个才能够运行。当所有的 Init 容器运行完成时,Kubernetes 才会为 Pod 初始化应用容器并像平常一样运行。

Init容器与普通的容器非常像,除了以下两点:

1、Init容器总是运行到成功完成且正常退出为止

2、只有前一个Init容器成功完成并正常退出,才能运行下一个Init容器。

如果Pod的Init容器失败,Kubernetes会不断地重启Pod,直到Init容器成功为止。但如果Pod对应的restartPolicy为Never,则不会重新启动。

在所有的 Init 容器没有成功之前,Pod 将不会变成 Ready 状态。 Init 容器的端口将不会在 Service 中进行聚集。 正在初始化中的 Pod 处于 Pending 状态,但会将条件 Initializing 设置为 true。

如果 Pod 重启,所有 Init 容器必须重新执行。

在 Pod 中的每个应用容器和 Init 容器的名称必须唯一;与任何其它容器共享同一个名称,会在校验时抛出错误。

Init 容器能做什么?

因为 Init 容器是与应用容器分离的单独镜像,其启动相关代码具有如下优势:

1、Init 容器可以包含一些安装过程中应用容器不存在的实用工具或个性化代码。例如,在安装过程中要使用类似 sed、 awk、 python 或 dig 这样的工具,那么放到Init容器去安装这些工具;再例如,应用容器需要一些必要的目录或者配置文件甚至涉及敏感信息,那么放到Init容器去执行。而不是在主容器执行。

2、Init 容器可以安全地运行这些工具,避免这些工具导致应用镜像的安全性降低。

3、应用镜像的创建者和部署者可以各自独立工作,而没有必要联合构建一个单独的应用镜像。

4、Init 容器能以不同于Pod内应用容器的文件系统视图运行。因此,Init容器可具有访问 Secrets 的权限,而应用容器不能够访问。

5、由于 Init 容器必须在应用容器启动之前运行完成,因此 Init 容器提供了一种机制来阻塞或延迟应用容器的启动,直到满足了一组先决条件。一旦前置条件满足,Pod内的所有的应用容器会并行启动。

Init 容器示例

下面的例子定义了一个具有 2 个 Init 容器的简单 Pod。 第一个等待 myservice 启动,第二个等待 mydb 启动。 一旦这两个 Init容器都启动完成,Pod 将启动spec区域中的应用容器。

Pod yaml文件

[root@k8s-master lifecycle]# pwd
/root/k8s_practice/lifecycle
[root@k8s-master lifecycle]# cat init_C_pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: myapp-busybox-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: registry.cn-beijing.aliyuncs.com/google_registry/busybox:1.24
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: registry.cn-beijing.aliyuncs.com/google_registry/busybox:1.24
    command: ['sh', '-c', "until nslookup myservice; do echo waiting for myservice; sleep 60; done"]
  - name: init-mydb
    image: registry.cn-beijing.aliyuncs.com/google_registry/busybox:1.24
    command: ['sh', '-c', "until nslookup mydb; do echo waiting for mydb; sleep 60; done"]

启动这个 Pod,并检查其状态,可以执行如下命令:

[root@k8s-master lifecycle]# kubectl apply -f init_C_pod.yaml 
pod/myapp-busybox-pod created 
[root@k8s-master lifecycle]# kubectl get -f init_C_pod.yaml -o wide  # 或者kubectl get pod myapp-busybox-pod -o wide
NAME                READY   STATUS     RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
myapp-busybox-pod   0/1     Init:0/2   0          55s   10.244.4.16   k8s-node01              

如需更详细的信息:

[root@k8s-master lifecycle]# kubectl describe pod myapp-busybox-pod 
Name:         myapp-busybox-pod
Namespace:    default
Priority:     0
…………
Node-Selectors:  
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age    From                 Message
  ----    ------     ----   ----                 -------
  Normal  Scheduled  2m18s  default-scheduler    Successfully assigned default/myapp-busybox-pod to k8s-node01
  Normal  Pulled     2m17s  kubelet, k8s-node01  Container image "registry.cn-beijing.aliyuncs.com/google_registry/busybox:1.24" already present on machine
  Normal  Created    2m17s  kubelet, k8s-node01  Created container init-myservice
  Normal  Started    2m17s  kubelet, k8s-node01  Started container init-myservice

如需查看Pod内 Init 容器的日志,请执行:

[root@k8s-master lifecycle]# kubectl logs -f --tail 500 myapp-busybox-pod -c init-myservice   # 第一个 init container 详情
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

waiting for myservice
nslookup: can't resolve 'myservice'
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
………………
[root@k8s-master lifecycle]# kubectl logs myapp-busybox-pod -c init-mydb   # 第二个 init container 详情
Error from server (BadRequest): container "init-mydb" in pod "myapp-busybox-pod" is waiting to start: PodInitializing

此时Init 容器将会等待直至发现名称为mydb和myservice的 Service。

Service yaml文件

[root@k8s-master lifecycle]# pwd
/root/k8s_practice/lifecycle
[root@k8s-master lifecycle]# cat init_C_service.yaml 
---
kind: Service
apiVersion: v1
metadata:
  name: myservice
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
---
kind: Service
apiVersion: v1
metadata:
  name: mydb
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9377

创建mydb和myservice的 service 命令:

[root@k8s-master lifecycle]# kubectl create -f init_C_service.yaml 
service/myservice created
service/mydb created

之后查看pod状态和service状态,能看到这些 Init容器执行完毕后,随后myapp-busybox-pod的Pod转移进入 Running 状态:

[root@k8s-master lifecycle]# kubectl get svc -o wide mydb myservice
NAME        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE   SELECTOR
mydb        ClusterIP   10.108.24.84             80/TCP    72s   
myservice   ClusterIP   10.105.252.196           80/TCP    72s   
[root@k8s-master lifecycle]# 
[root@k8s-master lifecycle]# kubectl get pod myapp-busybox-pod -o wide 
NAME                READY   STATUS    RESTARTS   AGE     IP            NODE         NOMINATED NODE   READINESS GATES
myapp-busybox-pod   1/1     Running   0          7m33s   10.244.4.17   k8s-node01              

由上可知:一旦我们启动了 mydb 和 myservice 这两个 Service,我们就能够看到 Init 容器完成,并且 myapp-busybox-pod 被创建。
进入myapp-busybox-pod容器,并通过nslookup查看这两个Service的DNS记录。

[root@k8s-master lifecycle]# kubectl exec -it myapp-busybox-pod sh
/ # nslookup mydb 
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      mydb
Address 1: 10.108.24.84 mydb.default.svc.cluster.local
/ # 
/ # 
/ # 
/ # nslookup myservice
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      myservice
Address 1: 10.105.252.196 myservice.default.svc.cluster.local

主容器生命周期事件的处理函数

Kubernetes 支持 postStart 和 preStop 事件。当一个主容器启动后,Kubernetes 将立即发送 postStart 事件;在主容器被终结之前,Kubernetes 将发送一个 preStop 事件。

postStart 和 preStop 处理函数示例

pod yaml文件:vi lifecycle-events.yaml

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo-pod
  namespace: default
  labels:
    test: lifecycle
spec:
  containers:
  - name: lifecycle-demo
    image: registry.cn-beijing.aliyuncs.com/google_registry/nginx:1.17
    imagePullPolicy: IfNotPresent
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo 'Hello from the postStart handler' >> /var/log/nginx/message"]
      preStop:
        exec:
          command: ["/bin/sh", "-c", "echo 'Hello from the preStop handler'   >> /var/log/nginx/message"]
    volumeMounts:         #定义容器挂载内容
    - name: message-log   #使用的存储卷名称,如果跟下面volume字段name值相同,则表示使用volume的nginx-site这个存储卷
      mountPath: /var/log/nginx/  #挂载至容器中哪个目录
      readOnly: false             #读写挂载方式,默认为读写模式false
  initContainers:
  - name: init-myservice
    image: registry.cn-beijing.aliyuncs.com/google_registry/busybox:1.24
    command: ["/bin/sh", "-c", "echo 'Hello initContainers'   >> /var/log/nginx/message"]
    volumeMounts:         #定义容器挂载内容
    - name: message-log   #使用的存储卷名称,如果跟下面volume字段name值相同,则表示使用volume的nginx-site这个存储卷
      mountPath: /var/log/nginx/  #挂载至容器中哪个目录
      readOnly: false             #读写挂载方式,默认为读写模式false
  volumes:              #volumes字段定义了paues容器关联的宿主机或分布式文件系统存储卷
  - name: message-log   #存储卷名称
    hostPath:           #路径,为宿主机存储路径
      path: /data/volumes/nginx/log/    #在宿主机上目录的路径
      type: DirectoryOrCreate           #定义类型,这表示如果宿主机没有此目录则会自动创建

启动pod,查看pod状态

# kubectl apply -f lifecycle-events.yaml 
pod/lifecycle-demo-pod created
# kubectl get pod -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
lifecycle-demo-pod   1/1     Running   0          5s    10.244.2.30   k8s-node02              

查看pod详情

# kubectl describe pod lifecycle-demo-pod
Name:         lifecycle-demo-pod
Namespace:    default
Priority:     0
Node:         k8s-node02/172.16.1.112
Start Time:   Sat, 23 May 2020 22:08:04  0800
Labels:       test=lifecycle
………………
Init Containers:
  init-myservice:
    Container ID:  docker://1cfabcb60b817efd5c7283ad9552dafada95dbe932f92822b814aaa9c38f8ba5
    Image:         registry.cn-beijing.aliyuncs.com/google_registry/busybox:1.24
    Image ID:      docker-pullable://registry.cn-beijing.aliyuncs.com/ducafe/busybox@sha256:f73ae051fae52945d92ee20d62c315306c593c59a429ccbbdcba4a488ee12269
    Port:          
    Host Port:     
    Command:
      /bin/sh
      -c
      echo 'Hello initContainers'   >> /var/log/nginx/message
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Sat, 23 May 2020 22:08:06  0800
      Finished:     Sat, 23 May 2020 22:08:06  0800
    Ready:          True
    Restart Count:  0
    Environment:    
    Mounts:
      /var/log/nginx/ from message-log (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-v48g4 (ro)
Containers:
  lifecycle-demo:
    Container ID:   docker://c07f7f3d838206878ad0bfeaec9b4222ac7d6b13fb758cc1b340ac43e7212a3a
    Image:          registry.cn-beijing.aliyuncs.com/google_registry/nginx:1.17
    Image ID:       docker-pullable://registry.cn-beijing.aliyuncs.com/google_registry/nginx@sha256:7ac7819e1523911399b798309025935a9968b277d86d50e5255465d6592c0266
    Port:           
    Host Port:      
    State:          Running
      Started:      Sat, 23 May 2020 22:08:07  0800
    Ready:          True
    Restart Count:  0
    Environment:    
    Mounts:
      /var/log/nginx/ from message-log (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-v48g4 (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  message-log:
    Type:          HostPath (bare host directory volume)
    Path:          /data/volumes/nginx/log/
    HostPathType:  DirectoryOrCreate
  default-token-v48g4:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-v48g4
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age        From                 Message
  ----    ------     ----       ----                 -------
  Normal  Scheduled    default-scheduler    Successfully assigned default/lifecycle-demo-pod to k8s-node02
  Normal  Pulled     87s        kubelet, k8s-node02  Container image "registry.cn-beijing.aliyuncs.com/google_registry/busybox:1.24" already present on machine
  Normal  Created    87s        kubelet, k8s-node02  Created container init-myservice
  Normal  Started    87s        kubelet, k8s-node02  Started container init-myservice
  Normal  Pulled     86s        kubelet, k8s-node02  Container image "registry.cn-beijing.aliyuncs.com/google_registry/nginx:1.17" already present on machine
  Normal  Created    86s        kubelet, k8s-node02  Created container lifecycle-demo
  Normal  Started    86s        kubelet, k8s-node02  Started container lifecycle-demo

此时在k8s-node02查看输出信息如下:

# pwd
/data/volumes/nginx/log
# cat message 
Hello initContainers
Hello from the postStart handler

由上可知,init Container先执行,然后当一个主容器启动后,Kubernetes 将立即发送 postStart 事件。
停止该pod

# kubectl delete pod lifecycle-demo-pod
pod "lifecycle-demo-pod" deleted

此时在k8s-node02查看输出信息如下:

# pwd
/data/volumes/nginx/log
# cat message 
Hello initContainers
Hello from the postStart handler
Hello from the preStop handler

由上可知,当在容器被终结之前, Kubernetes 将发送一个 preStop 事件。

为什么需要了解上述知识呢?因为postStart事件是阻塞的!

方案一:依赖postStart事件控制启动顺序

在官方文档里有这么一段话

Kubernetes 在容器创建后立即发送 postStart 事件 然而postStart 处理函数的调用不保证早于容器的入口点entrypoint的执行postStart 处理函数与容器的代码是异步执行的但 Kubernetes 的容器管理逻辑会一直阻塞等待 postStart 处理函数执行完毕

K8S 在启动同一个 Pod 里容器时其启动顺序会按照配置文件中的顺序去启动注意在这里它并不会等容器启动完成才继续而当你为某一个容器增加 postStart Hook 之后K8S 的启动流程会被这个 Hook 给阻塞住直到 Hook 完成了才会继续往下执行这样一来只需要在容器的 postStart Hook 里执行对当前容器的状态的查询代码直到容器内进程或服务状态正常才退出就可以保证在下一个容器启动前该容器能正常提供服务

这时候就能理解官方文档中的那句话了“postStart 处理函数与容器的代码是异步执行的但 Kubernetes 的容器管理逻辑会一直阻塞等待 postStart 处理函数执行完毕它的意思是——postStart 处理函数和容器启动是同时被调用的但是双方的实际执行却是互不干涉的Kubernetes 的容器管理逻辑不会等待容器启动完成但是会一直阻塞等待 postStart 处理函数执行完毕

那就可以在当前容器的postStart事件里放一个探测,直到当前容器服务正常后,才退出,K8S顺序启动后续容器

方案二:依赖启动命令控制启动顺序

我们在一个pod里面部署springboot和centos的2个容器作为示例,centos的启动需要依赖于springboot的服务启动正常再启动。

正常我们在一个pod中部署2个容器,启动的顺序都是随机的,其实我们在这里设置启动顺序就是通过脚本来判读springboot服务是否启动,如果启动了我再启动centos。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    k8s-app: container-start-order
    qcloud-app: container-start-order
  name: container-start-order
  namespace: test
spec:
  progressDeadlineSeconds: 600
  replicas: 0
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: container-start-order
      qcloud-app: container-start-order
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        k8s-app: container-start-order
        qcloud-app: container-start-order
    spec:
      containers:
      - args:
        - while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080)" != '200' ]]; do echo Waiting for springboot;sleep 5; done; echo springboot available; top -b
        command:
        - /bin/bash
        - -c
        image: centos:7
        imagePullPolicy: Always
        name: centos
        resources:
          limits:
            cpu: 500m
            memory: 1Gi
          requests:
            cpu: 250m
            memory: 256Mi
        securityContext:
          privileged: false
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      - image: nwx-test.tencentcloudcr.com/nwx/springboot:springboot-4801f1daf265728c1061f2fa0ff20b1eeedb9416
        imagePullPolicy: Always
        name: springboot
        resources:
          limits:
            cpu: 500m
            memory: 1Gi
          requests:
            cpu: 250m
            memory: 256Mi
        securityContext:
          privileged: false
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      imagePullSecrets:
      - name: tcr.ipstcr-erzjx59w-public
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30

我们在centos中设置依赖的启动命令,下面这条命令的意思是我们在centos中每隔5s去curl springboot的服务,如果正常启动,则启动centos,启动命令是top -b,如果是您的服务镜像这个设置成你自己的服务启动命令

while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080)" != '200' ]]; do echo Waiting for springboot;sleep 5; done; echo springboot available; top -b

下面我们启动pod,看看是否会达到我们预期目标,centos依赖springboot的服务启动后再启动

《Kubernetes--POD的启动流程及控制容器的启动顺序》

《Kubernetes--POD的启动流程及控制容器的启动顺序》

《Kubernetes--POD的启动流程及控制容器的启动顺序》

从事件和容器启动日志的时间,我们可以发现springboot是在6:41 56毫秒才访问成功,查看centos的日志可以发现,6:41 56毫秒前每隔5s探测一次springboot服务是否启动成功,到了6:41 56毫秒返回200后则执行top -b启动centos。

方案三:依赖initContainer控制启动顺序

这个方案其实是控制不同POD的容器的启动顺序

举一个最简单的例子,假设我们有一个 Web 服务,该服务又依赖于另外一个数据库服务。

但是在在启动这个 Web 服务的时候,我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内 Web 服务连接数据库异常。

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

但其实不用 initContainer 的话,这部分依赖的检查实际也可以移入应用容器的程序中,确实是如此。

那为什么 K8S 还要提供一个 initContainer 的入口呢?

这就好像,你使用计算器计算 5 个 88 相加的结果是使用 88+88+88+88+88,还是使用 88*5 是一个道理。

计算器提供这个功能,至于你用不用?怎么用?都取决于用户自己。

但从 K8S 这个平台来考虑,提供了这个接口,就可以把一个大而重的程序分割成多个小的细分模块,不仅有利于编码和维护,还能让应用程序的之间的依赖关系更加清晰。

点赞

发表回复