K8s基础-Pod
POD介绍
POD特点
- K8s最小部署单元,里面封装了一个或多个容器。
- pod内部的容器共享存储、网络、PID、IPC等。容器之间可以通过localhost:port互相访问。可以通过volume实现数据共享。
- 生命周期短暂,重启之后又变成新的POD。
POD存在的意义
- 对于多容器协作:pod的多容器管理更加高效,更加方便(比如pod内sidecar模式集成日志收集、服务网格等)
- 对于强依赖服务:pod把多容器放在一起,他们之间可以通过网络共享来通信,更加高效。
- 简化应用的生命周期管理:k8s对pod的readiness管理比容器完善。
- 兼容多种运行时:适应容器技术的变化。容器化技术不一定要用docker,换成别的也要支持。
Pause容器
-
每一个pod里面自动有一个根容器 pause(也叫infra容器),除此之外有许多业务容器(用户容器)。
-
kubectl get pods 看到的 READY 1/1 说明该pod里面有1个容器(其实还有个根容器但是默认不显示)。
-
pause容器作用:
-
以它为依据,评估其他容器的健康状态。
- 启动Pod时,会先启动⼀个pause容器,然后将后续的所有容器都link到这个pause容器,以实现⽹络共享。给他设置一个IP地址(POD IP),其他容器通过此IP来进行内部通信。
- 存储共享、网络共享都是通过pause实现的。

POD中的容器
Pod中可以同时运行多个容器。同一个Pod中的容器共享资源、网络环境,它们总是被同时调度,只有当你的容器需要紧密配合协作的时候才考虑用这种模式。例如,你有一个容器作为web服务器运行,需要用到共享的volume,有另一个“sidecar”容器来从远端获取资源更新这些文件。一些Pod有init容器和应用容器。在应用程序容器启动之前,运行初始化容器。
1)所有容器使用同一个network namespace,共享一个IP地址和端口空间。意味着容器之间可以通过localhost高效访问,不能有端口冲突。
2)允许容器之间共享存储卷,通过文件系统交互信息。当K8s挂载Volume到Pod上,本质上是将volume挂载到Pod中的每一个容器里。
进入容器
kubectl exec -it -c <container name> -- /bin/bash
POD应用示例
-
代码自动发版更新
-
假如生产环境部署了一个go的应用,而且部署了几百个节点,希望这个应用可以定时的同步最新的代码,以便自动升级线上环境。这时,我们不希望改动原来的go应用,可以开发一个Git代码仓库的自动同步服务,然后通过Pod的方式进行编排,并共享代码目录,就可以达到更新java应用代码的效果。

-
日志收集服务
-
某服务模块已经实现了一些核心的业务逻辑,并且稳定运行了一段时间,日志记录在了某个目录下,按照不同级别分别为 error.log、access.log、warning.log、info.log,现在希望收集这些日志并发送到统一的日志处理服务器上。
-
这时我们可以修改原来的服务模块,在其中添加日志收集、发送的服务,但这样可能会影响原来服务的配置、部署方式,从而带来不必要的问题和成本,也会增加业务逻辑和基础服务的藕合度。
-
如果使用Pod的方式,通过简单的编排,既可以保持原有服务逻辑、部署方式不变,又可以增加新的日志收集服务。
- 而且如果我们对所有服务的日志生成有一个统一的标准,或者仅对日志收集服务稍加修改,就可以将日志收集服务和其他服务进行Pod编排,提供统一、标准的日志收集方式。

Pod字段配置
端口号
容器的端口号spec.containers.ports.containerPort这个字段,仅仅是声明了容器暴露了哪个端口,方便查看。与实际容器里面程序暴露了什么端口没有关系。K8s并不知道程序暴露了哪个端口,并不是你pod设置了80端口,容器就暴露80,设置了81就暴露81。
所以一个pod内多个容器暴露的端口在设计程序的时候就要注意不能冲突,不是pod字段里面声不一样的端口就能避免的,避免不了。
启动命令
spec.containers.command和spec.containers.args两个字段,可以覆盖容器内的entrypoint和cmd。
resources
- requests的资源是直接划分给pod的,即使pod没有使用,所以有时候即使宿主机有资源,但是不能分配了,是因为pod的request已经把资源划分完了,只不过还没实际使用。(
free -m查看宿主机占用很小,但是实际上已经分配给了pod,所以新pod就pending了) - 注意:如果只配置了limits不配置requests,会自动帮你把request写成和limits一样的值。所以有时候看到没有配置requests,但是还是无法调度,可能就是这个原因。
环境变量
env:
- name: path
value: test
- name: POD_NAME
valuesFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: LABEL_APP
valueFrom:
fieldRef:
fieldPath: metadata.labels['run']
fieldRef可选的字段:
metadata.name
metadata.namespace
metadata.uid
metadata.labels[xxx]
metadata.annotations[xxx]
spec.nodeName
spec.serviceAccountName
status.hostIP
status.hostIPs
status.podIP
status.podIPs
POD重启策略
Pod.spec.restartPolicy字段
- Always:kubelet会定期查询容器的状态,一旦某个容器处于退出状态(正常退出后是Completed状态),就对其执行重启操作,这是默认值。【保持Always就行】
- OnFailure:容器异常退出(也就是退出码不为0)时重启。正常退出不会重启。
- Never: 不论任何状态,都不重启该容器。
测试yaml文件:
apiVersion: v1
kind: Pod
metadata:
name: bb-rp
labels:
app: busybox
spec:
containers:
- name: busybox
image: busybox:latest
command: ["/bin/sh"]
args: ["-c", "sleep 10, exit 1"]
restartPolicy: Always
POD生命周期
K8S文档:Pod 的生命周期 | Kubernetes

- kubelet创建pause根容器
- 运行Initcontainer
- 先于主容器运行一些自定义工具程序或自定义代码。串行进行,前一个失败不会运行后一个。
-
作用在整个pod范围的。
-
运行主容器
- 主容器启动之后--post start hook
- 存活性探测、就绪性探测
- 停止前钩子 (每个container都可以定义各自的钩子)
- pod终止过程
pod创建过程
-
用户通过kubectl或其他api客户端提交需要创建的pod信息给apiServer
-
apiServer开始生成pod对象的metadata,并将信息存入etcd,然后返回确认信息至客户端
-
其它组件使用watch机制来跟踪检查apiServer上的变动。scheduler发现有新的pod对象要创建,开始为Pod分配主机并将结果信息更新至apiServer
-
node节点上的kubelet发现有pod调度过来,调用容器运行时启动容器,并将结果回送至apiServer
-
apiServer将接收到的pod状态信息存入etcd中

Pod启动过程
kubectl create pod -- pending -- ContainerCreating -- InitContainer -- Container Running -- Startup Probe -- Liveness/Readiness Probes -- Endpoint添加pod IP
Startup Probe是在另外两个之前运行的。
pod删除过程
- 用户向apiServer发送删除pod的命令。
- apiServcer中的pod信息会在宽限期内(默认30s)被视为dead。以下三个步骤同步执行:
- kubelet将pod转为Terminating状态
- endpoint控制器监控到pod关闭,将与pod IP剔除。
-
如果当前pod对象定义了
preStop钩子处理器,terminating时同步执行。如果宽限期结束PreStop仍未结束,再获得两秒宽限期 -
宽限期结束后,若pod中还存在仍在运行的进程,那么pod会收到立即终止
SIGKILL的信号。 - kubelet请求apiServer将此pod资源的宽限期设置为0从而完成删除操作,此时pod对于用户已不可见。
# 强制删除pod
kuebctl delete po xxx --force --grace-period=0
钩子函数
介绍
在pod.spec.containers.lifecycle下面定义
- PostStart
- 启动后执行,如果执行失败,容器会按照重启策略重新启动。不需要传递任何参数。
- 用于资源部署、环境准备等。
注意:postStart并不是在容器启动命令之前运行的,并不能保证。可以理解为是同时运行的。所以这个功能并不适合做初始化操作。初始化操作最好加一个initContianer。一些不影响程序启动的命令可以加到preStart里面
- PreStop:
- 删除前执行,没执行完就会阻塞在这里
- 用于优雅关闭应用程序、通知其他系统等。
优雅关闭
当用户删除含有pod的资源对象时(如RC、deployment等),K8S为了让应用程序优雅关闭(即让程序完成正在处理的请求后再关闭),K8S提供两种信息通知:
-
默认:K8S通知node执行docker stop命令,docker会先向容器中PID为1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认超时时间(30s),会继续发送SIGKILL的系统信号强行kill掉进程。
-
使用pod生命周期(利用PreStop回调函数),它执行在发送终止信号之前。
默认情况下,所有的删除操作的优雅退出时间都在30秒以内。kubectl delete命令支持--grace-period=的选项,以运行用户来修改默认值。0表示删除立即执行,并且立即从API中删除pod。在节点上,被设置了立即结束的的pod,仍然会给一个很短的优雅退出时间段,才会开始被强制杀死。
示例
apiVersion: v1
kind: Pod
metadata:
name: life-demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx:latest
imagePullPolicy: IfNotPresent
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c","echo 'lifecycle hookshandler' > /usr/share/nginx/html/test.html"]
preStop:
exec:
command: ["/bin/sh", "-c", "sleep", "10"] # sleep是一个非常常用的preStop命令,就让容器等一段时间再退出。
# k8s 1.30以上可以直接用sleep作为preStop
# sleep:
# seconds: 60
Pod状态
| 状态 | 说明 |
|---|---|
| Pending(挂起) | Pod已被Kubernetes系统接收,但仍有一个或多个容器未被创建,可以通过kubectl describe查看处于Pending状态的原因 |
| Running(运行中) | Pod已经被绑定到一个节点上,并且所有的容器都已经被创建,而且至少有一个是运行状态,或者是正在启动或者重启,可以通过kubectl logs查看Pod的日志 |
| Succeeded(成功) | 所有容器执行成功并终止,并且不会再次重启,可以通过kubectl logs查看Pod日志 |
| Failed/Error(失败) | 所有容器都已终止,并且至少有一个容器以失败的方式终止,也就是说这个容器要么以非零状态退出,要么被系统终止,可以通过logs和describe查看Pod日志和状态 |
| Unknown(未知) | 通常是由于通信问题造成的无法获得Pod的状态 |
| ImagePullBackOff/ErrImagePull | 镜像拉取失败,一般是由于镜像不存在、网络不通或者需要登录认证引起的,可以使用describe命令查看具体原因 |
| CrashLoopBackOff | 表示 Pod 中发生的重启循环:Pod 中的容器已启动,但崩溃然后又重新启动,一遍又一遍。<本身并不是一个错误,而是表明发生了一个错误,导致 Pod 无法正常启动。重启的原因是pod的重启策略设置为Always或者OnFailure,可以通过logs命令查看具体原因,一般为启动命令不正确,健康检查不通过等。 BackOff含义是:重启之间的指数延迟(10 秒、20 秒、40 秒……),上限为 5 分钟。当 Pod 状态显示 CrashLoopBackOff 时,表示它当前正在等待指示的时间,然后再重新启动 Pod。除非它被修复,否则它可能会再次失败。 |
| OOMKilled | 容器内存溢出,一般是容器的内存Limit设置的过小,或者程序本身有内存溢出,可以通过logs查看程序启动日志 |
| Terminating | Pod正在被删除,可以通过describe查看状态 |
| SysctlForbidden | Pod自定义了内核配置,但kubelet没有添加内核配置或配置的内核参数不支持,可以通过describe查看具体原因 |
| Completed | 容器内部主进程退出,一般计划任务执行结束会显示该状态,此时可以通过logs查看容器日志 |
| ContainerCreating | Pod正在创建,一般为正在下载镜像,或者有配置不当的地方,可以通过describe查看具体原因 |
| Evited | 多见于系统内存或硬盘资源不足 |
InitContainer
-
spec字段下的initContainers。可以有一个或多个,如果多个按照定义的顺序依次执行,先执行初始化容器1,再执行初始化容器2等,等初始化容器执行完具体操作之后初始化容器就退出了,只有所有的初始化容器执行完后,主容器才启动。
-
由于一个Pod里容器存储卷是共享的,所以Init Container里产生的数据可以被主容器使用到,Init Container可以在多种K8S资源里被使用到,如Deployment、DaemonSet, StatefulSet、Job等,但都是在Pod启动时,在主容器启动前执行,做初始化工作。
-
初始化容器与主容器区别是:
-
初始化容器不支持 Readinessprobe,因为它们必须在Pod就绪之前运行完成。
-
每个Init容器必须运行成功,下一个才能够运行。
示例:
#主容器运行nginx服务,初始化容器用来给主容器生成index.html文件
apiVersion: v1
kind: Pod
metadata:
name: init-nginx
spec:
volumes:
- name: pod-workdir
emptyDir: {} #把物理机的目录挂进pod
initContainers:
- name: install
image: docker.io/library/busybox:1.28
imagePullPolicy: IfNotPresent
command:
- wget
- "-O"
- "/work-dir/index.html"
- "https://www.baidu.com" #把百度主页下下来存到这个目录
volumeMounts:
- name: pod-workdir
mountPath: /work-dir
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
volumeMounts:
- name: pod-workdir
mountPath: /usr/share/nginx/html #俩容器挂了同一个物理机存储,是文件共享的,html文件会同步进来。
pod-init 0/1 Init:0/2 0 7s
#Init: 0/2,表示两个初始化容器未完成
POD健康探测
说明
- 这三种probe需要在yaml文件里面自己配。
- StartupProbe探测成功后才会进行LivenessProbe和ReadinessProbe。后两者是并行的,没有先后关系。
四种探测方法
- Exec命令:在容器内执行指定命令,如果命令执行的
退出码为0,则认为程序正常;退出码非0,则不正常。 - TCPSocket:将会尝试访问容器的
IP:端口,如果能够建立这条连接(发现容器在监听这个端口),则认为程序正常,否则不正常。 -
HTTPGet:通过
容器IP+端口号+路径,调用HTTP GET,如果返回的状态码在200-399之间,则认为程序正常,否则不正常。 -
gRPC:GRPC协议的健康检查,如果响应的状态是"SERVING",则认为容器健康。
三种探针
StartupProbe
- 是为了解决程序启动时间很长,启动慢问题的。如果不配这个,程序启动慢,端口起不来,livenessProbe检测不过,会一直重复被liveness探针重启,重启完程序又没启动完,又被重启了。进入CrashLookBackoff状态。(把initialDelaySeconds调高点也行,但是调高点又会造成服务漂移之后启动太慢。而且有时候服务启动的时间不固定,这样还是startupProbe更好用)
- 当配置了startupProbe启动探针,会先禁用其他探针,直到startupProbe探针成功,成功后将退出不再进行探测;如果startupProbe探针探测失败,pod将会根据重启策略重启。
- 如果容器没有提供启动探测,则默认状态为成功Success。
示例
- Exec模式
apiVersion: v1
kind: Pod
metadata:
name: startupprobe
spec:
containers:
- name: startup
image: tomcat-8.5-jre8:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
startupProbe:
exec:
command:
- "/bin/sh"
- "-c"
- "ps aux | grep tomcat"
initialDelaySeconds: 20 #容器启动后多久开始探测
periodSeconds: 20 #执行探测的时间间隔
timeoutSeconds: 10 #探针执行检测请求后,等待响应的超时时间
successThreshold: 1 #成功多少次才算成功
failureThreshold: 3 #失败多少次才算失败
- tcp socket模式
对于startupProbe,更推荐用tcpSocket方式,因为端口起来了说明程序没问题了。
startupProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 10 #容器启动后多久开始探测
periodSeconds: 5 #执行探测的时间间隔
timeoutSeconds: 2 #探针执行检测请求后,等待响应的超时时间
successThreshold: 1 #成功多少次才算成功
failureThreshold: 30 #失败多少次才算失败
- http get模式
startupProbe:
httpGet:
path: /health # 检查路径
port: 8080
scheme: HTTP
# httpHeaders: # 可选,请求头。可以传入一个token做一个简单认证。
# - name: end-user
# value: Jason
# 默认探测localhost:8080
initialDelaySeconds: 20 # 容器启动后多久开始探测。
periodSeconds: 20 #执行探测的时间间隔
timeoutSeconds: 10 #探针执行检测请求后,等待响应的超时时间
successThreshold: 1 # 探测成功多少次才算成功
failureThreshold: 3 # 探测失败多少次才算失败
LivenessProbe
- 用指定的方式(exec、tcp、http)检测pod中的容器是否正常运行。
- 如果检测失败,则认为容器不健康,Kubelet杀死容器,根据restartPolicy判断是否重启。
- 如果容器配置中没有配置 livenessProbe,Kubelet 将认为存活探针探测一直为success(成功)状态。
注意
存活探针是一种从应用故障中恢复的强劲方式,但应谨慎使用。你必须仔细配置存活探针,确保它能真正标示出不可恢复的应用故障,例如死锁。
示例
- exec模式
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec
labels:
app: liveness
spec:
containers:
- name: liveness
image: busybox:1.28
imagePullPolicy: IfNotPresent
args: #创建测试探针探测的文件
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
initialDelaySeconds: 10 #延迟检测时间
periodSeconds: 5 #检测时间间隔
exec:
command:
- cat
- /tmp/healthy
# 容器在初始化后,首先创建一个 /tmp/healthy 文件,然后执行睡眠命令,睡眠 30 秒,到时间后执行删除 /tmp/healthy 文件命令。而设置的存活探针检检测方式为执行 shell 命令,用 cat 命令输出 healthy 文件的内容,如果能成功执行这条命令,存活探针就认为探测成功,否则探测失败。在前 30 秒内,由于文件存在,所以存活探针探测时执行 cat /tmp/healthy 命令成功执行。30 秒后 healthy 文件被删除,所以执行命令失败,Kubernetes 会根据 Pod 设置的重启策略来判断,是否重启 Pod。
- http模式
apiVersion: v1
kind: Pod
metadata:
name: liveness-http
labels:
test: liveness
spec:
containers:
- name: liveness
image: docker.io/mydlqclub/springboot-helloworld:0.0.1 #ctr -n=k8s.io images import springboot.tar.gz解压
imagePullPolicy: IfNotPresent
livenessProbe:
initialDelaySeconds: 20 #延迟加载时间
periodSeconds: 5 #重试时间间隔
timeoutSeconds: 10 #超时时间设置
httpGet:
scheme: HTTP
port: 8081
path: /actuator/health
#上面 Pod 中启动的容器是一个 SpringBoot 应用,其中引用了 Actuator 组件,提供了 /actuator/health 健康检查地址,存活探针可以使用 HTTPGet 方式向服务发起请求,请求 8081 端口的 /actuator/health 路径来进行存活判断:任何大于或等于200且小于400的代码表示探测成功。任何其他代码表示失败。如果探测失败,则会根据重启策略做后续操作。
- tcp模式
apiVersion: v1
kind: Pod
metadata:
name: liveness-tcp
labels:
app: liveness-tcp
spec:
containers:
- name: liveness
image: nginx:latest
imagePullPolicy: IfNotPresent
livenessProbe:
initialDelaySeconds: 15
periodSeconds: 20
tcpSocket:
port: 80
#探测80端口起来没起来
#停掉nginx
#k exec -it liveness-tcp -- /bin/bash
#nginx -s stop ==> 会被存活探测自动重启。
ReadinessProbe
-
用于检测容器中的应用是否可以接受请求,当探测成功后才使Pod对外提供网络访问,将容器标记为就绪状态,可以加到pod前端负载。
-
如果探测失败,则将容器标记为未就绪状态,会把pod从endpoint中移除。
-
ReadinessProbe 和 livenessProbe 可以使用相同探测方式,只是对 Pod 的处置方式不同:
-
readinessProbe 当检测失败后,将 Pod 的 IP:Port 从对应的 EndPoint 列表中删除。
-
livenessProbe 当检测失败后,将杀死容器并根据 Pod 的重启策略来决定是否重启。
注意:Readiness不重启容器,之后Liveness才会重启。
示例
apiVersion: v1
kind: Pod
metadata:
name: springboot
labels:
app: springboot
spec:
containers:
- name: springboot
image: docker.io/mydlqclub/springboot-helloworld:0.0.1
imagePullPolicy: IfNotPresent
ports:
- name: server
containerPort: 8080
- name: management
containerPort: 8081
readinessProbe:
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 10
httpGet:
scheme: HTTP
port: 8081
path: /actuator/health
---
apiVersion: v1
kind: Service
metadata:
name: springboot
labels:
app: springboot
spec:
selector:
app: springboot
type: NodePort
ports:
- name: server
port: 8080
targetPort: 8080
nodePort: 31180
- name: management
port: 8081
targetPort: 8081
nodePort: 31181
#Springboot 项目,设置 ReadinessProbe 探测 SpringBoot 项目的 8081 端口下的 /actuator/health 接口,如果探测成功则代表内部程序以及启动,就开放对外提供接口访问,否则内部应用没有成功启动,暂不对外提供访问,直到就绪探针探测成功。
三种probe混合使用
apiVersion: v1
kind: Pod
metadata:
name: springboot-live
labels:
app: springboot
spec:
containers:
- name: springboot
image: docker.io/mydlqclub/springboot-helloworld:0.0.1
imagePullPolicy: IfNotPresent
ports:
- name: server
containerPort: 8080
- name: management
containerPort: 8081
readinessProbe:
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 10
httpGet:
scheme: HTTP
port: 8081
path: /actuator/health
livenessProbe:
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 10
httpGet:
scheme: HTTP
port: 8081
path: /actuator/health
startupProbe:
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 2
httpGet:
scheme: HTTP
port: 8081
path: /actuator/health
---
apiVersion: v1
kind: Service
metadata:
name: springboot-live
labels:
app: springboot
spec:
selector:
app: springboot
type: NodePort
ports:
- name: server
port: 8080
targetPort: 8080
nodePort: 31180
- name: management
port: 8081
targetPort: 8081
nodePort: 31181
grpc模式(k8s 1.24+)
apiVersion: v1
kind: Pod
metadata:
name: etcd-with-grpc
spec:
containers:
- name: etcd
image: registry.cn-hangzhou.aliyuncs.com/google_containers/etcd:3.5.1-0
command: [ "/usr/local/bin/etcd", "--data-dir", "/var/lib/etcd", "--listen-client-urls", "http://0.0.0.0:2379", "--advertise-client-urls", "http://127.0.0.1:2379", "--log-level", "debug"]
ports:
- containerPort: 2379
livenessProbe:
grpc:
port: 2379 # 对于gRPC的服务,配一个gRPC的端口就行。
initialDelaySeconds: 10
生产环境建议
探针参数配置
注意
在生产环境中,健康检查接口是一定要配置的,否则在deployment的滚动更新中,新起来的pod就会直接顶替原来的pod,造成宕机。
pod可以通过存活探测和就绪探测对容器进行健康检查:
- 探测间隔时间太短,可能会增加集群的负载,并且可能会导致不必要的故障转移或服务中断
- 探测间隔时间太长,可能出现Pod里的容器已经出问题了,但是还没开始探测,pod就不会从svc移除,请求svc还会把流量转到没就绪的pod。
那如何设置探测时间、探测超时时间、探测失败次数,做到最大限度的做到业务无感知,需要根据具体的应用程序和环境来确定。以下是一些考虑因素和常见的最佳实践:
- startupProbe
- 初始延迟(initialDelaySeconds):默认是0s,但是建议设置为应用程序完成启动所需的时间,确保程序可以顺利完成初始化。一般设置为几秒就行。
- 探测间隔(periodSeconds):默认10s,设短一些,5s
- 探测超时时间(timeoutSeconds):默认1s,设置为应用正常响应的范围内即可,通常是1-5s。
-
连续失败阈值(failureThreshold):设高一点,30次失败才认为程序没起来。这样相当于给了305=150s的时间启动,只要启动起来了,就直接过了。*
-
livenessProbe
- 初始延迟(initialDelaySeconds):默认是0s,但是建议设置为应用程序完成启动所需的时间,确保程序可以顺利完成初始化。一般设置为几秒到几分钟
- 探测间隔(periodSeconds):默认是10s,但是建议根据应用特点进行调整。轻量级应用,可以设置较短间隔(5-10s);重型应用建议增加间隔(30-60s)
- 探测超时时间(timeoutSeconds):默认1s,设置为应用正常响应的范围内即可,通常是1-5s。
-
连续失败阈值(failureThreshold):一般连续2次失败即认为探测失败,减少因为短暂网络问题或偶发故障引起误报。
-
readinessProbe
- 其余参数和livenessProbe类似
- 连续成功阈值(successThreshold):一般设置1次成功即认为就绪,可以尽早把流量转发到已就绪的pod
k8s中探测间隔最小单位就是1s,如果需要更短的探测间隔,可以自己写脚本来实现:
- 在业务pod里面定义sidecar容器,执行一个shell脚本,每隔1ms请求一次探测接口(一般是代码里面预先留出的)
#!/bin/bash
while true; do
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health)
if [ "$response" != "200" ]; then
echo "Tomcat is not healthy. Exiting..."
exit 1
fi
sleep 0.001 # 控制每次检测间隔为1ms
Done
宽限期设置
宽限期默认就是30s,即使preStop设90s,也不行,等了30+2s宽限期过了之后,就强制kill pod了。
但是对于一些特殊业务, 特别是有一次长连接的,就是需要等待超过30s处理完流量连接才能平滑退出。怎么办?
改pod的宽限期:pod.spec.terminationGracePeriodSeconds: 90
零宕机发版的注意事项
- 对于启动慢的程序,用startupProbe;对于所有程序,最好都加上livenessProbe和readinessProbe。
- 对于优雅退出,要用preStop等待程序真正执行完再退出。
- 对于springCloud框架,不推荐在k8s上使用,因为K8s可以平替掉springCloud。如果真的要在K8s上跑springCloud,注意:
- pod都是注册了Pod IP到Eureka/Nacos注册中心,其他pod从注册中心拿最新的IP注册表,实现pod间访问,而不走k8s的svc。
- 这样pod轮替更新之后,注册中心可能还没更新pod IP,或者其他服务还没拿到更新的pod IP,就会导致旧的IP仍在被访问。
- 解决1:pod下线之前,请求Eureka的接口,下线这个pod IP,而且再让eureka通知其他客户端刷新pod IP。问题:开发不愿意实现。
- 解决2:pod下线之前,给程序发下线信号,再加一个
preStop: sleep的时间优雅退出。 -
解决3:当然还是迁移到k8s最好了。k8s svc endpoint的剔除、注册是非常快的。
-
httpGet是比tcpSocket更可靠的检查方式:
- 对于Java程序,可能会假死:即使端口通着,内部逻辑不执行了(可能因为内存溢出等原因)。这样tcpSocket可能探测不出来,httpGet才能检测出来。
- 但是如果开发不想去实现httpGet的接口,退而求其次用tcpSocket。
容器保持长时运行
-
如果容器的主进程退出,Pod 通常会自动重启该容器。然而,在某些情况下,主进程的退出可能是不可避免的,这时我们需要确保容器通过其他方式持续运行。有以下几种思路:
-
保持后台进程持续运行
apiVersion: v1
kind: Pod
metadata:
name: tail-pod
spec:
containers:
- name: nginx
image: nginx:latest
command: ["tail", "-f", "/dev/null"]
# tail 命令进入了一个无限循环状态,持续读取一个始终为空的文件(/dev/null),从而保持容器的持续运行。不执行任何实际操作,因此不会占用大量系统资源。
- 保持容器无限暂停
apiVersion: v1
kind: Pod
metadata:
name: sleep-pod
spec:
containers:
- name: alpine
image: alpine:latest
command: ["sleep", "infinity"]
# 容器会保持在一个无限期的休眠状态。这种方法非常轻量,因为 sleep 命令本身不消耗 CPU 资源,而仅仅占用少量内存
-
使用进程管理器保持容器运行
- 进程管理器是用于管理进程生命周期的工具,特别适用于需要在容器内运行多个进程的场景。Tini 是一种轻量级的进程管理器,设计用于在容器环境中运行,能够处理常见的
PID 1问题,如信号处理、孤儿进程清理等。 -
在容器化环境中,通常只有一个进程(通常是
PID 1)被直接运行,并且由它来管理整个容器的生命周期。然而,某些情况下,PID 1进程无法正常处理信号或清理子进程,从而导致容器中的进程管理混乱。Tini 作为容器的init系统,能够有效地接管PID 1的角色,处理信号转发、子进程清理等工作,从而保证容器内的多进程运行稳定。 -
构建镜像
- 进程管理器是用于管理进程生命周期的工具,特别适用于需要在容器内运行多个进程的场景。Tini 是一种轻量级的进程管理器,设计用于在容器环境中运行,能够处理常见的
FROM ubuntu:latest
ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["your-main-process"]
docker build -t your-image-with-tini .
- 创建pod
apiVersion: v1
kind: Pod
metadata:
name: tini-pod
spec:
containers:
- name: my-container
image: your-image-with-tini
command: ["your-main-process"]
查看pod日志
kubectl logs <pod name>
# 当pod处于crash状态的时候,容器不断重启,此时用 kubelet logs 可能出现一直捕捉不到日志。kubelet会保持前几个失败的容器,用--previous就能看到其日志。
kubectl logs --previous <pod name>
#查看pod内容器的日志
kubectl logs mypod --all-containers
#查看pod内某个容器日志
kubectl logs mypod -c container-name
#查看kube events
kubectl get events
kubectl get events --field-selector involvedObject.name=podName
探针失效真实案例
案例来源
本章节整理自《一场由健康探针引发的Pod重启风暴——K8s LivenessReadiness Probe配置不当的深度复盘》。保留事故时间线、错误配置、修复方案和工程治理措施,并对 PDB、HPA 的保护边界补充说明。
事故概况
一次旨在“更快发现不健康 Pod”的 Liveness Probe 参数调整,在数据库慢查询和连接池争用期间放大了瞬时抖动。订单服务的健康接口从约 50 ms 增长到 3.2 秒,刚好超过 3 秒超时阈值。Liveness 在 10 秒内连续失败两次后开始重启 Pod,剩余副本承担更多流量,进一步压垮数据库连接池,最终形成级联重启。
事故期间:
- 订单服务从 40 个 Pod 快速下降到约 6 个可用副本;
- 支付服务从 20 个 Pod 下降到约 3 个可用副本;
- 300 多个 Pod 在约 5 分钟内发生连锁重启;
- 上游订单服务故障继续通过 RPC 超时传播到支付服务;
- 核心业务最终出现大面积 502 和不可用。
触发事故的配置变化
变更前:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
变更后:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
这组参数意味着:
- 探针每 5 秒执行一次;
- 一次请求超过 3 秒即记为失败;
- 连续失败 2 次便触发容器重启;
- 从首次失败到触发重启,实际容错窗口只有约 10 秒。
更严重的是,/healthz 不只是检查进程是否存活,还同步检查数据库、Redis 和消息队列。外部依赖的瞬时抖动因此被错误解释为“进程已经无法恢复,必须重启”。
故障时间线
| 时间 | 事件 |
|---|---|
| 15:12 | 慢查询触发,订单服务数据库连接池开始紧张 |
| 15:13 | /healthz 响应达到 3.2 秒,第一次超过 3 秒超时 |
| 15:13 | 5 秒后第二次超时,Liveness 判定失败 |
| 15:13 | kubelet 开始重启订单服务 Pod |
| 15:14 | 多个 Pod 被终止,剩余 Pod 承担更多流量 |
| 15:14 | 剩余 Pod 的连接池进一步过载,健康检查普遍超时 |
| 15:15 | 订单服务 Pod 大量进入 CrashLoopBackOff |
| 15:16 | 支付服务因依赖订单服务而发生 RPC 超时,其健康检查也开始失败 |
| 15:17 | 支付服务 Pod 被连续重启 |
| 15:18 | 核心业务大面积不可用 |
故障形成了正反馈:
数据库短暂抖动
→ Liveness 超时
→ Pod 被重启
→ 剩余 Pod 流量和连接压力上升
→ 更多健康检查超时
→ 更多 Pod 被重启
为什么 Readiness 没有阻止重启风暴
当时的 Readiness 配置为:
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
Readiness 需要连续失败 3 次,每次间隔 10 秒,约 30 秒后才把 Pod 从 Service 后端摘除;Liveness 却约 10 秒便触发重启。结果是:
- Liveness 先于 Readiness 采取破坏性动作;
- Pod 还没来得及通过摘流获得恢复窗口,就被 kubelet 重启;
- 新容器启动后再次承受流量和依赖压力;
- 健康检查再次超时,进入循环。
Readiness 的职责是控制是否接收流量,Liveness 的职责是处理进程已经无法自愈的状态。前者通常应更快、更敏感;后者应更保守。
三个根因
1. Liveness 检查了外部依赖
Liveness 失败会触发容器重启,因此只应检测重启能够修复的进程内部故障,例如主循环卡死或不可恢复的死锁。
数据库、缓存、消息队列和下游 RPC 的失败通常不应直接导致 Liveness 失败。重启应用不会修复外部依赖,反而可能制造连接风暴和流量转移。
| 探针 | 要回答的问题 | 失败结果 | 推荐检查内容 |
|---|---|---|---|
| Startup | 应用是否已完成启动 | 超过启动容忍期后重启 | 初始化、迁移、缓存预热等启动条件 |
| Liveness | 进程是否已无法自愈 | 重启容器 | 进程内部状态,不依赖外部系统 |
| Readiness | 当前是否能安全接收请求 | 从 Service 后端摘除 | 处理请求所需的关键依赖和容量 |
2. Readiness 比 Liveness 更慢
原配置中,Readiness 约 30 秒后摘流,Liveness 约 10 秒后重启,破坏性动作先于保护性动作。
修复后的示例:
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
successThreshold: 1
livenessProbe:
httpGet:
path: /live
port: 8080
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 5
该示例让 Readiness 最快约 10 秒摘流,而 Liveness 约 100 秒后才考虑重启,为应用自愈和外部依赖恢复留下缓冲窗口。
不要机械套用倍数
“Liveness 容忍窗口至少是 Readiness 的两倍”可以作为保守启发式,但不是 Kubernetes 的通用公式。参数应根据应用启动时间、正常延迟分布、依赖恢复时间、错误预算和可接受故障发现时间,通过压测与故障演练确定。
3. 慢启动应用没有 Startup Probe
只靠 initialDelaySeconds 很难同时兼顾慢启动与运行期故障发现。Startup Probe 成功之前,Liveness 和 Readiness 不会开始执行,可把启动阶段与运行阶段分离。
startupProbe:
httpGet:
path: /startup
port: 8080
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /live
port: 8080
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 5
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
这里为启动阶段提供最多约 150 秒的容忍时间。一旦 Startup 成功,运行期探针才接管。
健康检查端点设计
建议把三个端点按语义拆开:
// /startup:检查初始化工作是否完成
func startupHandler(w http.ResponseWriter, r *http.Request) {
if !dbPool.IsInitialized() || !cache.IsWarmed() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}
// /live:极轻量,只证明进程仍能响应
func liveHandler(w http.ResponseWriter, r *http.Request) {
// 不检查数据库、缓存、消息队列等外部依赖
w.WriteHeader(http.StatusOK)
}
// /ready:检查当前是否可以接收业务流量
func readyHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Second)
defer cancel()
if err := dbPool.Ping(ctx); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}
设计原则:
- /live 不因外部依赖失败而返回非 200。 外部系统抖动通常不能通过重启当前容器修复。
- /ready 可以检查关键依赖,但必须有严格超时。 多个依赖最好分别设置超时,避免一个依赖拖住整个端点。
- 健康端点本身不能成为瓶颈。 避免复杂计算、长链路调用和大量 I/O。
- 谨慎决定 Readiness 是否检查共享依赖。 如果数据库整体故障导致所有 Pod 同时 NotReady,Service 可能失去全部后端;需要结合降级能力、故障模式和流量策略设计。
- 探针路径应有明确语义。 使用 /startup、/live、/ready 比笼统的 /healthz 更容易避免职责混淆。
监控与诊断
应监控探针成功率、延迟、容器重启和可用副本变化。指标名称取决于应用和监控组件,下面是原文给出的示意查询,不代表 kubelet 默认暴露同名指标:
rate(probe_success_total{probe="liveness"}[5m])
histogram_quantile(
0.99,
probe_duration_seconds{probe="liveness"}
)
现场排查至少包括:
kubectl describe pod <pod-name>
kubectl logs <pod-name> --previous
kubectl get events --field-selector involvedObject.name=<pod-name>
kubectl get pod <pod-name> -o yaml
重点确认:
- Last State 和容器退出原因;
- Events 中是否出现 Liveness probe failed;
- Restart Count 的增长速度;
- 探针实际超时、失败阈值与周期;
- 依赖延迟是否在重启前已经升高;
- 可用副本下降是否导致剩余 Pod 负载增加。
事故后的工程治理
- 建立 Probe 配置标准,生产探针变更必须经过 Code Review。
- 在 CI/CD 中检查高风险配置,例如 Liveness 引用外部依赖、过短容忍窗口、缺少 Startup Probe。
- 为 Pod 重启率、探针失败率和可用副本骤降设置告警。
- 探针参数变更先灰度到少量副本,并观察完整故障窗口。
- 定期进行依赖变慢、连接池耗尽和探针超时的故障演练。
- 结合滚动更新策略、拓扑分布、反亲和性和足够副本数降低同时失效风险。
PDB 与 HPA 的保护边界
PodDisruptionBudget 主要约束自愿中断,例如节点排空;它不能阻止 kubelet 因 Liveness 失败而重启容器,也不能直接阻止应用自身崩溃。HPA 的 minReplicas 只约束期望副本下限,不能保证这些副本在探针误杀期间保持 Ready。二者可以增强整体可用性,但不能代替正确的探针语义、参数和故障隔离。
复盘结论
- 瞬时依赖故障不等于进程死亡。
- 只有“重启能够修复”的故障才适合由 Liveness 处理。
- Readiness 应先摘流,Liveness 应谨慎重启。
- 慢启动应用应使用 Startup Probe 隔离启动阶段。
- 探针参数不是越敏感越好,必须根据真实延迟和恢复时间验证。
- 批量修改核心服务探针属于高风险变更,应灰度发布并准备快速回滚。