ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从Pod到Ingress:Kubernetes对象模型与生产部署实践

从Pod到Ingress:Kubernetes对象模型与生产部署实践

1. 从三台服务器到三百个容器:手工运维到底在哪一步崩掉

我第一次接触 Kubernetes,不是因为觉得它酷,而是因为被一堆docker run命令逼到了墙角。事情本身没什么稀奇:一个对外提供接口的服务,早期只跑在三台机器上,装个运行时、拉个镜像、写个 systemd 单元,重启就完事。后来访问量上来,机器加到十几台,服务拆成七八个模块,每个模块还要分测试环境和线上环境,运维脚本从三十行涨到八百行,脚本里开始出现"如果这台机器上的容器挂了,就去另一台机器上重新拉一个"这种逻辑——写到这里其实就已经在手工实现一个调度器了,只是实现得很糟糕。

真正的崩溃点往往不是某一次大故障,而是日常小事故累积起来的疲惫感:某台机器上的容器进程莫名其妙退出,没人知道;某个服务扩容之后忘了改负载均衡的配置,流量打到了一台已经不存在的实例上;发版的时候需要手动停掉一半实例再启动新版本,中间那几分钟的请求全部报错。这些问题单看都能靠脚本补一补,但脚本本身又变成了新的维护负担。Kubernetes 出现的意义,不是让你多学一套工具,而是把"我希望集群里始终有 3 个健康实例"这句话变成一条可以被机器持续执行的声明。你要做的只是把期望写下来,剩下的对齐工作交给集群自己。

所以这篇内容适合两类人看:一类是刚接手容器化环境的开发者,被一堆 Pod、Deployment、Service 的名词砸晕,想知道这些对象到底谁管谁;另一类是还在用手写脚本管理多台机器的运维或后端同学,想判断值不值得迁移过去。我会从对象模型讲到请求在集群内部的完整流转,再给出一份可以直接抄的部署清单,最后重点说几个新手几乎必踩的坑。原理讲透,命令给全,不绕弯子。

1.1 一个虚构团队的失控过程,几乎是所有团队的缩影

我参与过一个模拟项目的容器化改造,团队规模不大,六个后端加一个运维。改造前状态是:每台机器上跑三到四个容器,靠一份写死的部署清单加人工核对来维持。问题出在一次例行扩容上——业务方要求把某个查询服务从 4 个实例扩到 12 个,运维同学照着旧清单改了机器数量,但负载均衡那一侧的健康检查路径还是旧的,新起的实例因为健康检查失败被摘掉了流量,结果扩容之后实际可用的实例反而更少。这个事故排查了将近两个小时,因为从监控上看 CPU 和内存都很正常,服务也确实在跑,就是没有流量进来。

这类问题的共同特征是:配置分散在多个地方,彼此之间没有校验关系。机器清单在运维手里,负载均衡规则在另一份配置文件里,健康检查路径写在代码里,三者靠人的记忆对齐。Kubernetes 的做法是把这些关系收敛到同一套 API 对象里:Service 通过标签选择器找到 Pod,Pod 的健康状态由探针决定,探针失败就自动从 Endpoints 里摘掉。标签对不上,Service 就永远找不到后端,这个问题在kubectl describe service里一眼就能看到,不需要对着三份文件逐个核对。

这里有个容易忽略的细节:标签选择器是"精确匹配"而不是"模糊匹配",app: demo-web和app: demo-web-v2是完全不同的两组 Pod。写 YAML 时多一个后缀,就会导致 Service 后端为空,而集群不会报任何错,只是流量进不来。

1.2 Kubernetes 卖的不是容器,是"期望状态"这四个字

很多人把 Kubernetes 理解成"容器管理工具",这个理解会导致后面一系列困惑。容器运行时负责的是"把进程跑起来并隔离资源",Kubernetes 负责的是"让集群的实际状态不断逼近你声明的期望状态"。这两件事的层次完全不同,前者是一次性的动作,后者是一个永不停止的循环。

举个具体例子。你声明了一个 Deployment,replicas: 3。集群做的事情是:创建 ReplicaSet,ReplicaSet 创建 3 个 Pod,然后持续监听——只要有 Pod 因为节点故障、OOM、人为删除等原因消失,立刻补一个出来。这个"补"的动作不需要你写任何脚本,是控制器在调谐循环里自动完成的。同理,你修改镜像版本,控制器会按滚动策略逐步替换;你把副本数改成 5,它就去调度两个新的 Pod。

这个模型带来的直接好处是运维动作从"执行步骤"变成了"描述结果"。你不再需要关心"先停哪个再起哪个",只需要描述"最终要有 3 个跑着 1.1.0 版本的实例",中间的过渡过程由maxSurge和maxUnavailable这类参数控制。坏处是,如果期望状态写错了,集群会非常忠实地把错误状态维持下去——写错镜像标签,它就会一直尝试拉取一个不存在的镜像,并且不断重启,直到你发现为止。

1.3 有些场景硬上 Kubernetes,成本会远大于收益

说句实在话,不是所有项目都该用 Kubernetes。我在实际工作中见过最典型的错配是:一个只有两个服务、日请求量几千的项目,为了"技术栈先进"搭了一套三节点集群,结果光是维持控制面、监控、日志、证书轮换这些基础设施,就吃掉了团队大量的精力,业务代码反而没时间优化。

判断标准其实不难,我自己会看这么几条:第一,服务数量和变更频率——如果只有一两个服务,一年发版不到十次,直接用容器加编排脚本或者更轻量的方案就够;第二,是否有明确的弹性需求——流量有明显波峰波谷,需要按指标自动扩缩容,这时候调度和自动伸缩的价值才体现出来;第三,团队是否有足够的运维能力——Kubernetes 把复杂性从"机器"转移到了"对象和网络",它没有消灭复杂性,只是换了个地方,没人能看懂kubectl describe的输出,出了问题照样抓瞎。

反过来说,只要你的服务超过五六个、有多环境隔离需求、有滚动发布和回滚的硬性要求,那么 Kubernetes 提供的这套抽象就是划算的。它把"发布"这件事变成了一个可以审计、可以回滚、可以自动化的标准动作,这个价值在服务数量上去之后会被放大很多倍。

2. 先搞清对象模型:Pod、Deployment、Service 不是同级概念

新手看文档最容易被绕晕的地方,是把 Pod、Deployment、Service 当成三个并列的东西去记。实际上它们是有层次、有包含关系的:Deployment 管着 ReplicaSet,ReplicaSet 管着 Pod,Pod 里跑着容器;Service 不"拥有"任何 Pod,它只是通过标签选择器指向一组 Pod。理解了这个层次,很多"为什么我改了 Deployment 但 Service 没反应"的困惑就自动消失了——因为它们之间根本没有直接引用关系,只有标签匹配关系。

我会把这套对象模型拆成四组来讲:计算(Pod、Deployment、StatefulSet、DaemonSet、Job)、网络(Service、Ingress)、配置(ConfigMap、Secret)、存储(PV、PVC、StorageClass)。四组之间的边界很清晰,配置类对象不会管调度,存储类对象不会管流量,混着理解容易乱。

2.1 Pod 不是容器,它是"一起调度的一组容器"

Pod 是 Kubernetes 里最小的调度单位,注意是调度单位,不是运行单位。一个 Pod 里可以有一个容器,也可以有多个容器,它们共享同一个网络命名空间(也就是共享 IP 和端口空间)和同一组存储卷。最常见的多容器组合是"主业务容器 + 边车容器",比如主容器跑业务逻辑,边车容器负责日志采集或者代理转发。

为什么要有 Pod 这一层,而不是直接调度容器?核心原因是有些进程必须部署在一起、共享网络和文件。比如日志采集需要读业务容器的日志目录,如果用两个独立容器,就得走网络传输或者共享宿主机目录,前者有性能损耗,后者破坏隔离。Pod 把"必须在一起"的约束表达出来了,调度器把它们当成一个整体放到同一个节点上。

需要提醒的是:同一个 Pod 内的容器共享 IP,所以不能监听同一个端口。我见过有人在 Pod 里放两个容器都监听 8080,结果第二个容器起不来,报端口占用,排查半天才发现是这个原因。另外,Pod 本身是"用完即弃"的,它没有自愈能力,节点挂了 Pod 就没了,所以生产环境基本不会直接创建裸 Pod,而是通过 Deployment 这类控制器来管理。

2.2 Deployment 和 ReplicaSet:一条控制链,不是一个对象

kubectl get deployment和kubectl get replicaset是两个不同的东西,但很多人只知道前者。它们的关系是这样的:你创建 Deployment,Deployment 控制器会根据 Pod 模板创建一个 ReplicaSet,ReplicaSet 再根据副本数创建对应数量的 Pod。为什么要多这一层?因为 ReplicaSet 记录了"某一版 Pod 模板"的快照,滚动更新时,Deployment 会创建一个新的 ReplicaSet,然后逐步把旧 ReplicaSet 的副本数降到 0,新的升到目标值。旧 ReplicaSet 不会被立刻删除,而是保留下来(数量由revisionHistoryLimit控制),这就是回滚能工作的基础。

理解这条链的实际价值在于排查问题。当你发现 Pod 数量不对时,正确的排查路径是:先看 Deployment 的状态和事件,再看它关联的 ReplicaSet 是不是创建成功了,最后看 Pod 为什么起不来。如果跳过中间层直接看 Pod,很容易漏掉"ReplicaSet 因为配额限制根本没创建出来"这种情况。

不同工作负载的适用场景我整理了一张表,选错类型是新手常见问题:

对象类型适合的场景关键特征
Deployment无状态服务、Web 接口、API 网关Pod 可随意替换,不保存本地状态
StatefulSet数据库、消息队列、需要稳定标识的服务Pod 有固定名称和稳定网络标识,按序启停
DaemonSet日志采集、节点监控、网络插件每个节点(或指定节点)跑一个副本
Job一次性批处理任务运行到成功结束,不重启
CronJob定时任务按 cron 表达式周期性创建 Job

2.3 Service 和 Ingress:一个解决"找得到",一个解决"进得来"

Pod 的 IP 是会变的,重建一次就换一个。如果调用方直接写 Pod IP,那这个调用方基本没法维护。Service 解决的就是这个问题:它提供一个固定的虚拟 IP(ClusterIP)和 DNS 名称,通过标签选择器动态维护后端 Pod 列表,请求转发到 Service 的 IP 上,由 kube-proxy 或者 eBPF 实现负载均衡到具体 Pod。

Service 有几种类型,用途差别很大:ClusterIP是默认类型,只在集群内部可访问;NodePort在每个节点上开一个高端口(默认 30000-32767)转发进来,适合临时调试;LoadBalancer需要底层基础设施支持,会申请一个外部负载均衡器;ExternalName比较特殊,它不做转发,只是返回一个 CNAME 记录。还有一种是clusterIP: None的无头 Service,不给虚拟 IP,DNS 直接返回所有 Pod 的地址,StatefulSet 场景下常用。

Ingress 是更上面一层的东西,它做的是七层路由:根据域名和路径把请求分发到不同的 Service。比如demo.example.internal/api转发到后端服务,/static转发到静态资源服务。这里有个必须说清的点:Ingress 对象本身只是一份路由规则,真正干活的是 Ingress Controller,集群里没装 Controller,Ingress 写了也不会生效。这个坑我踩过,YAML 应用成功、kubectl get ingress也有输出,但访问就是 404,最后发现是 Controller 没部署。

2.4 配置与存储对象:ConfigMap、Secret、PVC 各自的边界

ConfigMap 用来存非敏感的配置,比如应用的环境变量、配置文件内容。Secret 用来存敏感信息,比如密码、令牌、证书,它的内容在 etcd 里可以做加密存储,但要注意默认的 base64 只是编码不是加密,任何有权限读 Secret 的人都能解出来。两者的用法类似,都可以通过环境变量注入或者挂载成文件,区别主要在权限控制和审计上。

存储这块稍微复杂一点。PV 是集群级别的存储资源抽象,PVC 是用户对存储的"申请",StorageClass 则定义了动态供给的方式——你创建一个 PVC,指定 StorageClass,底层插件就会自动创建一个对应的 PV 并绑定。这个设计的好处是应用不需要关心底层是本地盘、网络存储还是对象存储,只描述"我需要 20Gi 的读写盘"。

accessModes这几个参数特别容易配错:ReadWriteOnce表示只能被单个节点读写挂载(注意是节点级,不是 Pod 级),ReadOnlyMany允许多节点只读,ReadWriteMany允许多节点读写,但不是所有存储插件都支持。如果应用是多副本部署又需要写共享目录,选了 RWO 就会出现"第二个 Pod 挂载失败"的问题,日志里会明确报多节点挂载冲突。

3. 一条 kubectl apply 之后,请求在集群里走了哪些门

把 YAML 交给集群,看起来只是一个命令,实际背后是一整条流水线。理解这条流水线,最大的好处是排查问题时有方向感:到底是 API 层被拒绝了,还是调度器找不到合适节点,还是节点上的容器运行时拉不下镜像。这三种情况的报错位置完全不同,处理方法也完全不同。

我见过不少人遇到 Pod 一直 Pending 就慌了,其实只要看一眼kubectl describe pod最后那段 Events,答案基本都在里面:Insufficient cpu是资源不够,node(s) had untolerated taint是污点没容忍,no nodes available to schedule pods是节点都不可用。问题在于,很多人不知道去看 Events,而是去翻容器日志,方向一开始就错了。

3.1 控制面四件套:apiserver、etcd、scheduler、controller-manager

kube-apiserver是整个集群的唯一入口,所有组件都通过它读写状态,没有例外。它负责认证(你是谁)、授权(你能不能做这件事)、准入控制(这次请求要不要被修改或拒绝),处理完之后把对象持久化到etcd。这里的关键设计是:apiserver 是无状态的,所有状态都在 etcd 里,所以控制面可以做多副本,前面挂负载均衡。

etcd是键值存储,存的是整个集群的期望状态和实际状态。它的重要性怎么强调都不过分——etcd 挂了,集群不会立刻崩溃,已有的 Pod 还在跑,但你无法做任何变更,也无法感知新状态。所以生产环境 etcd 一定要做多节点集群、定期备份,并且单独放在性能稳定的磁盘上。

kube-scheduler负责决定 Pod 放到哪个节点。它监听"尚未绑定节点"的 Pod,执行一轮过滤和打分,把结果写回 Pod 的nodeName字段。kube-controller-manager里跑着一堆控制器,Deployment 控制器、ReplicaSet 控制器、节点控制器、ServiceAccount 控制器等等,每个控制器都在自己的调谐循环里把实际状态推向期望状态。

3.2 工作节点三件套:kubelet、容器运行时、kube-proxy

节点上最关键的是kubelet,它负责和 apiserver 通信,接收分配到本节点的 Pod,然后调用容器运行时把容器真正创建出来。它还会定期上报节点状态(心跳),执行探针检查,管理容器的生命周期。kubelet 不自带容器运行时,而是通过 CRI 接口调用外部实现,现在主流的是 containerd 这一类。

网络这块,Pod 之间的互通靠 CNI 插件实现,Service 的负载均衡规则维护靠 kube-proxy(或者某些方案里用 eBPF 替代)。DNS 解析靠 CoreDNS,它本身也是以 Pod 形式跑在集群里的。这里有个排查经验:如果 Pod 之间网络通,但域名解析失败,问题大概率在 CoreDNS 或者resolv.conf的配置上,而不是网络插件。

3.3 调度器怎么挑节点:Filter 和 Score 的真实逻辑

调度过程分两个阶段。第一阶段是过滤(Filter),把所有不满足硬性条件的节点剔除。常见的过滤插件包括:NodeResourcesFit(节点剩余资源是否够 Pod 的 requests)、TaintToleration(Pod 是否容忍节点污点)、NodeAffinity(节点标签是否匹配)、PodTopologySpread(拓扑分布约束)。第二阶段是打分(Score),给剩下的节点排序,比如ImageLocality会给已经缓存了目标镜像的节点加分,NodeResourcesBalancedAllocation会倾向于让 CPU 和内存利用率更均衡的节点。

理解这套机制的实际意义在于:调度只看 requests,不看实际使用量。一个节点实际 CPU 已经用到 90%,但只要 requests 总和没超卖,调度器依然会往上放 Pod。所以requests设置得是否合理,直接决定了集群会不会出现"看起来没满,却调度不上去"或者"调度上去了但节点被压垮"的情况。这个点我在第 5 节会详细展开。

3.4 调谐循环:为什么改 YAML 就能"自动修复"

调谐循环(reconcile loop)是整个 Kubernetes 最核心的设计模式,理解它比记 API 对象重要得多。每个控制器的逻辑都是:读取期望状态,读取实际状态,比较差异,执行动作缩小差异,然后重复。这个循环不关心"上一次是什么状态",只看"现在应该是什么状态"。

这个模式带来一个非常实用的特性:任何手动操作造成的偏离都会被自动纠正。比如你手动删了一个 Pod,ReplicaSet 控制器发现副本数不足,立刻补一个;你手动改了 Pod 的标签,让它脱离了 Service 的选择器,控制器不会管(因为 Pod 模板没变),但下次 Pod 重建又会恢复原样。反过来说,如果你想做临时修改,改 Pod 是没用的,因为它随时会被替换,要改就得改上层控制器。

4. 把第一个应用真正跑起来:从 YAML 到外部可访问

理论讲完,动手跑一遍才能把概念串起来。我选择的场景是一个最普通的 Web 服务:三个副本、带健康检查、通过 Service 暴露、通过 Ingress 对外提供访问。这个场景覆盖了绝大部分有状态服务之外的需求,把这份清单吃透,其他变体都是在这个基础上加东西。

动手之前先确认三件事,这三件事没确认就乱敲命令,很容易改到错误的集群上。第一是集群版本,不同版本的 API 字段有差异,比如 Ingress 在 1.19 之后从extensions/v1beta1换到了networking.k8s.io/v1,老 YAML 直接套用会报no matches for kind。第二是当前上下文,kubectl config current-context看一眼,确认自己连的是测试环境还是生产环境。第三是命名空间,不加-n参数默认落在default,生产环境通常要单独建命名空间做隔离。

4.1 上下文与命名空间的基础确认

kubectl version --short kubectl config current-context kubectl get nodes -o wide kubectl create namespace demo-app

这几条命令的输出里,我会重点看节点状态是不是Ready、节点数量够不够、以及节点的可用资源。如果节点都处于NotReady,后面所有操作都会卡在调度阶段。另外-o wide能看到节点的内网地址和操作系统信息,排查网络问题时有用。

4.2 一份可以直接抄的 Deployment 清单

下面这份清单我在多个模拟项目里用过,参数是经过权衡的,不是随手写的:

apiVersion: apps/v1 kind: Deployment metadata: name: demo-web namespace: demo-app labels: app: demo-web spec: replicas: 3 revisionHistoryLimit: 5 selector: matchLabels: app: demo-web strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: demo-web spec: containers: - name: web image: registry.example.internal/demo/web:1.0.0 ports: - name: http containerPort: 8080 resources: requests: cpu: 200m memory: 256Mi limits: cpu: "1" memory: 512Mi readinessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 livenessProbe: httpGet: path: /livez port: http initialDelaySeconds: 20 periodSeconds: 20 timeoutSeconds: 2 failureThreshold: 3 startupProbe: httpGet: path: /healthz port: http periodSeconds: 5 failureThreshold: 12

几个参数的选择理由值得说清楚。maxSurge: 1配合maxUnavailable: 0,意味着滚动更新时先多起一个 Pod,等它就绪后再删一个旧的,全程可用副本数不低于 3,代价是需要多占一个 Pod 的资源额度。对于对外服务,我会优先保证可用性;对于内部批处理类服务,把maxUnavailable调到 1 可以让发布更快。

revisionHistoryLimit: 5保留五个历史版本,够回滚用了,留太多会占用 etcd 空间。startupProbe是为了应对启动慢的应用——如果没有它,livenessProbe会在应用还没启动完成时就开始探测,连续失败直接触发重启,形成一个永远起不来的死循环。这个坑我在第 5 节会详细讲。

requests给 200m CPU 和 256Mi 内存,limits给 1 核和 512Mi,这个比例是根据实际压测结果定的:正常流量下 CPU 使用在 100m 到 300m 之间波动,内存稳定在 200Mi 左右,留出一定余量。如果一开始没有压测数据,宁可先给保守值,观察一段时间再调,不要一上来就写得很宽裕,那样会浪费集群容量。

应用之后,用这几条命令验证:

kubectl -n demo-app apply -f deployment.yaml kubectl -n demo-app rollout status deployment/demo-web --timeout=120s kubectl -n demo-app get pods -o wide kubectl -n demo-app describe deployment demo-web

rollout status会一直阻塞到发布完成或者超时,比get pods反复刷好用得多。

4.3 用 Service 和 Ingress 把流量接进来

apiVersion: v1 kind: Service metadata: name: demo-web namespace: demo-app spec: type: ClusterIP selector: app: demo-web ports: - name: http port: 80 targetPort: http

这里的targetPort用了端口名http而不是数字,好处是 Pod 模板里改了containerPort之后 Service 不需要跟着改,减少一处需要维护的对应关系。selector必须和 Deployment 模板里的标签完全一致,这是最容易出错的地方。

Ingress 的配置依赖集群里已经装好的 Controller:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-web namespace: demo-app spec: ingressClassName: nginx rules: - host: demo.example.internal http: paths: - path: / pathType: Prefix backend: service: name: demo-web port: number: 80

ingressClassName必须和集群里实际的 IngressClass 名字对上,kubectl get ingressclass可以查看。配完之后,如果访问返回 404,先查 Controller 的日志,再看 Ingress 的describe输出里有没有正确解析出后端 Service。

4.4 滚动发布与回滚的验证姿势

发布新版本时,直接改镜像标签再 apply:

kubectl -n demo-app set image deployment/demo-web web=registry.example.internal/demo/web:1.1.0 kubectl -n demo-app rollout status deployment/demo-web

发布过程中可以用kubectl -n demo-app get pods -w观察 Pod 的更替过程,正常情况下会看到新 Pod 先创建、进入 Ready、旧 Pod 才 Terminating。如果发现新 Pod 一直起不来,rollout status会超时,这时候要看新 Pod 的事件和日志。

回滚:

kubectl -n demo-app rollout history deployment/demo-web kubectl -n demo-app rollout undo deployment/demo-web --to-revision=1

回滚之所以能工作,就是因为前面保留了旧的 ReplicaSet。回滚不是万能的,如果新版本做了数据库结构变更,回滚代码不一定能恢复数据,这一点必须在发布流程里单独考虑,Kubernetes 管不了应用层的数据兼容性。

5. 资源、探针、网络:新手最容易在这里翻车

前面四节讲的是"应该怎么做",这一节讲"为什么照着文档做还是会出问题"。Kubernetes 的文档很全,但它不会告诉你:某些配置组合在特定场景下会引发连锁反应,而且报错信息往往指向不了真正的根因。以下四个问题是我在实际环境里遇到频率最高的,每一个都值得单独记一笔。

5.1 requests 和 limits 写错,比不写更危险

先说清楚这两个概念的区别。requests是调度依据,调度器按它判断节点放不放得下;limits是运行时上限,超过 CPU 限流,超过内存直接 OOMKill。很多人图省事,只写limits不写requests,结果 Kubernetes 会把requests默认成等于limits,导致节点上明明还有余量,新 Pod 却调度不上去。

更隐蔽的问题是资源超卖。假设一个节点有 4 核,你放了 10 个requests: 500m的 Pod,从调度器视角看资源刚好占满,但实际运行时这 10 个 Pod 可能同时冲高,CPU 互相抢占,延迟飙升。这种情况在监控上表现为"节点 CPU 使用率 100% 但没有任何报警",因为所有 Pod 都在 limits 以内。

我的经验是:requests 按 P50 使用量设置,limits 按 P99 的两倍设置,中间留出的空间用于应对突发。如果应用对延迟敏感,limits的 CPU 值不要设得太紧,因为 CPU 限流(throttling)会直接体现在响应时间上,哪怕平均负载并不高。内存的limits要设得比实际峰值高一些,OOMKill 的代价是容器重启,比 CPU 限流严重得多。

5.2 三种探针用反了,容器会陷入重启循环

三种探针的职责完全不同,混淆使用是最常见的配置错误。livenessProbe判断"容器还活着吗",失败就重启容器;readinessProbe判断"容器能接流量吗",失败就从 Service 后端摘掉,但不重启;startupProbe判断"容器启动完成了吗",在它成功之前,其他两种探针都不生效。

最典型的翻车场景是:livenessProbe里检查了一个外部依赖(比如数据库连接),数据库抖动一下,所有 Pod 的 liveness 同时失败,全部重启,重启之后依赖还没恢复,继续失败,形成雪崩。正确的做法是 liveness 只检查进程自身是否健康,依赖的可用性交给 readiness 去处理——依赖挂了就摘流量,等恢复了自动加回来,不要动不动就重启。

另一个高频错误是initialDelaySeconds设得太短。应用启动需要 30 秒,探针 5 秒就开始检查,连续失败三次直接杀掉重启,永远启动不完成。startupProbe就是为这个场景设计的,设置一个足够长的failureThreshold,让应用有充分的启动时间,启动成功后再交给 liveness 接管。

5.3 镜像拉取、DNS 和网络策略的三个高频故障

镜像拉取失败通常有三种原因:镜像地址写错、私有仓库凭证缺失、节点无法访问仓库。区分方法很直接,kubectl describe pod里会给出ErrImagePull或者ImagePullBackOff,如果是凭证问题,错误信息里会有unauthorized字样。私有仓库的凭证需要通过imagePullSecrets挂到 Pod 上,或者在命名空间里配置默认凭证。

DNS 解析失败的典型表现是应用日志里报no such host,但直接ping外部地址是通的。排查路径是:先确认 CoreDNS 的 Pod 正常运行,再kubectl exec进业务 Pod 看/etc/resolv.conf的nameserver是不是指向集群 DNS 的 Service IP,最后检查 NetworkPolicy 有没有把到 DNS 的 53 端口给挡了。最后这条特别隐蔽,因为网络策略默认是全通,一旦有人加了默认拒绝规则又忘了放行 DNS,整个命名空间的服务都会开始报解析错误。

网络策略的生效范围也需要理解清楚。NetworkPolicy 是命名空间级别的,一旦某个 Pod 被任何一条策略选中,它就进入"默认拒绝"模式,只有被显式允许的流量才能进出。这意味着你只加了一条允许入站 8080 的策略,出站的 DNS 查询也会被阻断,除非你同时写了出站规则。这个行为让很多第一次用网络策略的人困惑。

5.4 节点资源告急时,谁先被驱逐

节点内存不足时,kubelet 会主动驱逐 Pod,驱逐顺序由 QoS 类别决定:

QoS 类别判定条件驱逐优先级
BestEffort所有容器都没设 requests 和 limits最先被驱逐
Burstable至少一个容器设了 requests,但不满足 Guaranteed 条件中间
Guaranteed所有容器的 CPU 和内存都设了 requests 且等于 limits最后被驱逐

同一个 QoS 类别内部,会优先驱逐"实际使用量超过 requests"幅度更大的 Pod。这个机制的实际意义是:关键服务配成 Guaranteed,就能在节点资源紧张时活下来;而没设资源限制的 Pod,会成为第一批牺牲品。我在模拟环境里做过测试,一个没设 limits 的调试用 Pod 在节点内存紧张时几乎每次都第一个被清掉,这在生产环境里可能是好事,但在测试环境里会让人莫名其妙地丢容器。

6. 上生产之前,我会逐项打勾的几件事

从"能跑起来"到"敢放在生产环境",中间隔着一整套工程约束。这一节不是泛泛的检查项,而是我在多个项目里积累下来的具体动作,每一条都对应过真实的问题。

6.1 命名空间、标签与资源配额的约定

命名空间不是可选项,是必须项。按环境(测试、预发、生产)和按业务域划分是两种常见方式,我倾向于两者结合:环境一级,业务域二级。每个命名空间配一个ResourceQuota,限制总的 CPU、内存、Pod 数量,防止某个业务无节制地占满集群。再配一个LimitRange设置默认的 requests 和 limits,这样即使有人忘了写,也有个兜底值,不至于出现 BestEffort 的 Pod。

标签规范要提前定好,至少包含:应用名、所属业务域、版本、环境、负责人。这些标签在排查问题时价值巨大——kubectl get pods -l env=prod,team=alpha一条命令就能定位到具体范围。我见过标签写得随意的团队,出了问题只能靠 Pod 名字猜,效率差一个数量级。

6.2 可观测性:从 kubectl describe 到指标与日志

kubectl describe和kubectl logs是排查的第一层工具,但它们有三个局限:只能看当前状态(历史事件会被清除)、只能看单个对象、日志在 Pod 重建后就没了。所以生产环境必须补齐三样东西:集中式日志采集、指标监控、事件持久化。

日志采集通常用 DaemonSet 部署采集器,把容器标准输出收集到集中的存储里。指标监控关注几个关键维度:Pod 的 CPU 和内存实际使用量(用来校正 requests 和 limits)、重启次数(kube_pod_container_status_restarts_total)、Pod 是否处于非 Ready 状态、节点的资源水位。重启次数这个指标特别重要,一个安静地每天重启三次的容器,往往比一个直接挂掉的服务更危险,因为没人注意到它。

事件(Event)默认只保留一小时,排查隔夜问题时会发现事件已经被清空。生产环境建议把事件也采集到日志系统里,或者部署一个持续记录事件的组件,这个投入在排查疑难问题时会体现出价值。

6.3 权限与密钥:RBAC 和 Secret 的基本体面

RBAC 的原则是最小权限。默认的defaultServiceAccount 在很多集群里权限过宽,应用如果不需要访问 API,就应该显式设置automountServiceAccountToken: false,不要在容器里挂一个用不到的令牌。需要访问 API 的场景,单独建 ServiceAccount,配 Role 和 RoleBinding,权限精确到具体的资源和动作。

Secret 这块有三个基本要求:第一,开启 etcd 的静态加密,避免磁盘被拿走后密钥明文泄露;第二,不要用环境变量注入敏感信息(环境变量会出现在kubectl describe和进程信息里,挂载成文件相对安全);第三,定期轮换。另外要提醒一句,Secret 的 base64 编码不提供任何保密性,echo xxx | base64 -d就能还原,不要把"base64 过了"当成安全措施。

最后分享一个我自己的习惯:每次上线新服务之前,先在一个独立的命名空间里用同样的 YAML 跑一遍,把资源限制调到最小、故意触发几次故障(删 Pod、改错镜像标签、断掉依赖),看集群的反应是否符合预期。这个过程花不了多少时间,但能提前暴露掉大部分配置问题,比在生产环境上用手忙脚乱的方式学习要划算得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进