Kubernetes:Pod解析
1、Pod原理
我们了解到了 Pod 是 Kubernetes 集群中最基本的调度单元,我们平时在集群中部署的应用都是以 Pod 为单位的,而并不是我们熟知的容器,这样设计的目的是什么呢?为何不直接使用容器呢?
为什么需要Pod
假设 Kubernetes 中调度的基本单元就是容器,对于一个非常简单的应用可以直接被调度直接使用,没有什么问题,但是往往还有很多应用程序是由多个进程组成的,有的同学可能会说把这些进程都打包到一个容器中去不就可以了吗?理论上是可以实现的,但是不要忘记了 Docker 管理的进程是 pid=1 的主进程,其他进程死掉了就会成为僵尸进程,没办法进行管理了,这种方式本身也不是容器推荐的运行方式,一个容器最好只干一件事情,所以在真实的环境中不会使用这种方式。
那么我们就把这个应用的进程进行拆分,拆分成一个一个的容器总可以了吧?但是不要忘记一个问题,拆分成一个一个的容器后,是不是就有可能出现一个应用下面的某个进程容器被调度到了不同的节点上呀?往往我们应用内部的进程与进程间通信(通过 IPC 或者共享本地文件之类)都是要求在本地进行的,也就是需要在同一个节点上运行。
所以我们需要一个更高级别的结构来将这些容器绑定在一起,并将他们作为一个基本的调度单元进行管理,这样就可以保证这些容器始终在同一个节点上面,这也就是 Pod 设计的初衷。
Pod原理
在一个 Pod 下面运行几个关系非常密切的容器进程,这样一来这些进程本身又可以收到容器的管控,又具有几乎一致的运行环境,也就完美解决了上面提到的问题。
其实 Pod 也只是一个逻辑概念,真正起作用的还是 Linux 容器的 Namespace 和 Cgroup 这两个最基本的概念,Pod 被创建出来其实是一组共享了一些资源的容器而已。首先 Pod 里面的所有容器,都是共享的同一个 Network Namespace,但是涉及到文件系统的时候,默认情况下 Pod 里面的容器之间的文件系统是完全隔离的,但是我们可以通过声明来共享同一个 Volume。
对于共享同一个 Network Namespace 这个概念是不是比较熟悉,我们之前在 Docker 网络模式的章节中讲解了网络的 Container 模式,我们可以指定新创建的容器和一个已经存在的容器共享一个 Network Namespace,在运行容器的时候只需要指定–net=container:目标容器名这个参数就可以了,但是这种模式有一个明显的问题那就是容器的启动有先后顺序问题,那么 Pod 是怎么来处理这个问题的呢?那就是加入一个中间容器(没有什么架构是加一个中间件解决不了的?),这个容器叫做Infra 容器,而且这个容器在 Pod 中永远都是第一个被创建的容器,这样是不是其他容器都加入到这个 Infra 容器就可以了,这样就完全实现了 Pod 中的所有容器都和 Infra 容器共享同一个 Network Namespace 了,如下图所示:

所以当我们部署完成 Kubernetes 集群的时候,首先需要保证在所有节点上可以拉取到默认的 Infra 镜像,默认情况下 Infra 镜像地址为k8s.gcr.io/pause:3.1,这个容器占用的资源非常少,但是这个镜像默认是需要科学上网的,所以很多时候我们在部署应用的时候一直处于Pending状态,因为所有 Pod 最先启动的容器镜像都拉不下来,肯定启动不了,启动不了其他容器肯定也就不能启动了。
普通的容器加入到了 Infra 容器的 Network Namespace 中,所以这个 Pod 下面的所有容器就是共享同一个 Network Namespace 了,普通容器不会创建自己的网卡,配置自己的 IP,而是和 Infra 容器共享 IP、端口范围等,而且容器之间的进程可以通过 lo 网卡设备进行通信:
- 也就是容器之间是可以直接使用localhost进行通信的;
- 看到的网络设备信息都是和 Infra 容器完全一样的;
- 也就意味着同一个 Pod 下面的容器运行的多个进程不能绑定相同的端口;
- 而且 Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
对于文件系统 Kubernetes 是怎么实现让一个 Pod 中的容器共享的呢?默认情况下容器的文件系统是互相隔离的,要实现共享只需要在 Pod 的顶层声明一个 Volume,然后在需要共享这个 Volume 的容器中声明挂载即可。

比如下面的示例:
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
volumes:
- name: varlog
hostPath:
path: /var/log/counter
containers:
- name: count
image: busybox
args:
- /bin/sh
- -c
- >
i=0;
while true;
do
echo "$i: $(date)" >> /var/log/1.log;
i=$((i+1));
sleep 1;
done
volumeMounts:
- name: varlog
mountPath: /var/log
- name: count-log
image: busybox
args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
volumeMounts:
- name: varlog
mountPath: /var/log
示例中我们在 Pod 的顶层声明了一个名为 varlog 的Volume,而这个Volume的类型是hostPath,也就意味这个宿主机的/var/log/counter目录将被这个 Pod 共享,共享给谁呢?在需要用到这个数据目录的容器上声明挂载即可,也就是通过volumeMounts声明挂载的部分,这样我们这个 Pod 就实现了共享容器的/var/log目录,而且数据被持久化到了宿主机目录上。
这个方式也是 Kubernetes 中一个非常重要的设计模式:sidecar 模式的常用方式。典型的场景就是容器日志收集,比如上面我们的这个应用,其中应用的日志是被输出到容器的 /var/log 目录上的,这个时候我们可以把 Pod 声明的 Volume 挂载到容器的 /var/log 目录上,然后在这个 Pod 里面同时运行一个 sidecar 容器,他也声明挂载相同的 Volume 到自己容器的 /var/log (或其他)目录上,这样我们这个 sidecar 容器就只需要从 /var/log 目录下面不断消费日志发送到 Elasticsearch 中存储起来就完成了最基本的应用日志的基本收集工作了。
除了这个应用场景之外使用更多的还是利用 Pod 中的所有容器共享同一个 Network Namespace 这个特性,这样我们就可以把 Pod 网络相关的配置和管理也可以交给一个 sidecar 容器来完成,完全不需要去干涉用户容器,这个特性在现在非常火热的 Service Mesh(服务网格)中应用非常广泛,典型的应用就是Istio,不过也不用着急,后面也会和大家一起探讨的。
如何划分Pod
上面我们介绍了 Pod 的实现原理,了解到了应该把关系紧密的容器划分到同一个 Pod 中运行,那么怎么来区分“关系紧密”呢?举一个简单的示例,比如我们的 Wordpress 应用,是一个典型的前端服务器和后端数据服务的应用,那么你认为应该使用一个 Pod 还是两个 Pod 呢?
如果在同一个 Pod 中同时运行服务器程序和后端的数据库服务这两个容器,理论上肯定是可行的,但是不推荐这样使用,我们知道一个 Pod 中的所有容器都是同一个整体进行调度的,但是对于我们这个应用 Wordpress 和 MySQL 数据库一定需要运行在一起吗?当然不需要,我们甚至可以将 MySQL 部署到集群之外对吧?所以 Wordpress 和 MySQL 即使不运行在同一个节点上也是可行的,只要能够访问到即可。

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

将多个容器部署到同一个 Pod 中的最主要参考就是应用可能由一个主进程和一个或多个的辅助进程组成,比如上面我们的日志收集的 Pod,需要其他的 sidecar 容器来支持日志的采集。所以当我们判断是否需要在 Pod 中使用多个容器的时候,我们可以按照如下的几个方式来判断:
- 这些容器是否一定需要一起运行,是否可以运行在不同的节点上
- 这些容器是一个整体还是独立的组件
- 这些容器一起进行扩缩容会影响应用吗
基本上我们能够回答上面的几个问题就能够判断是否需要在 Pod 中运行多个容器了。
Pod的设计
其实在我们理解 Pod 的时候,有一个比较好的类比的方式就是把 Pod 看成我们之前的 “虚拟机”,而容器就是虚拟机中运行的一个用户程序,这样就可以很好的来理解 Pod 的设计。
2、Pod的状态
前面我们已经了解了 Pod 的设计原理,接下来我们来了解下 Pod 的生命周期。下图展示了一个 Pod 的完整生命周期过程,其中包含Init Container、Pod Hook、健康检查三个主要部分,接下来我们就来分别介绍影响 Pod 生命周期的部分:
首先在介绍 Pod 的生命周期之前,我们先了解下 Pod 的状态,因为 Pod 状态可以反应出当前我们的 Pod 的具体状态信息,也是我们分析排错的一个必备的方式。
Pod状态
首先先了解下 Pod 的状态值,我们可以通过kubectl explain pod.status命令来了解关于 Pod 状态的一些信息,Pod 的状态定义在PodStatus对象中,其中有一个phase字段,下面是phase的可能取值:
- 挂起(Pending):Pod 信息已经提交给了集群,但是还没有被调度器调度到合适的节点或者 Pod 里的镜像正在下载
- 运行中(Running):该 Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态
- 成功(Succeeded):Pod 中的所有容器都被成功终止,并且不会再重启
- 失败(Failed):Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止
- 未知(Unknown):因为某些原因无法取得 Pod 的状态,通常是因为与 Pod 所在主机通信失败导致的
除此之外,PodStatus对象中还包含一个PodCondition的数组,通过命令kubectl explain pod.status.conditions里面包含的属性有:
lastProbeTime:最后一次探测 Pod Condition 的时间戳。
lastTransitionTime:上次 Condition 从一种状态转换到另一种状态的时间。
message:上次 Condition 状态转换的详细描述。
reason:Condition 最后一次转换的原因。
status:Condition 状态类型,可以为 “True”, “False”, and “Unknown”.
type:Condition 类型,包括以下方面:
PodScheduled(Pod 已经被调度到其他 node 里)
Ready(Pod 能够提供服务请求,可以被添加到所有可匹配服务的负载平衡池中)
Initialized(所有的init containers已经启动成功)
Unschedulable(调度程序现在无法调度 Pod,例如由于缺乏资源或其他限制)
ContainersReady(Pod 里的所有容器都是 ready 状态)
3、Pod的重启策略
我们可以通过配置restartPolicy字段来设置 Pod 中所有容器的重启策略,其可能值为Always,OnFailure 和 Never,默认值为Always。restartPolicy 仅指通过 kubelet 在同一节点上重新启动容器。通过 kubelet 重新启动的退出容器将以指数增加延迟(10s,20s,40s…)重新启动,上限为 5 分钟,并在成功执行 10 分钟后重置。不同类型的的控制器可以控制 Pod 的重启策略:
- Job:适用于一次性任务如批量计算,任务结束后 Pod 会被此类控制器清除。Job 的重启策略只能是"OnFailure"或者"Never"。
- Replication Controller, ReplicaSet, or Deployment,此类控制器希望 Pod 一直运行下去,它们的重启策略只能是"Always"。
- DaemonSet:每个节点上启动一个 Pod,很明显此类控制器的重启策略也应该是"Always"。
4、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)
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
volumes:
- name: workdir
emptyDir: {}
initContainers:
- name: install
image: busybox
command:
- wget
- "-O"
- "/work-dir/index.html"
- http://www.baidu.com # https
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 的生命周期。
然后我们定义了一个初始化容器,该容器会下载一个 html 文件到/work-dir目录下面,但是由于我们又将该目录声明挂载到了全局的 Volume,同样的主容器 nginx 也将目录/usr/share/nginx/html声明挂载到了全局的 Volume,所以在主容器的该目录下面会同步初始化容器中创建的index.html文件。
直接创建上面的 Pod:
kubectl apply -f init-pod.yaml
创建完成后可以查看该 Pod 的状态:
kubectl get pods
NAME READY STATUS RESTARTS AGE
init-demo 0/1 Init:0/1 0 4s
可以发现 Pod 现在的状态处于Init:0/1状态,意思就是现在第一个初始化容器还在执行过程中,此时我们可以查看 Pod 的详细信息:
$ kubectl describe pod init-demo
Name: init-demo
Namespace: default
Priority: 0
Node: node2/10.151.30.23
Start Time: Thu, 22 Oct 2020 21:17:32 +0800
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"init-demo","namespace":"default"},"spec":{"containers":[{"image":"ngi...
Status: Running
IP: 10.244.2.9
IPs:
IP: 10.244.2.9
Init Containers:
install:
Container ID: docker://0d53780635c2cbf7865648dcb9a4c981e23c2ce95edcaa3f474029a9a1db972a
Image: busybox
Image ID: docker-pullable://busybox@sha256:a9286defaba7b3a519d585ba0e37d0b2cbee74ebfe590960b0b1d6a5e97d1e1d
Port: <none>
Host Port: <none>
Command:
wget
-O
/work-dir/index.html
http://www.baidu.com
State: Terminated
Reason: Completed
Exit Code: 0
Started: Thu, 22 Oct 2020 21:17:47 +0800
Finished: Thu, 22 Oct 2020 21:17:47 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-hpd7s (ro)
/work-dir from workdir (rw)
Containers:
web:
Container ID: docker://c31aed9916ed5277d0b9be96787d336186f8d312b20d59c1fcd59548f3942312
Image: nginx
Image ID: docker-pullable://nginx@sha256:ed7f815851b5299f616220a63edac69a4cc200e7f536a56e421988da82e44ed8
Port: 80/TCP
Host Port: 0/TCP
State: Running
Started: Thu, 22 Oct 2020 21:18:02 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/usr/share/nginx/html from workdir (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-hpd7s (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
workdir:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
SizeLimit: <unset>
default-token-hpd7s:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-hpd7s
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 27m default-scheduler Successfully assigned default/init-demo to node2
Normal Pulling 27m kubelet, node2 Pulling image "busybox"
Normal Pulled 27m kubelet, node2 Successfully pulled image "busybox" in 12.597844015s
Normal Created 27m kubelet, node2 Created container install
Normal Started 27m kubelet, node2 Started container install
Normal Pulling 27m kubelet, node2 Pulling image "nginx"
Normal Pulled 27m kubelet, node2 Successfully pulled image "nginx" in 13.743250204s
Normal Created 27m kubelet, node2 Created container web
Normal Started 27m kubelet, node2 Started container web
从上面的描述信息里面可以看到初始化容器已经启动了,现在处于Running状态,所以还需要稍等,到初始化容器执行完成后退出初始化容器会变成Completed状态,然后才会启动主容器。待到主容器也启动完成后,Pod 就会变成Running状态,然后我们去访问下 Pod 主页,验证下是否有我们初始化容器中下载的页面信息:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
init-demo 1/1 Running 0 28m 10.244.2.9 node2 <none> <none>
$ curl 10.244.2.9
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=
init-pod.yaml
# pod 当中的初始化容器
# 主要是创建Pod并声明了initContainers初始化容器
# 我们模拟了初始化容器当中拉取git当中的配置文件的操作 可以实战使用
# kubectl describe pod init-demo 查看容器的详细描述 可以看到初始化容器状态的变化 初始化容器执行完成后退出初始化容器会变成Completed状态然后再去启动主容器,
# 然后等待主容器启动完成之后pod就会是running状态了
apiVersion: v1
kind: Pod
metadata:
name: init-demo
namespace: default
spec:
volumes: # 声明一个volumes
- name: workdir # 将这个volumes命名为workdir
emptyDir: {} # 类型是一个emptyDir:{} 这是一个临时的目录,数据会保存在kubelet的工作目录下面,声明周期等同于Pod的声明周期
initContainers: # 初始化容器 重点!
- name: install
image: alpine:3.12 # 使用alpine:3.12吧 可以使用里面的apk add git来安装git服务然后拉取git当中的配置文件 更多alpine操作参考: https://blog.csdn.net/chenzuoli/article/details/124561644
command: ["/bin/sh"] # 启动初始化容器之后我们在容器内部执行命令
args: ["-c","apk add git; git clone https://15275411187:Huliang1991129@gitee.com/hushaoliang/config_server_java.git /work-dir"]
volumeMounts: # 在初始化容器当中也声明了挂载数据卷
- name: workdir # 这个name很重要 要和在Pod当中声明的volumes的name保持一致
mountPath: "/work-dir" # 这个表示在初始化容器当中挂载到的位置
containers:
- name: web
image: nginx
ports:
- containerPort: 80
volumeMounts: # 主容器当中也声明了数据卷的挂载 也是挂载了在pod当中声明的volumes的name保持一致
- name: workdir # 在pod当中声明的volumes的name保持一致
mountPath: /usr/share/nginx/html # 表示挂载到主容器的位置是/xxxxx/xxxxxx/xxx
# 在初始化容器当中下载了百度的首页写入到了初始化容器的/work-dir目录下边的index.html里面 这个时候Pod里面声明的enptyDir:{}当种也有了index.html文件 因为在主容器web当中也挂载了pod里面的emptydir:{} 所以在
# 主容器的/usr/share/nginx/html当中也有了index.html文件
# apiVersion: v1
# kind: Pod
# metadata:
# name: init-demo
# namespace: default
# spec:
# volumes:
# - name: workdir
# emptyDir: {}
# initContainers:
# - name: install
# image: busybox
# command: ["/bin/sh"] # 这个地方执行命令我们可以通过command结合args同时执行多条命令比如args里面的ifconfig;ls-l; 只是单存的举个例子哈!
# args: ["-c","ifconfig;ls -l"]
# volumeMounts:
# - name: workdir
# mountPath: "/work-dir"
# containers:
# - name: web
# image: nginx
# ports:
# - containerPort: 80
# volumeMounts:
# - name: workdir
# mountPath: /usr/share/nginx/html
5、Pod Hook钩子
我们知道 Pod 是 Kubernetes 集群中的最小单元,而 Pod 是由容器组成的,所以在讨论 Pod 的生命周期的时候我们可以先来讨论下容器的生命周期。实际上 Kubernetes 为我们的容器提供了生命周期的钩子,就是我们说的Pod Hook,Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中。我们可以同时为 Pod 中的所有容器都配置 hook。
Kubernetes 为我们提供了两种钩子函数:
- PostStart:这个钩子在容器创建后立即执行。但是,并不能保证钩子将在容器 ENTRYPOINT 之前运行,因为没有参数传递给处理程序。主要用于资源部署、环境准备等。不过需要注意的是如果钩子花费太长时间以至于不能运行或者挂起,容器将不能达到 running 状态。
- PreStop:这个钩子在容器终止之前立即被调用。它是阻塞的,意味着它是同步的,所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。如果钩子在执行期间挂起,Pod 阶段将停留在 running 状态并且永不会达到 failed 状态。
如果 PostStart 或者 PreStop 钩子失败, 它会杀死容器。所以我们应该让钩子函数尽可能的轻量。当然有些情况下,长时间运行命令是合理的, 比如在停止容器之前预先保存状态。
另外我们有两种方式来实现上面的钩子函数:
- Exec- 用于执行一段特定的命令,不过要注意的是该命令消耗的资源会被计入容器。
- HTTP- 对容器上的特定的端点执行 HTTP 请求。
以下示例中,定义了一个 Nginx Pod,其中设置了 PostStart 钩子函数,即在容器创建成功后,写入一句话到 /usr/share/message 文件中:(pod-poststart.yaml)
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:
$ kubectl apply -f pod-poststart.yaml
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hook-demo1 1/1 Running 0 43s
创建成功后可以查看容器中/usr/share/message文件是否内容正确:
$ kubectl exec -it hook-demo1 cat /usr/share/message
Hello from the postStart handler
当用户请求删除含有 Pod 的资源对象时(如 Deployment 等),K8S 为了让应用程序优雅关闭(即让应用程序完成正在处理的请求后,再关闭软件),K8S 提供两种信息通知:
- 默认:K8S 通知 node 执行docker stop命令,docker 会先向容器中 PID 为 1 的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认超时时间(30s),会继续发送SIGKILL的系统信号强行 kill 掉进程
- 使用 Pod 生命周期(利用PreStop回调函数),它在发送终止信号之前执行
默认所有的优雅退出时间都在30秒内。kubectl delete命令支持–grace-period=选项,这个选项允许用户用他们自己指定的值覆盖默认值。值’0’代表强制删除 pod。 在 kubectl 1.5 及以上的版本里,执行强制删除时必须同时指定–force --grace-period=0。
强制删除一个 pod 是从集群状态还有 etcd 里立刻删除这个 pod,只是当 Pod 被强制删除时, APIServer 不会等待来自 Pod 所在节点上的 kubelet 的确认信息:pod 已经被终止。在 API 里 pod 会被立刻删除,在节点上, pods 被设置成立刻终止后,在强行杀掉前还会有一个很小的宽限期。
以下示例中,定义了一个 Nginx Pod,其中设置了PreStop钩子函数,即在容器退出之前,优雅的关闭 Nginx:(pod-prestop.yaml)
apiVersion: v1
kind: Pod
metadata:
name: hook-demo2
spec:
containers:
- name: hook-demo2
image: nginx
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"] # 优雅退出
---
apiVersion: v1
kind: Pod
metadata:
name: hook-demo3
spec:
volumes:
- name: message
hostPath:
path: /tmp
containers:
- name: hook-demo2
image: nginx
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,一个是利用preStop来进行优雅删除,另外一个是利用preStop来做一些信息记录的事情,同样直接创建上面的 Pod:
$ kubectl apply -f pod-prestop.yaml
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hook-demo2 1/1 Running 0 67s
hook-demo3 1/1 Running 0 67s
创建完成后,我们可以直接删除 hook-demo2 这个 Pod,在容器删除之前会执行 preStop 里面的优雅关闭命令,这个用法在后面我们的滚动更新的时候用来保证我们的应用零宕机非常有用。第二个 Pod 我们声明了一个 hostPath 类型的 Volume,在容器里面声明挂载到了这个 Volume,所以当我们删除 Pod,退出容器之前,在容器里面输出的信息也会同样的保存到宿主机(一定要是 Pod 被调度到的目标节点)的/tmp目录下面,我们可以查看 hook-demo3 这个 Pod 被调度的节点:
$ kubectl describe pod hook-demo3
Name: hook-demo3
Namespace: default
Priority: 0
Node: node1/10.151.30.22
Start Time: Tue, 12 Nov 2019 14:33:54 +0800
可以看到这个 Pod 被调度到了node1这个节点上,我们可以先到该节点上查看/tmp目录下面目前没有任何内容:
$ ls /tmp/
现在我们来删除 hook-demo3 这个 Pod,安装我们的设定在容器退出之前会执行preStop里面的命令,也就是会往 message 文件中输出一些信息:
$ kubectl delete pod hook-demo3
pod "hook-demo3" deleted
$ ls /tmp/
message
$ cat /tmp/message
Hello from the preStop Handler
另外 Hook 调用的日志没有暴露个给 Pod,所以只能通过 describe 命令来获取,如果有错误将可以看到FailedPostStartHook或FailedPreStopHook这样的 event。
poststart-pod.yaml
# pod当中的 pod hook 之 postStart
# 主要讲解pod hook的使用当中的postStart 这个钩子在容器创建后立即执行
apiVersion: v1
kind: Pod
metadata:
name: hook-demo1
labels:
app: posthook
namespace: default
spec:
containers:
- name: hook-demo1
image: nginx
lifecycle: # 开始声明钩子
postStart: # 声明的钩子的类型是postStart 容器创建完成之后立马执行的部分
exec: # exec是两种执行方式的一种 另外一种是http
command: ["/bin/sh","-c","apt-get update && apt-get -y install vim"] # command当中可以同时执行多条记录 在这里不允许使用args nginx容器当中要使用的是apt-get apt-get -y install xxx表示遇到让你确认的直接选择Y通过
# 如何验证呢?
# 首先通过kubectl get pods -o wide 获取到hook-demo1这个pod所在的node节点 然后登陆到这个node节点当中去 然后通过命令 docker ps -a | grep hook-demo1
# 找到对应的容器 然后进入到容器内部 找到路径/usr/share/message进行查看即可
# 另外一种方式就是通过kubectl exec -it hook-demo1 cat /usr/share/message 查看 因为我们的hook-demo1这个pod当中就一个容器 所以默认不需要指定容器名称
# 如果是多个容器的话需要运行 kubectl exec -it hook-demo1 sh 进入容器内部查看即可
# 涉及到的命令:
# docker ps -a | grep hook-demo1 根据关键词名称查询容器
prestop-pod.yaml
# pod当中的 pod hook 之 pod-prestop
# 主要讲解pod hook的使用当中的preStop这个钩子在容器退出之前立马执行
apiVersion: v1
kind: Pod
metadata:
name: hook-dem3
labels:
app: hook-demo3
namespace: default
spec:
volumes: # 在顶层声明一个volumes
- name: message
hostPath: # 类型为hostPath 那肯定得挂载到宿主机了
path: /tmp # 宿主机根目录
containers:
- name: hook-demo3
image: nginx
ports:
- containerPort: 80
volumeMounts: # 容器内声明要挂载的数据卷
- name: message # 必须和顶层声明的volumes的name保持一致
mountPath: /usr/share/ # 容器内部的/usr/share将会和宿主机的/tmp文件实现共享
lifecycle: # 开始声明钩子
preStop: # 声明的钩子的类型是preStop 容器退出之前立马执行的部分
exec: # 具体的执行命令 优雅的关闭nginx服务
command: ['/bin/sh','-c','echo hello from the preStop handler > /usr/share/message'] # 具体执行的命令
---
apiVersion: v1
kind: Pod
metadata:
name: hook-demo2
spec:
containers:
- name: hook-demo2
image: nginx
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"] # 优雅退出
6、Pod健康检查
现在在 Pod 的整个生命周期中,能影响到 Pod 的就只剩下健康检查这一部分了。在 Kubernetes 集群当中,我们可以通过配置liveness probe(存活探针)和readiness probe(可读性探针)来影响容器的生命周期:
- kubelet 通过使用liveness probe来确定你的应用程序是否正在运行,通俗点将就是是否还活着。一般来说,如果你的程序一旦崩溃了, Kubernetes 就会立刻知道这个程序已经终止了,然后就会重启这个程序。而我们的 liveness probe 的目的就是来捕获到当前应用程序还没有终止,还没有崩溃,如果出现了这些情况,那么就重启处于该状态下的容器,使应用程序在存在 bug 的情况下依然能够继续运行下去。
- kubelet 使用readiness probe来确定容器是否已经就绪可以接收流量过来了。这个探针通俗点讲就是说是否准备好了,现在可以开始工作了。只有当 Pod 中的容器都处于就绪状态的时候 kubelet 才会认定该 Pod 处于就绪状态,因为一个 Pod 下面可能会有多个容器。当然 Pod 如果处于非就绪状态,那么我们就会将他从 Service 的 Endpoints 列表中移除出来,这样我们的流量就不会被路由到这个 Pod 里面来了。
- StartupProbe启动探针
和前面的钩子函数一样的,我们这两个探针的支持下面几种配置方式:
- exec:执行一段命令
- http:检测某个 http 请求
- tcpSocket:使用此配置,kubelet 将尝试在指定端口上打开容器的套接字。如果可以建立连接,容器被认为是健康的,如果不能就认为是失败的。实际上就是检查端口。
更多推荐



所有评论(0)