02-Kubernetes中的Pod

K8s官方文档:https://kubernetes.io/

K8s中文官方文档: https://kubernetes.io/zh/

K8s Github地址:https://github.com/kubernetes/

1、Pod是什么?

官方文档:https://kubernetes.io/docs/concepts/workloads/pods/

Pod是Kubernetes中的最小调度单元,Kubernetes是通过定义一个Pod的资源,然后在Pod里面运行容器,容器需要指定一个镜像,这样就可以用来运行具体的服务。一个Pod封装一个容器(也可以封装多个容器),Pod里的容器共享存储、网络等。也就是说,应该把整个pod看作虚拟机,然后每个容器相当运行在虚拟机的进程。

image-20220308095719184

Pod是需要调度到k8s集群的工作节点来运行的,具体调度到哪个节点,是根据scheduler调度器实现的。

1.1 Pod如何管理多个容器

Pod中可以同时运行多个容器。同一个Pod中的容器会自动分配到同一个Node节点上。同一个Pod中的容器共享资源、网络环境,它们总是被同时调度,在一个Pod中同时运行多个容器是一种比较高级的用法,只有当你的容器需要紧密配合协作的时候才考虑用这种模式。例如,有一个容器作为web服务器运行,需要用到共享的volume,有另一个sidecar容器来从远端获取资源更新这些文件。

一些Pod有init容器和应用容器。 在应用程序容器启动之前,运行初始化容器。

1.2 Pod网络

Pod是有IP地址的,每个pod都被分配唯一的IP地址(IP地址是靠网络插件calico、flannel、weave等分配的),POD中的容器共享网络名称空间,包括IP地址和网络端口。 Pod内部的容器可以使用localhost相互通信。 Pod中的容器也可以通过网络插件calico与其他节点的Pod通信。

1.3 Pod存储

创建Pod的时候可以指定挂载的存储卷。 POD中的所有容器都可以访问共享卷,允许这些容器共享数据。 Pod只要挂载持久化数据卷,Pod重启之后数据还是会存在的。

2、Pod的工作方式

在K8s中,所有的资源都可以使用一个yaml文件来创建,创建Pod也可以使用yaml配置文件。或者使用kubectl run在命令行创建Pod(不常用)。

2.1 自主式Pod

所谓的自主式Pod,就是直接定义一个Pod资源,示例如下:

# pod-tomcat.yaml
apiVersion: v1
kind: Pod
metadata:
  name: tomcat-test
  namespace: default
  labels:
    name: tomcat
spec:
  containers:
    - name: tomcat-java
      image: tomcat:8.5-jre8-alpine
      ports:
        - containerPort: 8080

然后通过kubectl apply创建Pod:

kubectl apply -f pod-tomcat.yaml

Kubernetes会自动调度并创建Pod:image-20220308101627361

此时,如果我们不小心删掉了这个Pod:

kubectl delete pod tomcat-test

然后Pod就不见了:image-20220308101926490

通过这个示例可以看到,如果直接定义一个Pod资源,那Pod被删除,就彻底被删除了,不会再创建一个新的Pod,这在生产环境还是具有非常大风险的,所以今后我们接触的Pod,都是控制器管理的。

2.2 控制器管理Pod

常见的管理Pod的控制器:ReplicasetDeploymentJobCronJobDaemonsetStatefulset

控制器管理的Pod可以确保Pod始终维持在指定的副本数运行。

示例(通过Deployment管理Pod):

# tomcat-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat-test
  labels:
    app: tomcat-deploy
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
        - name: my-tomcat
          image: tomcat:8.5-jre8-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080

更新资源清单文件:

kubect apply -f tomcat-deploy.yaml

# 查看deployment
kubectl get deployments.apps -l app=tomcat-deploy

# 查看Pod
kubectl get pods -l app=tomcat -o wide

# 查看replicas
kubectl get rs -l app=tomcat

此时Kubernetes会自动调度并启动对应的Pod:image-20220308103900928

此时如果我们删除其中一个Pod:

kubectl delete pod tomcat-test-77c495f7ff-jcbrh

可以发现如果我们删除其中一个Pod,Deployment为了让Pod数保持Replicas指定的数量,会自动拉起另一个Pod:image-20220308104211708

3、Pod创建流程

image-20220308104707948

Pod是Kubernetes中最基本的部署调度单元,可以包含container,逻辑上表示某种应用的一个实例。例如一个web站点应用由前端、后端及数据库构建而成,这三个组件将运行在各自的容器中,那么我们可以创建包含三个container的pod。

Kubernetes通过基于list-watch机制的控制器架构,实现组件之间互相解耦。

其他组件监控自己负责的资源,当这些资源发生变化时,kube-apiserver会通知这些组件,这个过程类似于发布与订阅。

  1. 客户端提交创建Pod的请求,可以通过调用API Server的RESTful API接口,也可以通过kubect命令行工具。
  2. 触发创建Pod之后,kube-apiserver会将创建Pod的信息(netadata)写入到etcd中,此时Pod并未被调度和创建。
  3. etcd写入成功之后会通知kube-apiserver信息写入成功。
  4. kube-apiserver接收到etcd写入成功的信息之后,除法watch机制,通知kube-schedluer有新的Pod需要被创建,schedluer根据调度算法,选择合适的节点进行调度,将调度的相关信息通知到kube-apiserver。
    • 调度器用一组规则过滤掉不符合要求的主机。比如Pod指定了所需要的资源量,那么可用资源比Pod需要的资源量少的主机会被过滤掉。
    • Scheduler查看Kubernetes API,类似于通知机制
      • 首先判断pod.spec.Node == null
      • 若为null则表示这个Pod创建请求是需要新建Pod,所以通过调度算法计算,找到最合适的节点,然后将信息记录在etcd中:pod.spec.Node = k8s-node01.staryjie.com(设置一个具体的节点)
  5. kube-apiserver将调度信息通知到etcd,etcd将信息写入成功后通知kube-apiserver。
  6. kube-apiserver接收到调度信息写入成功后通知kube-scheduler,并且同时通知对应节点上的kubelet。
  7. 对应节点上的kubelet接收到Pod创建的相关信息之后,通过调用docker的API。
  8. docker接收到kubelet的API调用之后开始拉取镜像并且创建需要的容器信息。并实时将容器相关信息通知到kubelet。
  9. kubelet将docker通知到的容器状态信息等发送给kube-apiserver。
  10. kube-apiserver将kubelet返回的信息写入到etcd中。
  11. 此时我们通过kubectl get pods就可以看到Pod对应的相关信息以及状态。

创建Pod的过程中并没有涉及到kube-controller-manager组件和kube-proxy组件,所以未在过程中提及。在创建deployment等控制相关的资源时就会调度到kube-controller-manager,创建对应的service时会涉及到kube-proxy组件。

4、如何创建一个Pod

4.1 编写资源清单文件

大多数时候我们都需要通过编写资源清单来创建对应的资源对象。资源清单可以方便我们后期维护更新对应的资源。

一份资源清单中,大概包含如下的字段信息:

apiVersion: v1                          # api版本
kind: Pod                               # 资源类型
metadata:                               # 元数据信息
  name: tomcat-test                     # 资源名称,这里是Pod的名称
  namespace: default                    # 所属的命名空间
  labels:                               # 标签信息
    name: tomcat                        # 设置具体的标签对,这里是 name: tomcat
spec:                                   # 资源对象具体的配置信息
  containers:                           # 容器资源的配置信息
    - name: tomcat-java                 # 指定容器名称
      image: tomcat:8.5-jre8-alpine     # 指定镜像及版本号
      ports:                            # 需要暴露的端口清单
        - containerPort: 8080           # 具体暴露的端口号
      imagePullPolicy: IfNotPresent     # 镜像拉取策略

但是在Kubernetes中有太多的资源对象,我们不可能能够清楚的记得每一个资源对象对应的资源清单字段有多少个,具体有哪些配置项的,所以Kubernetes也很友好的提供了对应的命令来供使用者查询。

kubectl explain [资源对象].[字段].[...]

可以参照下图的方式查看个子资源对象的字段和配置选项:image-20220308111016407

在编写资源清单文件的时候,我们可以通过kubect explain命令查询对应字段及相关配置说明,也可以借助一个比较好用的IDE和插件来提高编写效率。

Visual Studio Code这个IDE中,通过安装YAML插件和Kubernetes插件就可以很方便的编写资源清单文件。YAML插件提供了对yaml文件语法检查和自动格式化的功能,Kubernetes差价提供了对Kubernetes各种资源的自动提示和代码生成:image-20220308112433581

image-20220308112457284

4.2 通过资源清单创建Pod

# pod-first.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-first
  namespace: default
  labels:
    app: tomcat-pod-first
spec:
  containers:
    - name: tomcat-first
      image: tomcat:8.5-jre8-alpine
      ports:
        - containerPort: 8080
      imagePullPolicy: IfNotPresent

通过kubectl apply更新资源清单文件:

kubectl apply -f pod-first.yaml

Kubernetes接收到Pod创建请求之后,自动调度并创建Pod:image-20220308112858508

# 查看Pod日志
kubectl logs pod-first

通过kubectl logs查看Pod的日志image-20220308113004453

# 查看Pod中指定容器的日志
kubectl logs pod-first -c tomcat-first

查看Pod中指定容器的日志:image-20220308113331857

# 进入到Pod的容器中
kubectl exec -it pod-first -- /bin/sh

# 如果Pod中有多个容器,则需要指定容器名
kubectl exec -it pod-first -c tomcat-first -- /bin/sh

4.3 通过kubectl run创建Pod

kubectl run tomcat --image=tomcat:8.5-jre8-alpine --image-pull-policy="IfNotPresent" --port=8080

创建的Pod如下:image-20220308113900386

5、Pod资源清单详细解读

apiVersion: v1                                              # api版本号,例如:v1
kind: Pod                                                   # 资源类型,例如:Pod
metadata:                                                   # 元数据
  name: string                                              # Pod名称
  namespace: default                                        # 命名空间
  labels:                                                   # 自定义标签
    name: string                                            # key/value对,例如:name: my-pod
  annotations:                                              # 自定义注释列表
    name: string                                            # 注释
spec:                                                       # Pod中的详细定义
  containers:                                               # Pod中容器列表
  - name: string                                            # 容器名称
    image: <Image>                                          # 容器的镜像名称
    imagePullPolicy: [Always | Never | IfNotPresent]        # 镜像拉取策略 [总是下载 | 仅使用本地镜像 | 优先使用本地镜像]
    command: [string]                                       # 容器的启动命令列表,如果不指定,则使用打包时使用的启动命令
    args: [string]                                          # 容器的启动命令参数列表
    workingDir: string                                      # 容器的工作目录
    volumeMounts:                                           # 挂载到容器内部的存储卷配置
      - name: string                                        # 引用Pod定义的共享存储卷的名称,需用volumes[]部分定义的卷名
        mountPath: string                                   # 存储卷在容器内mount的绝对路径,应少于512字符
        readOnly: booleam                                   # 是否为只读模式
    ports:                                                  # 需要暴露的端口号
    - name: string                                          # 端口号名称
      - containerPort: int                                  # 容器需要监听的端口号
        protocol: string                                    # 端口协议,支持TCP和UDP,默认是TCP
    env:                                                    # 容器运行前需要设置的环境变量列表
    - name: string                                          # 环境变量名称
      value: string                                         # 环境变量的值
    resources:                                              # 资源限制和请求的设置
      limits:                                               # 资源限制设置
        memory: string                                      # 内存限制,单位可以为 Mib/Gib
        cpu: string                                         # cpu限制,单位为 core 数
      requests:                                             # 资源请求设置
        memory: string                                      # 内存请求,容器启动的初始可用内存
        cpu: string                                         # cpu请求,容器启动时初始可用数量
    livenessProbe:                                          # 存活检测,对Pod中的容器进行监控检查的设置,当探测几次后无响应,自动重启容器,检查方法有exec、httpGet和tcpSocket,对一个容器只需设置其中一种方法即可
      exec:                                                 # exec检查方式,执行命令检查
        command: [string]                                   # exec方式需要执行的命令或者脚本
      httpGet:                                              # httpGet检查方式,通过HTTP的GET请求来检查,需要设置Path和Port
        path: string                                        # HTTP发送GET请求的uri地址
        port: number                                        # HTTP发送GET请求的端口号
        host: string                                        # HTTP发送GET请求的主机或者域名
        scheme: string                                      # 用于连接到主机的方案,默认为HTTP
        httpHeaders:                                        # 自定义请求头信息
        - name: string                                      # 请求头字段名称
          value: string                                     # 请求头字段值
      tcpSocket:                                            # tcpSocket检查方式
        host: string                                        # tcpSocket的主机或者域名
        port: number                                        # tcpSocker的端口号
      initialDelaySeconds: int                              # 容器启动完成后,首次探测的时间,单位为秒
      timeoutSeconds: int                                   # 对容器监控检查探测等待响应的超时时间,单位为秒,默认1秒
      periodSeconds: int                                    # 执行探测的频率,单位为秒,默认值为10秒,最小值1秒
      successThreshold: int                                 # 探测失败后,再次探测视为成功的最小连续成功次数,默认为1,存活探测和就绪探测必须设置为1,最小值为1
      failureThreshold: int                                 # 探测成功后,再次探测视为失败的最小连续失败次数,默认值为3,最小值为1
    securityContext:                                        # 容器安全配置,SecurityContext 和 PodSecurityContext 中都存在一些字段。 当两者都设置时,SecurityContext 中的值优先。
      privileged: booleam                                   # 容器内是否开启特权模式
  restartPolicy: [OnFailure | Always | Never]               # Pod重启策略,[不管是何种方式失败总是重启 | Pod以非0状态码退出时重启 | 从不重启]
  nodeSelector: <map[string]string>                         # 标签选择器,表示将该Pod调度到包含该标签的节点上,以key: value的方式指定
  imagePullSecrets: object                                  # 拉取镜像时使用的secret名称,以key: secretkey的格式指定
  - name: string                                            # 拉取镜像时使用的secret名称
  hostNetwork: booleam                                      # 是否使用主机网络,默认为false,如果设置为true则表示使用主机网络
  volumes:                                                  # 在该Pod上定义共享存储卷
  - name: string                                            # 数据卷名称,必须是DNS_LABEL,并在在Pod中是唯一的
    emptyDir: {}                                            # 类型为emptyDir的存储卷,与Pod同生命周期的一个临时目录,为空值
    hostPath: string                                        # 类型未hostPath的存储卷,表示挂载在Pod所处的宿主机目录
      path: string                                          # Pod所在宿主机的目录,将用于Pod中容器mount的目录
    secret:                                                 # 类型为secret的数据卷,挂载集群中定义的secret对象到容器内部
      secretName: string                                    # secret名称,在集群中已经定义的secret
      items:                                                # secert键值对列表,如果未指定,则引用 Secret 的 Data 字段中的每个键值对都将作为文件投影到卷中,文件名是键,内容是值。
      - name: string                                        # 指定secret的key
        path: string                                        # 指定secret的中key对应的value值
    configMap:                                              # 类型为configMap的存储卷,挂载预定义的configMap对象到容器内部
      name: string                                          # configMap的名称
      items:                                                # configMap的键值对列表
      - key: string                                         # configMap中的key
        path: string                                        # configMap中key对应的value

6、节点选择器

我们在创建pod资源的时候,pod会根据schduler进行调度,那么默认会调度到随机的一个工作节点,如果我们想要pod调度到指定节点或者调度到一些具有相同特点的node节点,怎么办呢?
可以使用pod中的nodeName或者nodeSelector字段指定要调度到的node节点。

6.1 nodeName

指定pod节点运行在哪一个工作节点上。

# pod-node.yaml
apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  labels:
    app: myapp
    env: dev
spec:
  nodeName: k8s-node01.staryjie.com
  containers:
    - name: tomcat-pod-java
      image: tomcat:8.5-jre8-alpine
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 8080
    - name: busybox
      image: busybox:1.28
      imagePullPolicy: IfNotPresent
      command:
        - "/bin/sh"
        - "-c"
        - "sleep 3600"

更新资源配资清单:

kubectl apply -f pod-name.yaml

kubectl get pods -l env=dev -o wide

Pod确实被调度到了指定的工作节点上:image-20220309140621906

6.2 nodeSelector

指定pod调度到具有哪些标签的node节点上。

首先我们要在对应不同的节点上打上不同的标签:

kubectl label node k8s-node01.staryjie.com disk=ssd
kubectl label node k8s-node02.staryjie.com gpu=ture

image-20220309145423627

然后编写资源清单文件:

# pod-nodeSelector.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-nodeSelector
  namespace: default
  labels:
    app: myapp
    env: test
spec:
  nodeSelector:
    disk: ssd
  containers:
    - name: tomcat-pod-java
      image: tomcat:8.5-jre8-alpine
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 8080

更新资源配置清单:

# pod-nodeSelector.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-label-node
  namespace: default
  labels:
    app: myapp
    env: test
spec:
  nodeSelector:
    disk: ssd
  containers:
    - name: tomcat-pod-java
      image: tomcat:8.5-jre8-alpine
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 8080

更新资源配置清单:

kubectl apply -f pod-nodeSelector.yaml

Pod被调度到了匹配指定标签的节点上:image-20220309145029204

7、污点和容忍度

7.1 node节点亲和性

node节点亲和性调度:nodeAffinity

kubectl explain Pods.spec.affinity.nodeAffinity
  • preferredDuringSchedulingIgnoredDuringExecution:尽可能满足Pod中定义的亲和性要求。
  • requiredDuringSchedulingIgnoredDuringExecution:必须要满足Pod中定义的亲和性要求,如果没有满足亲和性要求的,则Pod会处于Pending状态。

7.1.1 硬亲和性

pod-nodeaffinity-demo.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-node-affinity-demo
  namespace: default
  labels:
    app: myapp
    tier: frontend
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: zone
                operator: In
                values:
                  - "foo"
                  - "bar"

把Pod调度到节点上有key为zone,并且对应的值是foo或者bar的节点上。

更新资源配置文件:

kubectl apply -f pod-nodeaffinity-demo.yaml

此时因为没有满足亲和性要求的节点,所以Pod调度不会成功:

此时Pod一直处于Pending状态。

如果我们给其中一个工作节点打上满足条件的标签,则Pod就会被正常调度到该节点上:

kubectl label nodes k8s-node01.staryjie.com zone=foo

Pod被调度到k8s-node01.staryjie.com这个节点上:

7.1.2 软亲和性

pod-nodeaffinity-demo2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-node-affinity-demo2
  namespace: default
  labels:
    app: myapp
    tier: frontend
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - preference:
            matchExpressions:
              - key: "room"
                operator: In
                values:
                  - "foo"
                  - "bar"
          weight: 60

尽可能的将Pod调度到具有room: foo或者room: bar的工作节点,如果所有的工作节点都不满足,则会根据其他调度算法选择一个更合适的节点。

更新资源配置文件:

kubectl apply -f pod-nodeaffinity-demo2.yaml

此时虽然没有满足亲和性要求的工作节点,但是Pod还是会被调度:

node节点亲和性针对的是Pod和node节点的关系。

7.2 Pod节点亲和性

pod自身的亲和性调度有两种表示形式:

  • podaffinity:pod和pod更倾向腻在一起,把相近的pod结合到相近的位置,如同一区域,同一机架,这样的话pod和pod之间更好通信,比方说有两个机房,这两个机房部署的集群有1000台主机,那么我们希望把nginx和tomcat都部署同一个地方的node节点上,可以提高通信效率。
  • podunaffinity:pod和pod更倾向不腻在一起,如果部署两套程序,那么这两套程序更倾向于反亲和性,这样相互之间不会有影响。

第一个Pod会根据Kubernetes的调度算法调度到一个符合条件的节点上,这个Pod作为评判后续Pod是否能够调度到该节点的方式,这就成为节点亲和性。至于节点位置、机房位置、机柜位置这些用于区分节点不同位置信息属性一般通过主机名和标签来进行划分。

至于如何使用Pod亲和性,可以通过如下命令查询帮助信息:

kubectl explain Pods.spec.affinity.podAffinity

# preferredDuringSchedulingIgnoredDuringExecution - 软亲和性
kubectl explain Pods.spec.affinity.podAffinity.preferredDuringSchedulingIgnoredDuringExecution

# requiredDuringSchedulingIgnoredDuringExecution - 硬亲和性
kubectl explain Pods.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution

7.2.1 Pod亲和性

Pod亲和性主要有以下几个配置的字段:

  • labelSelector:标签选择器,通过标签选择一组能够作为亲和对象的Pod资源
  • namespaces:通过namespace指定标签选择器选定Pod资源所在的命名空间,如果不指定,那么默认就在当前创建Pod的命名空间
  • topologyKey:位置拓扑的键,这个是必须字段,不能为空。该Pod必须与指定命名空间中标签选择器匹配的Pod位于同一位置(亲和)或者不位于同一位置(反亲和)

podaffinity-require-demo.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-first
  namespace: default
  labels:
    app: pod1
    tier: frontend
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-second
  namespace: default
  labels:
    app: pod2
    tier: db
spec:
  containers:
    - name: busybox
      image: busybox:1.28
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "sleep 3600"]
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - { key: app, operator: In, values: ["pod1"] }
          topologyKey: kubernetes.io/hostname

第二个Pod必须调度到与包含标签app: po1的节点上。

更新资源配置清单:

kubectl apply -f podaffinity-require-demo.yaml

pod-second被调度到和pod-first同一台工作节点上:

7.2.2 反亲和性

Pod反亲和性主要有以下几个配置的字段:

  • labelSelector:标签选择器,通过标签选择一组能够作为亲和对象的Pod资源
  • namespaces:通过namespace指定标签选择器选定Pod资源所在的命名空间,如果不指定,那么默认就在当前创建Pod的命名空间
  • topologyKey:位置拓扑的键,这个是必须字段,不能为空。该Pod必须与指定命名空间中标签选择器匹配的Pod位于同一位置(亲和)或者不位于同一位置(反亲和)

podantiaffinity-require-demo.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-first
  namespace: default
  labels:
    app: pod1
    tier: frontend
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-second
  namespace: default
  labels:
    app: pod2
    tier: db
spec:
  containers:
    - name: busybox
      image: busybox:1.28
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "sleep 3600"]
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - { key: app, operator: In, values: ["pod1"] }
          topologyKey: kubernetes.io/hostname

由于是反亲和,所以第二个pod会被调度到被匹配到app: pod1的Pod之外的其他工作节点。

更新资源配置清单:

kubectl apply -f podantiaffinity-require-demo.yaml

由于反亲和,所以pod-second被调度到了不和pod-dirst在同一个工作节点:

7.2.3 硬亲和和软亲和

更换topologyKey体验硬亲和和软亲和。

首先给两个工作节点打上标签:

kubectl label nodes k8s-node01.staryjie.com zone=foo --overwrite
kubectl label nodes k8s-node02.staryjie.com zone=foo --overwrite

此时查看节点标签:

7.2.3.1 硬亲和

通过修改上一个反亲和案例中的topologyKey

``

apiVersion: v1
kind: Pod
metadata:
  name: pod-first
  namespace: default
  labels:
    app: pod1
    tier: frontend
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-second
  namespace: default
  labels:
    app: pod2
    tier: db
spec:
  containers:
    - name: busybox
      image: busybox:1.28
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "sleep 3600"]
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - { key: app, operator: In, values: ["pod1"] }
          topologyKey: zone

更新资源配置清单:

kubectl apply -f podantiaffinity-require-zone.yaml

此时第二个Pod和第一个Pod位于同一个位置,又因为是反亲和,并且此时没有其他符合条件的位置,所以第二个Pod会一直处于Pending状态:

7.2.3.2 软亲和

如果修改上面硬亲和为软亲和,那么第二个Pod也会被调度.

podantiaffinity-preferred-zone.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-first
  namespace: default
  labels:
    app: pod1
    tier: frontend
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-second
  namespace: default
  labels:
    app: pod2
    tier: db
spec:
  containers:
    - name: busybox
      image: busybox:1.28
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "sleep 3600"]
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchExpressions:
                - { key: app, operator: In, values: ["pod1"] }
            topologyKey: zone

更新资源配置清单:

kubectl apply -f podantiaffinity-preferred-zone.yaml

因为是软亲和,所以第二个Pod也会被正常调度:

7.3 污点和容忍度

污点是定义在节点之上的键值属性数据,用于让节点有能力主动拒绝调度器将Pod调度运行到该节点上,除非该Pod对象接纳节点污点的容忍度。

容忍度(tolerations)则是定义在Pod对象上的键值属性数据,用于配置该Pod可容忍的节点污点。调度器插件TaintToleratesNodeZTains负责确保仅那些可容忍节点污点的Pod对象可调度运行在上面。

Pod亲和是Pod的属性,用在Pod上

污点是节点的属性,用在节点上

在pod对象定义容忍度的时候支持两种操作:

  1. 等值密钥:key和value上完全匹配
  2. 存在性判断:key和effect必须同时匹配,value可以是空

在pod上定义的容忍度可能不止一个,在节点上定义的污点可能多个,需要琢个检查容忍度和污点能否匹配,每一个污点都能被容忍,才能完成调度,如果不能容忍怎么办,那就需要看pod的容忍度了。

7.3.1 查看节点污点

kubectl describe nodes k8s-master01.staryjie.com | grep Taints

master节点的污点信息如下:

master节点的污点是NoSchedule,所以我们创建的Pod不会调度到master节点,因为我们创建的Pod没有容忍度。

再查看一下Kubernetes自身组件的Pod:

kubectl describe pods -n kube-system kube-apiserver-k8s-master01.staryjie.com | grep Tolerations

可以看到这个Pod的容忍度是NoExecute,所以它能够调度到master节点上:

7.3.2 节点污点

kubectl explain node.spec.taints
  • effect:定义节点对Pod对象的排斥等级(效果)
    • NoSchedule:仅影响pod调度过程,当pod能容忍这个节点污点,就可以调度到当前节点,后来这个节点的污点改了,加了一个新的污点,使得之前调度的pod不能容忍了,那这个pod还会在这个节点上继续运行,对现存的pod对象不产生影响
    • NoExecute:既影响调度过程,又影响现存的pod对象,如果现存的pod不能容忍节点后来加的污点,这个pod就会被驱逐
    • PreferNoSchedule:最好不,也可以,是NoSchedule的柔性版本
    • 排斥等级排序:NoExecute > NoSchedule > PreferNoSchedule
7.3.2.1 给节点打污点
kubectl taint --help

# 给节点打上污点
# kubectl taint node <node_name> key=value:effect
kubectl taint node k8s-node02.staryjie.com node-type=production:NoSchedule

此时的k8s-node02.staryjie.com节点就被打上了污点:

7.3.2.2 删除节点污点
# kubectl taint node <node_name> key:effect-
kubectl taint node k8s-node02.staryjie.com node-type:NoSchedule-

此时节点上的污点就被删除了:

7.3.3 测试污点容忍

7.3.3.1 NoSchedule

经过如下的目录我们给k8s-node02.staryjie.com节点打上了污点:

kubectl taint node k8s-node02.staryjie.com node-type=production:NoSchedule

此时的k8s-node02.staryjie.com节点上已经存在污点了,并且NoSchedule仅影响调度过程,对已经在该节点的Pod无影响,那么我们如果新建一个Pod不做污点容忍的话,该Pod就不会被调度到k8s-node02.staryjie.com节点上。

pod-taint.yaml

apiVersion: v1
kind: Pod
metadata:
  name: taint-pod
  namespace: default
  labels:
    app: taint-pod
spec:
  containers:
    - name: taint-pod
      image: tomcat:8.5-jre8-alpine
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 8080
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"
  restartPolicy: Always

更新资源配置文件:

kubectl apply -f pod-taint.yaml

该Pod被调度到k8s-node01.startjie.com这个工作节点上:

7.3.3.2 NoExecute

删除Pod,然后给另一个节点也打上污点:

# 删除上一步中创建的Pod
kubectl delete -f pod-taint.yaml

# 给另一个工作节点打上污点 NoExecute
kubectl taint node k8s-node01.staryjie.com node-type=dev:NoExecute

此时节点k8s-node01.staryjie.com上也存在污点了:

此时,原先在节点k8s-node01.staryjie.com上的Pod都会被驱逐。

定义一个污点容忍的Pod:

pod-tolerations.yaml

apiVersion: v1
kind: Pod
metadata:
  name: myapp-deploy
  namespace: default
  labels:
    app: myapp
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 80
          name: http
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"
  restartPolicy: Always
  tolerations:
    - key: "node-type"
      operator: "Equal"
      value: "production"
      effect: "NoExecute"
      tolerationSeconds: 3600

该Pod能够容忍污点的key为node-type并且值为production的节点,并且容忍度为NoExecute

但是由于operator的值为Equal所以该Pod无法被调度,会一直处于Pending状态。

更新资源配置清单:

kubectl apply -f pod-tolerations.yaml

Pod为Pending状态:

那么如果把operator改为Exists能不能正常调度呢?

apiVersion: v1
kind: Pod
metadata:
  name: myapp-deploy
  namespace: default
  labels:
    app: myapp
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 80
          name: http
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"
  restartPolicy: Always
  tolerations:
    - key: "node-type"
      operator: "Exists"
      # value: "production"  # operator为Exists时value必须为空,或者不写
      effect: "NoExecute"
      # tolerationSeconds: 3600  # 需要删除这里

此时的Pod应该能够调度到两个节点上,因为NoExecute容忍度最高:

如果保持operator仍旧为Equal,修改容忍等级的话,Pod会被调度到哪个节点呢?

apiVersion: v1
kind: Pod
metadata:
  name: myapp-deploy
  namespace: default
  labels:
    app: myapp
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 80
          name: http
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"
  restartPolicy: Always
  tolerations:
    - key: "node-type"
      operator: "Equal"
      value: "production"
      effect: "NoSchedule"
      # tolerationSeconds: 3600  # 容忍度为NoSchedule的时候需要删除该行

此时该Pod的容忍度完美契合k8s-node02.staryjie.com节点的污点。

更新资源配置文件:

kubectl apply -f pod-tolerations.yaml

此时该Pod只能被调度到k8s-node02.staryjie.com节点上:

那么如果只匹配污点的key的话,那么Pod可以无视污点容忍等级,只关注Key能都匹配:

kind: Pod
metadata:
  name: myapp-deploy
  namespace: default
  labels:
    app: myapp
spec:
  containers:
    - name: myapp
      image: ikubernetes/myapp:v1
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 80
          name: http
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"
  restartPolicy: Always
  tolerations:
    - key: "node-type"
      operator: "Exists"
      value: ""
      effect: ""

上述yaml资源配置清单中的effet为空的,那么只要能够匹配到节点上的污点的key为·node-type即可被视为能够接受的污点容忍,和污点容忍等级无关。

kubectl apply -f pod-tolerations.yaml

Pod能够成功调度:

测试完成记得删除污点:

kubectl taint node k8s-node01.staryjie.com node-type:NoExecute-
kubectl taint node k8s-node02.staryjie.com node-type:NoSchedule-

kubectl describe nodes k8s-node01.staryjie.com | grep Taints
kubectl describe nodes k8s-node02.staryjie.com | grep Taints

8、 Pod常见状态和重启策略

8.1 常见Pod状态

Pod的status定义在PodStatus对象中,其中有一个phase字段。它简单描述了Pod在其生命周期的阶段。熟悉Pod的各种状态对我们理解如何设置Pod的调度策略、重启策略是很有必要的。下面是 phase 可能的值,也就是pod常见的状态:

  • 挂起(Pending):我们在请求创建pod时,条件不满足,调度没有完成,没有任何一个节点能满足调度条件,已经创建了pod但是没有适合它运行的节点叫做挂起,调度没有完成,处于pending的状态会持续一段时间:包括调度Pod的时间和通过网络下载镜像的时间。

  • 运行中(Running):Pod已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态。

  • 成功(Succeeded):Pod 中的所有容器都被成功终止,并且不会再重启。

  • 失败(Failed):Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止。

  • 未知(Unknown):未知状态,所谓pod是什么状态是apiserver和运行在pod节点的kubelet进行通信获取状态信息的,如果节点之上的kubelet本身出故障,那么apiserver就连不上kubelet,得不到信息了,就会看Unknown

  • Evicted状态:出现这种情况,多见于系统内存或硬盘资源不足,可df-h查看docker存储所在目录的资源使用情况,如果百分比大于85%,就要及时清理下资源,尤其是一些大文件、docker镜像。

  • CrashLoopBackOff:容器曾经启动了,但可能又异常退出了

  • Error 状态:Pod 启动过程中发生了错误

如果Pod状态异常,可以通过下面的命令查看具体报错信息:

kubectl describe pod <pod_name>
kubectl logs <pod_name>

8.2 Pod重启策略

Pod的重启策略(RestartPolicy)应用于Pod内的所有容器,并且仅在Pod所处的Node上由kubelet进行判断和重启操作。当某个容器异常退出或者健康检查失败时,kubelet将根据 RestartPolicy 的设置来进行相应的操作。

Pod的重启策略包括AlwaysOnFailureNever,默认值为Always

  • Always:当容器失败时,由kubelet自动重启该容器。
  • OnFailure:当容器终止运行且退出码不为0时,由kubelet自动重启该容器。
  • Never:不论容器运行状态如何,kubelet都不会重启该容器。
apiVersion: v1
kind: Pod
metadata:
  name: pod-restartpolicy-demo
  namespace: default
  labels:
    app: myapp
spec:
  containers:
    - name: pod-restartpolicy-demo
      image: tomcat:8.5-jre8-alpine
      imagePullPolicy: IfNotPresent
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"
  restartPolicy: Always  # Pod中容器的重启策略

9、Pod生命周期

Pod对象从创建开始至终止退出之间的时间称为其生命周期,这段时间里面的某个时间点,Pod会处于某个特定的运行阶段或相位(phase),以概括描述其生命周期中所处的位置。Kubernetes为Pod资源严格定义了5种相位,并将特定Pod对象的当前相位存储在其内部的子对象PodStatus的phase字段上,因而它总是应该处于其生命进程中的几个相位之一。

  • Pending:API Server创建了Pod资源对象并已存入etcd中,但它尚未被调度完成或者仍处于从镜像仓库下载容器镜像的过程当中。
  • Running:Pod已被调度至某节点,所有容器都已经被kubelet创建完成,且至少有一个容器处于启动、重启或者运行过程中。
  • Succeeded:Pod中的所有容器成功终止且不会再重启。
  • Failed:所有容器都已经终止,但至少有一个容器终止失败,即容器以非0状态码退出或者已经被系统终止。
  • Unknown:API Server无法正常获取到Pod对象的状态信息,通常是由于其无法与所在工作节点的kubelet通信所致。

阶段仅是对Pod对象生命周期运行阶段的概括性描述,而非Pod或内部容器状态的综合汇总,因此Pod对象的status字段中的状态值未必一定是可用的相位,它也有可能是Pod的某个错误状态,例如:CrashLoopBackOffError等。

若用户给出了以上全部定义,则一个Pod对象生命周期的运行步骤如下:

  1. 在启动包括初始化容器在内的任何容器之前先创建pause基础容器,它初始化Pod环境并为后续加入的容器提供共享的名称空间。
  2. 按顺序以串行方式运行用户定义的各个初始化容器进行Pod环境初始化;任何一个初始化容器运行失败都将导致Pod创建失败,并按其restartPolicy的策略进行处理,默认为重启。
  3. 待所有初始化容器成功完成后,启动应用程序容器,多容器Pod环境中,此步骤会并行启动所有的应用容器,例如主容器和SideCar容器,它们各自按其自定义展开生命周期;本步及后面的几个步骤都将以主容器进行说明;容器启动的那一刻会同时运行主容器上的PostStart钩子事件,该步骤失败将导致相关容器被重启。
  4. 运行容器启动健康状态监测(startupProbe),判断该容器是否成功启动;该步骤失败照样参照restartPolicy策略进行处理;未定义时,默认状态为Success。
  5. 容器启动成功后,定期进行存活状态监测(livenessProbe)和就绪状态监测(readinessProbe);存活状态监测失败将导致容器重启,而就绪状态监测失败会使得该容器从其所属的Service对象的可用端点列表中移除。
  6. 终止Pod对象时,会先运行PreStop钩子事件,并在宽限期(terminationGrace-PeriodSeconds)结束后终止主容器,宽限期默认为30秒。

9.1 init容器

Pod 里面可以有一个或者多个容器,部署应用的容器可以称为主容器,在创建Pod时候,Pod 中可以有一个或多个先于主容器启动的Init容器,这个init容器就可以成为初始化容器,初始化容器一旦执行完,它从启动开始到初始化代码执行完就退出了,它不会一直存在,所以在主容器启动之前执行初始化,初始化容器可以有多个,多个初始化容器是要串行执行的,先执行初始化容器1,在执行初始化容器2等,等初始化容器执行完初始化就退出了,然后再执行主容器,主容器一退出,pod就结束了,主容器退出的时间点就是pod的结束点,它俩时间轴是一致的;

Init容器就是做初始化工作的容器。可以有一个或多个,如果多个按照定义的顺序依次执行,只有所有的初始化容器执行完后,主容器才启动。由于一个Pod里的存储卷是共享的,所以Init Container里产生的数据可以被主容器使用到,Init Container可以在多种K8S资源里被使用到,如Deployment、DaemonSet, StatefulSet、Job等,但都是在Pod启动时,在主容器启动前执行,做初始化工作。

Init容器与普通的容器区别是:

  1. Init 容器不支持 Readiness,因为它们必须在Pod就绪之前运行完成
  2. 每个Init容器必须运行成功,下一个才能够运行
  3. 如果 Pod 的 Init 容器失败,Kubernetes 会不断地重启该 Pod,直到 Init 容器成功为止,然而,如果Pod对应的restartPolicy值为 Never,它不会重新启动。

初始化容器的官方地址:https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#init-containers-in-use

示例:

init-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: init-pod-demo
  namespace: default
  labels:
    app: init-pod-demo
spec:
  containers:
    - name: init-myservice
      image: busybox:1.28
      command:
        - "sh"
        - "-c"
        - "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"
      imagePullPolicy: IfNotPresent
    - name: init-mydb
      image: busybox:1.28
      command:
        - "sh"
        - "-c"
        - "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"
      imagePullPolicy: IfNotPresent
  restartPolicy: Always

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: myservice
  ports:
    - port: 80
      protocol: TCP
      targetPort: 9736
---
apiVersion: v1
kind: Service
metadata:
  name: mydb
spec:
  selector:
    app: mydb
  ports:
    - port: 80
      protocol: TCP
      targetPort: 9377

更新资源配置文件:

kubectl apply -f service.yaml && kubectl apply -f init-pod.yaml && kubectl get pods -o wide

效果如下:

先执行init容器,init容器串行执行,只有init容器全部执行成功后,主容器才会启动。

9.2 主容器

9.2.1 主容器钩子事件

初始化容器启动之后,开始启动主容器,在主容器启动之前有一个post start hook(容器启动后钩子)和pre stop hook(容器结束前钩子),无论启动后还是结束前所做的事我们可以把它放两个钩子,这个钩子就表示用户可以用它来钩住一些命令,来执行它,做开场前的预设,结束前的清理,如awk有begin,end,和这个效果类似;

postStart:该钩子在容器被创建后立刻触发,通知容器它已经被创建。如果该钩子对应的hook handler执行失败,则该容器会被杀死,并根据该容器的重启策略决定是否要重启该容器,这个钩子不需要传递任何参数。

preStop:该钩子在容器被删除前触发,其所对应的hook handler必须在删除该容器的请求发送给Docker daemon之前完成。在该钩子对应的hook handler完成后不论执行的结果如何,Docker daemon会发送一个SGTERN信号量给Docker daemon来删除该容器,这个钩子不需要传递任何参数。

在k8s中支持两类对pod的检测:

第一类叫做livenesspPobe(pod存活性探测):存活探针主要作用是,用指定的方式检测pod中的容器应用是否正常运行,如果检测失败,则认为容器不健康,那么Kubelet将根据Pod中设置的 restartPolicy来判断Pod 是否要进行重启操作,如果容器配置中没有配置 livenessProbe,Kubelet 将认为存活探针探测一直为成功状态。

第二类是状态检readinessProbe(pod就绪性探测):用于判断容器中应用是否启动完成,当探测成功后才使Pod对外提供网络访问,设置容器Ready状态为true,如果探测失败,则设置容器的Ready状态为false。

9.3 创建Pod阶段

当用户创建pod时,这个请求给apiserver,apiserver把创建请求的状态保存在etcd中;

接下来apiserver会请求scheduler来完成调度,如果调度成功,会把调度的结果(如调度到哪个节点上了,运行在哪个节点上了,把它更新到etcd的pod资源状态中)保存在etcd中,一旦存到etcd中并且完成更新以后,如调度到xianchaonode1上,那么xianchaonode1节点上的kubelet通过apiserver当中的状态变化知道有一些任务被执行了,所以此时此kubelet会拿到用户创建时所提交的清单,这个清单会在当前节点上运行或者启动这个pod,如果创建成功或者失败会有一个当前状态,当前这个状态会发给apiserver,apiserver在存到etcd中;在这个过程中,etcd和apiserver一直在打交道,不停的交互,scheduler也参与其中,负责调度pod到合适的node节点上,这个就是pod的创建过程

pod在整个生命周期中有非常多的用户行为:

  1. 初始化容器完成初始化
  2. 主容器启动后可以做启动后钩子
  3. 主容器结束前可以做结束前钩子
  4. 在主容器运行中可以做一些健康检测,如livenessProbe,readnessProbe

10、Pod容器探测和钩子

10.1 容器钩子

postStart:容器创建成功后,运行前的任务,用于资源部署、环境准备等。

preStop:在容器被终止前的任务,用于优雅关闭应用程序、通知其他系统等。

可以通过下面的命令查看容器钩子资源清单的字段和配置信息:

kubectl explain Pods.spec.containers.lifecycle.postStart
kubectl explain Pods.spec.containers.lifecycle.preStop

示例:

pod-hook.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-hook-demo
  namespace: default
  labels:
    app: pod-hook
spec:
  containers:
    - name: pod-hook-container
      image: sample:v2
      imagePullPolicy: IfNotPresent
      lifecycle:
        postStart:
          exec:
            command:
              - "cp"
              - "/sample.war"
              - "/app"
        preStop:
          httpGet:
            host: monitor.com
            path: /waring
            port: 8080
            scheme: HTTP
  restartPolicy: Always

postStart实现了在主容器启动之前将对应的war包拷贝到指定的路径下。

preStop实现了在删除Pod资源之前向指定url发送了httpGet请求。

10.2 优雅的删除K8s资源

当用户请求删除含有pod的资源对象时(如RC、deployment等),K8S为了让应用程序优雅关闭(即让应用程序完成正在处理的请求后,再关闭软件),K8S提供两种信息通知。

10.2.1 通知工作节点删除资源

Kubernetes通知node节点执行docker stop命令,docker会先向容器中PID为1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认超时时间(30s),会继续发送SIGKILL的系统信号强行kill掉进程。

这是Kubernetes默认的处理方式。

10.2.2 通过Pod生命周期

使用pod生命周期(利用PreStop回调函数),它执行在发送终止信号之前。

默认情况下,所有的删除操作的优雅退出时间都在30秒以内。kubectl delete命令支持--grace-period的选项,以运行用户来修改默认值。0表示删除立即执行,并且立即从API中删除pod。在节点上,被设置了立即结束的的pod,仍然会给一个很短的优雅退出时间段,才会开始被强制杀死。

10.3 健康探测

Kubernetes针对Pod的健康探测都是为了能够让Pod中的服务能够更加稳定可靠,健康探测有livenessProbereadinessProbe两种。

目前LivenessProbe和ReadinessProbe两种探针都支持下面三种探测方法:

  1. ExecAction:在容器中执行指定的命令,如果执行成功,退出码为 0 则探测成功。
  2. TCPSocketAction:通过容器的 IP 地址和端口号执行 TCP 检 查,如果能够建立 TCP 连接,则表明容器健康。
  3. HTTPGetAction:通过容器的IP地址、端口号及路径调用 HTTP Get方法,如果响应的状态码大于等于200且小于400,则认为容器健康

探针探测结果有以下值:

  1. Success:表示通过检测。
  2. Failure:表示未通过检测。
  3. Unknown:表示检测没有正常进行。

两种探测方式的区别:

readinessProbe 和 livenessProbe 可以使用相同探测方式,只是对 Pod 的处置方式不同:

  • livenessProbe检测失败后,将杀死容器并根据 Pod 的重启策略来决定作出对应的措施。

  • readinessProbe 检测失败后,将 Pod 的 IP:Port 从对应的 EndPoint 列表中删除。

10.3.1 livenessProbe

许多应用程序经过长时间运行,最终过渡到无法运行的状态,除了重启,无法恢复。通常情况下,K8S会发现应用程序已经终止,然后重启应用程序pod。有时应用程序可能因为某些原因(后端服务故障等)导致暂时无法对外提供服务,但应用软件没有终止,导致K8S无法隔离有故障的pod,调用者可能会访问到有故障的pod,导致业务不稳定。K8S提供livenessProbe来检测容器是否正常运行,并且对相应状况进行相应的补救措施。

通过下面的命令查看Pod的存活性探测相关字段信息:

kubectl explain Pods.spec.containers.livenessProbe

常用的字段有下面几个:

  • initialDelaySeconds: Pod启动后首次进行检查的等待时间,单位“秒”。
  • periodSeconds: 检查的间隔时间,默认为10s,单位“秒”。
  • timeoutSeconds: 探针执行检测请求后,等待响应的超时时间,默认为1s,单位“秒”。
  • successThreshold:连续探测几次成功,才认为探测成功,默认为 1,在 Liveness 探针中必须为1,最小值为1。
  • failureThreshold: 探测失败的重试次数,重试一定次数后将认为失败,在 readiness 探针中,Pod会被标记为未就绪,默认为 3,最小值为 1
10.3.1.1 exec方式的存活性探测

pod-livenessProbe-exec.yaml

apiVersion: v1
kind: Pod
metadata:
  name: liveness-exec
  namespace: default
  labels:
    app: liveness
spec:
  containers:
    - name: liveness-exec-container
      image: busybox:1.28
      imagePullPolicy: IfNotPresent
      args:  # 创建测试探针的文件
        - "sh"
        - "-c"
        - "echo 'liveness Ok' > /tmp/healthy;sleep 30;rm -rf /tmp/healthy;sleep 600"
      livenessProbe:
        initialDelaySeconds: 10  # 延迟10秒之后开始存活性探测
        periodSeconds: 5  # 存活性探测间隔时间(单位秒)
        timeoutSeconds: 2  # 存活性探测命令执行的超时时间
        successThreshold: 1  # 连续一次探测成功才算成功,在livenessProbe中这里必须是1
        failureThreshold: 2  # 连续两次探测失败则视为失败
        exec:
          command:
            - "cat"
            - "/tmp/healthy"
  restartPolicy: Always

在容器初始化之后,创建一个/tmp/healthy并写入内容,睡眠30秒之后删除该文件。而存活性探测方式是执行shell命令,如果命令能够正常执行则表示存活性探测成功,否则探测失败。在前30秒内,该文件存在,所以探测的shell命令能够正常执行,此时探测成功,30秒之后,因为文件被删除,所以探测的shell命令执行报错,此时存活性探测失败,会根据Pod设置的重启策略进行操作,这里使用的Always,所以会重启该Pod。

更新资源配置清单:

kubectl apply -f pod-livenessProbe-exec.yaml

执行后实时查看Pod状态信息:

存活性探测失败后,Pod会自动被重启。通过查看Pod信息可以知晓:

kubectl describe pods liveness-exec

如下图,存活性探测失败,会根据restartPolicy: Always,自动重启该Pod:

10.3.1.2 httpGet方式的存活性探测

httpGet探测方式有如下可选的控制字段:

scheme: 用于连接host的协议,默认为HTTP。

host:要连接的主机名,默认为Pod IP,可以在http request head中设置host头部。

port:容器上要访问端口号或名称。

path:http服务器上的访问URI。

httpHeaders:自定义HTTP请求headers,HTTP允许重复headers。

pod-livenessProbe-httpGet.yaml

apiVersion: v1
kind: Pod
metadata:
  name: liveness-http
  namespace: default
  labels:
    app: liveness
spec:
  containers:
    - name: liveness-http-container
      image: mydlqclub/springboot-helloworld:0.0.1
      imagePullPolicy: IfNotPresent
      livenessProbe:
        initialDelaySeconds: 20  # 延迟启动存活性探测
        periodSeconds: 5  # 存活性探测间隔时间(单位秒)
        timeoutSeconds: 10  # 存活性探测命令执行的超时时间
        successThreshold: 1
        failureThreshold: 1
        httpGet:
          scheme: HTTP
          port: 8081
          path: /actuator/health
  restartPolicy: Always

上述资源配置清单中启动的容器是一个 SpringBoot 应用,其中引用了 Actuator 组件,提供了 /actuator/health 健康检查地址,存活探针可以使用 HTTPGet 方式向服务发起请求,请求 8081 端口的 /actuator/health 路径来进行存活判断,任何大于或等于200且小于400的代码表示探测成功。任何其他代码表示失败。

如果探测失败,会根据restartPolicy: Always对容器进行重启操作。

httpGet中如果host字段没有写,默认会使用pod启动后分配的p。

更新资源配置清单:

kubectl apply -f pod-livenessProbe-httpGet.yaml

执行后实时查看Pod状态信息:

存活性探测失败后,Pod会自动被重启。通过查看Pod信息可以知晓:

kubectl describe pods liveness-http

如下图,Pod存活性探测失败之后会根据restartPolicy: Always自动重启Pod:

10.3.1.3 tcpSocket方式的存活探测

pod-livenessProbe-tcpSocket.yaml

apiVersion: v1
kind: Pod
metadata:
  name: liveness-tcpsocket
  namespace: default
  labels:
    app: liveness
spec:
  containers:
    - name: liveness-tcpsocket
      image: nginx:1.17.1
      imagePullPolicy: IfNotPresent
      livenessProbe:
        initialDelaySeconds: 15
        periodSeconds: 20
        tcpSocket:
          port: 80
  restartPolicy: Always

TCP 检查方式和 HTTP 检查方式非常相似,在容器启动 initialDelaySeconds 参数设定的时间后,kubelet 将发送第一个 livenessProbe 探针,尝试连接容器的80端口,如果连接失败则将杀死 Pod 重启容器。

更新资源配置清单:

kubectl apply -f pod-livenessProbe-tcpSocket.yaml

创建Pod成功后,查看Pod状态信息:

kubectl describe pods liveness-tcpsocket

只要存活性探测成功,Pod就会一直处于Running状态:

10.3.2 readinessProbe

在没有配置readinessProbe的资源对象中,pod中的容器启动完成后,就认为pod中的应用程序可以对外提供服务,该pod就会加入相对应的service,对外提供服务。但有时一些应用程序启动后,需要较长时间的加载才能对外服务,如果这时对外提供服务,执行结果必然无法达到预期效果,影响用户体验。比如使用tomcat的应用程序来说,并不是简单地说tomcat启动成功就可以对外提供服务的,还需要等待spring容器初始化,数据库连接上等等。

通过下面的命令查看Pod的存活性探测相关字段信息:

kubectl explain Pods.spec.containers.readinessProbe

常用的字段有下面几个:

  • initialDelaySeconds: Pod启动后首次进行检查的等待时间,单位“秒”。
  • periodSeconds: 检查的间隔时间,默认为10s,单位“秒”。
  • timeoutSeconds: 探针执行检测请求后,等待响应的超时时间,默认为1s,单位“秒”。
  • successThreshold:连续探测几次成功,才认为探测成功,默认为 1,在 Liveness 探针中必须为1,最小值为1。
  • failureThreshold: 探测失败的重试次数,重试一定次数后将认为失败,在 readiness 探针中,Pod会被标记为未就绪,默认为 3,最小值为 1
10.3.2 httpGet方式的就绪探测

Pod 的ReadinessProbe 探针使用方式和 LivenessProbe 探针探测方法一样,也是支持三种,只是一个是用于探测应用的存活,一个是判断是否对外提供流量的条件。这里用一个 Springboot 项目,设置 ReadinessProbe 探测 SpringBoot 项目的 8081 端口下的 /actuator/health 接口,如果探测成功则代表内部程序以及启动,就开放对外提供接口访问,否则内部应用没有成功启动,暂不对外提供访问,直到就绪探针探测成功。

pod-readinessProbe-httpGet.yaml

apiVersion: v1
kind: Service
metadata:
  name: springboot
spec:
  selector:
    app: springboot
  type: NodePort
  ports:
    - name: server
      port: 8080
      targetPort: 8080
      nodePort: 31180
    - name: management
      port: 8081
      targetPort: 8081
      nodePort: 31181
---
apiVersion: v1
kind: Pod
metadata:
  name: springboot
  namespace: default
  labels:
    app: springboot
spec:
  containers:
    - name: springboot
      image: mydlqclub/springboot-helloworld:0.0.1
      imagePullPolicy: IfNotPresent
      ports:
        - name: server
          containerPort: 8080
        - name: management
          containerPort: 8081
      readinessProbe:
        initialDelaySeconds: 20
        periodSeconds: 5
        timeoutSeconds: 10
        successThreshold: 1
        failureThreshold: 1
        httpGet:
          scheme: HTTP
          port: 8081
          path: /actuator/health
  restartPolicy: Always

更新资源配置清单:

kubectl apply -f pod-readinessProbe-httpGet.yaml

创建Pod之后查看Pod状态:

kubectl apply -f pod-readinessProbe-httpGet.yaml

对应的Service和Pod创建成功:

Pod也成功加入到Service下的端点列表中:

10.3.3 livenessProbe和readinessProbe配合使用

一般程序中需要设置两种探针结合使用,并且也要结合实际情况,来配置初始化检查时间和检测间隔。

pod-liveness-readiness.yaml

apiVersion: v1
kind: Service
metadata:
  name: springboot
spec:
  selector:
    app: springboot
  type: NodePort
  ports:
    - name: server
      port: 8080
      targetPort: 8080
      nodePort: 31180
    - name: management
      port: 8081
      targetPort: 8081
      nodePort: 31181
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot
  labels:
    app: springboot
spec:
  replicas: 1
  template:
    metadata:
      name: springboot
      labels:
        app: springboot
    spec:
      containers:
        - name: springboot
          image: mydlqclub/springboot-helloworld:0.0.1
          imagePullPolicy: IfNotPresent
          ports:
            - name: server
              containerPort: 8080
            - name: management
              containerPort: 8081
          readinessProbe:
            initialDelaySeconds: 20
            periodSeconds: 5
            timeoutSeconds: 10
            successThreshold: 1
            failureThreshold: 1
            httpGet:
              scheme: HTTP
              port: 8081
              path: /actuator/health
          livenessProbe:
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            successThreshold: 1
            failureThreshold: 1
            httpGet:
              scheme: HTTP
              port: 8081
              path: /actuator/health
      restartPolicy: Always
  selector:
    matchLabels:
      app: springboot

更新资源配置清单:

kubectl apply -f pod-liveness-readiness.yaml

查看对应的Pod和Service:

11、Kubernetes三种探针

11.1 三种探针

  • livenessProbe:用于探测容器是否运行。如果存活探测失败,则 kubelet 会杀死容器,并且容器将受到其重启策略的影响决定是否重启。如果容器不提供存活探针,则默认状态为 Success。
  • readinessProbe:一般用于探测容器内的程序是否健康,容器是否准备好服务请求。如果就绪探测失败,endpoint将从与 Pod 匹配的所有 Service 的端点中删除该 Pod 的 IP 地址。初始延迟之前的就绪状态默认为 Failure。如果容器不提供就绪探针,则默认状态为 Success。
  • startupProbe: 探测容器中的应用是否已经启动。如果提供了启动探测(startup probe),则禁用所有其他探测,直到它成功为止。如果启动探测失败,kubelet 将杀死容器,容器服从其重启策略进行重启。如果容器没有提供启动探测,则默认状态为成功Success。

可以自定义在pod启动时是否执行这些检测,如果不设置,则检测结果均默认为通过,如果设置,则顺序为startupProbe>readinessProbe和livenessProbe。

11.2 startupProbe

在k8s中,通过控制器管理pod,如果更新pod的时候,会创建新的pod,删除老的pod,但是如果新的pod创建了,pod里的容器还没完成初始化,老的pod就被删除了,会导致访问service或者ingress时候,访问到的pod是有问题的,所以k8s就加入了一些存活性探针livenessProbe、就绪性探针readinessProbe以及这节课要介绍的启动探针startupProbe。

startupProbe是在k8s v1.16加入了alpha版,官方对其作用的解释是:

Indicates whether the application within the Container is started. All other probes are disabled if a startup probe is provided, until it succeeds. If the startup probe fails, the kubelet kills the Container, and the Container is subjected to its restart policy. If a Container does not provide a startup probe, the default state is Success
翻译:判断容器内的应用程序是否已启动。如果提供了启动探测,则禁用所有其他探测,直到它成功为后再启动其他探测。如果启动探测失败,kubelet将杀死容器,容器将服从其重启策略。如果容器没有提供启动探测,则默认状态为成功。
注意:不要将startupProbe和readinessProbe混淆

11.3 什么时候使用startupProbe

正常情况下,我们会在 pod template 中配置 livenessProbe 来探测容器是否正常运行,如果异常则会触发restartPolicy 重启容器(因为默认情况下 restartPolicy 设置的是 always)。

部分配置如下:

livenessProbe: 
  httpGet: 
    path: /test
    prot: 80
  failureThreshold: 1
  initialDelay:10
  periodSeconds: 10 

上面配置的意思是容器启动10s后每10s检查一次,允许失败的次数是1次。如果失败次数超过1则会触发restartPolicy。

但是有时候会存在特殊情况,比如服务A启动时间很慢,需要60s。这个时候如果还是用上面的探针就会进入死循环,因为上面的探针10s后就开始探测,这时候我们服务并没有起来,发现探测失败就会触发restartPolicy。这时候有的朋友可能会想到把initialDelay调成60s不就可以了?但是我们并不能保证这个服务每次起来都是60s,假如新的版本起来要70s,甚至更多的时间,我们就不好控制了。有的朋友可能还会想到把失败次数增加,比如下面配置:

livenessProbe: 
  httpGet:
    path: /test
    port: 80
  failureThreshold: 5
  initialDelay:60
  periodSeconds: 10 

这在启动的时候是可以解决我们目前的问题,但是如果这个服务挂了呢?如果failureThreshold=1则10s后就会报警通知服务挂了,如果设置了failureThreshold=5,那么就需要5*10s=50s的时间,在现在大家追求快速发现、快速定位、快速响应的时代是不被允许的。

在这时候我们把startupProbe和livenessProbe结合起来使用就可以很大程度上解决我们的问题。

部分配置如下:

livenessProbe: 
  httpGet: 
    path: /test
    prot: 80
  failureThreshold: 1
  initialDelay:10
  periodSeconds: 10
startupProbe:
  httpGet: 
    path: /test
    prot: 80
  failureThreshold: 10
  initialDelay:10
  periodSeconds: 10

上面的配置是只有startupProbe探测成功后再交给livenessProbe。我们startupProbe配置的是10*10s,也就是说只要应用在100s内启动都是OK的,而且应用挂掉了10s就会发现问题。

其实这种还是不能确定具体时间,只能给出一个大概的范围。因为对服务启动时间的影响因素太多了,有可能是应用本身,有可能是外部因素,比如主机性能等等。我们只有在最大程度上追求高效、稳定,但是我们不能保证100%稳定,像阿里这样的大企业对外宣称的也是5个9,6个9的稳定率,如果出问题了,不好意思你恰恰不在那几个9里面,所以我们自己要做好监控有效性,告警的及时性,响应的快速性,处理的高效性。

11.4 lvenessProbe和readinessProbe的启动顺序

livenessProbe会导致Pod被重启,readinessProbe只是会导致Pod不能提供服务。

大多数时候我们的理解是LivenessProbe会在ReadinessProbe成功后开始检查,但事实并非如此。

kubelet 使用存活探测器来知道什么时候要重启Pod。 例如,存活探测器可以捕捉到死锁(应用程序在运行,但是无法继续执行后面的步骤)。 这样的情况下重启容器有助于让应用程序在有问题的情况下可用。

kubelet 使用就绪探测器可以知道容器什么时候准备好了并可以开始接受请求流量, 当一个 Pod 内的所有容器都准备好了,才能把这个 Pod 看作就绪了。 这种信号的一个用途就是控制哪个 Pod 作为 Service 的后端。 在 Pod 还没有准备好的时候,会从 Service 的负载均衡器中被剔除的。
kubelet 使用启动探测器(startupProbe)可以知道应用程序容器什么时候启动了。 如果配置了这类探测器,就可以控制容器在启动成功后再进行存活性和就绪检查, 确保这些存活、就绪探测器不会影响应用程序的启动。 这可以用于对慢启动容器进行存活性检测,避免它们在启动运行之前就被杀掉。

真正的启动顺序 可以参考下面的链接:

官方文档:

Caution: Liveness probes do not wait for readiness probes to succeed. If you want to wait before executing a liveness probe you should use initialDelaySeconds or a startupProbe.

翻译:注意:存活探测不会等待就绪探测成功。 如果您想在执行活性探测之前等待,您应该使用 initialDelaySeconds 或 startupProbe。

也就是 Liveness probes并不会等到Readiness probes成功之后才运行 ,livenessProbe和readinessProbe应该存在某种并发关系。

想要控制livenessProbe和readinessProbe的执行顺序只能借助initialDelaySeconds 字段或者startupProbe探针。

posted @ 2022-05-17 22:32  StaryJie  阅读(141)  评论(0编辑  收藏  举报