抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Kubernetes 核心概念POD

image-20220525094307190

Pod概述

Pod 是 k8s 系统中可以创建和管理的最小单元,是资源对象模型中由用户创建或部署的最
小资源对象模型,也是在 k8s 上运行容器化应用的资源对象

Pod基本概念

  • 最小部署的单元
  • Pod里面是由一个或多个容器组成【一组容器的集合】
  • 一个pod中的容器是共享网络命名空间
  • Pod是短暂的
  • 每个Pod包含一个或多个紧密相关的用户业务容器

Pod存在的意义

  • 创建容器使用docker,一个docker对应一个容器,一个容器运行一个应用进程

  • Pod是多进程设计,运用多个应用程序,也就是一个Pod里面有多个容器,而一个容器里面运行一个应用程序

image-20220526151140500

Pod 特点

Pod有两个必须知道的特点

网络

​ 每一个Pod都会被指派一个唯一的Ip地址,在Pod中的每一个容器共享网络命名空间,包括Ip地址和网络端口,在同一个Pod中的容器可以同locahost进行互相通信,当Pod中的容器需要与Pod外的实体进行通信时,则需要通过端口等共享的网络资源。

存储

​ Pod能够配置共享存储卷,在Pod中所有的容器能够访问共享存储卷,允许这些容器共享数据,存储卷也允许在一个Pod持久化数据,以防止其中的容器需要被重启。

五种 Pod 共享资源

Pod 是 k8s 最基本的操作单元,包含一个或多个紧密相关的容器。

  • 一个 Pod 可以被一个容器化的环境看作应用层的“逻辑宿主机”;

  • 一个 Pod 中的多个容器应用通常是紧密耦合的,Pod 在 Node 上被创建、启动或者销毁;

  • 每个 Pod 里运行着一个特殊的被称之为 Volume 挂载卷,因此他们之间通信和数据交换更为高效,在设计时我们可以充分利用这一特性将一组密切相关的服务进程放入同一个 Pod 中。

同一个 Pod 里的容器之间仅需通过 localhost 就能互相通信,一个 Pod 中的应用容器共享五种资源

  • PID 命名空间:Pod 中的不同应用程序可以看到其他应用程序的进程 ID。
  • 网络命名空间:Pod 中的多个容器能够访问同一个 IP 和端口范围。
  • IPC 命名空间:Pod 中的多个容器能够使用 SystemV IPC 或 POSIX 消息队列进行通信。
  • UTS 命名空间:Pod 中的多个容器共享一个主机名。
  • Volumes(共享存储卷):Pod 中的各个容器可以访问在 Pod 级别定义的 Volumes。

Pod定义

下面是Pod的资源清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
apiVersion: v1     #必选,版本号,例如v1
kind: Pod   #必选,资源类型,例如 Pod
metadata:   #必选,元数据
name: string #必选,Pod名称
namespace: string #Pod所属的命名空间,默认为"default"
labels:    #自定义标签列表
- name: string  
spec: #必选,Pod中容器的详细定义
containers: #必选,Pod中容器列表
- name: string #必选,容器名称
image: string #必选,容器的镜像名称
imagePullPolicy: [ Always|Never|IfNotPresent ] #获取镜像的策略
command: [string] #容器的启动命令列表,如不指定,使用打包时使用的启动命令
args: [string] #容器的启动命令参数列表
workingDir: string #容器的工作目录
volumeMounts: #挂载到容器内部的存储卷配置
- name: string #引用pod定义的共享存储卷的名称,需用volumes[]部分定义的的卷名
mountPath: string #存储卷在容器内mount的绝对路径,应少于512字符
readOnly: boolean #是否为只读模式
ports: #需要暴露的端口库号列表
- name: string #端口的名称
containerPort: int #容器需要监听的端口号
hostPort: int #容器所在主机需要监听的端口号,默认与Container相同
protocol: string #端口协议,支持TCP和UDP,默认TCP
env: #容器运行前需设置的环境变量列表
- name: string #环境变量名称
value: string #环境变量的值
resources: #资源限制和请求的设置
limits: #资源限制的设置
cpu: string #Cpu的限制,单位为core数,将用于docker run --cpu-shares参数
memory: string #内存限制,单位可以为Mib/Gib,将用于docker run --memory参数
requests: #资源请求的设置
cpu: string #Cpu请求,容器启动的初始可用数量
memory: string #内存请求,容器启动的初始可用数量
lifecycle: #生命周期钩子
postStart: #容器启动后立即执行此钩子,如果执行失败,会根据重启策略进行重启
preStop: #容器终止前执行此钩子,无论结果如何,容器都会终止
livenessProbe: #对Pod内各容器健康检查的设置,当探测无响应几次后将自动重启该容器
exec:   #对Pod容器内检查方式设置为exec方式
command: [string] #exec方式需要制定的命令或脚本
httpGet: #对Pod内个容器健康检查方法设置为HttpGet,需要制定Path、port
path: string
port: number
host: string
scheme: string
HttpHeaders:
- name: string
value: string
tcpSocket: #对Pod内个容器健康检查方式设置为tcpSocket方式
port: number
initialDelaySeconds: 0 #容器启动完成后首次探测的时间,单位为秒
timeoutSeconds: 0    #对容器健康检查探测等待响应的超时时间,单位秒,默认1秒
periodSeconds: 0    #对容器监控检查的定期探测时间设置,单位秒,默认10秒一次
successThreshold: 0
failureThreshold: 0
securityContext:
privileged: false
restartPolicy: [Always | Never | OnFailure] #Pod的重启策略
nodeName: <string> #设置NodeName表示将该Pod调度到指定到名称的node节点上
nodeSelector: obeject #设置NodeSelector表示将该Pod调度到包含这个label的node上
imagePullSecrets: #Pull镜像时使用的secret名称,以key:secretkey格式指定
- name: string
hostNetwork: false #是否使用主机网络模式,默认为false,如果设置为true,表示使用宿主机网络
volumes: #在该pod上定义共享存储卷列表
- name: string #共享存储卷名称 (volumes类型有很多种)
emptyDir: {} #类型为emtyDir的存储卷,与Pod同生命周期的一个临时目录。为空值
hostPath: string #类型为hostPath的存储卷,表示挂载Pod所在宿主机的目录
path: string    #Pod所在宿主机的目录,将被用于同期中mount的目录
secret:    #类型为secret的存储卷,挂载集群与定义的secret对象到容器内部
scretname: string
items:
- key: string
path: string
configMap: #类型为configMap的存储卷,挂载预定义的configMap对象到容器内部
name: string
items:
- key: string
path: string

Pod 使用

使用方式

自主式Pod

​ 这种Pod本身是不能自我修复的,当Pod被创建后(不论是由你直接创建还是被其他Controller),都会被Kuberentes调度到集群的Node上,直到Pod的进程终止、被删掉、因为缺少资源而被驱逐、或者Node故障之前这个Pod都会一直保持在那个Node上,Pod不会自愈。

​ 如果Pod运行的Node故障,或者是调度器本身故障,这个Pod就会被删除,同样的,如果Pod所在Node缺少资源或者Pod处于维护状态,Pod也会被驱逐。

创建资源清单

通过yaml文件或者json描述Pod和其内容器的运行环境和期望状态,例如一个最简单的运行nginx应用的pod,定义如下

1
vi nginx-pod.yml
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.12
ports:
- containerPort: 80
创建POD

使用kubectl命令创建pod,并查看pod的状态

1
2
kubectl apply -f nginx-pod.yml
kubectl get pod -o wide

image-20220526152543499

访问pod

可以通过k8s创建的虚拟IP进行访问,可以在k8s的任何一个节点访问

1
curl 10.244.1.10

image-20220526152607563

删除Pod

可以使用delete删除Pod,删除后不能进行恢复

1
kubectl delete pod nginx

image-20220526152659462

我们发现Pod节点故障后将不会被自动恢复,这种情况下是不安全的,我们需要使用控制器来运行Pod

控制器管理的Pod

​ Kubernetes使用更高级的称为Controller的抽象层,来管理Pod实例,Controller可以创建和管理多个Pod,提供副本管理、滚动升级和集群级别的自愈能力。

​ 例如,如果一个Node故障,Controller就能自动将该节点上的Pod调度到其他健康的Node上。虽然可以直接使用Pod,但是在Kubernetes中通常是使用Controller来管理Pod的

创建资源清单

通过yaml文件或者json描述Pod和其内容器的运行环境和期望状态,例如一个最简单的运行nginx应用的pod,定义如下

1
vi nginx-pod.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.12
ports:
- containerPort: 80
创建Pod
1
2
3
kubectl apply -f nginx-pod.yml

kubectl get pods -o wide

创建后发现,有两个nginx的pod在运行,符合我们的预期

image-20210519174032817

删除Pod

这里可以尝试删除Pod

1
2
3
kubectl delete pod nginx-deployment-f77774fc5-cgs82
# 查看Pod详情
kubectl get pods -o wide

删除Pod后发现重新新建了一个Pod,这是因为有控制器发现少了一个Pod就会进行重新拉起来一个

image-20210520090704613

Pod策略管理

镜像拉取策略

pod的镜像拉取策略分为三种:

  • always(总是从官方下载镜像)
  • never(从不下载镜像)
  • ifnotpresent(如果本地没有镜像就从官方下载镜像)。

Kubernetes集群默认使用IfNotPresent策略

配置示例

我们以下面的Always来举例,看下Pod的镜像拉取策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.12
imagePullPolicy: Always #总是从官方下载镜像
ports:
- containerPort: 80
生效配置

使用如下命令生效配置,并查看K8s在本地拥有镜像的情况下是否重新拉取镜像

1
kubectl apply -f nginx-pod.yml

image-20220526153736556

我们可以通过如下命令来查看k8s创建Pod的过程

1
kubectl describe pod nginx-deployment

我们发现在Node1节点已经存在镜像的情况下重新进行拉取镜像了,其他的两种方式大家可以试一试

image-20220526153940335

重启策略

在Pod中的容器可能会由于异常等原因导致其终止退出,Kubernetes提供了重启策略以重启容器。

​ Pod通过restartPolicy字段指定重启策略,重启策略类型为:Always、OnFailure 和 Never,默认为 Always,重启策略对同一个Pod的所有容器起作用,容器的重启由Node上的kubelet执行。Pod支持三种重启策略,在配置文件中通过restartPolicy字段设置重启策略:

重启策略 说明
Always 当容器失效时,由kubelet自动重启该容器
OnFailure 当容器终止运行且退出码不为0时,由kubelet自动重启该容器
Never 不论容器运行状态如何,kubelet都不会重启该容器

注意:注意:K8S 中不支持重启Pod资源,只有删除重建,这里的重启是指在Pod的宿主Node上进行本地重启,而不是调度到其它Node上

容器重启演示

当Container退出后,Container可能会被重启,那么Container是如何被重启的呢?是kubelet调用类似于docker start的API拉起?还是重新创建一个docker容器?

​ 答案是,kubelet会重新创建一个docker容器,我们给一个例子,创建一个如下的Pod,然后每隔10S返回0

配置资源清单

1
vi nginx-pod.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
restartPolicy: Always
containers:
- name: nginx
image: nginx:1.12
ports:
- containerPort: 80
args: ["/bin/sh","-c"," sleep 10;exit 0"]

我们运行并查看POD的状态变化

1
2
kubectl apply -f nginx-pod.yml
kubectl get pod -o wide -w

我们发现POD在不断的重启

image-20220526184436892

我们登录node1节点查看下节点的容器的情况

1
docker ps|grep nginx

我们发现间隔一段时间执行上面的命令,我们发现容器的ID已经不一样了,说明我们的容器已经被重建了

image-20220526184421571

OnFailure重启测试

上面我们验证了K8s判断一旦容器出现问题就会重启,但是我们使用的是Always策略,下面我们改为OnFailure策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
restartPolicy: OnFailure
containers:
- name: nginx
image: nginx:1.12
ports:
- containerPort: 80
args: ["/bin/sh","-c"," sleep 10;exit 0"]

启动并进行验证

1
2
kubectl apply -f nginx-pod.yml
kubectl get pod -o wide -w

我们发现如果使用OnFailure策略,如果推出了将不会重启

image-20220526185234988

Pod 原理

为什么需要POD

​ 假设 Kubernetes 中调度的基本单元就是容器,对于一个非常简单的应用可以直接被调度直接使用,没有什么问题,但是往往还有很多应用程序是由多个进程组成的,有的同学可能会说把这些进程都打包到一个容器中去不就可以了吗?理论上是可以实现的,但是不要忘记了容器运行时管理的进程是 pid=1 的主进程,其他进程死掉了就会成为僵尸进程,没办法进行管理了,这种方式本身也不是容器推荐的运行方式,一个容器最好只干一件事情,所以在真实的环境中不会使用这种方式。

​ 那么我们就把这个应用的进程进行拆分,拆分成一个一个的容器总可以了吧?但是不要忘记一个问题,拆分成一个一个的容器后,是不是就有可能出现一个应用下面的某个进程容器被调度到了不同的节点上呀?往往我们应用内部的进程与进程间通信(通过 IPC 或者共享本地文件之类)都是要求在本地进行的,也就是需要在同一个节点上运行。

​ 所以我们需要一个更高级别的结构来将这些容器绑定在一起,并将他们作为一个基本的调度单元进行管理,这样就可以保证这些容器始终在同一个节点上面,这也就是 Pod 设计的初衷。

POD实现原理

在一个 Pod 下面运行几个关系非常密切的容器进程,这样一来这些进程本身又可以收到容器的管控,又具有几乎一致的运行环境,也就完美解决了上面提到的问题

​ 其实 Pod 也只是一个逻辑概念,真正起作用的还是 Linux 容器的 Namespace 和 Cgroup 这两个最基本的概念,Pod 被创建出来其实是一组共享了一些资源的容器而已,首先 Pod 里面的所有容器,都是共享的同一个 Network Namespace,但是涉及到文件系统的时候,默认情况下 Pod 里面的容器之间的文件系统是完全隔离的,但是我们可以通过声明来共享同一个 Volume。

​ 我们可以指定新创建的容器和一个已经存在的容器共享一个 Network Namespace,在运行容器(docker 容器)的时候只需要指定 –net=container:目标容器名 这个参数就可以了,但是这种模式有一个明显的问题那就是容器的启动有先后顺序问题,那么 Pod 是怎么来处理这个问题的呢?那就是加入一个中间容器(没有什么架构是加一个中间件解决不了的?),这个容器叫做 Infra 容器,而且这个容器在 Pod 中永远都是第一个被创建的容器,这样是不是其他容器都加入到这个 Infra 容器就可以了,这样就完全实现了 Pod 中的所有容器都和 Infra 容器共享同一个 Network Namespace 了,如下图所示:

image-20211104110057554

​ 所以当我们部署完成 Kubernetes 集群的时候,首先需要保证在所有节点上可以拉取到默认的 Infra 镜像,默认情况下 Infra 镜像地址为 k8s.gcr.io/pause:3.5,这个容器占用的资源非常少,但是这个镜像默认是需要科学上网的,所以很多时候我们在部署应用的时候一直处于 Pending 状态或者报 sandbox image 相关的错误信息,大部分是因为所有 Pod 最先启动的容器镜像都拉不下来,肯定启动不了,启动不了其他容器肯定也就不能启动了。

网络

普通容器不会创建自己的网卡,配置自己的 IP,而是和 Infra 容器共享 IP、端口范围等,而且容器之间的进程可以通过 lo 网卡设备进行通信:

  • 也就是容器之间是可以直接使用 localhost 进行通信的;
  • 看到的网络设备信息都是和 Infra 容器完全一样的;
  • 也就意味着同一个 Pod 下面的容器运行的多个进程不能绑定相同的端口;
  • 而且 Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
存储

对于文件系统 Kubernetes 是怎么实现让一个 Pod 中的容器共享的呢?

​ 默认情况下容器的文件系统是互相隔离的,要实现共享只需要在 Pod 的顶层声明一个 Volume,然后在需要共享这个 Volume 的容器中声明挂载即可

image-20211104185341050

如何划分 Pod

​ 面我们介绍了 Pod 的实现原理,了解到了应该把关系紧密的容器划分到同一个 Pod 中运行,那么怎么来区分“关系紧密”呢?举一个简单的示例,比如我们的 Wordpress 应用,是一个典型的前端服务器和后端数据服务的应用,那么你认为应该使用一个 Pod 还是两个 Pod 呢?

​ 如果在同一个 Pod 中同时运行服务器程序和后端的数据库服务这两个容器,理论上肯定是可行的,但是不推荐这样使用,我们知道一个 Pod 中的所有容器都是同一个整体进行调度的,但是对于我们这个应用 Wordpress 和 MySQL 数据库一定需要运行在一起吗?当然不需要,我们甚至可以将 MySQL 部署到集群之外对吧?所以 Wordpress 和 MySQL 即使不运行在同一个节点上也是可行的,只要能够访问到即可。

pod wordpress demo1

​ 但是如果你非要强行部署到同一个 Pod 中呢?从某个角度来说是错误的,比如现在我们的应用访问量非常大,一个 Pod 已经满足不了我们的需求了,怎么办呢?扩容对吧,但是扩容的目标也是 Pod,并不是容器,比如我们再添加一个 Pod,这个时候我们就有两个 Wordpress 的应用和两个 MySQL 数据库了,而且这两个 Pod 之间的数据是互相独立的,因为 MySQL 数据库并不是简单的增加副本就可以共享数据了,所以这个时候就得分开部署了,采用第二种方案,这个时候我们只需要单独扩容 Wordpress 的这个 Pod,后端的 MySQL 数据库并不会受到扩容的影响。

pod wordpress demo2

将多个容器部署到同一个 Pod 中的最主要参考就是应用可能由一个主进程和一个或多个的辅助进程组成,比如上面我们的日志收集的 Pod,需要其他的 sidecar 容器来支持日志的采集。所以当我们判断是否需要在 Pod 中使用多个容器的时候,我们可以按照如下的几个方式来判断:

  • 这些容器是否一定需要一起运行,是否可以运行在不同的节点上
  • 这些容器是一个整体还是独立的组件
  • 这些容器一起进行扩缩容会影响应用吗

Pod的生命周期

前面我们已经了解了 Pod 的设计原理,接下来我们来了解下 Pod 的生命周期。

​ 下图展示了一个 Pod 的完整生命周期过程,其中包含 Init ContainerPod Hook健康检查 三个主要部分,接下来我们就来分别介绍影响 Pod 生命周期的部分:

image-20211104221055851

POD的五种状态

我们先了解下 Pod 的状态,因为 Pod 状态可以反应出当前我们的 Pod 的具体状态信息,也是我们分析排错的一个必备的方式

​ 首先先了解下 Pod 的状态值,我们可以通过 kubectl explain pod.status 命令来了解关于 Pod 状态的一些信息,Pod 的状态定义在 PodStatus 对象中,其中有一个 phase 字段,下面是 phase 的可能取值:

1
kubectl explain pod.status
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
phase        <string>
The phase of a Pod is a simple, high-level summary of where the Pod is in
its lifecycle. The conditions array, the reason and message fields, and the
individual container status arrays contain more detail about the pod's
status. There are five possible phase values:

Pending: The pod has been accepted by the Kubernetes system, but one or
more of the container images has not been created. This includes time
before being scheduled as well as time spent downloading images over the
network, which could take a while.

Running: The pod has been bound to a
node, and all of the containers have been created. At least one container
is still running, or is in the process of starting or restarting.

Succeeded: All containers in the pod have terminated in success, and will
not be restarted.

Failed: All containers in the pod have terminated, and at
least one container has terminated in failure. The container either exited
with non-zero status or was terminated by the system.

Unknown: For some
reason the state of the pod could not be obtained, typically due to an
error in communicating with the host of the pod.

下面是五种状态的描述

状态值 说明
挂起(Pending) Pod 已被 Kubernetes 系统接受,但有一个或者多个容器镜像尚未创建,等待时间包括调度 Pod 的时间和通过网络下载镜像的时间。
运行中(Running) 该 Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态。
成功(Succeeded) Pod 中的所有容器都被成功终止,并且不会再重启。
失败(Failed) Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止。
未知(Unknown) 因为某些原因无法取得 Pod 的状态,通常是因为与 Pod 所在主机通信失败。

初始化容器

了解了 Pod 状态后,首先来了解下 Pod 中最新启动的 Init Container,也就是我们平时常说的初始化容器

使用场景

Init Container就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行。

​ 我们知道一个 Pod 里面的所有容器是共享数据卷和 Network Namespace的,所以 Init Container 里面产生的数据可以被主容器使用到。从上面的 Pod 生命周期的图中可以看出初始化容器是独立与主容器之外的,只有所有的初始化容器执行完之后,主容器才会被启动,那么初始化容器有哪些应用场景呢:

等待其他模块 Ready

​ 这个可以用来解决服务之间的依赖问题,比如我们有一个 Web 服务,该服务又依赖于另外一个数据库服务,但是在我们启动这个 Web 服务的时候我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内 Web 服务连接数据库异常。要解决这个问题的话我们就可以在 Web 服务的 Pod 中使用一个 InitContainer,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们主容器的 Web 服务才被启动起来,这个时候去连接数据库就不会有问题了

做初始化配置

​ 比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群

其它场景

​ 如将 Pod 注册到一个中央数据库、配置中心等

演示示例

比如现在我们来实现一个功能,在 Nginx Pod 启动之前去重新初始化首页内容,如下所示的资源清单:(init-pod.yaml)

1
vi init-demo.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
volumes:
- name: workdir
emptyDir: {} #注意:这个是emptyDir{}类型的存储,你可认为是一种临时存储
initContainers:
- name: install
image: busybox
command:
- /bin/sh
- "-c"
- echo "刘备" > /work-dir/index.html
volumeMounts:
- name: workdir
mountPath: "/work-dir"
containers:
- name: web
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: workdir
mountPath: /usr/share/nginx/html
配置描述

​ 上面的资源清单中我们首先在 Pod 顶层声明了一个名为 workdir 的 Volume,前面我们用了 hostPath 的模式,这里我们使用的是 emptyDir{}这个是一个临时的目录,数据会保存在 kubelet 的工作目录下面,生命周期等同于 Pod 的生命周期。

​ 然后我们定义了一个初始化容器,该容器会将刘备写入到 /work-dir 目录下面,但是由于我们又将该目录声明挂载到了全局的 Volume,同样的主容器 nginx 也将目录 /usr/share/nginx/html 声明挂载到了全局的 Volume,所以在主容器的该目录下面会同步初始化容器中创建的 index.html 文件

运行测试

启动这个POD并且创建完成后可以查看该 Pod 的状态

1
2
kubectl apply -f init-demo.yaml
kubectl get po -o wide

可以发现 Pod 现在的状态处于 Init:0/1 状态,意思就是现在第一个初始化容器还在执行过程中

image-20220527094500391

此时我们可以查看 Pod 的详细信息:

1
kubectl describe pod init-demo

image-20220527100413718

从上面的描述信息里面可以看到初始化容器已经启动了,现在处于 Running 状态,所以还需要稍等,到初始化容器执行完成后退出初始化容器会变成 Completed 状态,然后才会启动主容器。

访问测试

待到主容器也启动完成后,Pod 就会变成Running 状态,然后我们去访问下 Pod 主页,验证下是否有我们初始化容器中下载的页面信息:

1
curl 10.244.1.13

image-20220527100537268

Pod 钩子

我们知道 Pod 是 Kubernetes 集群中的最小单元,而 Pod 是由容器组成的,所以在讨论 Pod 的生命周期的时候我们可以先来讨论下容器的生命周期。

​ 实际上 Kubernetes 为我们的容器提供了生命周期的钩子,就是我们说的 Pod Hook,Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中,我们可以同时为 Pod 中的所有容器都配置 hook。

钩子类型

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

PostStart

这个钩子在容器创建后立即执行

​ 但是,并不能保证钩子将在容器 ENTRYPOINT 之前运行,因为没有参数传递给处理程序,主要用于资源部署、环境准备等,不过需要注意的是如果钩子花费太长时间以至于不能运行或者挂起,容器将不能达到 running 状态

PreStop

这个钩子在容器终止之前立即被调用

它是阻塞的,意味着它是同步的,所以它必须在删除容器的调用发出之前完成,主要用于优雅关闭应用程序、通知其他系统等,如果钩子在执行期间挂起,Pod 阶段将停留在 running 状态并且永不会达到 failed 状态

使用注意

注意:PostStart用的不是很多,而PreStop用的相对很多;

​ 如果 PostStart 或者 PreStop 钩子失败, 它会杀死容器,所以我们应该让钩子函数尽可能的轻量,当然有些情况下,长时间运行命令是合理的, 比如在停止容器之前预先保存状态。

实现方式

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

  • Exec - 用于执行一段特定的命令,不过要注意的是该命令消耗的资源会被计入容器。
  • HTTP - 对容器上的特定的端点执行 HTTP 请求。
通知类型

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

  • 默认:K8S 通知 node 执行容器 stop 命令,容器运行时会先向容器中 PID 为 1 的进程发送系统信号 SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认超时时间(30s),会继续发送 SIGKILL 的系统信号强行 kill 掉进程
  • 使用 Pod 生命周期(利用 PreStop 回调函数),它在发送终止信号之前执行
PostStart演示示例

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

1
vi pod-postStart.yaml
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: hook-demo1
spec:
containers:
- name: hook-demo1
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
启动POD

直接创建上面的 Pod:

1
2
kubectl apply -f pod-postStart.yaml
kubectl get pod -o wide

image-20220527103529464

查看POD的详情

1
kubectl describe pod hook-demo1

image-20220527103735650

查看内容

创建成功后可以查看容器中 /usr/share/message 文件是否内容正确

1
kubectl exec -it hook-demo1 -- cat /usr/share/message

image-20220527104104878

PreStop演示示例

以下示例中,定义了一个 Nginx Pod,其中设置了 PreStop 钩子函数,即在容器退出之前,优雅的关闭 Nginx:

1
vi pod-prestop.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: v1
kind: Pod
metadata:
name: hook-demo2
spec:
containers:
- name: hook-demo2
image: nginx:1.12
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"] # 优雅退出,注意,如果程序有这个优雅退出的命令,应应可能使用这个preStop hook功能;

---
apiVersion: v1
kind: Pod
metadata:
name: hook-demo3 #这边使用这个demo来测试验证下preStop hook的效果
spec:
volumes:
- name: message
hostPath:
path: /tmp/k8s
type: DirectoryOrCreate
containers:
- name: hook-demo2
image: nginx:1.12
ports:
- containerPort: 80
volumeMounts:
- name: message
mountPath: /usr/share/
lifecycle:
preStop:
exec:
command: ['/bin/sh', '-c', 'echo Hello from the preStop Handler > /usr/share/message']
创建POD

上面定义的两个 Pod,一个是利用 preStop 来进行优雅删除,另外一个是利用 preStop 来做一些信息记录的事情,同样直接创建上面的 Pod

1
2
kubectl apply -f pod-prestop.yaml
kubectl get po -o wide

image-20220527105914744

优雅停机

创建完成后,我们可以直接删除 hook-demo2 这个 Pod,在容器删除之前会执行 preStop 里面的优雅关闭命令,这个用法在后面我们的滚动更新的时候用来保证我们的应用零宕机非常有用。

1
kubectl delete pod hook-demo2

image-20220527110607785

持久化数据

​ 第二个 Pod 我们声明了一个 hostPath 类型的 Volume,在容器里面声明挂载到了这个 Volume,所以当我们删除 Pod,退出容器之前,在容器里面输出的信息也会同样的保存到宿主机(一定要是 Pod 被调度到的目标节点)的 /tmp/k8s 目录下面,我们可以查看 hook-demo3 这个 Pod 被调度的节点:

1
kubectl get po -o wide

image-20220527111249730

可以看到这个 Pod 被调度到了 node01 这个节点上,我们可以先到该节点上查看 /tmp/k8s 目录下面目前没有任何内容:

1
ls /tmp/k8s/

image-20220527111329843

现在我们来删除 hook-demo3 这个 Pod,安装我们的设定在容器退出之前会执行 preStop 里面的命令,也就是会往 message 文件中输出一些信息:

1
2
3
kubectl delete pod hook-demo3
# node01节点执行
ls /tmp/k8s/

image-20220527111548012

Pod 健康检查

强大的自愈能力是Kubernetes这类容器编排引擎的一个重要特性,自愈的默认实现方式是自动重启发生故障的容器。

为什么需要健康检查

用户还可以利用Liveness和Readiness探测机制设置更精细的健康检查,进而实现如下需求:

  • 零停机部署。
  • 避免部署无效的镜像。
  • 更加安全的滚动升级。
检查策略

在Pod部署到Kubernetes集群中以后,为了确保Pod处于健康正常的运行状态,Kubernetes提供了两种探针,用于检测容器的状态:

存活探测

Liveness是检查容器是否处于运行状态,如果检测失败,kubelet将会杀掉掉容器,并根据重启策略进行下一步的操作,如果容器没有提供Liveness Probe,则默认状态为Success;

​ Liveness探测器是让Kubernetes知道你的应用是否活着,如果你的应用还活着,那么Kubernetes就让它继续存在,如果你的应用程序已经死了,Kubernetes将移除Pod并重新启动一个来替换它。

​ 让我们想象另一种情况,当我们的应用在成功启动以后因为一些原因“宕机”,或者遇到死锁情况,导致它无法响应用户请求。
​ 在默认情况下,Kubernetes会继续向Pod发送请求,通过使用存活探针来检测,当发现服务不能在限定时间内处理请求(请求错误或者超时),就会重新启动有问题的pod。

img

就绪探测

Readiness 是检查容器是否已经处于可接受服务请求的状态,如果Readiness Probe失败,端点控制器将会从服务端点(与Pod匹配的)中移除容器的IP地址,Readiness的默认值为Failure,如果一个容器未提供Readiness,则默认是Success。

​ 就绪探针旨在让Kubernetes知道你的应用是否准备好为请求提供服务,Kubernetes只有在就绪探针通过才会把流量转发到Pod,如果就绪探针检测失败,Kubernetes将停止向该容器发送流量,直到它通过。

​ 一个应用往往需要一段时间来预热和启动,比如一个后端项目的启动需要连接数据库执行数据库迁移等等,一个Spring项目的启动也需要依赖Java虚拟机。即使该过程已启动,您的服务在启动并运行之前也无法运行。应用在完全就绪之前不应接收流量,但默认情况下,Kubernetes会在容器内的进程启动后立即开始发送流量。通过就绪探针探测,直到应用程序完全启动,然后才允许将流量发送到新副本。

img

两者对比
  • Liveness探测和Readiness探测是两种Health Check机制,如果不特意配置,Kubernetes将对两种探测采取相同的默认行为,即通过判断容器启动进程的返回值是否为零来判断探测是否成功。
  • 两种探测的配置方法完全一样,支持的配置参数也一样,不同之处在于探测失败后的行为:Liveness探测是重启容器;Readiness探测则是将容器设置为不可用,不接收Service转发的请求。
  • Liveness探测和Readiness探测是独立执行的,二者之间没有依赖,所以可以单独使用,也可以同时使用,用Liveness探测判断容器是否需要重启以实现自愈;用Readiness探测判断容器是否已经准备好对外提供服务
如何配置

对于LivenessProbe和ReadinessProbe用法都一样,拥有相同的参数和相同的监测方式。

  • initialDelaySeconds:用来表示初始化延迟的时间,也就是告诉监测从多久之后开始运行,单位是秒
  • timeoutSeconds: 用来表示监测的超时时间,如果超过这个时长后,则认为监测失败
  • periodSeconds:指定每多少秒执行一次探测,Kubernetes如果连续执行3次Liveness探测均失败,则会杀掉并重启容器
使用场景
  • 如果容器中的进程能够在遇到问题或不健康的情况下自行崩溃,则不一定需要存活探针; kubelet 将根据 Pod 的restartPolicy 自动执行正确的操作。

  • 如果希望容器在探测失败时被杀死并重新启动,那么请指定一个存活探针,并指定restartPolicy 为 Always 或 OnFailure。

  • 如果要仅在探测成功时才开始向 Pod 发送流量,请指定就绪探针,在这种情况下,就绪探针可能与存活探针相同,但是 spec 中的就绪探针的存在意味着 Pod 将在没有接收到任何流量的情况下启动,并且只有在探针探测成功后才开始接收流量。

  • 如果您希望容器能够自行维护,您可以指定一个就绪探针,该探针检查与存活探针不同的端点。

  • 如果您只想在 Pod 被删除时能够排除请求,则不一定需要使用就绪探针;在删除 Pod 时,Pod 会自动将自身置于未完成状态,无论就绪探针是否存在,当等待 Pod 中的容器停止时,Pod 仍处于未完成状态。

默认的健康检查

我们首先学习Kubernetes默认的健康检查机制:每个容器启动时都会执行一个进程,此进程由Dockerfile的CMD或ENTRYPOINT指定。

​ 如果进程退出时返回码非零,则认为容器发生故障,Kubernetes就会根据restartPolicy重启容器

创建资源清单

Pod的restartPolicy设置为OnFailure,默认为Always,sleep 10; exit 1模拟容器启动10秒后发生故障

1
vi pod-default-health.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: pod-default-health
namespace: default
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.12
ports:
- containerPort: 80
args: ["/bin/sh","-c"," sleep 10;exit 1"]
创建容器
1
kubectl apply -f pod-default-health.yml

image-20210520094040357

监控Pod变化
1
kubectl get pods -o wide -w

该命令可以不断显示容器的因为失败不断重启

image-20210520094001981

在上面的例子中,容器进程返回值非零,Kubernetes则认为容器发生故障,需要重启。

​ 有不少情况是发生了故障,但进程并不会退出,比如访问Web服务器时显示500内部错误,可能是系统超载,也可能是资源死锁,此时httpd进程并没有异常退出,在这种情况下重启容器可能是最直接、最有效的解决方案,那我们如何利用HealthCheck机制来处理这类场景呢?

探针类型

探针类型是指通过何种方式来进行健康检查,K8S有三种类型的探测:HTTP,Command和TCP。

exec存活探针

对于命令探测,是指Kubernetes在容器内运行命令,如果命令以退出代码0返回,则容器将标记为正常。否则,它被标记为不健康

​ 下面的资源会在先创建一个nginx的任务,生存测试探针livenessProbe会执行test -e /tmp/healthy命令检查文件是否存在, 若文件存在则返回状态码 0,表示成功通过测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: pod-nginx-demo
namespace: default
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.12
ports:
- containerPort: 80
livenessProbe:
initialDelaySeconds: 5
periodSeconds: 3
exec:
command: ["test","-e","/tmp/healthy"]

创建pod

创建pod

1
2
kubectl create -f pod-demo.yaml
kubectl get pod -w -o wide

启动后不断检测/tmp/healthy是否存在,不存在重启容器

image-20210513144851553

创建文件

新开一个窗口写入登录pod容器,写入healthy文件

1
2
3
4
5
# 登录pod容器
kubectl exec pod-nginx-demo -it /bin/bash
echo healthy > /tmp/healthy
# 在html目录写入healthy文件
echo healthy > /tmp/healthy

image-20210513145201116

查看原来的pod状态

1
kubectl get pods -o wide -w

我们发现pod不在不断地重启了

image-20210513145217566

HTTP就绪探针

HTTP探测可能是最常见的探针类型,即使应用不是HTTP服务,也可以创建一个轻量级HTTP服务器来响应探测,比如让Kubernetes通过HTTP访问一个URL,如果返回码在200到300范围内,就将应用程序标记为健康状态,否则它被标记为不健康。

​ 上面 清单 文件 中 定义 的 httpGet 测试 中, 请求的资源路径 为/healthy, 地址 默认 为 Pod IP, 端口使用了容器中定义的端口名称 HTTP, 这也是明确为容器指明要暴露的端口的用途之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: pod-nginx-demo
namespace: default
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.12
ports:
- containerPort: 80
readinessProbe:
initialDelaySeconds: 5
periodSeconds: 3
httpGet:
port: 80
path: /healthy
scheme: HTTP

创建pod

1
2
3
kubectl create -f pod-demo.yaml
#查看pod状态
kubectl get pods -o wide -w

我们发现nginx 一致处于未未就绪状态

image-20210513142840969

查看pod详情

1
kubectl describe pod pod-nginx-demo

image-20210513143053405

创建文件

新开一个窗口写入登录pod容器,写入healthy文件

1
2
3
4
# 登录pod容器
kubectl exec pod-nginx-demo -it /bin/bash
# 在html目录写入healthy文件
echo healthy > /usr/share/nginx/html/healthy

查看原来的pod状态

1
kubectl get pods -o wide -w

image-20210513144218434

访问

1
curl 10.244.1.30/healthy

image-20210513105648936

再次删除healthy文件

1
rm -f /usr/share/nginx/html/healthy

image-20210513144331658

再次查看pod状态,进入未就绪状态

1
kubectl get pods -o wide -w

image-20210513144414486

评论