ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes资源限制与探针实战:避免Pod重启和调度失败

Kubernetes资源限制与探针实战:避免Pod重启和调度失败 生产环境里最常被问到的问题一个是 Pod 无缘无故重启另一个是节点负载不高但 Pod 一直调度不上去。这两类问题背后绝大多数都和两个配置有关系资源限制requests/limits和探针Probe。这一篇把这两块拆开讲透。作为系列第八篇前面我们在部署、网络、存储上都踩过不少坑今天聊的这两个点恰恰是线上稳定性最容易翻车的地方也是面试里出现频率极高的考点。文章不会讲太虚的概念尽量基于 kubelet、containerd、cgroup 这一条真实执行链路来说配合可以直接抄走的 YAML 和排查命令。看完之后你至少能回答三个问题requests 和 limits 到底谁在起作用三种探针分别守护什么线上 Pod 被重启或者被驱逐时怎么从事件里快速定位。1. 先搞清楚 requests 和 limits 在 Kubernetes 里到底卡住了谁很多刚接触 Kubernetes 的同学以为 requests 和 limits 是同一个作用的两套配置大不了一个写大一个写小。其实这两个字段的执行位置完全不同一个管调度一个管运行搞混了就会出大问题。1.1 调度器只看 requestsPod 能不能落在这个节点上kube-scheduler 在给 Pod 选节点时只累加节点上所有 Pod 的 requests 总量再看这个总量是否超过节点的 Allocatable。Allocatable 不是节点的物理总容量而是扣掉了系统预留 kube-reserved、system-reserved 以及驱逐阈值之后的可用资源。公式大概是Allocatable Node Capacity - 系统预留 - kubelet 预留 - eviction-thresholds每个新 Pod 要调度调度器会先在 Node 上模拟加上这个 Pod 的 requests如果总和大于 Allocatable就跳过这个节点。如果所有节点都不满足Pod 会卡在 Pendingevents 里出现Insufficient cpu或者Insufficient memory。这里有个常见的隐蔽点只配置 limits 而不配置 requests 时Kubernetes 会把 requests 默认继承为 limits 的值。比如你写limits: cpu: 2那 requests 自动变成cpu: 2调度器会按 2 核去算。很多人以为不写 requests 就等于没有资源占用结果节点上堆积了一堆“虚拟大胖子”。requests 代表的是这个 Pod 最低要保障的资源。调度器只看 requests是因为 limits 是运行上限Pod 不一定真的会用满调度器无法预知实际用量按保障值来分配才是最安全的策略。1.2 运行时靠 limits cgroup内存和 CPU 被锁死Pod 调度到节点之后kubelet 会把 requests 和 limits 通过 CRI 传给容器运行时。以 containerd 为例kubelet 调用 containerd 的 CRI 接口创建容器时containerd 会通过 runc 最终把这些参数落到 cgroup 上。这也是很多运维想了解的调用链kubelet - CRI - containerd - runc - cgroup。CPU 是压缩资源limits 会转换成 CFS 配额。在 cgroup v2 下对应的文件是/sys/fs/cgroup/cpu.max内容类似50000 100000表示这个容器在 100ms 周期内最多使用 50ms CPU换算出来就是 0.5 核。如果容器超过了这个配额内核会强制做 CPU throttle表现为 CPU 使用率被压平但进程不会被杀掉。内存是非压缩资源limits 对应 cgroup v2 的/sys/fs/cgroup/memory.max。一旦容器内所有进程的内存占用超过这个值内核 OOM killer 就会介入优先杀掉容器内占用内存最高的进程。反映到 Pod 上就是容器状态变成 OOMKilled退出码 137Restarts 次数上涨。我见过很多人对内存超限的认知是“Kubernetes 把容器杀了”严格说不对。是内核 OOM killer 杀的进程Kubernetes 只是观察到了退出码并按照 restartPolicy 重启容器。这个细节在排查时很重要因为日志里不一定有 Kubernetes 的错误信息反而是内核日志或者容器退出状态能说明问题。1.3 QoS 等级从资源配置一眼看出 Pod 的优先级Kubernetes 会根据资源配置自动给 Pod 打 QoS 等级一共三种。QoS 等级触发条件典型特征Guaranteed每个容器都同时设置了 CPU 和内存的 requests 和 limits且 requests limits核心业务常用最不容易被驱逐Burstable至少有一个容器设置了 requests 或 limits但又不满足 Guaranteed大部分中间件、普通业务的默认状态BestEffort所有容器都没有设置任何 requests 和 limits临时任务、测试 Pod被驱逐风险最高QoS 等级直接影响节点内存压力时的驱逐顺序。节点内存不足时kubelet 按 BestEffort - Burstable - Guaranteed 的顺序清理 Pod。同等级内部再按 usage/request 的比值排序谁的用量超出保障值比例高谁先被驱逐。这里要特别提醒QoS 是 Pod 维度不是容器维度。只要 Pod 里有一个容器不满足 Guaranteed 条件整个 Pod 就降级成 Burstable。所以生产环境里想给某个 Pod 最高保障每个容器都得写满 requests 和 limits不能漏。2. 三种探针的分工liveness、readiness、startup 别搞混探针这部分我见过最大的问题不是不会写而是把三种探针混用。很多人只听说过 liveness就所有服务都配一个 httpGet结果启动慢的应用被反复重启。要搞清楚三种探针是服务于不同阶段的职责完全不同。2.1 livenessProbe容器死了就重启livenessProbe 解决的是“容器进程还活着但业务已经不可用”的情况。比如 Java 应用线程池耗尽进程没退出但所有请求都超时或者一个 worker 进程死锁了不干活也不退出。这时候 liveness 探测失败kubelet 会杀掉容器再根据 restartPolicy 决定是否重启。注意 liveness 失败不一定导致重启。如果 restartPolicy 是 Never容器被标记为 Failed但不会被重启。如果 restartPolicy 是 OnFailure只有当容器退出码非 0 时才会重启。生产环境默认的 restartPolicy 一般是 Always所以大多数情况下 liveness 失败会触发重启。liveness 的检测逻辑应该尽量轻量。只检查当前进程是否活着、核心依赖是否还在不要在 liveness 里去做复杂的业务检查。不要把数据库、外部缓存的状态也塞进 liveness否则数据库抖动一次所有 Pod 跟着重启那就是事故。2.2 readinessProbe没准备好就别接流量readinessProbe 解决的是“这个 Pod 现在能不能接收流量”的问题。它失败时不会重启容器而是把 Pod 的 Ready 状态置为 False然后从 Service 的 Endpoints 列表里摘掉。等探针恢复成功后Pod 会重新被加回 Endpoints流量恢复。它最常见的应用场景是滚动更新。新版本 Pod 启动后如果 readiness 一直不通过Deployment 的滚动更新会卡住不会继续把旧 Pod 全部删掉。这其实是保护机制不是 bug。如果你在发布期间发现“新 Pod 起来了但流量没切过去”先看 readiness 有没有过。readiness 里适合做相对完整的业务健康检查比如检查应用能否连接配置中心、能否连上数据库、能否正常响应健康检查接口。因为它的失败代价是摘流量而不是重启所以可以适当严格一点。2.3 startupProbe慢启动容器的保护伞startupProbe 是后来才加入的探针专门解决慢启动容器被 liveness 误杀的问题。它的行为是容器启动后先执行 startupProbe在 startupProbe 成功之前liveness 和 readiness 都不会执行如果 startupProbe 失败达到 failureThreshold容器会被杀。举个例子一个 Java 应用启动要 40 秒如果 liveness 配置的是initialDelaySeconds: 10那 10 秒后就开始探测40 秒时应用还没起来连续几次失败容器就会被杀陷入反复重启。有了 startupProbe 之后可以把它配成periodSeconds: 5failureThreshold: 30这样启动阶段最多能容忍 150 秒等应用真正起来后再交给 liveness 和 readiness 接管。这个探针对于重内存、有初始化加载、依赖网络拉配置的应用非常实用。如果你的服务启动时间超过 10 秒强烈建议把 startupProbe 配上然后让 liveness 和 readiness 的 initialDelaySeconds 可以设得保守一点。3. 探针的三种检测机制与参数调优别让探针变成杀手探针类型决定“什么时候检测”检测方式决定“怎么检测”。Kubernetes 支持三种检测方式exec、httpGet、tcpSocket选型不合适或者参数太激进探针本身就会变成故障源。3.1 exec、httpGet、tcpSocket 各自适合什么场景检测方式工作方式适用场景关键注意点execkubelet 通过 CRI 在容器内执行命令退出码为 0 则成功没有 HTTP 服务的进程、CLI 工具、数据库命令本身不能太重避免产生过多僵尸进程httpGetkubelet 从节点侧发起 HTTP GET 请求到 Pod IP 的指定端口Web 服务、有健康检查接口的应用应用必须监听在容器网络可达的地址上监听 127.0.0.1 会探测失败tcpSocketkubelet 发起 TCP 连接能建立连接即成功非 HTTP 的 TCP 服务如 MySQL、Redis只能证明端口通不能证明业务正常httpGet 和 tcpSocket 都是从 kubelet 节点侧发起请求的目标地址是 Pod IP。很多人误以为 httpGet 是在容器内部访问 localhost其实不是。如果你的应用只监听了 127.0.0.1kubelet 从节点访问 Pod IP 是连不上的。这种现象在本地开发容器里经常出现因为本地跑容器时端口映射是通的但放到 Kubernetes 里 Pod IP 的监听地址不对就失败。exec 方式有一个隐藏问题kubelet 每次探测都会通过 CRI 调 containerd 做一次 ExecSync实质上是在容器里启动一个进程。如果探测脚本特别重或者调用频率太高会产生很多一次性进程容器里会出现僵尸进程。所以 exec 探测的命令要尽量简单比如用pgrep、test -f这类轻量命令别写复杂的 shell 脚本。3.2 探针参数逐个拆解initialDelaySeconds 到 failureThreshold探针对象里的参数不多但每个都会影响探测行为。默认值很多人记不准确建议直接看官方文档我列一份最常用的表格。参数默认值作用建议initialDelaySeconds0容器启动后等待多久才开始第一次探测慢启动应用要调大避免启动期误判periodSeconds10每隔多少秒探测一次不要小于 1对性能敏感的服务建议调大timeoutSeconds1单次探测的超时时间如果探测超时频繁先看是不是探测逻辑太重successThreshold1连续成功多少次才认为健康liveness 和 startup 必须为 1failureThreshold3连续失败多少次才认为不健康保守一点可以设 5 或更大避免抖动误杀一个比较容易踩的坑是 timeoutSeconds 和 periodSeconds 的关系。如果单次探测超过 periodSeconds探针不会自动重叠执行而是等上一次完成后再等下一个周期实际探测频率会低于预期。这个在配置时不用太担心但要意识到 timeout 不是越大越好调大 timeout 只是为了给慢接口更多时间而不是让它一直在跑。还有一个细节livenessProbe 和 startupProbe 的 successThreshold 必须为 1因为这两类探针只执行一次成功判定不适合连续成功多次的逻辑。readinessProbe 的 successThreshold 可以大于 1用来避免网络抖动导致 Pod 频繁被摘流量但一般保持 1 就够。3.3 这些参数组合起来会产生什么行为很多人单独看参数能懂组合到一起就不知道下一次探测是什么时候。我直接算一个例子。假设配置如下livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3执行时间线是容器启动后第 10 秒发起第一次探测超时上限 2 秒之后每 5 秒一次。第一次失败没关系连续失败 3 次才判定不健康。这里有个容易误解的点failureThreshold 是在“首次探测失败”之后连续计数不是从容器启动开始算。也就是说如果第 10 秒失败第 15 秒失败第 20 秒失败第 20 秒之后容器就会被杀。这个时间长度大致是initialDelaySeconds (failureThreshold - 1) * periodSeconds timeoutSeconds但实际还要考虑探针执行时间不用算得太精确重点是理解“连续失败”的含义。探针参数调整时建议遵循“先放宽后收紧”的原则。上线初期把 failureThreshold 调大、period 调大等确认探针稳定后再逐步收紧。不要一开始就追求秒级发现故障那通常意味着探针本身会成为故障源。4. 一个同时包含资源限制和探针的完整 YAML 部署实战光讲参数不落地没有意义。这一节给一个完整的 Deployment 示例包含资源限制、三种探针、资源字段注释然后说明怎么验证。4.1 完整 YAML 代码与字段注释下面这段 YAML 直接可以用于部署一个模拟 Web 服务的应用。它设置了 Guaranteed 级别的资源限制同时也配了三种探针。apiVersion: apps/v1 kind: Deployment metadata: name: demo-web namespace: production spec: replicas: 2 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: demo-web image: registry.example.com/demo-web:1.2.0 ports: - containerPort: 8080 name: http resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 512Mi startupProbe: httpGet: path: /startup port: http initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30 readinessProbe: httpGet: path: /ready port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3几点解释resources 里 requests 和 limits 的 CPU、内存都写了且值相等所以这个 Pod 是 Guaranteed QoS节点资源紧张时它被驱逐的优先级最低。startupProbe 的 failureThreshold 是 30periodSeconds 是 5意味着启动阶段最多容忍 150 秒。如果应用在 150 秒内没起来容器会被杀。readinessProbe 配置了 timeoutSeconds: 2要求 2 秒内返回的 /ready 接口才能认为就绪否则就绪状态失败。livenessProbe 的 initialDelaySeconds 是 20确保 startup 探针成功后再给应用一点稳定时间才开始判断存活。4.2 部署后如何验证资源限制真的生效部署完成后不要只看 Pod Running 就结束了要验证资源限制有没有真正落到 cgroup 上。第一步看节点资源分配情况kubectl describe node node-name输出里有 Allocated resources 部分能看到当前节点上所有 Pod 的 CPU 和内存 requests 总和。这个值会直接影响后续 Pod 能否调度到这个节点。第二步进入容器查看 cgroup 文件kubectl exec -it pod-name -n production -- cat /sys/fs/cgroup/cpu.max kubectl exec -it pod-name -n production -- cat /sys/fs/cgroup/memory.max在 cgroup v2 的节点上cpu.max 输出类似50000 100000memory.max 输出类似536870912换算过来正好是 512Mi。如果看到的数值和 YAML 里配置的对不上说明容器运行时或 kubelet 的设置有问题需要排查。第三步用压测工具验证 CPU throttle。比如在容器里跑一个死循环消耗 CPU然后通过kubectl top pod观察 CPU 使用率应该被限制在 500m 左右。如果看到 CPU 使用率长时间超过 500m说明 limits 可能没生效需要检查节点的 cgroup 驱动配置。验证内存限制更直接配合应用的内存压力测试或者故意触发 OOM观察容器退出码是否为 137事件里是否出现 OOMKilled。正常预期是容器会被杀掉并且重启证明 memory.limit 生效。4.3 如何验证探针真的在干活探针验证的核心思路是人为制造故障观察 Kubernetes 的反应。readiness 探针验证方法找到 /ready 接口临时让它返回非 200。可以直接进入容器停掉提供 /ready 接口的服务或者改成直接 kill 应用对应的监听进程然后看kubectl get pod -n production -o wide观察 READY 列变成 0/1同时查看 Endpointskubectl get endpoints service-name -n production你会发现这个 Pod 的 IP 已经从 Endpoints 里消失。等接口恢复后READY 会自动变回 1/1IP 重新加回来。liveness 探针验证方法把 /healthz 接口弄挂或者直接 kill 掉容器里的主进程。观察kubectl get pod -n productionRESTARTS 列会开始增加describe 里能看到 Last State 的退出码和事件。如果 liveness 配置正确kubelet 会自动重启容器这个时间窗口取决于你的 periodSeconds 和 failureThreshold。startupProbe 的验证相对简单观察容器启动阶段在kubectl describe pod的 Events 里能看到Started container以及探针相关的信息只要应用能正常启动就不会触发 liveness。如果想测试 startup 失败可以把镜像换成启动必然失败的版本看容器是否按 failureThreshold 计数后被重启。5. 我在生产环境踩过的坑资源、探针和节点驱逐最后这部分是真正的运维经验。这些坑单独看都挺小但叠加在一起足以让一个业务频繁抖动。5.1 OOMKilledlimits.memory 设置低于实际需求引发的重启印象最深的一次是给 Java 应用设置 memory limit 4GiJVM 堆参数也是 4G。按道理没问题但运行一段时间后容器频繁被 OOMKilled。后来才发现JVM 除了堆内存之外还有 Metaspace、线程栈、JIT 编译缓存、Direct Memory这些都不在-Xmx的控制范围内。堆设 4G整个进程实际占用可能超过 4.5Glimit 一卡就挂。正确的做法是给 JVM 容器加上-XX:MaxRAMPercentage70.0这类容器感知参数让 JVM 自动根据 cgroup 限制计算堆大小而不是直接指定-Xmx。或者把 memory limit 上调到比 JVM 峰值占用高 20% 到 30%。排查 OOM 时不要一上来就怀疑内存泄漏先看容器退出码和 cgroup 限制值很多 OOM 就是“limit 设小了”。5.2 liveness 探针太激进导致容器不断被误杀另一个典型场景是 liveness 探针配置得跟 readiness 一样严格periodSeconds 设 1 秒timeout 设 1 秒failureThreshold 设 2。结果应用一遇到 Full GC 或者 CPU 抖动探针就超时连续两次失败后容器被重启然后重启后又需要预热又触发探针失败形成重启风暴。这里的教训是liveness 探针一定要“钝”一点。它只负责检测最核心的死锁、进程异常不要追求秒级发现。如果担心故障恢复速度可以靠 readiness Service 摘流量的机制来切走流量让 liveness 保持相对迟钝。还有一次是团队把数据库探活脚本写进了 liveness exec结果数据库主从切换瞬间连接失败所有依赖数据库的 Pod 全部被 liveness 杀掉重启实际数据库 30 秒后就恢复了但 Pod 已经重启了一轮。从那之后我严格规定liveness 只能检查本地进程状态不能依赖外部服务。5.3 节点资源压力与 Pod 驱逐QoS 和优先级决定的命运节点内存被打满时kubelet 会触发驱逐。这个动作不是马上杀容器而是有一个 eviction 阈值默认通常是memory.available 100Mi这一类。一旦触发kubelet 会优先驱逐 BestEffort 和 Burstable 的 Pod。我维护的集群里发生过一次误伤一个离线计算任务的 Pod 以 BestEffort 方式运行把节点内存吃满结果同一节点上另一个 Burstable 的业务 Pod 被驱逐了。当时业务方很委屈说“我明明配了 requests 和 limits为什么还被驱逐”。原因就是 QoS 是 Burstable在 BestEffort 被清掉之后它就是下一个被清理的对象。如果你的业务核心程度很高唯一的保障就是把它变成 Guaranteed。也就是每个容器都同时设置 CPU 和内存的 requests limits。Guaranteed 虽然不能保证绝对不驱逐但至少在节点资源紧张时它是最后被考虑的。另外requests 不要随便填一个小于实际使用的值因为在驱逐排序时usage/request 比例越高越容易被优先驱逐。合理评估每个 Pod 的基准使用量requests 要能兜住常态运行而不是随便写个 10m。5.4 排查链路从 kubectl describe 和 events 看真相最后给一个标准的排查链路遇到容器重启、驱逐、流量异常时按这个顺序看。第一步获取 Pod 当前状态kubectl get pods -n namespace -o wide kubectl describe pod pod-name -n namespace重点看 describe 输出的 Last State、Exit Code、Reason、Events 末尾段落。如果 Reason 是 OOMKilled直接往内存 limit 方向查如果 Reason 是 Unhealthy再往上翻 Events能看到具体的探针失败信息。第二步看节点状态和资源情况kubectl describe node node-name kubectl top node如果节点有 MemoryPressure 或者 DiskPressure 状态说明已经进入了驱逐流程。这时候要关注哪些 Pod 被驱逐了事件里能看到。第三步查看 kubelet 和容器运行时的日志。如果是 containerd可以看journalctl -u kubelet -n 200 --no-pager journalctl -u containerd -n 200 --no-pagerliveness 探针失败时kubelet 日志会留痕。exec 探针失败还经常会有 ExecSync 相关的错误信息。第四步查看集群事件kubectl get events --sort-by.lastTimestamp -n namespace这个命令能按时间倒序看到整个故障周期内发生的所有事件尤其是长时间没有探针事件、但容器重启了的情况往往说明探针没有生效或者配置写错了。如果让我给新人一个最朴素的建议所有核心服务先按 Guaranteed 来配探针先配 readiness稳定后再加 liveness参数从宽松到严格慢慢收紧。资源限制和探针是两个独立又相互影响的功能但线上故障经常是它们一起搞出来的。踩过几次坑之后我每次上线前都会问自己三句话这个容器内存会不会超这个探针失败会怎样如果节点被打满谁会先滚蛋把这三个问题想清楚大部分稳定性事故都能提前规避。
返回列表