Kubernetes最基本的调度单元Pod

Pod

Pod是 Kubernetes 集群进行管理的最小调度单元,而并非容器。这意味着容器并不会直接被调度到集群中的节点上,而是封装在 Pod 对象中,作为一个整体来进行编排和管理。一个 Pod 可以包含一个或多个容器,尽管可以在一个 Pod 中运行多个容器,但通常情况下,一个 Pod 仅运行一个容器,每个容器内部运行一个独立的进程。
在这里插入图片描述

为什么需要 Pod

容器就好比是未来云计算系统中的进程,而容器镜像就是这个系统里的程序,kubemetes就是云时代的操作系统,Pod扮演的就是进程组的角色。那么把如果把kubemetes按操作系统来理解,我们知道在linux操作系统中,进程并不是孤苦伶仃的独自运行,而是以进程组的方式有原则的组织在一起,可以通过命令pstree -g进行查看。Kubemetes将进程组的概念映射到容器技术中,假如一个进程组有三个进程,这三个进程必须在同一个节点运行,否则它们之间通信和交换文件都会出问题(超亲密关系),由于受限于同期的单进程模型,这三个进程必须制作为三个容器。每个容器需要1G内存,进程1被调度到了内存剩余2.5G的node1节点,恰巧这台节点可用内存空间还剩1.5G,当进程二被调度后将无法调度进程三到node1节点,但这三个节点又必须在同一个节点运行,这就是一个典型的成组调度的问题。在kubemetes中,调度器是按pod资源需求进行调度,而非容器。所以在pod中自然会选择可用内存为3G的节点进行绑定,解决了超亲密的调度问题。这3个容器将被当做一个原子单元统一调度到node节点上。

容器的单进程模型并不是指容器里只能运行一个进程,而是指容器无法管理多个进程。这是因为容器里 PID=1 的进程就是应用本身,其他进程都是这个 PlD=1 进程的子进程。然而用户编写的应用并不像正常操作系统里的init进程或者systemd那样拥有进程管理的功能。比如你的应用是一个Java Web程序(PID=1),然后执行docker exec命令进入容器,并在后台启动了一个Nginx进程(PID=3)。当这个PID=1的Java Web程序进程异常退出时,其它进程将成为孤儿进程,没有人能够去回收它们的资源。或者也可以将容器中PID=1的进程改为systemd,由systemd来管理应用进程,那么当这个容器中的应用程序异常退出了,我们无从得知,因为该容器管理的是systemd。所以容器中我们无法管理多个进程,因此需要pod。

其实 Pod 也只是一个逻辑概念,真正起作用的还是 Linux 容器的 NamespaceCgroup 这两个最基本的概念,Pod 被创建出来其实是一组共享了一些资源的容器而已,这样Pod模拟出的效果就跟虚拟机里程序与程序之间的关系非常类似了。同一pod中容器之间可以共享网络空间、存储卷、IPC(进程间通信)等。比如A容器使用了80端口后,B容器就无法使用80端口(同一pod内不同容器之间共用一个网络),可以理解为同一个主机的不同进程。但是涉及到文件系统的时候,默认情况下 Pod 里面的容器之间的文件系统是完全隔离的,但是我们可以通过声明来共享同一个 Volume。

WAR包与WEB服务器
比如现在有一个Java Web应用的WAR包,它需要被放在Tomcat的webapps目录下运行起来,如果用docker可以通过以下方法实现:
一种方法是把WAR包直接放在tomcat镜像的webapps目录下,做成一个新的镜像运行起来。但是如果更新WAR包的内容,或者要升级tomcat镜像,就要重新制作一个新的发布镜像,非常麻烦。然而另一种方法则是压根不管WAR包,永远只发布一个tomcat容器,声明一个hostPath类型的Volume,从而把宿主机上的WAR包挂载到tomcat容器当中运行起来。不过这样你就必须要解决一个问题:如何让每一台宿主机,都预先准备好这个存储有WAR包的目录呢?所以在k8s中我们可以把WAR包和tomcat分别做成镜像,然后把它们作为一个pod里的两个容器组合在一起。

apiVersion: v1
kind: Pod
metadata:
  name: javaweb-2
spec:
  initContainers:
  - image: geektime/sample:v2
    name: war
    command: ["cp", "/sample.war", "/app"]
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: geektime/tomcat:7.0
    name: tomcat
    command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
    volumeMounts:
    - mountPath: /root/apache-tomcat-7.0.42-v2/webapps
      name: app-volume
    ports:
    - containerPort: 8080
      hostPort: 8001 
  volumes:
  - name: app-volume
    emptyDir: {}

在这个pod中,我们定义了两个容器,第一个容器使用的镜像是sample:v2,这个镜像里只有一个WAR包放在根目录下。而第二个容器则使用的是一个标准tomcat镜像。war包类型的容器是init container类型,这个类型的容器启动后,执行cp命令,把WAR包拷贝到/app目录下。会按顺序逐一启动,知道都启动并且退出了,用户容器才会启动。
接下来tomcat容器声明挂载app-volume到自己的webapps目录下。等tomcat容器启动时,它的webapps目录下就一定会存在sample.war文件,这个文件正是WAR包容器启动时拷贝到这个volume里面的,而这个volume是被两个容器共享的。这样,解决了WAR包与Tomcat容器之间的耦合关系问题。这种模式也被称为sidecar。
sidecar指的是我们可以在一个pod中,启动一个辅助容器,来完成一些独立主进程之外的工作。

Sidecar pattern(边车模型或跨斗模型)

为 Pod 主应用容器提供协同的辅助应用容器,每个应用独立运行,最为典型的代表是将主应用容器中的日志使用 agent 收集至日志服务器中时,可以将 agent 运行为辅助应用容器,即 sidecar。另一个典型的应用是为主应用容器中的 database server 启用本地缓存,模型如下图:
在这里插入图片描述
以下是一个使用 Sidecar 模式来进行日志收集的简单案例:

apiVersion: v1
kind: Pod
metadata:
  name: my-app-pod
spec:
  containers:
    - name: my-app
      image: my-app-image:latest
      # 主应用程序容器
    - name: log-collector
      image: log-collector-image:latest
      # Sidecar 容器,用于日志收集
  ...

主容器:这是你的应用程序容器,负责运行你的应用。
Sidecar 容器:这是一个专门用于日志收集的容器,通常运行一个日志收集代理或代理程序,比如Fluentd、Filebeat 或 Logstash。Sidecar 容器的任务是捕获主容器生成的日志,并将其发送到一个集中式日志存储或分析系统中。
这种模式的优点是你可以将日志收集逻辑与应用程序分开,这有助于保持容器的单一责任原则,并提供更灵活的日志处理和集成选项。同时,你可以在不更改主应用程序代码的情况下,轻松地添加、删除或更新 Sidecar 容器以满足不同的需求。

Ambassador pattern(大使模型)

为远程服务创建一个本地代理,代理应用运行于容器中,主容器中的应用通过代理容器访问远程服务。
一个典型的使用示例是主应用容器的进程访问 “一主多从” 模型的远程 Redis 应用时,可在当前 Pod 容器中为 Redis 服务创建一个 Ambassador container,主应用容器中的进程直接通过 locahost 接口访问 Ambassador container 即可。即便是 Redis 主从集群架构发生变动时,也仅需要将 Ambassador container 加以修改即可,主应用容器无须对此作出任何反应。
在这里插入图片描述
创建了一个 Pod,其中包含了两个容器:Ambassador 容器和后端服务容器。Ambassador 容器通常会监听一个特定的端口,例如 80 端口,以接收外部请求,并将它们路由到后端服务容器。
所有请求会先到达代理程序,同一个pod中的容器通过localhost通信,代理程序转给主容器,主容器也会先把响应返回给代理服务器,代理服务器再响应给客户端

apiVersion: v1
kind: Pod
metadata:
  name: ambassador-pod
spec:
  containers:
    - name: ambassador
      image: your-ambassador-image:latest
      ports:
        - containerPort: 80
    - name: backend-service
      image: your-backend-service-image:latest
      ports:
        - containerPort: 8080

Adapter pattern(适配器模型)

一般用于将主应用容器中的内容进行标准化输出。例如:日志数据或指标数据的输出,这有助于调用者统一接收数据的接口,如下图所示。另外某应用滚动升级后的版本不兼容旧的版本时,其报告信息的格式也存在不兼容的可能性,使用 Adapter container 有助于避免调用此报告数据的应用发生错误。
在这里插入图片描述

适配器容器的主要作用就是将业务容器暴露的接口转换成适应不同需求的格式或者路径。这种做法在微服务架构中非常常见,特别是当不同的团队或者服务需要使用相同的数据或功能,但希望以不同的方式访问时,适配器容器可以提供统一的接口。举例来说,当监控系统需要访问 /healthz 而业务容器只提供 /metrics 时,你可以创建一个适配器容器,将 /healthz 请求转发到 /metrics,使监控系统能够使用新的路径,而无需修改业务容器的代码。这样即便是监控系统升级修改了url,业务容器可以继续提供原有的接口,而适配器容器可以根据需要进行调整,而不影响其他服务。

将 /healthz 请求映射到 /metrics,从而适应监控系统的需求,而主业务容器不需要进行任何修改

apiVersion: v1
kind: Pod
metadata:
  name: adapter-pod
spec:
  containers:
    - name: main-container
      image: your-main-app-image:latest		#业务容器,运行你的应用程序或服务,并监听端口 8080,暴露 /metrics 接口
      ports:
        - containerPort: 8080
    - name: adapter-container
      image: your-adapter-image:latest		#适配器容器,运行一个适配器服务。这个适配器容器监听端口 8081,将 /healthz 请求转发到 /metrics
      ports:
        - containerPort: 8081

Pod的详细描述信息

nginx-3这个pod为例,如下

[root@k8s-master1 ~]# kubectl describe pod nginx-3
Name:             nginx-3		#pod名字
Namespace:        default		#pod所处的名称空间
Priority:         0				#优先级
Service Account:  default		#默认使用default
Node:             k8s-node1.guoguo.com/192.168.1.101	#被调度到的节点
Start Time:       Sun, 06 Aug 2023 17:32:26 +0800		#创建的时间
Labels:           app=nginx								#标签
                  env=dev
Annotations:  <none>				#资源注解
Status:           Running			#pod当前状态,pod的相位
IP:               192.26.131.132	#pod的ip
IPs:
  IP:  192.26.131.132
Containers:						#容器信息
  nginx:
    Container ID:   containerd://57a204528d873a006134dcb7e58739b6adcbbd1b299d4b7e4984c451d47af86c
    Image:          harbor.guoguo.com/apps/ubuntu-nginx:1.22.1
    Image ID:       harbor.guoguo.com/apps/ubuntu-nginx@sha256:8818a74320ea5451b340d47faadba66cc9f582c9b734526c65076808412803a1
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Mon, 07 Aug 2023 07:24:36 +0800
    Last State:     Terminated
      Reason:       Unknown
      Exit Code:    255
      Started:      Sun, 06 Aug 2023 21:17:40 +0800
      Finished:     Mon, 07 Aug 2023 07:23:29 +0800
    Ready:          True
    Restart Count:  2		#容器重启次数
    Environment:    <none>
    Mounts:	
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-9lwkh (ro)
Conditions:		#通常一个condition必须包含type(状态类型)和status(状态值)两个信息。只有所有的条件为 True 时,Pod 才可以提供服务
  Type              Status		  #类型   状态的名称
  Initialized       True		  #Pod的初始化是否全部完成
  Ready             True		  #pod是否就绪;只有pod中的所有container就绪,且pod的readiness probe也完成了,意味着pod可以对外提供服务了,才是ready状态
  ContainersReady   True		  #pod中的所有 container 是否全部就绪;但这并不意味着 pod 也 ready
  PodScheduled      True		  #pod调度成功,意味着 pod 是否已经被调度到某个node
Volumes:			#卷组
  kube-api-access-9lwkh:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort		#服务质量类别
Node-Selectors:              <none>			#没有节点选择,为none就说明没有给Node-selectors提特殊的节点选择
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s		#容忍
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:         #事件,这里将记录Pod整个生命周期中的各个状态,排查问题也主要是看这个地方
  Type     Reason          Age                   From             Message
  ----     ------          ----                  ----             -------
  Warning  NodeNotReady    5m8s                  node-controller  Node is not ready
  Normal   SandboxChanged  112s (x2 over 2m36s)  kubelet          Pod sandbox changed, it will be killed and re-created.
  Normal   Pulled          112s                  kubelet          Container image "nginx:1.23" already present on machine
  Normal   Created         112s                  kubelet          Created container mynginx
  Normal   Started         111s                  kubelet          Started container mynginx

Pod的生命周期

Pod 对象自从其创建开始至其终止退出的时间范围称为其生命周期。在这段时间中,Pod 会处于多种不同的状态,并执行一些操作。其中,创建主容器(main container)为必须的操作,其他可选的操作还包括进行初始化容器(init container)、容器启动后钩子(post start hook)、容器的存活性探测(liveness probe)、就绪型探测(readiness probe)以及容器终止前钩子(pre stop hook)等,这些操作是否执行则取决于 Pod 的定义。

从下面这张图可以直观的看到PostStartPreStop包括livenessreadiness是属于主容器的生命周期范围内的,而Init Container是独立于主容器之外的,当然他们都属于pod的生命周期范畴之内的
在这里插入图片描述

Pod的创建与销毁

Pod 是 Kubernetes 的基础单元,理解它的创建过程对于了解系统运作有很大帮助。
Pod的创建过程:
1、用户通过kubectl其它API客户端将要创建的Pod信息提交给API Server
2、API Server 尝试着将 Pod 的信息存入 etcd 中,并把写入etcd的结果信息返回至客户端
3、API Server开始反映etcd中的pod对象的变化,其它组件均使用watch机制来跟踪检查API Server上的变动
4、scheduler通过其watch觉察到 API Server 创建了新的 Pod 对象但尚未绑定至任何工作节点,scheduler 会为 Pod 对象挑选一个工作节点进行调度,并将调度的结果由 API Server更新至 etcd 存储系统
5、Pod 被调度到的目标工作节点上的 kubelet 尝试在当前节点上调用 Docker 启动容器,并将容器的结果状态送至 API ServerAPI ServerPod 状态信息存入 etcd 系统中
6、在 etcd 确认写入操作成功完成后,API Server 将确认信息发送至相关的 kubelet,事件将通过它被接受
在这里插入图片描述

Pod 对象代表了在 Kubernetes 集群节点上运行的进程,它可能曾用于处理生产数据或向用户提供服务等,但是当 Pod 本身不再具有存在的价值时,如何将其优雅地终止就显得尤为重要了,而用户也需要能够在正常提交删除操作后可以获知其如何开始终止并最终完成。当用户提交删除请求后,系统就会进行强制删除操作的宽限期倒计时,并将 TERM 信息发送给 Pod 对象的每个容器中的主进程。宽限期倒计时结束后,这些进程将啊收到强制终止的 KILL 信号,Pod 对象随即也将由 API Server 删除。如果在等待进程终止的过程中,kubelet 或容器管理器发生了重启,那么终止操作会重新获得一个满额的删除宽限期并重新进行删除操作。
Pod的终止过程:
1、用户向apiServer发送删除pod对象的命令
2、pod对象信息会随着时间的推移而更新,在宽限期内(默认30s)完成销毁,超出这个时间后pod将被强制终止。
3、在这期间,pod状态被标记为Terminating
4、kubelet监控到pod对象的状态转为Terminating时将会执行pod关闭过程
5、Endpoint控制器监控到pod对象的关闭行为时将其从所有匹配到此端点的service资源的端点列表中移除
6、如果当前pod对象定义了preStop钩子处理器,则在其标记为Terminating后即会以同步的方式启动执行
7、pod对象中的容器进程收到停止信号
8、宽限期结束后,若pod中还存在仍在运行的进程,那么pod对象会收到立即终止的信号
9、kubelet请求apiServer将此pod资源的宽限期设置为0从而完成删除操作,此时pod对于用户已不可见
在这里插入图片描述

Pod的相位(状态)

Pod 的相位(状态)是对 Pod 在生命周期中所属位置的一种简单、宏观的概述。可以通过命令kubectl explain pod.status 来了解关于 Pod 状态的一些信息,Pod的 status 字段是一个 PodStatus 对象,该阶段并不是对容器或 Pod 状态的综合汇总。

相位值说明
Pending未调度成功或镜像处于下载过程中,pod已被kubernetes系统接受,但尚有一个或多个容器镜像未能创建。比如没有被调度器调度到合适的节点或者镜像正在下载
Runningpod已绑定到Node,所有的容器均已创建。至少有一个容器还在运行,或者正在启动或重启
Succeededpod中的所有容器都已成功终止,并且不会重新启动
Failedpod中的所有容器都已终止,并且至少有一个容器表现出来失败的终止状态。也就是说,容器要么以非0状态退出,要么被系统终止
Unknown由于某种原因无法获得pod状态,通常是pod所在的宿主机通信出错而导致的

一旦在集群节点中创建pod,首先进入Pending状态。待到pod中的所有容器都已启动并正常运行,pod将进入Running状态。如果pod被终止并且所有容器退出时的状态码都为0,pod就会进入Succeeded状态。
Failed状态,通常是以下原因:
①pod启动时,只要有一个容器运行失败,pod将会从Pending状态进入Failed状态
②pod为Running状态,若pod中的一个容器突然损坏或在退出时状态码不为0,Pod将会从Pending状态进入Failed状态
③pod正常关闭的时候,只要有一个容器的退出状态不为0,pod就会进入Failed状态

在这里插入图片描述

pod内部容器状态

Kubernetes 会跟踪 Pod 中每个容器的状态, 当Pod被调度到某个节点上,kubelet 就通过容器运行时开始为 Pod 创建和管理容器。 容器的状态有三种:Waiting(等待)Running(运行中)Terminated(已终止),可以通过命令 kubectl describe pod <pod 名称> 查看 Pod 中每个容器的状态。

状态说明
Waiting处于 Waiting 状态的容器仍在运行它完成启动所需要的操作:例如从镜像仓库拉取容器镜像,或者向容器应用 Secret 数据等等。 当你使用 kubectl 来查询包含 Waiting 状态的容器的 Pod 时,你也会看到一个 Reason 字段,其中给出了容器处于等待状态的原因
Running处于Running 状态表明容器正在执行状态并且没有问题发生。 如果配置了 postStart 回调,那么该回调已经执行且已完成。 如果你使用 kubectl 来查询包含 Running 状态的容器的 Pod 时,你也会看到关于容器进入 Running 状态的信息
Terminated处于 Terminated 状态的容器已经开始执行并且或者正常结束或者因为某些原因失败。 如果你使用 kubectl 来查询包含 Terminated 状态的容器的 Pod 时,你会看到容器进入此状态的原因、退出代码以及容器执行期间的起止时间。如果容器配置了 preStop 回调,则该回调会在容器进入 Terminated 状态之前执行

通过以下命令可以看出nginx和test两个pod的phase字段值都为Running,但test并没有真正的Running,这里的Running仅仅代表pod在Running阶段而已

[root@master ~]# kubectl get pod
NAME                                READY   STATUS             RESTARTS   AGE
ack-place-holder-5dbc547d6c-ghgvg   1/1     Running            1          176d
nginx-75d85f4fb4-kpfbd              1/1     Running            1          108d
test                                0/1     CrashLoopBackOff   13722      48d

[root@master ~]# kubectl get pod nginx-75d85f4fb4-kpfbd -o yaml
  phase: Running
[root@master ~]# kubectl get pod test -o yaml
  phase: Running 

state即为容器状态,Reason即为状态原因,Last State为上次的状态和原因,状态为Running的时候没有原因,所以我们在get pod的时候看到的status字段的值若不为Running的时候取自Reason字段值,若为Running时则为Running

[root@iZ2zef1e034gg98bajpqw6Z ~]# kubectl describe pod test
... ...
Containers:
... ...
    State:       Waiting			#状态		
      Reason:    CrashLoopBackOff	#原因
    Last State:  Terminated			#上次的状态和原因
      Reason:    Error				#
      Exit Code: 2					#退出状态码
... ...

[root@iZ2zef1e034gg98bajpqw6Z ~]# kubectl describe pod nginx-75d85f4fb4-kpfbd
... ...
Containers:
... ...
    State:          Running
      Started:      Fri, 13 May 2022 08:06:18 +0800
    Last State:     Terminated
      Reason:       Error
      Exit Code:    255
... ...

处于 Waiting 状态的容器仍在运行它完成启动所需要的操作:例如,从某个容器镜像仓库拉取容器镜像,或者向容器应用 Secret 数据等等。当你使用 kubectl 来查询包含 Waiting 状态的容器的 Pod 时,你也会看到一个 Reason 字段,其中给出了容器处于等待状态的原因

[root@k8s-master ~]# kubectl describe pod nginx-deploy-7ff6dd9bd-6tlt2
Name:         nginx-deploy-7ff6dd9bd-6tlt2
Namespace:    default
...
Containers:
  mynginx:
    Container ID:
    Image:          nginx:1.23.222
    Image ID:
    Port:           <none>
    Host Port:      <none>
    State:          Waiting
      Reason:       ImagePullBackOff
    Ready:          False
...

Running 状态表明容器正在执行状态并且没有问题发生。 如果配置了 postStart 回调,那么该回调已经执行且已完成。 如果你使用 kubectl 来查询包含 Running 状态的容器的 Pod 时,你也会看到关于容器进入 Running 状态的信息

[root@k8s-master ~]# kubectl describe pod nginx-deploy-69c765d6d-hmtkj
Name:         nginx-deploy-69c765d6d-hmtkj
Namespace:    default
...
Containers:
  mynginx:
    Container ID:   docker://e3962726018c97b3b3825941ed0416f2e25d3e28b4ddd08bc85ef881df4d5ec0
    Image:          nginx:1.23
    Image ID:       docker-pullable://nginx@sha256:790711e34858c9b0741edffef6ed3d8199d8faa33f2870dea5db70f16384df79
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Mon, 15 Aug 2022 19:55:20 +0800
    Ready:          True
...

处于 Terminated 状态的容器已经开始执行并且或者正常结束或者因为某些原因失败。 如果你使用 kubectl 来查询包含 Terminated 状态的容器的 Pod 时,你会看到容器进入此状态的原因、退出代码以及容器执行期间的起止时间。

[root@k8s-master ~]# kubectl describe pod nginx-deploy-69c765d6d-7g89j
Name:           nginx-deploy-69c765d6d-7g89j
Namespace:      default
...
Containers:
  mynginx:
    Container ID:   docker://aa9002c779143377889ad586e95483b1be6fcf7d10ddf01d76752392393e5419
    Image:          nginx:1.23
    Image ID:       docker-pullable://nginx@sha256:790711e34858c9b0741edffef6ed3d8199d8faa33f2870dea5db70f16384df79
    Port:           <none>
    Host Port:      <none>
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Mon, 15 Aug 2022 21:06:01 +0800
      Finished:     Mon, 15 Aug 2022 22:10:08 +0800
    Ready:          False
...

容器状态区别于Pod状态,是通过Containers.State字段来定义的,PodStatus 对象中还包含一个 PodCondition 的数组,里面包含的属性有:

[root@master1 ~]#kubectl explain pod.status.conditions
lastProbeTime:			#最后一次探测 Pod Condition 的时间戳。
lastTransitionTime:	#上次 Condition 从一种状态转换到另一种状态的时间。
message:				#上次 Condition 状态转换的详细描述。
reason:				#Condition 最后一次转换的原因。
status:				#Condition 状态类型,可以为 “True”, “False”, and “Unknown”.
type:					#Condition 类型,包括以下方面:
	PodScheduled 		#Pod已调度到一个节点
	Ready 				#Pod能够提供服务请求,可以被添加到所有可匹配服务的负载平衡池中
	Initialized 		#所有的init containers已经启动成功
	Unschedulable 		#调度程序现在无法调度 Pod,例如由于缺乏资源或其他限制
	ContainersReady 	#Pod 里的所有容器都是 ready 状态

pod的重启策略

我们可以通过配置 restartPolicy 字段来设置 Pod 中所有容器的重启策略,其可能值为AlwaysOnFailureNever,默认值为 Always。重启策略适用于Pod对象中的所有容器,首次需要重启的容器,将在其需要时立即进行重启,随后再次需要重启的操作将由Kubelet延迟一段时间后进行,且反复的重启操作的延迟时长为10s,20s,40s,80s,160s,300s,300s是最大延迟时长。不同类型的的控制器可以控制 Pod 的重启策略

重启策略说明
Always只要Container退出就重启,即使它的退出码为0(即成功退出),这也是默认值
OnFailure如果Container的退出码不是0(即失败退出),就重启
Never不论状态为何,都不重启该容器

Pod 的重启策略是控制容器的重启而不是整个 Pod 的重启。当容器根据重启策略需要重启时,Kubernetes 会尝试重新启动该容器,而不会影响 Pod 中其他容器的状态。这允许每个容器在 Pod 中独立地管理其生命周期。虽然restartPolicy字段是Pod的配置,但是其实是作用于Pod的Container,换句话说,不应该叫Pod的重启策略,而是叫Container的重启策略,Pod中的所有Container都适用于这个策略。
所谓容器的退出码,就是容器中进程号为1的进程的退出码。每个进程退出时都有一个退出码,我们常见的提示exit 0表示退出码为0(即成功退出)。举个例子:shell命令cat /tmp/file,如果文件/tmp/file存在,则该命令(进程)的退出码为0。

对于Always重启策略的Pod来说,Pod中容器重启时,查看Pod 状态字段值是有规则的,Pod中容器整个重启过程Pod状态会从Running -> NotReady -> CrashLoopBackOff -> Running,周而复始。Pod中重启的容器以k8s_<ContainerName_In_Pod>_<PodName>_<Namespace>_<PodID>_<Index>规则命名。

Never重启策略的Pod:
Pod中只要有容器停止,并且停止容器的状态码都为0,那么Pod状态为Completed。
Pod中只要有容器停止,并且存在非0退出状态码,那么Pod状态为Error。

OnFailure重启策略的Pod:
Pod中容器停止,并且停止容器的状态码为0,那么Pod状态为Completed。
Pod中容器停止,并且停止容器的状态码为非0,那么Pod就会像Always策略那样重启容器。

同一个Pod中的一个容器宕掉时,它的故障通常不会直接影响其他容器的运行。其他容器仍然可以继续运行,除非它们与宕掉容器之间存在依赖关系。查看Pod RESTARTS字段值是有规则的,RESTARTS字段表示整个Pod中所有容器的累计重启次数。如果任何一个容器在Pod中重启,RESTARTS字段的值会增加。这意味着即使只有一个容器重启了,整个Pod的RESTARTS字段值也会增加。

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

Always

我们给一个例子,创建一个如下的Pod,这个Pod中有两个Container,其中demo1这个Container在启动后60秒就会退出(退出码为0),demo2这个Container则会一直运行。

apiVersion: v1
kind: Pod
metadata:
  name: test-restartpolicy
  namespace: test-restartpolicy
spec:
  restartPolicy: Always
  containers:
  - name: demo1
    image: busybox:1.31.1
    command:
    - /bin/sh
    - -c
    - sleep 60
  - name: demo2
    image: tomcat:9.0.37

然后观察demo1这个Container的docker名字与ID,发现名字(docker name字段)为k8s_demo1_test-restartpolicy_test-restartpolicy_88d4575b-5d59-4e25-8b61-ea0e0bfef035_0,ID为f09dd4d59d76。

[root@node1 test]# docker ps|grep demo1
f09dd4d59d76   1c35c4412082    "/bin/sh -c 'sleep 6…"   32 seconds ago  Up 30 seconds  k8s_demo1_test-restartpolicy_test-restartpolicy_88d4575b-5d59-4e25-8b61-ea0e0bfef035_0

再过一分钟左右,我们再看这个Container的名字和ID,发现名字为k8s_demo1_test-restartpolicy_test-restartpolicy_88d4575b-5d59-4e25-8b61-ea0e0bfef035_1,ID为7ba3cc9685eb。这说明,重启后已经不是同一个docker容器了。

[root@node1 test]# docker ps|grep demo1
7ba3cc9685eb   1c35c4412082  "/bin/sh -c 'sleep 6…"   14 seconds ago    Up 13 seconds  k8s_demo1_test-restartpolicy_test-restartpolicy_88d4575b-5d59-4e25-8b61-ea0e0bfef035_1

同理,再过一分钟左右,我们再看这个Container的名字和ID,发现名字为k8s_demo1_test-restartpolicy_test-restartpolicy_88d4575b-5d59-4e25-8b61-ea0e0bfef035_2,ID为d71f73027239。这说明,重启后已经不是同一个docker容器了。

d71f73027239   1c35c4412082    "/bin/sh -c 'sleep 6…"   7 seconds ago   Up 5 seconds   k8s_demo1_test-restartpolicy_test-restartpolicy_88d4575b-5d59-4e25-8b61-ea0e0bfef035_2

如果我们细心的会发现,docker容器的名字其实是有规则的,为k8s_<ContainerName_In_Pod>_<PodName>_<Namespace>_<PodID>_<Index>,其中最后一个一开始为0,每重启一次就会递增1。

查看Pod中容器重启时Pod的状态变化

#demo1容器第一次重启后,Pod容器状态从NotReady变成了Running
[root@node1 test]# kubectl get pods -n=test-restartpolicy
NAME                 READY   STATUS    RESTARTS   AGE
test-restartpolicy   2/2     Running   0          1s
[root@node1 test]# kubectl get pods -n=test-restartpolicy
NAME                 READY   STATUS     RESTARTS   AGE
test-restartpolicy   1/2     NotReady   0          65s
[root@node1 test]# kubectl get pods -n=test-restartpolicy
NAME                 READY   STATUS    RESTARTS   AGE
test-restartpolicy   2/2     Running   1          67s

#在demo1容器第二次重启后,Pod容器状态从NotReady先变成了CrashLoopBackOff再变成Running
[root@node1 test]# kubectl get pods -n=test-restartpolicy
NAME                 READY   STATUS     RESTARTS   AGE
test-restartpolicy   1/2     NotReady   1          2m20s
[root@node1 test]# kubectl get pods -n=test-restartpolicy
NAME                 READY   STATUS             RESTARTS   AGE
test-restartpolicy   1/2     CrashLoopBackOff   1          2m21s
[root@node1 test]# kubectl get pods -n=test-restartpolicy	
NAME                 READY   STATUS    RESTARTS   AGE			
test-restartpolicy   2/2     Running   2          2m23s

过一段时间后,可以看到Pod容器状态大部分时间为CrashLoopBackOff,这是因为容器频繁重启的话会有退避延迟时间,首次需要重启的容器,将在其需要时立即进行重启,随后再次需要重启的操作将由Kubelet延迟一段时间后进行,且反复的重启操作的延迟时长为10s,20s,40s,80s,160s,300s,300s是最大延迟时长,过一段时间后demo1容器的重启延迟时间变成了300s,此时Pod状态为CrashLoopBackOff,重启延迟时间过后demo1容器状态会变成running,再过60s,demo1容器退出后,demo1的状态又会从NotReady变成CrashLoopBackOff,之后周而复始。
如果我们细心的会发现,对于Always重启策略的Pod来说,Pod中容器重启时,查看Pod 状态字段值是有规则的,Pod中容器整个重启过程Pod状态会从Running -> NotReady -> CrashLoopBackOff -> Running,周而复始。

此时我们查看Pod的yaml文件,会发现demo1这个Container的restartCount为8,demo2这个Container的restartCount为0。

[root@node1 ~]# kubectl get pods -n=test-restartpolicy
NAME                 READY   STATUS    RESTARTS   AGE
test-restartpolicy   2/2     Running   8          24m
[root@node1 ~]# kubectl get pods -n=test-restartpolicy -o yaml
......
status:
 containerStatuses:
    - containerID: docker://75a85ae72574d5edb42486dc58d3526b3c7ed5d29c9959c898a4689f12fef7f5
      ......
      name: demo1
      ready: true
      restartCount: 8
      started: true
      ......
- containerID: docker://1d7fe0cb75d5a6aa9074f2ceb0dc7515df0aa6082b1e4bb9e77931b5724445ca
    ......
    name: demo2
    ready: true
    restartCount: 0
    started: true
    ......

此时删除test-restartpolicy这个Pod,修改Pod规格配置文件如下,让demo2这个Container在启动30秒后也退出(退出码为0)。

apiVersion: v1
kind: Pod
metadata:
  name: test-restartpolicy
  namespace: test-restartpolicy
spec:
  restartPolicy: Always
  containers:
  - name: demo1
    image: busybox:1.31.1
    command:
    - /bin/sh
    - -c
    - sleep 60
  - name: demo2
    image: busybox:1.31.1
    command:
    - /bin/sh
    - -c
    - sleep 30
  nodeSelector:
    kubernetes.io/hostname: node1

接下来我们创建test-restartpolicy这个Pod,过几分钟查看Pod

[root@node1 test]# kubectl get pods -n=test-restartpolicy
NAME                 READY   STATUS             RESTARTS   AGE
test-restartpolicy   1/2     CrashLoopBackOff   7          4m27s

此时我们查看Pod的yaml文件,会发现demo1这个Container的restartCount为3,demo2这个Container的restartCount为4

[root@node1 test]# kubectl get pods -n=test-restartpolicy -o yaml
......
  status:
    ......
    containerStatuses:
    - containerID: docker://a34abd3d662c0d5c309f7967ccb2e3a70e25bda520a8b55b63eee8cff7892762
      ......
      name: demo1
      ready: true
      restartCount: 3
      started: true
      ......
    - containerID: docker://4f97cccf338fff56f0d894cf4f89bda48b6108046d3ff6cfd8097bd2e6efe86b
      ......
      name: demo2
      ready: false
      restartCount: 4
      started: false
      .......

如果我们细心的会发现,查看Pod RESTARTS字段值是有规则的,RESTARTS字段表示整个Pod中所有容器的累计重启次数。如果任何一个容器在Pod中重启,RESTARTS字段的值会增加。这意味着即使只有一个容器重启了,整个Pod的RESTARTS字段值也会增加。
同一个Pod中每个容器都有自己的进程空间, 当同一个Pod中的一个容器宕掉时,它的故障通常不会直接影响其他容器的运行。其他容器仍然可以继续运行,除非它们与宕掉容器之间存在依赖关系。

Never

示例一:
创建一个如下的Pod,重启策略为Nerver

[root@node1 ~]# cat restartpolicy-nerver.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: restartpolicy-nerver
  namespace: test-restartpolicy
spec:
  containers:
  - name: demo1
    image: nginx:1.14-alpine
    ports:
    - name: nginx-port
      containerPort: 80
    livenessProbe:
      httpGet:
        scheme: HTTP
        port: 80
        path: /hello
  restartPolicy: Never
[root@node1 ~]# kubectl apply -f restartpolicy-nerver.yaml 
pod/restartpolicy-nerver created

查看状态码,Pod中容器退出状态码为0,这说明存活探针是正常的使容器停止。

[root@node1 ~]# kubectl describe pods -n test-restartpolicy restartpolicy-nerver
......
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Sun, 02 Jul 2023 05:48:38 +0800
      Finished:     Sun, 02 Jul 2023 05:49:07 +0800
    Ready:          False
    Restart Count:  0

Pod创建30秒后再查看Pod状态,Pod状态为Completed。

[root@node1 ~]# kubectl get pods -n test-restartpolicy
NAME                      READY   STATUS             RESTARTS       AGE
restartpolicy-nerver      0/1     Completed          0              2m17s

Pod的Completed状态表示其中的所有容器已成功完成任务并终止。一旦Pod中的所有容器完成其工作并退出,Pod的状态将变为Completed。这可以是一个批处理作业、定时任务或其他短暂的工作。一旦任务完成,Pod将进入Completed状态,并保持在该状态,直到被删除。Completed状态的Pod不会自动重启,也不会再次运行其中的容器。它们保留在集群中的历史记录中,但不会继续占用资源。

查看Pod日志,虽然Pod状态是Completed,但是是能查看Pod日志的,那么Never重启策略的Pod在某些场景下是非常有用的,比如Pod中的容器频繁重启,捕捉停止容器日志比较麻烦,这时如果Pod是默认的Always,可以通过临时将Pod的重启策略改成Never,这样可以通过describe Pod和查看Pod日志来分析Pod重启原因。

[root@node1 ~]# kubectl logs -f -n test-restartpolicy restartpolicy-nerver
2023/06/14 07:57:11 [error] 7#7: *1 open() "/usr/share/nginx/html/hello" failed (2: No such file or directory), client: 10.233.64.1, server: localhost, request: "GET /hello HTTP/1.1", host: "10.233.64.161:80"
10.233.64.1 - - [14/Jun/2023:07:57:11 +0000] "GET /hello HTTP/1.1" 404 169 "-" "kube-probe/1.21" "-"
2023/06/14 07:57:21 [error] 7#7: *2 open() "/usr/share/nginx/html/hello" failed (2: No such file or directory), client: 10.233.64.1, server: localhost, request: "GET /hello HTTP/1.1", host: "10.233.64.161:80"
10.233.64.1 - - [14/Jun/2023:07:57:21 +0000] "GET /hello HTTP/1.1" 404 169 "-" "kube-probe/1.21" "-"
2023/06/14 07:57:31 [error] 7#7: *3 open() "/usr/share/nginx/html/hello" failed (2: No such file or directory), client: 10.233.64.1, server: localhost, request: "GET /hello HTTP/1.1", host: "10.233.64.161:80"
10.233.64.1 - - [14/Jun/2023:07:57:31 +0000] "GET /hello HTTP/1.1" 404 169 "-" "kube-probe/1.21" "-"

示例二:
创建一个如下的Pod,重启策略为Nerver

[root@node1 ~]# cat restartpolicy-nerver.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: restartpolicy-nerver
  namespace: test-restartpolicy
spec:
  restartPolicy: Never
  containers:
  - name: demo1
    image: busybox:1.31.1
    command: ["/bin/sh","-c","sleep 60,exit 1"]

[root@node1 ~]# kubectl apply -f restartpolicy-nerver.yaml 
pod/restartpolicy-nerver created

查看Pod详情,可以看到Pod中容器退出状态码是1

[root@node1 ~]# kubectl describe pod -n test-restartpolicy restartpolicy-nerver
    State:          Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Tue, 05 Sep 2023 08:20:55 +0800
      Finished:     Tue, 05 Sep 2023 08:20:55 +0800
    Ready:          False
    Restart Count:  0

查看Pod状态,Pod中容器退出状态码为非0的话,Pod状态为Error

[root@node1 ~]# kubectl get pods -n=test-restartpolicy
NAME                   READY   STATUS   RESTARTS   AGE
restartpolicy-nerver   0/1     Error    0          2m25s

示例三:
创建一个如下的Pod,重启策略为Nerver

[root@node1 ~]# cat restartpolicy-nerver.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: restartpolicy-nerver
  namespace: test-restartpolicy
spec:
  restartPolicy: Never
  containers:
  - name: demo1
    image: busybox:1.31.1
    command: ["/bin/sh","-c","sleep 60,exit 1"]
  - name: demo2
    image: busybox:1.31.1
    command:
    - /bin/sh
    - -c
    - sleep 3000

[root@node1 ~]# kubectl apply -f restartpolicy-nerver.yaml 
pod/restartpolicy-nerver created

查看Pod状态,只要Pod中有一个容器异常退出,那么它的状态就会变成error

[root@node1 ~]# kubectl get pods -n=test-restartpolicy
NAME                   READY   STATUS   RESTARTS   AGE
restartpolicy-nerver   1/2     Error    0          3m7s

对于Never重启策略的Pod,通过以上三个示例可以得出如下结论:
Pod中只要有容器停止,并且停止容器的状态码都为0,那么Pod状态为Completed。
Pod中只要有容器停止,并且存在非0退出状态码,那么Pod状态为Error。
不管退出状态码是Completed还是Error,都能查看Pod日志的,那么Never重启策略的Pod在某些场景下是非常有用的,比如Pod中的容器频繁重启,这时如果Pod是默认的Always,可以通过临时将Pod的重启策略改成Never,这样可以通过describe Pod和查看Pod日志来分析Pod重启原因。
同一个Pod中的一个容器宕掉时,它的故障通常不会直接影响其他容器的运行。其他容器仍然可以继续运行,除非它们与宕掉容器之间存在依赖关系。

OnFailure

示例一:
创建一个如下的Pod重启策略为OnFailure

[root@node1 ~]# cat restartpolicy-onfailure.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: restartpolicy-onfailure
  namespace: test-restartpolicy
spec:
  containers:
  - name: demo1
    image: nginx:1.14-alpine
    ports:
    - name: nginx-port
      containerPort: 80
    livenessProbe:
      httpGet:
        scheme: HTTP
        port: 80
        path: /hello
  restartPolicy: OnFailure

[root@node1 ~]# kubectl apply -f restartpolicy-onfailure.yaml 
pod/restartpolicy-onfailure created

Pod中的demo1容器在启动30秒后会因为存活探针检测导致容器停止,容器退出码是0,此时查看Pod信息,Pod状态是Completed。查看Pod信息时Pod RESTARTS字段重启次数为4,我们知道存活探针检查失败的容器退出状态码为0,而这里重启次数为什么是4,目前这里没搞清楚,是bug,还是这块研究不深,有大神可以评论给解惑下

[root@node1 ~]# kubectl get pods -n test-restartpolicy
NAME                      READY   STATUS      RESTARTS   AGE
restartpolicy-onfailure   0/1     Completed   4          3m27s
[root@node1 ~]# kubectl describe pods -n test-restartpolicy restartpolicy-onfailure
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Tue, 05 Sep 2023 08:36:48 +0800
      Finished:     Tue, 05 Sep 2023 08:37:18 +0800
    Ready:          False
    Restart Count:  4

示例二:
创建一个如下的Pod,重启策略为OnFailure

[root@node1 ~]# cat restartpolicy-onfailure.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: restartpolicy-onfailure
  namespace: test-restartpolicy
spec:
  restartPolicy: OnFailure
  containers:
  - name: demo1
    image: busybox:1.31.1
    command: ["/bin/sh","-c","exit 1"]
[root@node1 ~]# kubectl apply -f restartpolicy-onfailure.yaml 
pod/restartpolicy-onfailure created

创建几分钟后,查看Pod状态,可看到容器当前已经重启了 5 次

[root@node1 ~]# kubectl get pods -n=test-restartpolicy
NAME                      READY   STATUS             RESTARTS      AGE
restartpolicy-onfailure   0/1     CrashLoopBackOff   3 (38s ago)   79s

[root@node1 ~]# kubectl describe pod -n test-restartpolicy restartpolicy-onfailure
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Tue, 05 Sep 2023 08:44:14 +0800
      Finished:     Tue, 05 Sep 2023 08:44:14 +0800
    Ready:          False
    Restart Count:  3

对于OnFailure重启策略的Pod,通过以上两个示例可以得出如下结论:
Pod中容器停止,并且停止容器的状态码为0,那么Pod状态为Completed。
Pod中容器停止,并且停止容器的状态码为非0,那么Pod就会像Always策略那样重启容器。

pause(infra)容器

Kubernetes中的pause容器可以说是网络模型的精髓,理解pause容器能够更好地理解Kubernetes Pod的设计初衷。pause容器全称infrastucture container(又叫infra)基础容器。该镜像非常小,只有几百KB,由于它总是处于 Pause (暂时)状态,所以取名叫 pause
我们知道Kubernetes的Pod抽象基于Linux的namespace和cgroups,为容器提供了隔离的环境。从网络的角度看,同一个Pod中的不同容器犹如运行在同一个主机上,可以通过localhost进行通信。一个Pod中的容器可共享PID、Network、IPC和UTS名称空间,但Mount和USER名称空间却各自独立,因而跨容器的进程彼此间默认无法基于共享的存储空间交换文件或数据。

容器通过Namespacecgroups进行隔离,pause容器就是为了解决pod中容器间网络通信而产生的,实现了容器与容器之间的网络共享。比如现在有一个pod,里面包含了一个容器A和一个容器B,同一pod中的这两个容器通过加入(join)Infra container容器来共享整个pod的Network Namespace。其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中,所以说一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的,即:它们看到的网络设备、IP 地址、Mac 地址等等,所以在同一pod中容器与容器之间端口不能一样。如果没有 pause容器,那么容器A和B要实现网络共享,要么是A加入B的network namespace,要么就是B加入A的network namespace。无论是谁加入谁,只要network的owner退出了,该 Pod 里的所有其他容器网络都会立马异常,这显然是不合理的。

由于所有的业务容器都要依赖于pause容器,因此在pod启动时,它总是第一个启动,可以说pod的生命周期就是pause容器的生命周期,与容器 A 和 B 是无关的。这也是为什么在 Kubernetes 里面,它是允许去单独更新 pod 里的某一个镜像的,即:做这个操作,整个 Pod 不会重建,也不会重启,这是非常重要的一个设计。

在kubelet的配置文件中指定了拉取的pause镜像地址

[root@master ~]# cat /var/lib/kubelet/kubeadm-flags.env 		#此处已替换为阿里云,所以下载infra容器的话就从阿里云下载
KUBELET_KUBEADM_ARGS="--network-plugin=cni --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.6"

Kubernetes 集群部署完成后,首先需要保证在所有节点上可以拉取到默认的 Infra 镜像,默认情况下 Infra 镜像地址为 k8s.gcr.io/pause,这个镜像默认是需要科学上网的,所以很多时候我们在部署应用的时候一直处于 Pending 状态,因为所有 pod 最先启动的容器镜像都拉不下来,所以肯定启动不了,因此其它容器也就不能启动了

[root@master ~]# kubelet --help |grep infra
      --pod-infra-container-image string                         Specified image will not be pruned by the image garbage collector. When container-runtime is set to 'docker', all containers in each pod will use the network/ipc namespaces from this image. Other CRI implementations have their own configuration to set this image. (default "k8s.gcr.io/pause:3.6")

kubernetes中的pause容器主要为每个业务容器提供以下功能:
PID命名空间:Pod中的不同应用程序可以看到其他应用程序的进程ID。
网络命名空间:Pod中的多个容器能够访问同一个IP和端口范围。
IPC命名空间:Pod中的多个容器能够使用SystemV IPC或POSIX消息队列进行通信。
UTS命名空间:Pod中的多个容器共享一个主机名。
Volumes(共享存储卷):Pod中的各个容器可以访问在Pod级别定义的Volum。
在这里插入图片描述

通过docker模拟出一个符合k8s的pod模型,但是是它并不由k8s进行管理。在节点上运行一个pause容器,将宿主机的 8888 端口映射到容器的 80 端口

docker run -d --name pause -p 8080:80 --ipc=host registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6

然后再运行一个nginx容器

cat <<EOF >> nginx.conf
error_log stderr;
events { worker_connections  1024; }
http {
    access_log /dev/stdout combined;
    server {
        listen 80;
        server_name localhost;
        location / {
            proxy_pass http://127.0.0.1:2368;
        }
    }
}
EOF

docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf:rw --net=container:pause --ipc=container:pause --pid=container:pause nginx

--net:指定 nginx 要 join 谁的 network namespace,当然是前面创建的pause容器
--ipc:指定 ipc mode,一样指定前面创建的pause
--pid:指定 nginx 要 join 谁的 pid namespace,照旧是前面创建的pause

最后创建一个博客软件ghost容器

docker run -d --name ghost --net=container:pause --ipc=container:pause --pid=container:pause ghost

pause容器将内部的80端口映射到宿主机的8080端口,pause容器在宿主机上设置好了网络namespace后,nginx容器加入到该网络namespace中,我们看到nginx容器启动的时候指定了–net=container:pause。ghost容器同样加入到了该网络namespace中,这样三个容器就共享了网络,互相之间就可以使用localhost直接通信,–ipc=contianer:pause --pid=container:pause就是三个容器处于同一个namespace中,init进程为pause,这时我们进入到ghost容器中查看进程情况

[root@localhost ~]# docker exec -it ghost bash
root@cfa367869c60:/var/lib/ghost# ps aux
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 13:12 ?        00:00:00 /pause
root       756     0  0 13:37 ?        00:00:00 nginx: master process nginx -g daemon off;
101        785   756  0 13:37 ?        00:00:00 nginx: worker process
node       786     0  2 13:37 ?        00:00:08 node current/index.js
root      1181     0  0 13:41 pts/0    00:00:00 bash
root      1495  1181  0 13:42 pts/0    00:00:00 ps -ef

在ghost容器中同时可以看到pause和nginx容器的进程,并且pause容器的PID是1。而在kubernetes中容器的PID=1的进程即为容器本身的业务进程
在这里插入图片描述

如果使用 docker 创建三个容器pause、nginx、ghost,当你想销毁这个服务时,则需要删除三个容器。在k8s中,这三个容器逻辑上就是一个整体,通过pod 创建这三个容器,同样删除这个pod,这三个容器也被删除,从管理上来讲,方便了不少。
在这里插入图片描述

创建一个ConfigMap对象,用来存储 nginx.conf 文件

cat <<EOF >> nginx.conf
error_log stderr;
events { worker_connections  1024; }
http {
    access_log /dev/stdout combined;
    server {
        listen 80;
        server_name localhost;
        location / {
            proxy_pass http://127.0.0.1:2368;
        }
    }
}
EOF

[root@master ~]# kubectl create configmap nginx-config --from-file=nginx.conf
configmap/nginx-config created

查看configmap配置信息

[root@master ~]# kubectl get cm nginx-config -o yaml
apiVersion: v1
data:
  nginx.conf: |
    error_log stderr;
    events { worker_connections  1024; }
    http {
        access_log /dev/stdout combined;
        server {
            listen 80;
            server_name localhost;
            location / {
                proxy_pass http://127.0.0.1:2368;
            }
        }
    }

接着执行如下命令创建一个 ghost.yaml 文件

cat <<-EOF >> ghost.yaml
apiVersion: v1
kind: Pod
metadata:
  name: ghost
  namespace: default
spec:
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP
      hostPort: 8888
    volumeMounts:
    - mountPath: /etc/nginx/
      name: nginx-config
      readOnly: true
  - image: ghost
    name: ghost
  volumes:
  - name: nginx-config
    configMap:
      name: nginx-config
EOF

直接 apply 该文件就可以创建一个 ghost 服务,从输出可以看到这里的 READY 变成了 2/2,意思是该 Pod 总共包含 2 个容器,目前已经全部准备就绪

[root@master ~]# kubectl apply -f ghost.yaml 
pod/ghost created
[root@master ~]# kubectl get pod -o wide 
NAME    READY   STATUS    RESTARTS   AGE     IP           NODE    NOMINATED NODE   READINESS GATES
ghost   2/2     Running   0          7m43s   10.244.2.2   node2   <none>           <none>

在这里插入图片描述

登陆到该 pod 所在的 worker 节点,使用 docker ps 查看容器,发现pause容器

[root@node2 ~]# docker ps |grep ghost
fcb45dc6c567   ghost                                                "docker-entrypoint.s…"   23 minutes ago   Up 23 minutes             k8s_ghost_ghost_default_70ed7e20-504c-43b8-a827-98628c348b41_0
9f4261589731   605c77e624dd                                         "/docker-entrypoint.…"   24 minutes ago   Up 24 minutes             k8s_nginx_ghost_default_70ed7e20-504c-43b8-a827-98628c348b41_0
557a0f5114d9   registry.aliyuncs.com/google_containers/pause:3.6    "/pause"                 24 minutes ago   Up 24 minutes             k8s_POD_ghost_default_70ed7e20-504c-43b8-a827-98628c348b41_0

初始化容器

初始化容器是在主容器启动之前运行的,用于完成主容器所需的前置工作。一个 pod 可以包含一个或多个初始化容器,但它们不能同时运行。每个初始化容器必须在下一个初始化容器启动之前成功完成。如果某个初始化容器失败,后续的初始化容器将不会被启动。
初始化容器是独立于主容器的,只有所有的初始化容器都成功完成后,主容器才会启动。如果初始化容器失败,主容器将无法启动。初始化容器的运行失败会根据 Pod 的 restartPolicy 进行处理。如果 restartPolicy 是 OnFailure 或 Always,则失败的初始化容器将一直重试,直到成功。如果 restartPolicy 是 Never,则不会重试失败的初始化容器。一旦初始化容器成功运行,不管 restartPolicy 如何设置,都不会再次重启。

初始化容器使用 Linux Namespace,因此相对于应用程序容器来说,它具有不同的文件系统视图。这使得初始化容器可以具有访问 Secret 等资源的权限,而应用程序容器则不能。如果主容器需要一些文件,但是由于安全性或数据冗余的原因,不希望将这些文件加载到主容器内部,可以将这个任务交给初始化容器。初始化容器可以在 Pod 启动之前准备必需的数据,并在完成后退出,从而确保主容器在启动时可以访问所需的资源,而不会包含不必要的数据或文件。这提高了容器的安全性、可维护性和可用性。

钩子函数启动完后才会把pod置为running状态,而主容器的进程什么时候启动,钩子函数是无法确定的,所以有些需要在主进程启动之前运行,则需要初始化容器。在一个 Pod 中,所有容器共享相同的网络命名空间和数据卷挂载点,这意味着初始化容器可以在其运行期间创建或修改数据,并且这些数据可以在主容器中访问到。

Kubernetes初始化容器最常见的几个应用场景:
数据库迁移:在进行数据库迁移时,初始化容器可以用来执行一些预操作,例如准备数据库环境、数据备份等。
资源初始化:在Pod启动之前,可以通过初始化容器来创建一些共享的资源,例如配置文件、证书、密钥等。
应用程序初始化:在部署应用程序时,可以使用初始化容器来执行应用程序所需的一些初始化操作,例如安装依赖、初始化数据库连接等。
代码编译:在部署应用程序时,可以使用初始化容器来执行应用程序的代码编译操作,以确保应用程序能够正常运行。
系统初始化:在部署系统时,可以使用初始化容器来执行一些系统初始化操作,例如配置文件的准备、安装系统组件、初始化系统状态等。

示例:解决服务之间的依赖问题
比如我们有一个web服务,该服务又依赖于另外一个数据库服务,但是在我们启动这个web服务的时候我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内web服务连接数据库异常。要解决这个问题的话我们就可以在web服务的pod中使用一个initContainer,让这个初始化容器去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们的主容器web服务被启动起来,这个时候去连接数据库就不会有问题了。
做初始化配置:比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。

[root@localhost home]# vim init_pod.yaml
apiVersion: v1		#必选,版本号,例如v1
kind: Pod			#必选,资源类型,例如 Pod
metadata:			#必选,元数据
  name: init-demo	#必选,Pod名称
  labels:
    demo: init-demo
spec:				#必选,Pod中容器的详细定义
  containers:		#必选,Pod中容器列表
  - name: init-demo		#必选,容器名称
    image: busybox		#必选,容器的镜像名称
    imagePullPolicy: IfNotPresent		#获取镜像的策略 
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']		#容器的启动命令列表,如不指定,使用打包时使用的启动命令。跟docker cmd命令同理。输出一段话The app is running,然后就开始休眠6分钟,这6分钟之内这个容器不会退出
  initContainers:			#init容器,如果启动容器需要指定init
  - name: init-demo1		#初始化容器的名字
    image: busybox			#使用的镜像
    #until当条件为真退出循环,nslookup 容器名:对这个容器解析,解析成功退出循环;输出并循环; sleep 2 休眠两秒
    command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice;sleep 2; done;']
  - name: init-demo2		#初始化容器的名字
    image: busybox			#使用的镜像
    command: ['sh', '-c', 'until nslookup mysql; do echo waiting for mysql; sleep 2;done;']
---
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  ports:
  - port: 1234
    targetPort: 4567
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
  - port: 4321
    targetPort: 7654
    protocol: TCP

[root@localhost home]# kubectl create -f init_pod.yaml		#create创建,-f从哪个文件指定

查看pod状态

[root@master ~]# kubectl get pods
NAME                                      READY   STATUS      RESTARTS   AGE
init-demo                                 0/1     Init:0/2    0          6s
#READY 0/1:需要1个就绪,0代表没就绪
#STATUS	Init:0/2:当前的状态,一共有2个init容器,现在初始化了0个

[root@master ~]# kubectl get pods
NAME                                      READY   STATUS      RESTARTS   AGE
init-demo                                 1/1     Running     0          4m24s

[root@master ~]# kubectl get service
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        13d
myservice    ClusterIP   10.108.202.229   <none>        1234/TCP       4m40s
mysql        ClusterIP   10.110.222.227   <none>        4321/TCP       4m39s

[root@master ~]# kubectl logs init-demo
the app is running1

钩子函数Pod Hook

初始化容器运行完后,就会启动主容器。kubernetes在主容器的启动之后和停止之前提供了两个钩子函数:

  • PostStart:这个钩子在容器创建后立即执行。由于是异步执行,并不能保证钩子将在容器 ENTRYPOINT 之前运行,因为没有参数传递给处理程序。主要用于资源部署、环境准备等。不过需要注意的是如果钩子花费太长时间以至于不能运行或者挂起,容器将不能达到 running 状态,只有启动钩子函数执行完后Pod才会变为running状态。
  • PreStop:这个钩子在容器终止之前立即被调用。它是阻塞的,意味着它是同步的,所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。如果钩子在执行期间挂起,Pod 阶段将停留在 running 状态并且永不会达到 failed 状态。

钩子函数能够感知自身生命周期中的事件,并在相应的时刻到来时运行用户指定的程序代码,用于容器创建后或停止前执行某些操作。Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中。我们可以同时为 Pod 中的所有容器都配置 hook。如果 PostStart 或者 PreStop 钩子失败, 它会杀死容器。
在这里插入图片描述

钩子处理器支持使用下面三种方式定义动作:exec、tcpSocket、httpGet
exec回调:用于执行一段特定的命令,不过要注意的是该命令消耗的资源会被计入容器

……
  lifecycle:
    postStart或preStop: 
      exec:
        command:
        - cat
        - /tmp/healthy
……

tcpSocke回调:在当前容器尝试访问指定的socket

……      
  lifecycle:
    postStart或preStop:
      tcpSocket:
        port: 8080
……

httpGet回调:在当前容器中向某url发起http请求

……
  lifecycle:
    postStart或preStop:
      httpGet:
        path: / 	#请求的URI地址
        port: 80 	#端口号
        host: 192.168.5.3 	#请求的ip地址或域名
        scheme: HTTP 		#支持的协议,http或者https
……

定义一个 Nginx Pod,其中设置了 PostStart 钩子函数,即在容器创建成功后,写入一句话到 /usr/share/message 文件中

vim pod-poststart.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-hook-exec
  namespace: dev
spec:
  containers:
  - name: main-container
    image: nginx:1.17.1
    ports:
    - name: nginx-port
      containerPort: 80
    lifecycle:
      postStart: 
        exec: # 在容器启动的时候执行一个命令,修改掉nginx的默认首页内容
          command: ["/bin/sh", "-c", "echo postStart... > /usr/share/nginx/html/index.html"]
      preStop:
        exec: # 在容器停止之前停止nginx服务,优雅退出
          command: ["/usr/sbin/nginx","-s","quit"]

创建pod

[root@k8s-master01 ~]# kubectl create -f pod-hook-exec.yaml
pod/pod-hook-exec created

查看pod

[root@k8s-master01 ~]# kubectl get pods pod-hook-exec -n dev -o wide
NAME           READY   STATUS     RESTARTS   AGE    IP            NODE    
pod-hook-exec  1/1     Running    0          29s    10.244.2.48   node2   

访问pod

[root@k8s-master01 ~]# curl 10.244.2.48
postStart...

在容器删除之前会执行 preStop 里面的优雅关闭命令,允许 pod 在退出前执行一段脚本用以清除必要的资源等,这个用法在滚动更新时保证我们的应用零宕机非常有用

kubectl delete pod pod-hook-exec

当用户请求删除含有 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 被强制删除时, APIServer 不会等待来自 Pod 所在节点上的 kubelet 的确认信息:pod 已经被终止。在 API 里 pod 会被立刻删除,在节点上, pods 被设置成立刻终止后,在强行杀掉前还会有一个很小的宽限期。
注意:这个 --force --grace-period=0命令能不用就不用,这个可能不会删除一些依赖信息,最好还是让系统删除,进行一些垃圾回收工作;

pod的健康检查

当创建一个 Pod 时,Kubernetes 会首先创建一个名为 pause 的容器,然后在创建初始化容器。初始化容器运行完成后,就开始启动正式容器。在正式容器刚刚创建成功之后,就会触发 PostStart 事件。而在整个容器持续运行的过程中,可以设置存活探针(liveness probe)和就绪探针(readiness probe)来持续检查容器的健康状况。在容器即将终止之前,会触发 PreStop 事件。由于初始化容器在执行完毕后会退出,所以在初始化容器中执行探测是不合适的。

为什么需要健康检查

比如在生产环境中某个业务配置了 CPU 限制,业务运行一段时间后,由于业务应用没有做好垃圾回收甚至产生死锁的情况,导致它的 CPU 占用率一直处在限制值附近。此时,虽然 Pod 的业务进程仍在运行,但实际上它已经无法得到更多的 CPU 时间片了。此时它处理业务请求会非常缓慢,甚至会完全不可用。

除了因为资源不足导致业务不可用以外,还有一类典型的场景:在进行水平扩容时,由于新创建的 Pod 的业务进程需要的启动时间较长(例如对于大型的 Java 应用,往往需要数分钟的启动时间),如果在业务启动时就将 Pod 标记为 Ready 状态并接收外部请求流量,将会有部分请求得到错误的返回,如下图所示
在这里插入图片描述
这两种情况都引出了一个非常有意思的场景:因为业务进程处于运行状态,Pod 和容器也处于 Running 状态,所以 Kubemetes会认为当前 Pod 是就绪的。但实际上,业务可能正处于启动中或者出现资源不足的情况,暂时无法对外提供服务。这两个例子告诉我们,在一般情况下,Pod 就绪(Ready)不等于业务健康。所以这时候我们就需要用到 Kubemetes探针,才能让 Kubemetes 感知到业务真实的健康状态。

Kubemetes的健康检查类型一共有三种,分别是:

  • liveness probe:存活探针,用于检测应用实例当前是否处于正常运行状态,如果存活探针返回Failure,kubelet会终止容器并遵循其重启策略来确定是否需要重启。如果没有定义存活探针检测,kubelet认为只要容器未终止即为健康。
  • readiness probe:就绪探针,用于检测容器是否已准备好为请求提供服务。如果就绪探针返回Failure,Endpoint控制器会从Service 的 Endpoints 列表中移除此pod的ip地址,这样我们的流量就不会被路由到这个 Pod 里面来了。只有当 Pod 中的容器都处于就绪状态的时候 kubelet 才会认定该 Pod 处于就绪状态,因为一个 Pod 下面可能会有多个容器。如果没有定义就绪探针检测,只要容器未终止,即为就绪。
  • startupProbe:启动探针,用于检测容器刚运行时,是否启动成功。作用是让存活检查和就绪检查的开始探测时间延后,只有startrup探针正常结束退出后,存活探针和就绪探针才开始启动,通常用于避免业务进程启动慢导致存活检查失败而被无限重启。如果启动探测失败,kubelet将杀死容器,容器服从其重启策略进行重启。如果容器没有提供启动探测,则默认状态为成功Success。

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

探针是由kubelet对容器执行的定期诊断,目前三种探针都支持如下支持三种探测方式:

  • ExecAction:在容器内执行指定命令。如果命令退出时返回码为0则认为诊断成功
  • TCPSocketAction:对指定端口上的容器的IP地址进行TCP检查。如果端口正常连接,则诊断被认为是成功
  • HTTPGetAction:对指定的端口和路径上的容器的IP地址执行HTTP Get请求。如果响应的状态码大于等于200且小于400,则诊断被认为是成功的。推荐使用HTTP探针进行健康检查,因为它比简单的端口检测更精确。在某些情况下,例如NGINX服务,只监测80端口是否打开并不能确保所提供的页面能够正常访问。通过使用HTTP探针,我们可以发送实际的HTTP请求并检查响应的状态码,从而确认服务的可用性。这样可以更可靠地确保服务能够正确地处理请求并提供有效的响应。

每次探测都将获得以下三种结果之一:

  • Success:容器通过了诊断
  • Failure:容器没有通过诊断
  • Unknown:诊断失败,不会采取任何措施

还有很多配置字段,可以使用这些字段精确的控制存活和就绪检测的行为:

  • initialDelaySeconds:容器启动后延迟 n 秒钟再进行第一次探针检查,默认是 0 秒,最小值是 0。
  • periodSeconds:循环探测的时间间隔,每 n 秒钟轮询检测 1 次,默认是 10 秒。最小值是 1。
  • timeoutSeconds:探测的超时后等待多少秒。默认值是 1 秒。最小值是 1。
  • successThreshold:探测器在失败后,被视为成功的最小连续成功数。默认值是 1。 存活和启动探测的这个值必须是 1。最小值是 1。
  • failureThreshold:当探测失败时,Kubernetes 的重试次数。 存活探测情况下的放弃就意味着重新启动容器。 就绪探测情况下的放弃 Pod 会被打上未就绪的标签。默认值是 3。最小值是 1。
$ kubectl explain pod.spec.containers.livenessProbe
FIELDS:
   exec 		<Object>  	#命令式探针				
   tcpSocket    <Object>	#tcp socket类型的探针
   httpGet      <Object>	#http get类型的探针
   initialDelaySeconds  <integer>  # 容器启动后等待多少秒执行第一次探测
   timeoutSeconds       <integer>  # 探测超时时间。默认1秒,最小1秒
   periodSeconds        <integer>  # 执行探测的频率。默认是10秒,最小1秒
   failureThreshold     <integer>  # 连续探测失败多少次才被认定为失败。默认是3。最小值是1
   successThreshold     <integer>  # 连续探测成功多少次才被认定为成功。默认是1

启动探针startupProbe

使用启动探测器用于保护慢启动容器,避免业务进程启动慢导致存活检查失败而被无限重启,只有startrup探针正常结束退出后,存活探针和就绪探针才开始启动。如果启动探测失败,kubelet将杀死容器,容器服从其重启策略进行重启。
对于一些大型的 Java 应用来说,往往需要数分钟时间才能够完成启动。如果将livenessProbe中把延迟初始时间 initialDelaySeconds 字段的值从 10 秒钟修改到 120 秒,这当然是可以的,但如果启动时间不是 120 秒,或者你不太确定呢?那似乎可以在修改了 initialDelaySeconds 的基础上再将失败次数 failureThreshold 或者探测间隔 periodSeconds 加大。这种做法虽然能解决问题,但缺点也是明显的。为了兼容应用启动慢的问题,我们主动降低了 K8s 检测 Pod 健康状态的频率,这会延迟 K8s 感知故障的速度。另外,因为 livenessProbe 在启动过程还没运行,因此 Pod 得不到反馈,Events 看不到内容,如果 initialDelaySeconds 是 10 分钟,那这 10 分钟内你不知道在发生什么。
增加 livenessProbe 的失败次数。即 failureThreshold*periodSeconds 的乘积足够大,这样可以挺过容器启动,虽然简单粗暴,但是在容器在初次成功启动后,由于livenessProbe 的失败次数过大,就算容器死锁或以其他方式挂起,livenessProbe 也会不断探测到,这样就失去了存活检查的意义。livenessProbe的设计是为了在 Pod 启动成功后进行健康探测,最好前提是 Pod 已经启动成功,在启动阶段的多次失败是没有意义的。
其实,Readiness 和 Liveness 主要用来检查的是处于运行过程中业务,因为启动过慢的问题而去调整 Readiness 和 Liveness 参数并不是最佳实践。这类问题我们可以通过 StartupProbe 来解决。

startupProbe首先检测,该应用程序最多有5分钟(30 * 10 = 300s)完成启动。一旦startupProbe成功一次,livenessProbe将接管,以对后续运行过程中容器死锁提供快速响应。如果startupProbe从未成功,则容器将在300秒后被杀死。

startupProbe:
  httpGet:
    path: /test
    port: liveness-port
  failureThreshold: 30
  periodSeconds: 10

慢启动容器:指需要大量时间(一到几分钟)启动的容器。启动缓慢的原因可能有多种:
长时间的数据初始化:只有第一次启动会花费很多时间。
负载很高:每次启动都花费很多时间。
节点资源不足/过载:即容器启动时间取决于外部因素。

在这里插入图片描述
Pod 启动时,三种探针的执行顺序主要可以分成三个阶段。
第一阶段:Pod 已启动,容器已启动,业务进程正在启动中,此时 StartupProbe 开始工作,由于 StartupProbe 还未成功,当前 Pod 的容器处于 Not Ready(0/1) 的状态。
第二阶段:随着时间推移,StartupProbe 探针成功,Readiness 和 Liveness 探针开始并行工作,此时由于 Readiness 探针还未成功,当前 Pod 的容器仍然处于 Not Ready(0/1) 的状态。
第三阶段:随着 Readiness 和 Liveness 的探测成功,当前 Pod 的容器转为 Ready(1/1) 的状态,Service EndPoints 将 Pod 加入到列表当中,Pod 开始接收外部请求。

存活探针LivenessProbe

存活性探测是隶属于容器级别的配置,可以用于检测应用实例当前是否处于正常运行状态,如果存活性探测返回失败,kubelet会根据容器的重启策略决定是否需要重启该容器。如果没有定义存活性探测,kubelet会默认认为容器一直处于运行状态。需要注意的是,存活性探测的失败只会触发容器的重启,而不会影响整个 Pod 的运行状态。只有当所有容器都失败并且根据重启策略不再重启时,整个 Pod 才会终止。

在这里插入图片描述
在这里插入图片描述

Exec探测

exec 类型的探测通过在目标容器中执行由用户定义的命令来判断容器的健康状态。如果指定的命令返回状态码为 0,则表示探测成功。如果返回值非 0 则为探测失败。

可以通过在 PodSpec 中的 livenessProbe 字段中的 exec.command 字段中指定要执行的命令

……
  livenessProbe:
    exec:
      command:
      - cat
      - /tmp/healthy
……

创建pod-liveness-exec.yaml

[root@k8s-master01 ~]# vim pod-liveness-exec.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-liveness-exec
  namespace: dev
spec:
  containers:
  - name: nginx
    image: nginx:1.17.1
    ports: 
    - name: nginx-port
      containerPort: 80
    livenessProbe:
      exec:
        command: ["/bin/cat","/tmp/hello.txt"] 		# 执行一个查看文件的命令
      initialDelaySeconds: 10	#在容器启动之后,延迟 10 秒钟再进行第一次探针检查。
      failureThreshold: 5		#如果连续 5 次失败则代表失败
      periodSeconds: 10			#探针每 10 秒钟轮询检测 1 次
      successThreshold: 1		#只要探针成功 1 次就代表探针成功了
      timeoutSeconds: 5			#探测超时时间,默认为1秒

[root@master ~]# kubectl apply -f pod-liveness-exec.yaml
pod/pod-liveness-exec created

查看Pod详情

[root@master ~]#  kubectl describe pods pod-liveness-exec -n dev
......
Containers:
    Liveness:       exec [/bin/cat /tmp/hello.txt] delay=10s timeout=5s period=10s #success=1 #failure=5
......
Events:
  Type     Reason     Age               From               Message
  ----     ------     ----              ----               -------
  Normal   Scheduled  55s               default-scheduler  Successfully assigned dev/pod-liveness-exec to node2
  Normal   Pulling    54s               kubelet            Pulling image "nginx:1.17.1"
  Normal   Pulled     24s               kubelet            Successfully pulled image "nginx:1.17.1" in 29.591347248s
  Normal   Created    24s               kubelet            Created container nginx
  Normal   Started    24s               kubelet            Started container nginx
  Warning  Unhealthy  5s (x2 over 15s)  kubelet            Liveness probe failed: /bin/cat: /tmp/hello.txt: No such file or directory

观察上面的信息就会发现nginx容器启动之后就进行了健康检查。检查失败之后,容器被kill掉,然后尝试进行重启(这是重启策略的作用)。稍等一会之后,再观察pod信息,就可以看到RESTARTS不再是0,而是一直增长

[root@master ~]# kubectl get pods pod-liveness-exec -n dev
NAME                READY   STATUS    RESTARTS      AGE
pod-liveness-exec   1/1     Running   3 (30s ago)   2m30s

创建文件/tmp/hello.txt

[root@master ~]# kubectl exec -it pod-liveness-exec -n dev -- touch /tmp/hello.txt
[root@master ~]# kubectl get pods pod-liveness-exec -n dev
NAME                READY   STATUS    RESTARTS      AGE
pod-liveness-exec   1/1     Running   5 (75s ago)   4m15s
TCPSocket探测

对指定端口上的容器的IP地址进行TCP检查。如果端口正常连接,则诊断被认为是成功

……      
  livenessProbe或readinessProb:
    tcpSocket:
      port: 8080
……

创建pod-liveness-tcpsocket.yaml

[root@master ~]# vim pod-liveness-tcpsocket.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-liveness-tcpsocket
  namespace: dev
spec:
  containers:
  - name: nginx
    image: nginx:1.17.1
    ports: 
    - name: nginx-port
      containerPort: 80
    livenessProbe:
      tcpSocket:
        port: 8080 		# 尝试访问8080端口

[root@master ~]# kubectl create -f pod-liveness-tcpsocket.yaml
pod/pod-liveness-tcpsocket created

查看Pod详情

[root@k8s-master01 ~]# kubectl describe pods pod-liveness-tcpsocket -n dev
......
Containers:
    Liveness:       tcp-socket :8080 delay=0s timeout=1s period=10s #success=1 #failure=3
......
Events:
  Type     Reason     Age   From               Message
  ----     ------     ----  ----               -------
  Normal   Scheduled  30s   default-scheduler  Successfully assigned dev/pod-liveness-tcpsocket to node1
  Normal   Pulling    29s   kubelet            Pulling image "nginx:1.17.1"
  Normal   Pulled     1s    kubelet            Successfully pulled image "nginx:1.17.1" in 27.352743729s
  Normal   Created    1s    kubelet            Created container nginx
  Normal   Started    1s    kubelet            Started container nginx
  Warning  Unhealthy  0s    kubelet            Liveness probe failed: dial tcp 10.244.1.3:8080: connect: connection refused

观察上面的信息就会发现nginx容器启动之后就进行了健康检查。检查失败之后,容器被kill掉,然后尝试进行重启(这是重启策略的作用)。稍等一会之后,再观察pod信息,就可以看到RESTARTS不再是0,而是一直增长

[root@master ~]# kubectl get pods pod-liveness-tcpsocket -n dev 
NAME                     READY   STATUS             RESTARTS      AGE
pod-liveness-tcpsocket   0/1     CrashLoopBackOff   4 (43s ago)   3m14s
HTTPGet探测

对容器IP地址的指定端口和路径执行HttpGet请求,如果返回的状态码在200和400之间,则认为程序正常,否则不正常

……
  livenessProbe或readinessProb:
    httpGet:
      path: / 	#URI地址
      port: 80 	#访问的容器的端口名字或者端口号。端口号必须介于1和65535之间
      host: 127.0.0.1 	#连接的主机名,默认连接到pod的IP
      scheme: HTTP 		#支持的协议,HTTP或者HTTPS,默认HTTP
……

创建pod-liveness-httpget.yaml

[root@master ~]# vim pod-liveness-httpget.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-liveness-httpget
  namespace: dev
spec:
  containers:
  - name: nginx
    image: nginx:1.17.1
    ports:
    - name: nginx-port
      containerPort: 80
    livenessProbe:
      httpGet: 	 	 #其实就是访问http://127.0.0.1:80/hello  
        scheme: HTTP #支持的协议,http或者https
        port: 80 	 #端口号
        path: /healthy #URI地址
      initialDelaySeconds: 10	#在容器启动之后,延迟 10 秒钟再进行第一次探针检查。
      failureThreshold: 5		#如果连续 5 次探针失败则代表 Readiness 探针失败,Pod 状态为 NotReady,此时 Pod 不会接收外部请求
      periodSeconds: 10			#探针每 10 秒钟轮询检测 1 次
      successThreshold: 1		#只要探针成功 1 次就代表探针成功了,Pod 状态为 Ready 表示可以接收外部请求
      timeoutSeconds: 5			#探测超时时间,默认为1秒

创建Pod

[root@master ~]# kubectl create -f pod-liveness-httpget.yaml
pod/pod-liveness-httpget created

查看Pod详情

[root@master ~]# kubectl describe pod pod-liveness-httpget -n dev
......
Containers:
    Liveness:       http-get http://:80/healthy delay=10s timeout=5s period=10s #success=1 #failure=5
......
Events:
  Type     Reason     Age               From               Message
  ----     ------     ----              ----               -------
  Normal   Scheduled  45s               default-scheduler  Successfully assigned dev/pod-liveness-httpget to node1
  Normal   Pulled     45s               kubelet            Container image "nginx:1.17.1" already present on machine
  Normal   Created    45s               kubelet            Created container nginx
  Normal   Started    45s               kubelet            Started container nginx
  Warning  Unhealthy  6s (x3 over 26s)  kubelet            Liveness probe failed: HTTP probe failed with statuscode: 404

观察上面信息,尝试访问路径出现404错误。稍等一会之后,再观察pod信息,就可以看到RESTARTS不再是0,而是一直增长

[root@master ~]# kubectl get pod pod-liveness-httpget -n dev
NAME                   READY   STATUS             RESTARTS     AGE
pod-liveness-httpget   0/1     CrashLoopBackOff   3 (5s ago)   3m56s

探针请求的完整路径为:http://PodIP:80/healthy,相当于以 Get 的方式主动访问业务接口,当接口返回 200-399 状态码时,则视为本次探针请求成功。此时给容器中创建一个healthy文件,让Readiness Probe成功,则容器会处于Ready状态。

[root@master ~]# kubectl get pod pod-liveness-httpget -n dev
NAME                   READY   STATUS             RESTARTS     AGE
pod-liveness-httpget   0/1     CrashLoopBackOff   3 (5s ago)   3m56s
[root@master ~]# kubectl exec pod-liveness-httpget -n dev -- /bin/bash -c "echo hello > /usr/share/nginx/html/healthy"
[root@master ~]# kubectl get pod -n dev
NAME                     READY   STATUS             RESTARTS        AGE
pod-liveness-httpget     1/1     Running            4 (73s ago)     5m13s

就绪检测探针

就绪探针,用于检测容器是否已准备好为请求提供服务。如果就绪探针返回Failure,Endpoint控制器会从Service 的 Endpoints 列表中移除此pod的ip地址,这样我们的流量就不会被路由到这个 Pod 里面来了。只有当 Pod 中的容器都处于就绪状态的时候 kubelet 才会认定该 Pod 处于就绪状态,因为一个 Pod 下面可能会有多个容器。如果没有定义就绪探针检测,只要容器未终止,即为就绪。

在这里插入图片描述
在这里插入图片描述

HTTP GET

Readiness Probe向Pod发送HTTP请求,当Probe(探头,Kubelet中创建的HTTP客户端)收到2xx或3xx返回时,说明Pod已经就绪

[root@master ~]# vim readiness-httpget-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: readiness-httpget-pod
  namespace: dev		#这个pod所在名称空间
  labels:
    app: nginx
spec:
  containers:
  - name: readiness-httpget-container
    image: nginx:1.17.1	
    imagePullPolicy: IfNotPresent	#镜像下载策略,默认策略是从远程下载,IfNotPresent代表如果本地有就不会在远程下载
    readinessProbe:			#就绪检测
      httpGet:				#检测方案
        port: 80				#检测端口
        path: /index1.html		#检测的路径
      initialDelaySeconds: 10	#在容器启动之后,延迟 10 秒钟再进行第一次探针检查。
      failureThreshold: 5		#如果连续 5 次探针失败则代表 Readiness 探针失败,Pod 状态为 NotReady,此时 Pod 不会接收外部请求
      periodSeconds: 10			#探针每 10 秒钟轮询检测 1 次
      successThreshold: 1		#只要探针成功 1 次就代表探针成功了,Pod 状态为 Ready 表示可以接收外部请求
      timeoutSeconds: 5			#探测超时时间,默认为1秒

在 Pod 启动之后,如果在 60 秒内(initialDelaySeconds + failureThreshold * periodSeconds) 不能通过健康检查, Pod 将处于非就绪状态。当 Pod 处于正常的运行状态时,如果业务突然产生故障,健康检查会在 50 秒内(failureThreshold * periodSeconds) 识别出业务的不健康状态;当 Pod 业务恢复健康时,健康检查会在 10 秒内(successThreshold * periodSeconds) 识别出业务已恢复。

创建pod发现READY这一列的值为0/1,表示容器没有Ready。因为nginx镜像不包含/index1.html这个文件。

[root@master ~]# kubectl apply -f readiness-httpget-pod.yaml 
pod/readiness-httpget-pod created
[root@master ~]# kubectl get pod -n dev
NAME                    READY   STATUS    RESTARTS   AGE
readiness-httpget-pod   0/1     Running   0          2m5s

创建Service

[root@master ~]# vim readiness-httpget-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx 
  namespace: dev     
spec:
  selector:         
    app: nginx
  ports:
  - name: service-httpget
    targetPort: 80  
    port: 8080      
    protocol: TCP   
  type: ClusterIP

[root@master ~]# kubectl apply -f readiness-httpget-svc.yaml 
service/nginx created

查看Service,发现Endpoints一行的值为空,表示没有Endpoints

[root@master ~]# kubectl get svc -n dev
NAME    TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
nginx   ClusterIP   10.96.22.44   <none>        8080/TCP   22s
[root@master ~]# kubectl describe svc -n dev nginx
Name:              nginx
......
Endpoints:        
......
[root@master ~]# kubectl get ep -n dev
NAME    ENDPOINTS   AGE
nginx               113s

原因是就绪检测指定的path路径不存在,解决:创建路径

[root@master ~]# kubectl describe pod readiness-httpget-pod -n dev
Containers:
……
    Readiness:      http-get http://:80/index1.html delay=10s timeout=5s period=10s #success=1 #failure=5
……
Events:
  Type     Reason     Age   From               Message
  ----     ------     ----  ----               -------
  Normal   Scheduled  4s    default-scheduler  Successfully assigned dev/readiness-httpget-pod to node2
  Normal   Pulled     3s    kubelet            Container image "nginx:1.17.1" already present on machine
  Normal   Created    3s    kubelet            Created container readiness-httpget-container
  Normal   Started    3s    kubelet            Started container readiness-httpget-container
  Warning  Unhealthy  0s    kubelet            Readiness probe failed: HTTP probe failed with statuscode: 404

探针请求的完整路径为:http://PodIP:80/index1.html,相当于以 Get 的方式主动访问业务接口,当接口返回 200-399 状态码时,则视为本次探针请求成功。此时给容器中创建一个index1.html文件,让Readiness Probe成功,则容器会处于Ready状态。

[root@master ~]# kubectl get pod -n dev
NAME                    READY   STATUS    RESTARTS   AGE
readiness-httpget-pod   0/1     Running   0          10m
[root@master ~]# kubectl exec readiness-httpget-pod -n dev -- /bin/bash -c "echo hello > /usr/share/nginx/html/index1.html"
[root@master ~]# kubectl get pod -n dev
NAME                    READY   STATUS    RESTARTS   AGE
readiness-httpget-pod   1/1     Running   0          11m

再查看Endpoints

[root@master ~]# kubectl get endpoints -n dev
NAME    ENDPOINTS        AGE
nginx   10.244.1.15:80   8m12s
[root@master ~]# kubectl describe svc -n dev nginx
Name:              nginx
......
Endpoints:         10.244.1.15:80
......

[root@master ~]# curl 10.244.1.15/index1.html
hello

此时删除pod容器中的index1.html文件,会发现容器处于Not Ready状态,endpoints里面的Pod地址信息也会删除掉,并且容器重启次数还是0。这说明就绪探针的作用只是会在service负载中去掉非就绪的容器,但是并不会重启容器,所以为了让容器进行重启需要配合存活探针一起使用。

[root@master ~]# kubectl exec readiness-httpget-pod -n dev -- /bin/bash -c "rm -rf /usr/share/nginx/html/index1.html"
[root@master ~]# kubectl get pod -n dev 
NAME                    READY   STATUS    RESTARTS   AGE
readiness-httpget-pod   0/1     Running   0          17m
[root@master ~]# kubectl get endpoints -n dev
NAME    ENDPOINTS   AGE
nginx               13m

注意:就算就绪探针探测连续失败次数超过failureThreshold也不会重启Pod,通过describe pod可以查看检测失败次数。
在这里插入图片描述

TCP Socket

TCP Socket(Kubelet中创建的TCP客户端)尝试与容器指定端口建立TCP连接,如果连接成功建立,说明容器是健康的

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:alpine
        name: container-0
        resources:
          limits:
            cpu: 100m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        readinessProbe:             # readinessProbe
          tcpSocket:                # TCP Socket定义
            port: 80
Exec

Exec即执行具体命令,具体机制是Probe执行容器中的命令(Kubelet调用CRI接口进入容器内部执行命令)并检查命令退出的状态码,如果状态码为0则说明健康,定义方法如下所示。这个探针执行ls /ready命令,如果这个文件存在,则返回0,说明Pod就绪了,否则返回其他状态码。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:alpine
        name: container-0
        resources:
          limits:
            cpu: 100m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        readinessProbe:      # Readiness Probe
          exec:              # 定义 ls /ready 命令
            command:
            - ls
            - /ready

就绪探针+存活探针

[root@master ~]# vim live-read-httpget.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: live-readi-httpgat	# Pod 名称
  namespace: dev			# Pod 所在的命名空间
spec:
  containers:
  - name: live-readi-httpgat-container    # 容器名称
    image: nginx:1.17.1				      # 镜像
    imagePullPolicy: IfNotPresent         # 镜像下载策略
    ports:
    - name: http
      containerPort: 80         # 容器端口
    readinessProbe:             # 就绪探测
      httpGet:                  # 检测方案
        path: /index1.html      # 检测路径
        port: 80                # 检测端口
      initialDelaySeconds: 3    
      periodSeconds: 3          
    livenessProbe:              # 存活探测
      httpGet:                  # 检测方案
        port: http              # 检测协议
        path: /index.html       # 检测路径
      initialDelaySeconds: 3    # 启动后多少秒开始第一次检测
      periodSeconds: 3          # 每次间隔检测时间
      timeoutSeconds: 10        # 探测的超时时间

创建pod

[root@master ~]# kubectl create -f live-read-httpget.yaml 
pod/live-readi-httpgat created

pod虽然是 Running 状态,但是还没 Read

[root@master ~]# kubectl get pod live-readi-httpgat -n dev
NAME                 READY   STATUS    RESTARTS   AGE
live-readi-httpgat   0/1     Running   0          73s

查看pod事件发现就绪探针没有检测到index1.html这个文件

[root@master ~]# kubectl describe pod live-readi-httpgat -n dev 
Events:
  Type     Reason     Age   From               Message
  ----     ------     ----  ----               -------
  Normal   Scheduled  6s    default-scheduler  Successfully assigned dev/live-readi-httpgat to node1
  Normal   Pulled     5s    kubelet            Container image "nginx:1.17.1" already present on machine
  Normal   Created    5s    kubelet            Created container live-readi-httpgat-container
  Normal   Started    5s    kubelet            Started container live-readi-httpgat-container
  Warning  Unhealthy  2s    kubelet            Readiness probe failed: HTTP probe failed with statuscode: 404

[root@master ~]# kubectl exec live-readi-httpgat -n dev -- /bin/bash -c "echo hello > /usr/share/nginx/html/index1.html"
[root@master ~]# kubectl get pod live-readi-httpgat -n dev -o wide
NAME                 READY   STATUS    RESTARTS   AGE    IP               NODE    NOMINATED NODE   READINESS GATES
live-readi-httpgat   1/1     Running   0          9m3s   10.244.166.173   node1   <none>           <none>
[root@master ~]# curl 10.244.166.173/index1.html
hello

删除index.html,这时就会发现restarts为2,liveness检测发现文件不在,然后liveness会把容器干掉,进行重启

[root@master ~]# kubectl exec live-readi-httpgat -n dev -- /bin/sh -c "rm -f /usr/share/nginx/html/index.html"
[root@master ~]# kubectl get pod live-readi-httpgat -n dev
NAME                 READY   STATUS    RESTARTS      AGE
live-readi-httpgat   0/1     Running   1 (15s ago)   21m

就绪探针+存活探针+启动探针

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-probe
spec:
  replicas: 3
  selector:
    matchLabels:
     app: deployment-probe
  template:
    metadata:
      labels:
        app: deployment-probe
    spec:
      containers:
      - name: nginx
        image: nginx:1.17.1
        
        # readinessProbe,就绪探针
        readinessProbe:
          httpGet:
            path: /index.html
            port: nginx
          # 延迟多久后开始探测
          initialDelaySeconds: 10
          # 执行探测频率(秒) 【 每隔秒执行一次 】
          periodSeconds: 10
          #  超时时间
          timeoutSeconds: 1
          # 处于成功状态时,探测连续失败几次可被认为失败。
          failureThreshold: 3
          # 处于失败状态时,探测连续成功几次,被认为成功。
          successThreshold: 1
        
        # livenessProbe,存活探针
        livenessProbe:
          httpGet:
            path: /index.html
            port: nginx
          # 延迟多久后开始探测
          initialDelaySeconds: 10
          # 执行探测频率(秒) 【 每隔秒执行一次 】
          periodSeconds: 10
          #  超时时间
          timeoutSeconds: 1
          # 处于成功状态时,探测连续失败几次可被认为失败。
          failureThreshold: 3
          # 处于失败状态时,探测连续成功几次,被认为成功。
          successThreshold: 1
        
        # startupProbe,启动探针
        startupProbe:
          httpGet:
            path: /index.html
            port: nginx
          # 延迟多久后开始探测
          initialDelaySeconds: 10
          # 执行探测频率(秒) 【 每隔秒执行一次 】
          periodSeconds: 10
          #  超时时间
          timeoutSeconds: 1
          # 处于成功状态时,探测连续失败几次可被认为失败。
          failureThreshold: 3
          # 处于失败状态时,探测连续成功几次,被认为成功。
          successThreshold: 1

crictl pull nginx:1.17.1
kubectl apply -f deployment-probe.yaml
kubectl get pod,deploy

探针启动顺序

创建一个pod来演示各探针执行顺序

[root@master ~]# vim test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-hook-exec
spec:
  replicas: 1
  selector:
    matchLabels:
     app: pod-hook-exec
  template:
    metadata:
      labels:
        app: pod-hook-exec
    spec:
      terminationGracePeriodSeconds: 5 # 设置5秒宽限时间,默认是30s
      nodeName: node1 # 为了测试方便,指定调度机器
      initContainers:
      - name: init-containers
        image: busybox
        command: ["sh","-c","echo init-containers...|tee -a /tmp/pod-hook-exec.log;sleep 10s"]
        volumeMounts:
        - name: logs
          mountPath: /tmp
      containers:
      - name: main-container
        image: busybox
        command: ["sh","-c","echo main-container...|tee -a /tmp/pod-hook-exec.log;sleep 3600s"] # 只有这个才会输出到屏幕,也就是通过logs只能查看主容器日志
        volumeMounts:
        - name: logs
          mountPath: /tmp
        startupProbe:
          exec:
            command: ["sh","-c","echo startupProbe...|tee -a /tmp/pod-hook-exec.log;sleep 5s"]
          timeoutSeconds: 10
        livenessProbe:
          exec:
            command: ["sh","-c","echo livenessProbe...|tee -a /tmp/pod-hook-exec.log;sleep 10s"]
          timeoutSeconds: 10
        readinessProbe:
          exec:
            command: ["sh","-c","echo readinessProbe...|tee -a /tmp/pod-hook-exec.log;sleep 5s"]
          timeoutSeconds: 10
        lifecycle:
          postStart:
            exec: #在容器启动的时候执行一个命令
              command: ["sh","-c","echo postStart...|tee -a /tmp/pod-hook-exec.log;sleep 5s"]
          preStop: # 在pod停止之前执行
            exec:
              command: ["sh","-c","echo preStop...|tee -a /tmp/pod-hook-exec.log"]
      volumes:
      - name: logs #和上面保持一致 这是本地的文件路径,上面是容器内部的路径
        hostPath:
          path: /tmp

创建pod,并观察pod状态

kubectl apply -f test.yaml ;kubectl get pods -w |grep pod-hook-exec

在这里插入图片描述
从下图的日志就可看出,被分为5个执行阶段,其实严格来讲是有6个阶段,执行的先后顺序:initContainers →【main-container、postStart】→ startupProbe → readinessProbe → livenessProbe → preStop
注意:main-container 和 postStart 是同时执行,虽然readinessProbe 和 livenessProbe 也是同时执行,但是它们第一次执行不是真正的并行执行,也有先后顺序的,后面就是并行执行了。
在这里插入图片描述

pod安全上下文

kubernetes为安全运行pod及容器运行设计了安全上下文机制,该机制允许用户和管理员定义pod或容器的特权与访问控制,已配置容器与主机以及主机之上的其它容器间的隔离级别。安全上下文就是一组用来决定容器时如何创建和运行的约束条件,这些条件代表创建和运行容器时使用的运行时参数。需要提升容器权限时,用户通常只应授予容器执行其工作所需的访问权限,以最小权限法则来抑制容器对基础架构及其它容器产生的负面影响。

kubernetes支持用户在pod及容器级别配置安全上下文,并允许管理员通过pod安全策略在集群全局级别限制用户在创建和运行pod时可设定安全上下文。

安全上下文(Security Context)定义 Pod 或 Container 的特权与访问控制设置。 安全上下文包括但不限于:

自主访问控制(DAC):传统UNIX的访问控制机制,它允许对象的所有者基于UUID和GID设定对象的访问权限。
Linux功能:Linux为突破系统上传统的两级用户(root和普通用户)授权模型,而将内核管理权限打散成多个不同维度或级别的权限子集,每个子集称为一种功能或能力,例如CAP_NET_ADMIN、CAP_SYS_TIME、CAP_SYS_PTRACE和CAP_SYS_ADMIN等,从而允许进程仅具有一部分内核管理功能就能完成必要的管理任务。
seccomp:全称为secure computing mode,是linux内核的安全模型,用于为默认可发起的任何系统调用进程施加控制机制,人为地禁止它能够发起的系统调用,有效降低了程序被劫持的危害级别。
AppArmor:全称为Application Armor,意为应用盔甲,是linux内核的一个安全模块,通过加载带内核的配置文件来定义对程序的约束与控制。
SELinux:全称为Security-Enhanced Linux,意为安全加强的Linux,是linux内核的一个安全模块,提供了包括强制访问控制在内的访问控制安全策略机制。
Privileged模式:即特权模式容器,该模式下容器中的root用户拥有所有的内核功能,即具有真正的管理员权限,它能看到主机上的所有设备,能够挂载文件系统,甚至可以在容器中运行容器;容器默认运行于非特权模式。
AllowPrivilegeEscalation:控制是否允许特权升级,及进程是否能够获取比父进程更多的特权;运行于特权模式或具有CAP_SYS_ADMIN能力的容器默认允许特权升级。

这些安全上下文相关的特性多数嵌套定义在pod或容器的securityContext字段中,而且有些特性对应的嵌套字段还不止一个。而seccomp和AppArmor的安全上下文则需要以资源注解的方式进行定义,而且技能由管理员在集群级别进行pod安全策略配置。

kubernetes默认以非特权模式创建并允许容器,同时禁用了其他与管理功能相关的内核能力,但未额外设定其他上下文参数。

apiVersion: v1
kind: Pod
metadata:
  name: securitycontext-capabilities-demo
  namespace: default
spec:
  securityContext:            #pod级别的安全上下文,对内部多有容器均有效
    runAsUer <integer>        #以指定的用户身份运行容器进程,默认由镜像中的USER指定
    runAsGroup <integer>      #以指定的用户组运行容器进程,默认使用的组随容器运行时设定
    supplementalGroups <[] integer>  #为容器中1号进程的用户添加的附加组
    fsGroup <integer>         #为容器中的1号进程附加一个专用组,其功能类似于sgid
    runAsNonRoot <boolean>    #是否以非root身份运行
    seLinuxOptions <object>   #SELINUX的相关配置
    sysctl <[] object>        #应用到当前pod名称空间级别的sysctl参数设置列表
    windowsOptions <object>   #windows容器专用配置
  containers:
  - name: ...
    image: ...
    securityContext:      #容器级别的安全上下文,仅在当前容器生效
      runAsUer <integer>        #以指定的用户身份运行容器进程,默认由镜像中的USER指定
      runAsGroup <integer>      #以指定的用户组运行容器进程,默认使用的组随容器运行时设定
      runAsNonRoot <boolean>    #是否以非root身份运行
      allowPrivilegeEscalation <boolean> #是否允许特权升级
      capabilities <object>     #为当前容器添加或删除内核能力
        add <[] string>         #添加由列表定义的各项内核能力
        drop <[] string>        #移除由列表定义的各项内核能力
      privileged <boolean>      #是否允许为特权容器
      procMount <string>        #设置容器procMount类型,默认为DefaultProcMount
      readOnlyRootFilesystem <boolean>  #是否将根文件系统设置为只读模式
      seLinuxOptions <object>   #selinux相关配置
      windowsOptions <object>   #windows相关配置

管理容器的内核功能
传统linux仅实现了特权和非特权两类进程,前者是指以0号UID身份运行的进程,而后者则是从属非0号UID用户的进程。linux内核从2.2版本开始将附加在超级用户的权限分割为多个独立单元,这些单元是线程级别的,它们可配置在每个线程之上为其赋予特定的管理能力。linux内核常用的功能包括但不限于如下这些:

CAP_CHOWN:改变文件的UID和GID。
CAP_MKNOD:借助系统调用mknod()创建设备文件。
CAP_NET_ADMIN:网络管理相关的操作,可用于管理网络接口、netfilter上的iptables规则、路由表、透明代理、TOS、清空驱动统计数据、设计混杂模式和启动多播功能等。
CAP_NET_BIND_SERVICE:绑定小于1024的特权端口,但该功能在重新映射用户后可能会失效。
CAP_NET_RAW:使用RAW或PACKET类型的套接字,并可绑定任何地址进行透明代理。
CAP_SYS_ADMIN:支持内核上的很大一部分管理功能。
CAP_SYS_BOOT:重启系统
CAP_SYS_CHROOT:使用chroot()进行根文件系统切换,并能够调用setns()修改Mount名称空间
CAP_SYS_MODULE:转载内核模块
CAP_SYS_TIME:设定系统时钟和硬件时钟
CAP_SYSLOG:调用syslog()执行日志相关的特权操作。

系统管理员可以通过get命令获取程序文件上的内核功能,并可使用setcap为程序文件设定内核功能或取消其已有的内核功能。而kubernetes上运行的进程设定内核功能则需要在pod容器的安全上下文中嵌套capabilities字段,添加和移除内核能力还需要分别在下一级嵌套中使用add或drop字段。这两个字段可接收以内核能力名称为列表项,但引用个内核能力名称时序移除CAP_前缀,例如可使用NET_ADMIN等。

apiVersion: v1
kind: Pod
metadata:
  name: securitycontext-capabilities-demo
  namespace: default
spec:
  containers:
  - name: demo
    image: 192.168.174.120/baseimages/tomcat-app1:v1.0
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        add: ['NET_ADMIN']
        drop: ['CHOWN']

特权模式容器

kubernetes集群中,kube-proxy中的容器就运行在特权模式。

# kubectl get pods calico-node-pbfcs -n kube-system -o yaml
apiVersion: v1
kind: Pod
metadata:
    ...
  - image: 192.168.174.120/baseimages/calico-pod2daemon-flexvol:v3.19.2
    imagePullPolicy: IfNotPresent
    name: flexvol-driver
    resources: {}
    securityContext:
      privileged: true   #运行在特权模式
    ....

在pod中使用sysctl

kubernetes允许在pod中独立安全地设置支持名称空间级别的内核参数,它们默认处于启动状态,而节点级别内核参数则被认为是不安全地,它们默认处于禁用状态。kernel.shm_rmid_force、net.ipv4.ip_local_range和net.ipv4.tcp_sysncookies这3个内核参数被kubernetes视为安全参数,它们在pod安全上下文的sysctl参数内嵌套使用,而余下的绝大多数的内核参数都是非安全参数,需要管理员手动在每个节点上通过kubelet选项逐个启用后才能配置到pod上。例如在各工作节点上编辑/etc/default/kubelet文件,添加以下内容允许在pod上使用指定的两个非安全地内核参数,并重启kubelet服务使之生效。

KUBELET_EXTRA_ARGS='--allowed-unsafe-sysctls=net.core.somaxconn,net.ipv4.ip_unprivileged_port_start'

配置样例

apiVersion: v1
kind: Pod
metadata:
  name: securitycontext-sysctls-demo
  namespace: default
spec:
  securityContext:
    sysctls:
    - name: kernel.shm_rmid_forced
      value: "0"
    - name: net.ipv4.ip_unprivileged_port_start
      value: "0"
  containers:
  - name: demo
    image: 192.168.174.120/baseimages/tomcat-app1:v1.0
    imagePullPolicy: IfNotPresent
    securityContext:
      runAsUser: 1001
      runAsGroup: 1001

为 Pod 设置安全性上下文

要为 Pod 设置安全性设置,可在 Pod 规约中包含 securityContext 字段。securityContext 字段值是一个 PodSecurityContext 对象。你为 Pod 所设置的安全性配置会应用到 Pod 中所有 Container 上。 下面是一个 Pod 的配置文件,该 Pod 定义了 securityContext 和一个 emptyDir 卷:

apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  volumes:
  - name: sec-ctx-vol
    emptyDir: {}
  containers:
  - name: sec-ctx-demo
    image: busybox:1.28
    command: [ "sh", "-c", "sleep 1h" ]
    volumeMounts:
    - name: sec-ctx-vol
      mountPath: /data/demo
    securityContext:
      allowPrivilegeEscalation: false

在配置文件中,runAsUser 字段指定 Pod 中的所有容器内的进程都使用用户 ID 1000 来运行。runAsGroup 字段指定所有容器中的进程都以主组 ID 3000 来运行。 如果忽略此字段,则容器的主组 ID 将是 root(0)。 当 runAsGroup 被设置时,所有创建的文件也会划归用户 1000 和组 3000。 由于 fsGroup 被设置,容器中所有进程也会是附组 ID 2000 的一部分。 卷 /data/demo 及在该卷中创建的任何文件的属主都会是组 ID 2000。

为 Pod 配置卷访问权限和属主变更策略

默认情况下,Kubernetes 在挂载一个卷时,会递归地更改每个卷中的内容的属主和访问权限, 使之与 Pod 的 securityContext 中指定的 fsGroup 匹配。 对于较大的数据卷,检查和变更属主与访问权限可能会花费很长时间,降低 Pod 启动速度。 你可以在 securityContext 中使用 fsGroupChangePolicy 字段来控制 Kubernetes 检查和管理卷属主和访问权限的方式。

fsGroupChangePolicy - fsGroupChangePolicy 定义在卷被暴露给 Pod 内部之前对其 内容的属主和访问许可进行变更的行为。此字段仅适用于那些支持使用 fsGroup 来 控制属主与访问权限的卷类型。此字段的取值可以是:

OnRootMismatch:只有根目录的属主与访问权限与卷所期望的权限不一致时, 才改变其中内容的属主和访问权限。这一设置有助于缩短更改卷的属主与访问 权限所需要的时间。
Always:在挂载卷时总是更改卷中内容的属主和访问权限。

例如:
说明: 此字段对于 secret、 configMap 和 emptydir 这类临时性存储无效。

securityContext:
  runAsUser: 1000
  runAsGroup: 3000
  fsGroup: 2000
  fsGroupChangePolicy: "OnRootMismatch"

将卷权限和所有权更改委派给 CSI 驱动程序

特性状态: Kubernetes v1.26 [stable]
如果你部署了一个容器存储接口 (CSI) 驱动,而该驱动支持 VOLUME_MOUNT_GROUP NodeServiceCapability, 在 securityContext 中指定 fsGroup 来设置文件所有权和权限的过程将由 CSI 驱动而不是 Kubernetes 来执行。在这种情况下,由于 Kubernetes 不执行任何所有权和权限更改, fsGroupChangePolicy 不会生效,并且按照 CSI 的规定,CSI 驱动应该使用所指定的 fsGroup 来挂载卷,从而生成了一个对 fsGroup 可读/可写的卷.

为 Container 设置安全性上下文

若要为 Container 设置安全性配置,可以在 Container 清单中包含 securityContext 字段。securityContext 字段的取值是一个 SecurityContext 对象。你为 Container 设置的安全性配置仅适用于该容器本身,并且所指定的设置在与 Pod 层面设置的内容发生重叠时,会重写 Pod 层面的设置。Container 层面的设置不会影响到 Pod 的卷。

下面是一个 Pod 的配置文件,其中包含一个 Container。Pod 和 Container 都有 securityContext 字段:
apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo-2
spec:
  securityContext:
    runAsUser: 1000
  containers:
  - name: sec-ctx-demo-2
    image: gcr.io/google-samples/node-hello:1.0
    securityContext:
      runAsUser: 2000
      allowPrivilegeEscalation: false

进程以用户 2000 运行。该值是在 Container 的 runAsUser 中设置的。 该设置值重写了 Pod 层面所设置的值 1000。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
2000         1  0.0  0.0   4336   764 ?        Ss   20:36   0:00 /bin/sh -c node server.js
2000         8  0.1  0.5 772124 22604 ?        Sl   20:36   0:00 node server.js

为容器设置 Seccomp 配置

若要为容器设置 Seccomp 配置(Profile),可在你的 Pod 或 Container 清单的 securityContext 节中包含 seccompProfile 字段。该字段是一个 SeccompProfile 对象,包含 type 和 localhostProfile 属性。 type 的合法选项包括 RuntimeDefault、Unconfined 和 Localhost。 localhostProfile 只能在 type: Localhost 配置下才可以设置。 该字段标明节点上预先设定的配置的路径,路径是相对于 kubelet 所配置的 Seccomp 配置路径(使用 --root-dir 设置)而言的。

下面是一个例子,设置容器使用节点上容器运行时的默认配置作为 Seccomp 配置:
...
securityContext:
  seccompProfile:
    type: RuntimeDefault

下面是另一个例子,将 Seccomp 的样板设置为位于 <kubelet-根目录>/seccomp/my-profiles/profile-allow.json 的一个预先配置的文件。

...
securityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: my-profiles/profile-allow.json

为 Container 赋予 SELinux 标签

若要给 Container 设置 SELinux 标签,可以在 Pod 或 Container 清单的 securityContext 节包含 seLinuxOptions 字段。 seLinuxOptions 字段的取值是一个 SELinuxOptions 对象。
下面是一个应用 SELinux 标签的例子:

...
securityContext:
  seLinuxOptions:
    level: "s0:c123,c456"

说明: 要指定 SELinux,需要在宿主操作系统中装载 SELinux 安全性模块。

高效重打 SELinux 卷标签
特性状态: Kubernetes v1.27 [beta]
默认情况下,容器运行时递归地将 SELinux 标签赋予所有 Pod 卷上的所有文件。 为了加快该过程,Kubernetes 使用挂载可选项 -o context=

要使用这项加速功能,必须满足下列条件:

  必须启用 ReadWriteOncePod 和 SELinuxMountReadWriteOncePod 特性门控。
  Pod 必须以 accessModes: ["ReadWriteOncePod"] 模式使用 PersistentVolumeClaim。
  Pod(或其中使用 PersistentVolumeClaim 的所有容器)必须设置 seLinuxOptions。
  对应的 PersistentVolume 必须是:
    使用传统树内(In-Tree) iscsi、rbd 或 fs 卷类型的卷。
    或者是使用 {< glossary_tooltip text="CSI" term_id="csi" >}} 驱动程序的卷 CSI 驱动程序必须能够通过在 CSIDriver 实例中设置 spec.seLinuxMount: true 以支持 -o context 挂载。

对于所有其他卷类型,重打 SELinux 标签的方式有所不同: 容器运行时为卷中的所有节点(文件和目录)递归地修改 SELinux 标签。 卷中的文件和目录越多,重打标签需要耗费的时间就越长。

Pod 的安全上下文适用于 Pod 中的容器,也适用于 Pod 所挂载的卷(如果有的话)。 尤其是,fsGroup 和 seLinuxOptions 按下面的方式应用到挂载卷上:

fsGroup:支持属主管理的卷会被修改,将其属主变更为 fsGroup 所指定的 GID, 并且对该 GID 可写。

seLinuxOptions:支持 SELinux 标签的卷会被重新打标签,以便可被 seLinuxOptions 下所设置的标签访问。通常你只需要设置 level 部分。 该部分设置的是赋予 Pod 中所有容器及卷的 多类别安全性(Multi-Category Security,MCS)标签。

警告: 在为 Pod 设置 MCS 标签之后,所有带有相同标签的 Pod 可以访问该卷。 如果你需要跨 Pod 的保护,你必须为每个 Pod 赋予独特的 MCS 标签。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值