「伸手摘星,即使一无所获,亦不致满手污泥」。
「请关注公众号:星河之码」
pod是kubernetes中最小的运行单元,也是核心组件之一,pod从创建到销毁经历了好几个过程,这些过程即是它的生命周期。
在了解Pod的生命周期之前先来看看「Pod的结构」
在上述中可以看到「pod内部可以包含一个或者多个容器,容器类型分为两部分」
「用户容器」
用户以容器形式在pod中运行一个或多个的程序,
「Pause」
Pod根容器,每个pod都会拥有
以根容器为依据评估整个pod健康状态 「可以在根容器上设置IP地址,其它容器都共享此IP(Pod的IP),以实现Pod内部的网络通信」
我们知道Pod被创建之后会分配一个容器Ip,
假设这个Ip为10.84.73.10
Pod中有两个用户容器,分别是nginx服务暴露80端口,mysql服务暴露3306端口
此时,我们在容器内就可以通过
10.84.73.10:80访问Nginx
10.84.73.10:3306访问mysql
一、pod的生命周期是什么
「pod对象从开始创建到终止退出的时间范围称为生命周期」。它主要包含以下的过程:
pod创建过程 运行初始化容器(init container)过程 运行主容器(main container) 容器启动后钩子(post start)、容器终止前钩子(pre stop) 容器的存活性检测(liveness probe)、就绪性检测(readiness probe) pod终止过程
下图是pod生命周期的执行过程演示图
接下来以这张图来分析分析Pod的整个生命周期都做了什么
二、Pod的相位(状态)
既然Pod有生命周期,那么它的「生命周期之内必然会存在做不同事情的不同的时期,这些时期就是Pod的状态,也称之为相位」。
kubectl get pod -o wide
通过上述命令可以看到在pod的信息中有一个status,这就是pod的状态,也就是相位,「无论是手动创建还是通过控制器创建pod,pod对象最终总是处于其生命周期中以下五种相位之一」:
「pending」
apiserver创建了pod资源对象并存入etcd中,但它尚未被调度完成或者仍处于下载镜像的过程中
「running」
Pod 包含的所有容器都已经成功创建完成,并且已经被调度至某节点运行起来
「succeeded」
Pod中的所有容器都已经成功终止并且不会被重启
「failed」
所有容器都已经终止,但至少有一个容器终止失败,即容器返回了非0值的退出状态或已经被系统终止。
「unknown」
apiserver无法正常获取到pod对象的状态信息,通常是由于其无法于所在工作节点的 kubelet通信所致。
上图并没有体现unknown状态,这是因为unknown是apiserver无法感知而进入的状态,并不是Pod的正常状态流转进入的。
因为apiserver无法感知的原因有很多,apiserver无法感知Pod时,Pod也有可能处于任何状态进入unknown
「以上5中状态只是pod常见的状态,也是最终的状态,但是在pod的状态转变过程还有很多其他的状态」,如下
状态 | 说明 |
---|---|
Unschedulable | Pod不能被调度,kube-scheduler没有匹配到合适的node节点 |
PodScheduled | pod正处于调度中,在kube-scheduler刚开始调度的时候,还没有将pod分配到指定的node,在筛选出合适的节点后就会更新etcd数据,将pod分配到指定的node |
Failed | Pod中有容器启动失败而导致pod工作异常 |
Unknown | 由于某种原因无法获得pod的当前状态,通常是由于与pod所在的node节点通信错误 |
Initialized | 所有pod中的初始化容器已经完成了 |
ContainerCreating | pod正在创建中 |
Running | Pod内部的容器已经被创建并且启动 |
Ready | 表示pod中的容器已经可以提供访问服务。 |
Completed | pod运行完成 |
Error | pod启动过程中发生错误 |
NodeLost | Pod所在节点失联 |
Waiting | Pod等待启动 |
Terminating | Pod正在被销毁 |
CrashLoopBackOff | pod创建失败,但是kubelet正在将它重启 |
ErrImagePull | 镜像拉取出错,超时或下载被强制终止 |
ImagePullBackOff | Pod所在的node节点下载镜像失败 |
Pending | 正在创建Pod但是Pod中的容器还没有全部被创建完成=处于此状态的Pod应该检查Pod依赖的存储是否有权限挂载等。 |
InvalidImageName | node节点无法解析镜像名称导致的镜像无法下载 |
ImageInspectError | 无法校验镜像,镜像不完整导致 |
ErrImageNeverPull | 策略禁止拉取镜像,镜像中心权限是私有等 |
RegistryUnavailable | 镜像服务器不可用,网络原因或harbor宕机 |
CreateContainerConfigError | 不能创建kubelet使用的容器配置 |
CreateContainerError | 创建容器失败 |
RunContainerError | pod运行失败,容器中没有初始化PID为1的守护进程等 |
ContainersNotInitialized | pod没有初始化完毕 |
ContainersNotReady | pod没有准备完毕 |
PodInitializing | pod正在初始化中 |
DockerDaemonNotReady | node节点decker服务没有启动 |
NetworkPluginNotReady | 网络插件没有启动 |
三、Pod的创建过程
「Pod的创建由客户端调用api server开始,到启动容器总共经历以下九个步骤」:
用户通过kubectl或其他api客户端提交创建的pod信息给api server
api server尝试着将pod对象的相关信息存入etcd中,待写入操作执行完成,api server即会返回 确认信息至客户端。
api server开始反映etcd中pod对象的状态变化,所有的k8s组件均使用watch机制来跟踪检查api server上的相关变动
kube-scheduler通过其watch觉察到api server创建了新的pod对象但尚未绑定至任何工作节点
kube-scheduler为pod对象挑选一个工作节点(bind pod)并将结果信息更新至api server
调度结果信息由api server更新至etcd,而且api server也开始反映此pod对象的调度结果
某个node节点上的kubectl发现有pod调度过来,kubelet尝试在当前节点上调用docker启动容器,并将容器的结果状态回送至api server
apiserver将接收到的pod状态信息存入etcd中
在etcd确认写入操作成功完成后,api server将确认信息发送至相关的kubelet
「这个九个步骤可以通过以下时序图体现」:
四、初始化容器
4.1 initC是什么
从上面的Pod生命周期可以看出,除了创建应用容器(main container)之外,还可以为pod对象定义其生命周期中的多种行为,如初始化容器、存活性探测及就绪性探测等。
「初始化容器即在pod的应用程序主容器启动之前要运行的容器,常用于为主容器执行一些预置操作」
其实就跟Spring启动的时候,初始化一样
它们具有两种典型特征
「初始化容器必须运行完成直至结束,若某初始化容器运行失败,那么k8s需要重启它直到成功完成」
「每个初始化容器都必须按定义的顺序串行运行」
比如上面Pod生命周期图中,必须init container1执行完后再执行init container2,然后是init container3
4.2 initC应用场景
很多时候,我们部署应用需要「等待其他相关联组件服务可用、基于环境变量或配置模板为应用程序生成配置文件、从配置中心获取配置等」,这些都需要在应用容器启动之前进行部分初始化操作,初始化容器常见的应用场景如下:
用于运行特定不适于包含在主容器镜像中的工具程序 提供主容器镜像中不具备的工具程序或自定义代码 为容器镜像的构建和部署人员提供了分离、独立工作的途径,使得它们不必协同起来制作单个镜像文件 初始化容器和主容器处于不同的文件系统视图中,因此可以分别安全地使用敏感数据,例如 secrets资源 初始化容器要先于应用容器串行启动并运行完成,因此可用于延后应用容器的启动直至其依赖的条件得到满足
「pod资源的spec.initContainers字段以列表的形式定义可用的初始容器,其嵌套可用字段类似于 spec.containers」
4.3initC特点
initC总是运行到成功完成为止 每个initC容器都必须在下一个initC启动之前成功完成 如果initC容器运行失败,K8S集群会不断的重启该pod,直到initC容器成功为止 如果pod对应的restartPolicy为never,它就不会重新启动
4.4 initC(initContainers)示例
以busybox镜像为例,在创建一个Pod之前让它等待两个initC的启动
「先拉取一个busybox镜像」
docker pull busybox:1.32.0
「编写initcPod.yml」
apiVersion: v1
kind: Pod
metadata:
name: initc-test-pod
labels:
app: initc-app
spec:
# 这个是主容器
containers:
- name: initc-app-container
image: busybox:1.32.0
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
# 这里定义了两个InitC 这两个initC按顺序执行完成后才会执行主容器
initContainers:
#第一个initC 是init-service
- name: init-service
image: busybox:1.32.0
imagePullPolicy: IfNotPresent
#第一个initC去循环检查一个myservice的服务,直到检测到为止 检测到后睡眠两秒
command: [ 'sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;' ]
#第一个initC 是init-db
- name: init-db
image: busybox:1.32.0
imagePullPolicy: IfNotPresent
#第二个initC去循环检查一个mydb的服务,直到检测到为止 检测到后睡眠两秒
command: [ 'sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;' ]
restartPolicy: Always「上传initcPod.yml」
我是直接通过idea的Remote Host上传
「启动initcPod.yml」
kubectl apply -f initcPod.yml
「查看Pod的详情」
kubectl describe pod initc-test-pod
「查看日志」
通过上面的pod的信息,可以直到第一个initContainers已经启动,但是还没有执行完成,接下来看init-service的日志
kubectl logs initc-test-pod -c init-service
通过日志可以看到,init-service这个initContainers在找一个myservice的服务,没有找到,所以一直没有启动成功,既然如此,那就给他创建一个service
「创建myservice.yml」
apiVersion: v1
kind: Service
metadata:
#这个名字要跟上面initC中找的service名字一样,不然还是找不到
name: myservice
spec:
selector:
app: myservice
ports:
- port: 80
targetPort: 8990
protocol: TCP「上传并执行myservice.yml」
kubectl apply -f myservice.yml
「查看pod」
kubectl get pod -o wide
这个过程可能有点慢,可能需要一两分钟才刷新
可以看到,pod的init已经成功启动了一个
「再次查看pod的详细信息」
kubectl describe pod initc-test-pod
第一个initc已经执行完成,现在在执行第二个initc,第二个initc也没有执行完成,可以继续看看第二个initc init-db的日志
「查看init-db的日志」
kubectl logs initc-test-pod -c init-db
通过日志,init-db也在循环找一个mydb的服务,没有找到,所以一直卡着,那就给它mydb的服务
「创建mydb.yml」
apiVersion: v1
kind: Service
metadata:
#这个名字同样要跟上面initC中找的service名字一样,不然还是找不到
name: mydb
spec:
selector:
app: mydb
ports:
- port: 80
targetPort: 8991
protocol: TCP「上传mydb.yml」
「执行mydb.yml」
kubectl apply -f mydb.yml
「查看pod」
kubectl get pod -o wide
「等待pod刷新命令」
在执行mydb.yml等服务后,pod要等一会才会刷新状态,我们可以手动执行
kubectl get pod
,多刷几次,也可以执行以下命令,等待刷新kubectl get pod -w
五、容器探测
5.1容器探测的探针种类
「容器探测用于检测容器中的应用实例是否能正常工作,是保障业务可用性的一种传统机制。它是kubelet对容器周期性执行的健康状态诊断,诊断操作由容器的处理器进行定义」。
如果经过探测,实例的状态不符合预期,那么K8S就会把该问题实例剔除,不承担业务流量
「k8s提供了两种探针来实现容器探测」,分别是:
「容器的存活性检测(liveness probe)」
用于检测应用实例当前是否处于正常运行状态
一旦存活性检测未通过,kubelet将杀死容器并根据restartPolicy决定是否将其重启;未定义存活性检测的容器的默认状态未success
「就绪性检测(readiness probe)」
用于判断容器是否准备就绪并可对外提供服务
一旦存绪性检测未通过,端点控制器会将其IP从所有匹配到此pod对象的service对象的端点列表中移除,此容器不接受请求
检测通过之后,会再次将其IP添加至端点列表中。
5.2 容器探测的探测方式
「k8s的存活性检测和绪性检测目前均支持三种容器探针用于pod探测」
跟钩子函数的三种方式一样,但是他们的节点有一些差别
「Exec命令」
「在容器中执行一个命令,并根据其返回的状态码进行诊断的操作称为Exec探测,状态码为0表示成功,否则即为不健康状态」
......
livenessProbe:
exec:
command:
- cat
- tmp/healthy
......
「TCPSocket」
「通过与容器的某TCP端口尝试建立连接进行诊断,端口能够成功打开即为正 常,否则为不健康状态」
......
livenessProbe:
tcpSocket:
port: 8080
......
「HTTPGet」
「通过向容器IP地址的某指定端口的指定path发起HTTP GET请求进行诊断,响应码大于等于200且小于400时即为成功,否则不正常。」
......
lifecycle:
postStart:
httpGet:
path: #uri地址
port:
host:
scheme: HTTP #支持的协议,http或者https
......
以上三种任何一种探测方式都可能存在三种结果:
success(成功):容器通过了诊断
failure(失败):容器未通过了诊断
unknown(未知):诊断失败,因此不会采取任何行动
5.3 就绪探测(readiness probe)示例
通过Nginx容器,探测Nginx容器是否正常启动,检测方式就以HTTPGet方式,去请求一个index1.html
「编写readiness-probe.yml」
apiVersion: v1
kind: Pod
metadata:
name: readiness-probe-pod
labels:
app: readiness-probe-pod
spec:
containers:
- name: nginx-container-edwin
image: nginx:1.17.10-alpine
ports:
- name: nginx-port
containerPort: 80
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 80
#访问容器根目录下的index1.html
path: index1.html
#表示在容器启动后,延时多少秒才开始探测
initialDelaySeconds: 1
#表示多久进行一次探测,默认是10S,最小值是1
periodSeconds: 3
restartPolicy: Always「上传执行readiness-probe.yml」
kubectl apply -f readiness-probe.yml
「查看pod详细信息」
kubectl describe pod readiness-probe-pod
「进入pod」
#进入pod内部,因为是alpine系统,需要使用sh命令
kubectl exec -it readiness-probe-pod sh「进入容器目录」
#Nginx默认访问这个目录
cd usr/share/nginx/html/「创建index1.html并且追加内容」
echo "welcome Edwin Nginx index" >> index1.html
#退出容器
exit「再次查看Pod的状态」
kubectl get pods -o wide
5.4 存活探测(liveness probe)示例
就绪探测在实际的工作中用的很多,还是以上述Nginx为例子,分别以Exec命令、TCPSocket、HTTPGet三种方式实现就绪探测
5.4.1 Exec命令就绪探测
「编写liveness-probe-exec.yml」
yml文件做三件事
apiVersion: v1
kind: Pod
metadata:
name: liveness-probe-exec-pod
labels:
app: liveness-probe-exec-pod
spec:
containers:
- name: liveness-probe-exec-container
image: nginx:1.17.10-alpine
imagePullPolicy: IfNotPresent
#容器启动之后去/usr/share/nginx/html目录下创建一个index1.html,30秒后删除
command: ["/bin/sh","-c","touch /usr/share/nginx/html/index1.html; sleep 30; rm -rf /usr/share/nginx/html/index1.html; sleep 3600"]
livenessProbe:
exec:
#去/usr/share/nginx/html/下检测有没有index1.html文件 如果没有检测到,认为当前容器没有存活,会重新启动
command: [ "test","-e","/usr/share/nginx/html/index1.html" ]
#表示在容器启动后,延时多少秒才开始探测
initialDelaySeconds: 1
#表示多久进行一次探测,默认是10S,最小值是1
periodSeconds: 3
#重启机制
restartPolicy: Always启动一个Nginx容器,并且启动后创建一个 /usr/share/nginx/html/index1.html
,30秒后删除通过存活探测,3秒检测一次 /usr/share/nginx/html/index1.html
是否存在如果不存在则认为当前容器不再存活状态,会重新启动 「上传并执行liveness-probe-exec.yml」
kubectl apply -f liveness-probe-exec.yml
「查看pod的启动情况」
#-w 可以一直监听
kubectl get pod -w「等待30秒后,发现pod的RESTARTS值从0变为1,说明pod已经重启一次」。
「CrashLoopBackOff」
重启一定次数之后,STATUS可能会变成CrashLoopBackOff,这是一个提示,告诉我们pod多次重启失败
「CrashLoopBackOff:是一种 Kubernetes 状态,表示 Pod 中发生的重启循环:Pod 中的容器已启动,但崩溃然后又重新启动,一遍又一遍」。
5.4.2 TCPSocket就绪探测
「编写liveness-probe-tcpocket.yml」
apiVersion: v1
kind: Pod
metadata:
name: liveness-probe-tcpocket-pod
labels:
app: liveness-probe-tcpocket-pod
spec:
containers:
- name: liveness-probe-tcpocket-container
image: nginx:1.17.10-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: nginx-httpget
livenessProbe:
httpGet:
port: 80
#Nginx默认访问/usr/share/nginx/html/index.html
path: /index.html
#表示在容器启动后,延时多少秒才开始探测
initialDelaySeconds: 1
#表示多久进行一次探测,默认是10S,最小值是1
periodSeconds: 3
#探针需要的时间,执行探测的超时的秒数,默认值 1,最小值 1
timeoutSeconds: 10
#重启机制
restartPolicy: Always「上传并执行liveness-probe-tcpocket.yml」
kubectl apply -f liveness-probe-tcpocket.yml
「查看pod的启动情况」
#-w 可以一直监听
kubectl get pod -w「等待一会后,发现pod的RESTARTS值从0开始累加,说明pod已经开始一次次的重启」。
5.4.3 HTTPGet就绪探测
「编写liveness-probe-httpget.yml」
apiVersion: v1
kind: Pod
metadata:
name: liveness-probe-httpget-pod
labels:
app: liveness-probe-httpget-pod
spec:
containers:
- name: liveness-probe-httpget-container
image: nginx:1.17.10-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: nginx-httpget
livenessProbe:
httpGet:
port: 80
#Nginx默认访问/usr/share/nginx/html/index.html
path: /index.html
#表示在容器启动后,延时多少秒才开始探测
initialDelaySeconds: 1
#表示多久进行一次探测,默认是10S,最小值是1
periodSeconds: 3
#探针需要的时间,执行探测的超时的秒数,默认值 1,最小值 1
timeoutSeconds: 10
#重启机制
restartPolicy: Always「上传并执行liveness-probe-tcpocket.yml」
kubectl apply -f liveness-probe-httpget.yml
「查看pod的启动情况」
#-w 可以一直监听
kubectl get pod -w「等待一会后,发现pod的RESTARTS值从0开始累加,说明pod已经开始一次次的重启」。
「删除容器的index.html」
既然是Nginx中的index.html已经存在的,那我们去吧它删掉看看
# 当前xshell终端保持 -w 监听 另外起一个终端通过sh进入pod内部删除/usr/share/nginx/html/index.html文件
kubectl exec -it liveness-probe-httpget-pod sh
rm -rf /usr/share/nginx/html/index.html也可以通过Curl访问以下index.html文件,看看是否是新的pod,新的pod肯定可以访问到index.html,因为原来pod中的index.html已经被删除
「删除容器内文件也可以用以下命令」
kubectl exec -it liveness-probe-httpget-pod -- rm -rf usr/share/nginx/html/index.html
六、钩子函数
6.1 钩子函数的作用
「容器生命周期钩子函数能够感知其自身生命周期管理中的事件,并在相应的时刻到来时运行由用户指定的处理程序代码」。k8s为容器提供了主容器的启动之后和停止之前两种生命周期钩子:
「容器启动后钩子(post start)」
在容器创建完成之后立即运行的钩子处理器(handler),不过k8s无法确保它一定会于容器中的entrypoint之前运行
「容器终止前钩子(pre stop)」
在容器终止操作之前立即运行的钩子处理器,执行完成之后容器将成功终止,它以同步的方式调用,因此「在其完成之前会阻塞删除容器的操作调用」。
6.2 钩子函数的使用方式
钩子处理器支持使用【「Exec命令、TCPSocket、HttpGet」】三种方式定义动作:
「Exec命令」
「在容器内执行一次命令」
......
lifecycle:
postStart:
exec:
command:
- cat
- tmp/healthy
......
「TCPSocket」
「在当前容器尝试访问指定的socket」
......
lifecycle:
postStart:
tcpSocket:
port: 8080
......
「HttpGet」
「在当前容器中向某url发起http请求」
......
lifecycle:
postStart:
httpGet:
path: #uri地址
port:
host:
scheme: HTTP #支持的协议,http或者https
......
6.3 钩子函数示例
通过Exec命令的方式来演示一下钩子函数的使用,通过启动一个nginx容器,在pod创建之前去修改Nginx的首页信息
「拉取Nginx的镜像」
docker pull nginx:1.17.10-alpine
「编写文件lifeclepod」
apiVersion: v1
kind: Pod
metadata:
name: nginx-hook-exec-pod
spec:
containers:
- name: main-container
image: nginx:1.17.10-alpine
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"]「上传执行lifeclepod.yml」
kubectl apply -f lifeclepod.yml
「进入容器中」
kubectl exec -it nginx-hook-exec-pod sh
cat /usr/share/nginx/html/index.htmlpreStop停止的时候不好进容器,不好体现
七、容器的重启策略
「容器程序发生奔溃或容器申请超出限制的资源等原因都可能会导致pod对象的终止,当Pod的发生终止的时候,K8S的会根据重启策略进行重启」,具体的重启策略在编写Yml文件的时候也有用到过,看一下案例
apiVersion: v1
kind: Pod
metadata:
#给pod起一个名字
name: edwin-tomcat-test-yml
labels:
app: edwin-tomcat-test-yml
spec:
containers:
- name: edwin-tomcat-test-yml
#pod的基础镜像
image: tomcat:9.0.20-jre8-alpine
imagePullPolicy: IfNotPresent
restartPolicy: Always
#restartPolicy:
# Always:只要退出就重启。
# OnFailure:失败退出时(exit code不为0)才重启
# Never:永远不重启
K8S通过restartPolicy属性的定义来觉得Pod异常是否重启,已经怎么重启。restartPolicy有三个值:
「Always」
pod对象终止就将其重启,「默认策略」。
「OnFailure」
当容器异常退出(退出状态码非0)时,才重启容器。适于job
「Never」
当容器终止退出,从不重启容器。适于job
八、Pod终止过程
Pod可以通过yml文件或者名称被删除,命令如下
kubectl delete pod podname
kubectl apply -f xxx.yml
当用户提交删除请求(上述命令)之后,系统就会进行强制删除操作的宽限期倒计时,并将TERM信息发送给pod对象的每个容器中的主进程。宽限期倒计时结束后,这些进程将收到强制终止的KILL信号,pod对象随即也将由api server删除。
如果在等待进程终止的过程中,kubelet或容器管理器发生了重启,那么终止操作会重新获得一个满额的删除宽限期并重新执行删除操作。
「一个完整的pod对象终止流程具体如下」
用户发送删除pod对象的命令
api服务器中的pod对象会随着时间的推移而更新,在宽限期内(默认30s),pod 被视为【dead】状态
将pod标记为【Terminating】状态
与第三步同时运行,kubelet在监控到pod对象转为【Terminating】状态的同时启动pod关闭过程
与第三步同时运行,端点控制器监控到pod对象的关闭行为时将其从所有匹配到此端点的service 资源的端点列表中移除
如果当前pod对象定义了【preStop】钩子处理器,则在其标记为【Terminating】后即会以同步的方式启动执行该钩子处理器
若宽限期结束后,preStop仍未执行结束,则第二步会被重新执行并额外获取一个时长为2s 的小宽限期
pod对象中的容器进程收到TERM信号
宽限期结束后,若存在任何一个仍在运行的进程,那么pod对象即会收到【SIGKILL】信号
kubelet请求api server将此pod资源的宽限期设置为0从而完成删除操作,它变得对用户不再可 见。