1. Pod是什么?为什么K8s不直接部署容器?
1.1 从容器到Pod:为什么中间多了一层
很多人刚开始学Kubernetes的时候,脑子里都会冒出一个问题:明明我Docker里跑容器跑得好好的,迁移到K8s之后,怎么反而不直接操作容器了,非要先套一层Pod?这个问题想通了,K8s的门槛就跨过去一半。
先直接给结论:Pod是Kubernetes里最小的调度和部署单元,不是容器。K8s不直接管容器,而是管Pod,Pod里面再装容器。为什么不直接管容器?核心原因是"一组容器需要作为一个整体来调度、共享资源和网络"。
我举个例子你就明白了。假设你写了一个Web服务,它由一个Node.js进程和一个日志采集进程组成。日志采集进程要读Node.js写出来的日志文件,这两个进程必须跑在同一台机器上,最好还能共享文件系统。如果你把它们拆成两个独立的容器各跑各的,调度器就可能把A容器放到节点1,把B容器放到节点2,两个进程无法通信,日志也读不到。哪怕K8s有调度约束能把它们强制安排到同一台机器,那也要额外配置,麻烦得很。
而在K8s的模型里,Pod天然就是"同生共死"的一组容器。K8s保证同一个Pod里的所有容器一定落在同一个节点上,而且它们共享同一个网络命名空间和存储卷。这意味着:
- 同一个Pod里的容器,通过localhost就能互相访问,端口不能冲突
- 它们共享同一个IP地址,对Pod外部来说,整个Pod就是一个整体
- 它们可以挂载同一个Volume,实现文件共享
所以说,Pod是K8s对"进程组"这个概念的一种实现。你把容器想象成一个个单独的进程,而Pod就是把这些进程打包成一个调度单位。这就像搬家用纸箱——纸箱不是货物本身,但纸箱决定了货物怎么被搬运、怎么被堆放。
1.2 Pod与容器的关系:一个Pod里到底放几个容器
先讲一下最实际的问题:一个Pod里该放几个容器?答案是,绝大多数情况下,一个Pod放一个容器就够了。如果你在做微服务架构,每个服务一个Pod,Pod里一个容器,这是最常规的用法。但K8s之所以引入Pod,是为了支持那些"紧密耦合"的多容器场景。
我总结下来,多容器Pod最典型的使用场景就这三种:
第一种是Sidecar模式,也是最常见的。主容器负责核心业务逻辑,旁边的Sidecar容器负责辅助功能,比如日志采集、流量转发、配置热更新。典型的例子是主容器跑Nginx,Sidecar容器跑filebeat,定期把Nginx的访问日志转发给日志系统。
第二种是Adapter模式,主要是把主容器输出的数据做格式转换。比如主容器输出的是JSON日志,旁边的容器把它转成其他系统需要的行格式。
第三种是Ambassador模式,简单说就是给主容器提供一个代理。主容器只连localhost,由旁边的代理容器负责连外部服务,这样外部服务的地址变了,主容器代码不用改。
不过我必须提醒一句:这几种多容器模式确实是官方推荐的架构实践,但实际业务里能用简单的方案绝不要搞复杂的。多容器Pod意味着你要管理多个容器的启动顺序、日志聚合、资源分配,排查问题的复杂度直线上升。我见过不少团队为了"酷炫"把日志采集器塞进业务Pod里,结果Pod一扩容日志采集器也跟着扩,资源浪费不说,日志还重复采集。后来老老实实改成DaemonSet方式部署采集器,问题瞬间清爽。
2. Pod的完整生命周期与状态解读
2.1 从创建到销毁:Pod的一生
你执行了一条 kubectl apply -f pod.yaml,K8s到底做了什么?整个流程你如果心里没个数,后面排障基本靠猜。
我一步一步拆给你看:
第一步,你的命令发给API Server,Kubernetes的"总入口"。API Server校验YAML格式和权限,通过后把Pod的期望状态写入etcd。
第二步,调度器(kube-scheduler)一直盯着API Server,发现有新的Pod需要调度,就开始给这个Pod"找房子"。它先做过滤(Filtering),把不满足条件的节点剔除掉,比如CPU内存不够的、贴了污点(Taint)的,然后做打分(Scoring),选出最合适的节点。
第三步,被选中的节点上的kubelet发现了这个Pod,开始干活。kubelet调用容器运行时(比如containerd)创建Pod的沙箱环境,然后依次把Pod里的容器拉起来。
第四步,容器全部启动成功后,kubelet通过探针(Probe)持续检查容器状态。一切正常,Pod就稳定运行;如果探针失败到一定次数,K8s就会按照你配置的策略杀掉容器重启。
第五步,当你主动删除Pod,kubelet会先触发preStop钩子(如果你配了),再发送SIGTERM信号给主进程,等待一段宽限期(terminationGracePeriodSeconds,默认30秒),超时没退出就SIGKILL强杀。
整个过程中,任何一个环节卡住了,Pod的状态都会表现为各种异常。所以排障的时候,你要顺着这条链路一步步查:先看Pod是不是被调度了,再看镜像是不是拉下来了,最后看容器有没有起来、起来了有没有崩。
2.2 五种状态的判断逻辑
K8s的Pod状态字段是status.phase,它有且只有五种取值:Pending、Running、Succeeded、Failed、Unknown。别小看这个枚举值,很多人排障半天其实是被状态语义搞混了。
Pending意味着Pod已经被API Server接受了,但还没被调度或者还在拉镜像、启动容器。这个状态最常见的两种原因:一是没有节点能满足调度条件,二是镜像拉取特别慢。
Running表示Pod已经绑定到节点,所有容器都创建成功,并且至少有一个容器仍在运行(或者正在启动)。注意,Running不代表"健康",只是代表"在跑"。
Succeeded表示Pod里的所有容器都成功退出,且退出码是0。这个状态多出现在Job任务里,一个Pod跑完批处理任务正常结束就会这样。对常规服务型Pod来说,出现Succeeded反而说明你没配对重启策略。
Failed表示Pod里至少有一个容器以非零退出码结束了,任务没跑成。
Unknown比较特殊,意思是状态信息无法获取,通常是因为API Server与kubelet之间通信出了问题,比如节点宕机或者网络分区。遇到Unknown,先去看节点状态,别一门心思盯着Pod。
这里要特别提醒一句话:别用status.phase判断Pod是否真正可用。因为Running状态太粗了,一个崩溃到一半的容器也可能短暂处于Running。要想准确判断业务是否就绪,必须依赖就绪探针(readinessProbe),后面我会详细说。
2.3 重启策略:RestartPolicy怎么选
Pod的restartPolicy只有三个取值:Always、OnFailure、Never,默认是Always。
它们的区别很简单:
- Always:不管容器以什么方式退出,都自动重启。这是服务型Pod的标准配置,比如你的Web服务崩了,kubelet会把它捞起来。
- OnFailure:只有容器以非零退出码退出时才重启。正常退出码为0就让它保持退出状态。
- Never:容错退出了也不重启。这适用于一次性任务,跑完就拉倒。
实际使用的时候有个特别容易踩的坑:很多人把服务型Pod的restartPolicy改成OnFailure或者Never,结果容器一崩溃,服务直接没了,也没有人来捞。反过来,跑批处理任务的人用默认的Always,结果Job跑完了,容器退出码是0,按理说Pod应该进入Succeeded状态,但重启策略是Always的话,容器会被反复拉起来,整个任务永远结束不了。
我的建议很简单粗暴:凡是长跑服务,一律Always;凡是Job、CronJob这种短任务,要么OnFailure要么Never,具体看你的业务能不能容忍失败后的重跑。
另外补充一个细节:重启策略只在Pod所在的节点上由kubelet执行,也就是说容器挂了,kubelet会在本节点重启它,但这个Pod不会自动被调度到别的节点。想真正做到高可用,得靠Deployment、StatefulSet这些控制器去管,而不是靠Pod自带的重启机制。这点新手特别容易误解,以为Pod自带重启就等于高可用,差远了。
3. Pod的创建与管理:手写第一个YAML
3.1 最基本的Pod YAML逐字段拆解
理论讲再多,不如动手写一份YAML。下面这个例子是我平时用来测试的最简Pod配置,我会逐字段解释它到底在干什么。
apiVersion: v1 kind: Pod metadata: name: nginx-demo labels: app: nginx env: dev namespace: default spec: restartPolicy: Always containers: - name: nginx image: nginx:1.25-alpine imagePullPolicy: IfNotPresent ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256MiapiVersion、kind、metadata、spec是每个K8s资源的四件套,缺一不可。apiVersion: v1表示Pod属于稳定的核心API组,不会轻易变化。metadata里的name必须符合DNS规范,小写字母、数字、短横线。namespace默认就是default,生产环境一定要显式指定,别指望默认值。
labels是个很关键的东西,它不是可有可无的装饰。K8s里所有资源都是通过label和label selector关联起来的,Deployment靠它找到自己管理的Pod,Service靠它把流量转发给Pod。所以创建Pod之前,先想清楚你的label体系怎么定义,我建议至少包含app(业务名)和env(环境名)两个维度,后面维护起来会省很多事。
spec.containers里的name是同一个Pod内部唯一的容器名,可以随意起,但建议和镜像的作用保持一致,别起个nginx结果里面跑的是redis。
imageTag我用了alpine版本,原因很简单:小。nginx正式版加一堆依赖动辄两三百兆,alpine版只有几十兆,拉取快、占用小,适合测试环境。生产环境请根据你的安全要求来定,别盲抄。
3.2 镜像拉取策略与imagePullPolicy
imagePullPolicy决定kubelet什么时候去拉镜像,有三个取值:
- IfNotPresent:节点上没有这个镜像才拉取,有就直接用
- Always:每次创建容器都重新拉取
- Never:绝不拉取,只在节点本地找镜像
如果这个字段不写,K8s有个隐含规则:镜像tag是latest或者没有tag,默认Always;其他具体tag默认IfNotPresent。
实际项目中我踩过一个很典型的坑:测试环境用latest给某个服务打镜像,镜像tag完全一样,但内容隔几天就换。按照默认规则,latest应该每次拉取,但因为某个节点上之前已经缓存过同名镜像,加上我后来又手动把这个字段写成IfNotPresent,更新后的镜像死活不生效。排查半天,发现是镜像缓存的问题。
所以我的建议是:
- 生产环境必须用具体版本的tag,每次发版都换新tag,这样就算拉取策略是IfNotPresent也不会命中旧缓存
- 如果非要固定用latest,必须显式指定imagePullPolicy: Always,确保每次部署拉最新的
- 离线环境要小心Always策略,节点拉不到镜像会一直ImagePullBackOff,这种情况要么本地导入镜像,要么改成IfNotPresent
3.3 环境变量、资源限制与探针配置
给Pod传配置的方式有很多,最基础的是直接写在YAML里:
env: - name: APP_ENV value: "production" - name: DB_HOST valueFrom: configMapKeyRef: name: app-config key: db_host上面第一段是写死一个值,第二段是从ConfigMap里取。更复杂一点还可以从Secret里取,或者从Pod自身字段里取(fieldRef,比如取Pod的IP)。用ConfigMap和Secret而不是把敏感信息写死在YAML里,是生产环境的基本素养,不用多说。
资源限制这块我多说两句。requests表示调度器做节点选择时的最低保障,limits表示运行时不能超过的上限。CPU的单位是m(millicore),1000m等于一个完整CPU核心;内存单位是Mi、Gi等二进制单位。
那问题来了:limits设多大合适?CPU的limits设了也未必真的严格限制,因为CPU是压缩型资源(compressible),超出的话只会被限流,不会杀容器。内存不一样,它是不可压缩资源,一旦超标就可能OOM,容器会被杀掉。所以内存limit设置的策略是宁宽勿紧,给足余量;CPU的requests则重要过limits,因为调度器是根据requests算账的。
还有一个新手必踩的坑:只设limits不设requests。K8s会直接把limits的值复制成requests。你以为给Pod限了2核4G,结果调度器认为每个Pod要占2核4G,集群资源利用率立刻惨不忍睹。所以请求值和限制值建议分开写。
探针(Probe)是Pod健壮性的关键。探针有三种:
- livenessProbe:存活探针,失败了就重启容器
- readinessProbe:就绪探针,失败了就把Pod从Service的Endpoints里摘掉,不给它发流量
- startupProbe:启动探针,专门给启动特别慢的容器用的,启动探针成功后,其他探针才开始工作
我强烈建议每个核心业务容器至少配置readinessProbe,这是上线前的第一道防线。用个最简单的HTTP探针例子:
readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3initialDelaySeconds很关键。如果你家的服务启动要10秒,你却把initialDelaySeconds设成2,探针一开始就失败,容器会被反复误杀。这个参数要根据实际启动速度来,宁可多等几秒,也别误杀。
4. Pod的调度与节点选择
4.1 调度器是怎么选择节点的
每个Pod都要有一个归宿,调度器干的事就是通过过滤和打分两步走选出那个归宿。
先说过滤。调度器把集群里所有节点过一遍,完全不满足条件的直接排除。常见的过滤条件包括:节点资源是否满足Pod的requests、节点是否被打了污点(taint)而Pod又没有对应的容忍(toleration)、Pod是否要求节点有某些标签(nodeSelector)、节点端口是否冲突等等。
然后是打分。剩下的节点里,调度器按一系列优先级算分数,比如资源余量多的节点分数高,调度到相同节点上的镜像相同的话分数高(因为镜像已经在本地了),还有Pod亲和性、节点亲和性等规则都会影响分数。最后选分数最高的节点。
这里有个小程序设计值得一提:调度不是"把Pod塞进资源最多的节点",而是要考虑集群整体的均衡。资源利用率最差且最多的节点反而容易成为热点,因为你永远不知道下一个Pod要多少资源。
实际中你可能会遇到一个现象:节点明明还有资源,Pod却一直Pending。这时候要查的不仅是资源,还要看节点上的taint。节点被打了taint之后,普通Pod默认是不允许调度上去的,除非Pod显式声明了toleration。这种设计是为了保护一些特殊节点,比如有GPU的节点只给特定任务用,或者某些节点只给存储类Pod用。
4.2 nodeSelector、nodeAffinity与反亲和性
想把Pod调度到特定节点上,最简单的就是nodeSelector:
spec: nodeSelector: disktype: ssd意思是只调度到打了"disktype: ssd"标签的节点。但是nodeSelector有个硬伤:它只支持等值匹配,而且一旦节点没有这个标签,Pod就永远Pending,一点灵活性都没有。
所以推荐用nodeAffinity,表达能力更强,而且支持软需求:
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: zone operator: In values: - az-a前面那个required结尾的策略是硬性条件,调度时必须满足,不满足就Pending;后面preferred开头的是偏好条件,满足加分、不满足不影响调度。后半段那个IgnoredDuringExecution的意思是:调度时执行这个规则,但节点运行期间标签变了也不重新调度,K8s默认都是这种执行语义。
再说Pod亲和性。有时候你不只是想把Pod放到某个节点的特定标签上,还想让它跟另一个Pod靠近(比如两个服务互相调用频繁,放一个节点上流量走本机回环更快),或者反过来,想让它们尽量不在一起(比如两个高负载任务,放一起资源争抢太严重)。前者是podAffinity,后者是podAntiAffinity。
反亲和性有个经典用法:同一个Deployment的Pod尽量不要落在同一个节点上。K8s里一个Deployment可以有好几个副本,默认情况调度器很可能把它们分布在多个节点,但并不能保证。你一旦想保证,就得写在反亲和性里,比如topologyKey: kubernetes.io/hostname,意思是以节点为维度,同一个节点上不希望出现两个同标签的Pod。
这部分的经验是:能用label和nodeSelector解决的,就别上亲和性;用亲和性解决的,别过度设计。调度约束越复杂,集群的资源碎片化越严重,到最后可能一个100节点的集群,核心服务反而调度不进去。
5. Pod的控制器:为什么你很少需要直接创建Pod
5.1 控制器是什么,为什么要用控制器
聊到这里,你可能会问:既然我会写Pod的YAML了,那我以后是不是就直接创建Pod了?
答案是否定的。生产环境中,直接创建Pod几乎只有测试和排障时会干。因为裸Pod的命运太脆弱了——节点宕机了,裸Pod不会自动在其他节点重建,K8s默认不会帮你拉起来;副本数想扩一个,你得手动重新创建一个。
应该用控制器来管Pod。控制器的核心思想是:你声明期望状态,控制器负责把实际状态逼近期望状态。打个比方,裸Pod就像一个单干的自由职业者,干不了就不干了;Deployment就像一家公司,你告诉它"我要保持10个员工在岗",它就会盯着人数,谁走了马上补一个新员工进来。
K8s里最常用的几个控制器:
- Deployment:无状态应用,管副本数、滚动更新、回滚
- StatefulSet:有状态应用,稳定网络标识、有序部署、独立存储
- DaemonSet:确保每个节点都运行一个Pod副本,比如日志采集、监控agent
- Job和CronJob:一次性任务和定时任务
5.2 Deployment、StatefulSet、DaemonSet的区别
我分头讲。
Deployment是最常用的。你只需要写好Pod模板和副本数,Deployment负责滚动更新。它内部其实还套了一层ReplicaSet,每次更新Deployment就会创建一个新的ReplicaSet,把新版本的Pod逐个建起来,同时缩掉旧版本Pod,这就是滚动更新的原理。想回滚就kubectl rollout undo deployment,简单粗暴。
StatefulSet和Deployment的最大区别在于,它给每个Pod一个稳定的身份和有序的生命周期。Pod的网络标识是固定的,比如一个三节点的ZooKeeper集群,Pod名分别是zk-0、zk-1、zk-2,各自有对应的DNS记录,重启之后名字和网络标识都不变。存储也一样,每个Pod挂载的PVC是独立且持久的。因为它太"有状态",删除Pod时并不会立刻删掉,而是会等待它优雅退出,顺序上也有讲究,扩容时一个个来,缩容时从最后一个开始。
DaemonSet的逻辑最简单:每个节点上都有一个Pod。注意,它受节点数量影响,集群加了新节点,它会自动在新节点上部署一个Pod。典型场景是日志采集器、节点监控组件、网络插件。
给新手的选型建议是这样的:默认无脑用Deployment;数据库、消息队列、注册中心这类需要身份和持久化的用StatefulSet;跑在所有节点上的系统级组件用DaemonSet;跑批处理任务用Job;周期性批处理用CronJob。90%的业务场景用Deployment就够用了。
6. 常见问题与排查实录
6.1 Pod一直Pending怎么办
Pending是新手遇到最多的状态。按照我的习惯,排查顺序是这样的:
第一步,kubectl describe pod <名字>,看Events段。Events里会直接写明原因,比如0/3 nodes are available,意思是三个节点都不行,后面会跟各种原因。
第二步,如果是资源不足,kubectl top nodes看集群整体资源用量,然后用kubectl describe node <节点名>查看具体哪个节点还差多少。
第三步,如果Events显示node(s) had taint,说明节点打了污点,而你的Pod没有容忍。要么去掉污点,要么给Pod加toleration。
第四步,有个不太常见但很坑的情况:动态创建PVC失败导致Pending。比如Pod里声明了一个PVC,但StorageClass不存在或者存储插件不可用,Pod就会一直Pending不调度。这种要kubectl get pvc,看PVC状态有没有Bound。
6.2 ImagePullBackOff的常见原因
ImagePullBackOff字面意思就是镜像拉取失败,被反复重试后进入退避状态。
常见的几种原因和对应排查方法:
一是镜像地址写错了。这个最简单,仔细核对仓库地址、项目名、tag。尤其是私有仓库地址带端口号的,特别容易抄错。
二是私有镜像仓库需要认证,但Pod没有配置imagePullSecrets。解决办法是先创建Secret,然后在Pod模板里指定imagePullSecrets。用Deployment的话,改Pod模板后记得重新发布。
三是节点上访问不了镜像仓库。比如生产集群在内网环境,节点无法访问公网仓库,或者仓库只允许特定网段访问。这种要么配置镜像仓库的代理地址,要么手动把镜像导入节点。
四是镜像tag不存在。查一下仓库里到底有没有这个tag,很多人发布流程有问题,镜像没push上去就开始滚动更新,于是全部Pod拉不下来。
拉镜像慢导致的ImagePullBackOff也见过很多,其实显示的是ErrImagePull,还没到BackOff那么严重。这种一般是仓库带宽问题或镜像太大,先手动登录仓库确认能拉通,再考虑换成小的基础镜像。
6.3 CrashLoopBackOff排查思路
CrashLoopBackOff的意思是容器启动后反复崩溃,每次崩溃后重启,等待时间渐次变长(10秒、20秒、40秒……一直到300秒封顶),以此循环。
排这种问题,第一步永远是kubectl logs <pod名> --previous,看上一次崩溃时的日志。注意必须加--previous,因为当前容器还在running,你看到的可能只是一半启动日志或者空日志。
看完日志,代码层面没问题的话,就要考虑是不是启动时依赖的东西没就绪。比如数据库连接超时、DNS解析不了别的服务、配置文件里某个路径不存在。容器里能不能访问到这些依赖,可以用kubectl exec进容器手动测。
有一种CommandCrashLoopBackOff的变体,是启动命令本身写错了。比如镜像是nginx,但你command写成了python,容器一启动就报executable file not found,这种看日志前面几行就能看出来,不用怀疑代码逻辑。
还有一个经常被忽略的原因:内存limit设得太小,容器一启动就因为OOM被杀。这种在describe里能看到Exceeded memory limit或者OOMKilled字样,如果看到就调整资源limit。
7. 实操心得与避坑指南
7.1 我踩过的几个坑
讲了这么多理论,最后分享几个我实际踩过、费过不少时间的坑,希望你能少走弯路。
第一个坑:删除Pod时忽略了优雅终止。我早期调服务,每次更新都是直接kubectl delete pod,让Deployment重新拉一个起来。结果服务正在处理的请求被硬生生掐断,上游客户端报各种5xx。后来才去翻Pod终止流程,发现SIGTERM发出来之后只给你30秒(默认terminationGracePeriodSeconds=30,实际是新版默认可能不同,以版本为准),你要在preStop钩子里做优雅退出,把处理中的请求完成后才退出。从那以后我再也不随便delete Pod,要么用kubectl rollout restart,要么改镜像后apply新版本。
第二个坑:Pod里的容器时区问题。用很多官方镜像的时候,容器内默认是UTC时区,日志时间比中国本地时间慢8个小时。这个在排障的时候特别懵,查了半天日志怎么都对不上。解决方法是挂个TZ环境变量,或者直接把宿主机的/etc/localtime挂进容器,但注意别为了省事就把整个时区文件挂成只读,容易踩权限问题。
第三个坑:资源limits引起的血泪教训。有一次我压测,突然一堆Pod被OOMKilled,大家都在怀疑代码内存泄漏。查了几轮才发现是Deployment里memory limit设得太小,把某个接口一次性返回大量数据时的峰值内存算漏了。从此我养成了一个习惯:在线下用压测工具把峰值内存测出来,再给limit留至少30%的余量。
第四个坑:imagePullPolicy不显式指定。前面说过了,tag是latest会默认Always,具体版本号默认IfNotPresent。有人发了新版本镜像,tag用latest,但自己又在YAML里没写imagePullPolicy,结果继续跑老镜像。这个我在测试环境遇到过不止一次,最后干脆团队约定:所有测试环境的imagePullPolicy必须显式写Always。
7.2 一些实用命令与配置模板
排障和日常管理Pod,下面这些命令我几乎天天用,建议先记下来:
# 查看Pod基本信息 kubectl get pod # 查看Pod的详细信息和事件 kubectl describe pod <pod-name> # 看容器日志 kubectl logs <pod-name> -c <container-name> # 看上一次崩溃时的日志 kubectl logs <pod-name> --previous # 持续跟踪日志 kubectl logs -f <pod-name> # 进入容器 kubectl exec -it <pod-name> -- /bin/sh # 临时创建一个测试Pod,验证镜像或配置 kubectl run test --image=nginx:1.25-alpine --restart=Never --rm -it如果某个Pod反复出问题,我通常会在不知从哪下手时用describe看到底,然后根据Events里的关键词顺着查。记住一点:get是看状态,describe是看事件,logs是看输出,三者缺一不可,但顺序上永远先describe再logs。
最后给大家一个我常用的、生产级别的Nginx服务Pod配置模板,可以直接抄作业:
apiVersion: v1 kind: Pod metadata: name: nginx-prod namespace: default labels: app: nginx env: prod spec: restartPolicy: Always containers: - name: nginx image: nginx:1.25-alpine imagePullPolicy: IfNotPresent ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi readinessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 15 periodSeconds: 20 failureThreshold: 3 env: - name: TZ value: "Asia/Shanghai"需要注意,/healthz这个路径必须是你业务里真实存在的健康检查接口。如果你的镜像默认没有这个接口,探针会一直失败,Pod会被一直重启,这个比我前面说的任何坑都好踩。
我个人在实际操作中的体会是,Pod这套东西光看文档是记不住的,一定要自己动手敲几遍YAML,故意制造几个故障来观察状态变化。比如把镜像名改错看ImagePullBackOff,把启动命令写错看CrashLoopBackOff,把requests调得特别大看Pending。这么折腾一遍,你对Pod的理解会比我在这里讲一万个字都深刻。这篇精简版讲到的内容,其实已经覆盖了你日常使用中90%以上的场景,剩下的细节,建议你配合官方文档按版本号去查阅,毕竟K8s版本迭代快,有些字段行为在不同版本之间确实有差异。