首页 / 资讯中心 / 文章详情

容器资源限制与调度机制:从cgroups到Kubernetes的实战解析

容器资源限制与调度机制:从cgroups到Kubernetes的实战解析 ★ FEATURED ARTICLE
1. 先说清楚容器资源限制到底在限制什么做容器逃不过两个词资源限制、调度。很多人把这两个词混在一起谈觉得给容器设了 CPU 和内存上限调度器就能好好安排它了。实际上这是两套完全独立的机制——资源限制解决的是单机上这个进程能从内核要多少资源调度解决的是这一堆容器应该各自落在哪台机器上。但它们又紧密咬合没有资源限制调度器算出来的剩余资源就是笔糊涂账没有调度资源限制只能保证单机不被打爆保证不了全局的合理分配。我把这套东西彻底搞明白是在一次线上事故之后。那是一个深夜告警一台物理机上跑了 12 个容器其中一个是内部压测服务代码里有个内存泄漏跑了三天把内存吃到了 30GB。我当时想的是反正是物理机 64GB还能扛就没给容器设内存上限。结果半夜另一个核心业务容器申请内存被内核 OOM Killer 盯上直接被 kill 掉流量切到备用节点备用节点也跟着抖动最后整条链路雪崩。复盘的时候大家说加个内存限制就好了但问题没有这么简单——限制加多少虚拟机里跑容器和物理机跑容器限制方式一样吗容器内看到的可用内存和宿主机的剩余内存为什么总是对不上调度器到底靠什么判断这台机器还能不能再塞一个容器如果你也是一路踩坑走过来的这篇内容应该能帮你把这些概念串起来。我会把容器资源限制的底层机制、调度器的工作逻辑以及两者结合时最容易踩的坑通通拆开讲最后补上我实际排查事故的思路链。先说基础概念。容器资源限制本质上是通过 Linux 内核的cgroups 机制Control Groups控制组实现的。它不是一个虚拟机管理软件给容器设的配置文件而是内核本身提供的一种资源记账和隔离能力。每创建一个容器容器运行时比如 Docker 或 containerd就会在宿主机上创建一组 cgroup 目录然后把容器里所有进程的 PID 写进去之后这个 cgroup 下所有进程消耗的 CPU、内存、磁盘 IO、网络带宽都会被内核统一记账并且可以被配额机制卡上限。这里有个特别容易误解的点限制limit不等于隔离isolation。命名空间namespace隔离的是你能看到什么——容器里的进程看不到宿主机的其他进程文件系统、网络栈也都是独立的视图。而 cgroups 管的是你能用多少——即使你可以看到全部 CPU你也只能用配额范围内的那部分。两者缺一不可只有 namespace 没有 cgroups容器就是一群能互相看见但不受约束的进程只有 cgroups 没有 namespace那只是普通的进程限速工具。Docker 之所以能提供像虚拟机一样的体验正是因为这两套内核机制被同时用上了。资源限制需要覆盖哪些维度实际生产中我至少会关注以下四项CPU限制容器能占用的 CPU 核心数与时间片比例。决定了服务是能抢占整台物理机的所有核还是只能老老实实待在配额内。内存限制容器的物理内存使用量这是最硬的限制超了就会被 OOM Killer 杀掉。磁盘 IO限制容器的读写带宽和 IOPS。被忽视的重灾区尤其是日志量大的服务。进程数PID限制防止某个容器 fork 出海量进程把宿主机的 PID 空间耗尽。磁盘空间也是常见限制维度比如 Docker 的--storage-opt或 K8s 里的 emptyDir 大小限制不过它更偏存储层面和 cgroup 的记账逻辑不太一样。我见过很多团队上了 Kubernetes 之后只管给容器写个resources.limits底层细节完全不看。结果一遇到 CPU 节流throttling问题就懵了明明 Pod 的 CPU 用到了 limit 上限但容器内的应用性能就是上不去查监控还看不出什么异常。原因往往出在他们根本不理解 CPU 限制是靠时间片配额实现的而不是靠绑核实现的。这一点我会在下一节详细展开。2. CPU 与内存限制的底层机制和参数细抠2.1 CPU 限制配额时间片和绑核是两回事先做一个随手实验。在一台 8 核的 Linux 机器上用 Docker 跑一个容器指令示例docker run --rm --cpus1 -it ubuntu:22.04 bash进入容器后我们执行nproc输出很可能是 8而不是 1。为什么因为--cpus1并没有让容器看到1 个 CPU——nproc读的是 CPU 亲和性掩码而 Docker 默认不会给容器设置 CPU 亲和性除非你用了--cpuset-cpus。这个参数真正做的事是通过 CPU 带宽bandwidth控制来限制容器内的进程最多只能使用1 个 CPU 核心等效的计算时间。这里引入两个关键内核参数cpu.cfs_period_us默认值 100000微秒也就是 100ms表示一个调度周期。cpu.cfs_quota_us默认值 -1表示不限制。如果设置为 100000就表示每个周期内这个 cgroup 最多能跑满 100ms 的 CPU 时间也就是 1 核。--cpus1在 Docker 里实际做的事是把cpu.cfs_period_us设成 100000cpu.cfs_quota_us设成 100000。这样内核在每 100ms 的窗口内会统计这个 cgroup 里所有线程的实际运行时间超过 100ms 之后剩余的线程就会被强制休眠直到下一个周期重新获得配额。如果--cpus4quota 就是 400000容器最多能用满 4 个核心的等效时间。这个机制决定了两个事情第一容器内的进程不会死锁在某个核上它会跑到哪个核由内核 CFS 调度器决定第二如果你的服务是延迟敏感型且 CPU 使用率经常逼近 limit你会看到 CPU 节流带来的 毛刺——不是没资源可用时间是分片给的拼凑不出整段的计算能力。典型场景是 Java 服务GC 线程、业务线程都要 CPU一旦被节流GC 停顿时间会明显拉长接口耗时跟着上涨而你从top里看到的进程 CPU 占用率却并不高因为统计窗口被拉长了。另外一个常见参数是--cpu-shares在 Kubernetes 里对应requests.cpu它设定的是相对权重而不是硬性上限。假设两个容器 A、B 分别设置 shares 为 1024 和 512当宿主机 CPU 空闲时两者都可以自由使用空闲 CPU但当 CPU 资源紧张时内核会按 2:1 的比例分配时间片给它们。这个参数是软限制性质的主要用于保证服务的基线资源而不是卡死上限。如果你的服务对 CPU 缓存有强依赖比如搜索类、数据库类可以再加上--cpuset-cpus把容器绑定到固定的物理核上这样能减少上下文切换和 CPU 缓存失效。例如docker run --cpuset-cpus0,1 --cpus2 -it ubuntu:22.04 bash这样既绑定了 0、1 号物理核又限制了总 CPU 时间为 2 核。生产环境我建议除非明确知道服务需要否则不要轻易绑核因为绑核会让调度器在资源碎片化的机器上很难做装箱反而降低集群整体的资源利用率。2.2 内存限制为什么超限就会被直接杀死内存限制比 CPU 限制要硬得多。只要这个 cgroup 内所有进程使用的物理内存包括 page cache 中不可回收的部分超过限制内核就会触发回收如果回收不掉就会按照 cgroup 内进程的 oom_score 挑一个进程杀掉。在 Docker 里设置--memory4g容器最多使用 4GB 物理内存。这个限制包括匿名内存堆、栈和部分文件缓存page cache但在多数发行版上内核会尽量回收 page cache 而不是直接杀进程所以实际遇到最多的还是堆内存和元数据把内存打爆。这里有一个非常值得注意的细节swap 和内存限制的关系。如果你在宿主机上开了 swap且没有给容器单独设置--memory-swapDocker 默认的 swap 上限会是内存上限的两倍。也就是说--memory4g不额外设置 swap 时容器实际上最多用 4GB 内存 4GB swap。这会导致一个现象容器内存到了 4GB 后不会立刻被杀而是开始用 swap整体性能断崖式下降但进程活着。很多人在排查时只盯着 RSS 没涨为什么不响应完全没意识到 swap 已经被吃满了。处理建议分两种场景生产环境我几乎都会关闭容器 swap--memory-swap4g让它和内存上限一致迫使超过 4GB 时立刻触发 OOM 机制尽早暴露问题而不是让服务在 swap 里假死。但如果你是靠内存换性能的批处理任务比如大量读文件可以保留少量 swap 作为缓冲。再补一个 cgroup v2 的注意点。现在 Debian/Ubuntu 新版本和主流云厂商的内核都已经切到 cgroup v2v2 里内存限制的写法变了memory.max直接对应原来的memory.limit_in_bytes而且 v2 默认会把 swap 单列不再默认给两倍 swap的规则。在容器平台层做监控采集的时候要注意把 cgroup v1 和 v2 的路径区分开否则图表上会出现内存使用率超过 100%的怪事。2.3 为什么容器内看到的 CPU/内存和宿主机对不上这个问题至少碰到过十次以上。你进到容器里执行free -h发现总内存是宿主机的大小执行top发现所有 CPU 核都在列表里。但宿主机明明通过 cgroup 限制了这个容器最多只能用 2 核 4GB。这会让很多人误以为限制没生效。原因我刚才零散提过/proc/meminfo、/proc/cpuinfo这些文件反映的是内核的全局视图而不是 cgroup 的视图。它们不受 namespace 隔离影响。除非容器平台特意做了一层虚拟化或者通过 lxcfs 这类工具把 cgroup 信息映射到/proc里否则你在容器里看宿主机是看不到自己这台容器当前被限制了多少的。所以排障时不要依赖容器内free、top的绝对值要看 cgroup 目录里的统计文件。举例# cgroup v1 cat /sys/fs/cgroup/memory/memory.usage_in_bytes cat /sys/fs/cgroup/cpu/cpu.stat # cgroup v2 cat /sys/fs/cgroup/memory.current cat /sys/fs/cgroup/cpu.stat这也是为什么我强烈建议监控系统直接采集 cgroup 数据而不是依赖容器内部 agent 上报。否则资源限制有没有生效、有没有触发节流你根本看不到。3. 从单机到集群调度器如何理解还有多少可用资源3.1 调度本质一个带约束条件的装箱问题如果说资源限制是单个容器在一台机器上能用多少的规则表那调度就是这张规则表贴在哪台机器上最合适的决策过程。把调度抽象成一句话有一批待调度的 Pod/容器有一批候选节点每个节点初始可用资源已知每个 Pod 声明了自己需要的资源requests和最多能用的资源limits调度器要在满足各种约束资源够不够、端口冲不冲突、数据盘在不在、亲和性满不满足的前提下为每个 Pod 选一个节点。这里最重要的是理解 Kubernetes 调度器kube-scheduler是怎么看待资源够不够的。它判断的依据不是top看到的节点实时负载而是申请值requests的累加。举个例子一个节点有 8 核 16GB上面已经运行了三个 PodPodrequests.cpurequests.memoryA1核2GBB2核4GBC1核1GBkube-scheduler 在调度一个新的 Pod D 时会先计算节点已分配allocated的 requests 4核 7GB然后看看 D 如果申请 2核 4GB加上去是 6核 11GB没有超过节点的总量 8核 16GB于是认为这个节点可以调度。注意它完全不管 A、B、C 是不是真的在用这么多资源甚至不管节点是否因为其他原因接近崩溃。这意味着什么意味着如果一个 Pod 只设了非常大的 limits、但 requests 设得很小调度器会认为它不占地方一股脑往同节点塞。等它们真正跑起来实际使用量远超 requests节点就可能会被击穿。所以在做容量规划时requests 和 limits 必须一起看。3.2 requests 和 limits 是两套不同的语义Kubernetes 的资源模型区分得很清楚requests调度依据。告诉调度器这个容器至少需要这么多资源调度和节点容量校验基于它。limits运行时限制。告诉节点上的 kubelet这个容器最多只能用这么多资源一旦超过就会被限流或杀掉。CPU 超限会限流内存超限会 OOM Kill。很多人刚上手时喜欢把 requests 和 limits 写得一样大理由是这样服务质量有保证。这在资源充足时没问题但如果你关心集群利用率就会发现在绝大多数服务不是时刻打满的情况下requests limits 等于主动放弃了超卖红利——你为每一份空闲资源付了预定费用但并没有真正使用它。反过来如果 requests 设得太小而 limits 很大调度器会认为这个容器很省把大量高 limits 容器叠在同一节点上一旦洪峰到来整节点直接被打爆。这里需要找到一个平衡点。我常用的策略是在线业务延迟敏感如 API 服务requests 设为基线负载的 120% 或 P50 的 1.5 倍limits 设为能承受的峰值尽量控制在 requests 的 2~4 倍以内。离线业务大数据计算、批量任务requests 可以设小一点limits 设大利用超卖。但必须搭配优先级和抢占机制保证在线业务需要资源时能把离线任务挤走。关键中间件etcd、数据库、消息队列requests limits不做任何超卖。这是我的底线因为它们的内存和 CPU 抖动会直接拖垮下游。我还碰到过一个容易忽略的场景GPU 节点的调度。GPU 在 Kubernetes 里通常是通过设备插件以 Extended Resource 的形式暴露的比如nvidia.com/gpu调度器在计算这类资源时走的是整数资源计数逻辑不是 CPU 时间片那种连续数值。你必须在 Pod 里显式声明resources.limits[nvidia.com/gpu]: 1节点上这个资源够数才调度否则即使节点有 GPU也轮不到你的 Pod 用。这里 requests 和 limits 的语义近似等价因为 GPU 分配是独占式的。很多人在混用 CPU 和 GPU 节点时踩过坑所有 Pod 都声明了 CPU requests但只有带 GPU 的 Pod 才声明 GPU 数量调度器在 GPU 节点上还要单独考虑 CPU 是否够分不然 GPU 资源满了 CPU 没满或者反过来都会导致调度失败。3.3 资源抢占与调度失败的真实场景资源不足时的表现不一。我先说最常见的 Pending 场景。你创建一个 DeploymentPod 一直卡在 Pendingkubectl describe pod里写着0/4 nodes are available: 1 Insufficient cpu, 2 Insufficient memory, 1 node(s) had untolerated taint。Insufficient cpu和Insufficient memory不用解释——候选节点上当前已分配 requests 新 Pod requests 超出节点容量。had untolerated taint是节点上有污点taint而你的 Pod 没有对应的容忍toleration调度器直接排除节点。另一个更微妙的场景节点的实际负载很高但所有已分配 Pod 的 requests 加起来还没超过节点容量。调度器认为这是个轻载节点继续往里塞 Pod结果新 Pod 的 CPU 时间片不够用应用卡成幻灯片。这在自建 Kubernetes 集群里特别常见因为 kube-scheduler 默认根本不看节点的真实负载指标比如节点 CPU 使用率、内存压力。怎么解决两个方向安装descheduler定期把过载节点上的 Pod 重新调度或者给节点设置较高的资源余量。在调度阶段就用动态资源感知的调度器扩展比如基于 Prometheus 采集的节点实时负载做过滤和打分。如果你的集群规模不大最省事的做法是把新节点的 requests 总和保持在节点容量的 70% 以内给自己留出突发余量。抢占地盘的机制也要理解。当一个高优先级 Pod 调度失败且节点上存在低优先级 Pod 时kube-scheduler 会尝试抢占——驱逐低优先级 Pod给高优先级 Pod 腾位置。这里比较坑的是被抢占的低优先级 Pod 如果不是 Deployment/StatefulSet 管理的它被删掉之后就不会自动重建造成服务长时间中断。所以生产环境建议所有工作负载都通过控制器管理不要用裸 Pod。4. 调度的实际组合把限制变成集群级的策略4.1 节点选择从硬性过滤到软性打分当容器数量少、节点只有三五台时调度可以完全依赖资源 requests 是否超出容量。但集群规模到上百节点后该把 Pod 放哪就不只是资源问题还涉及可用性、亲和性、甚至成本。kube-scheduler 的调度流程简单概括是预选Filter→ 优选Score→ 选定Select。预选阶段把所有不满足硬性条件的节点过滤掉。比如资源不足、端口冲突、节点 taint 不匹配、Pod 有nodeSelector而节点没有对应 label 等。优选阶段对通过预选的节点打分。打分维度包括资源富余程度、Pod 与节点之间的亲和性/反亲和性、Pod 分布的分散程度等。选定阶段选最高分节点执行绑定。其中资源分数的细节影响很大。默认的LeastRequestedPriority偏向选择资源富余度高的节点让集群负载趋于分散而MostRequestedPriority偏向把 Pod 集中到少量节点提升装箱效率。Kubernetes 推荐配置通常会把节点原有资源利用率、新 Pod requests 考虑进去但你生产中可能需要根据场景调整权重。比如批处理集群可以用装箱策略提高吞吐在线交易系统则用分散策略提高容错。4.2 亲和性、反亲和性和拓扑分布只用资源大小决定调度会有几个问题两个互相调用的服务被调度到不同交换机下的节点跨机房的网络延迟把接口耗时拉高一倍。某个服务的多个副本落到同一台物理机机器一挂这个服务的所有副本同时不可用。GPU 节点上排满了一个服务的副本其他服务反而抢不到 GPU。亲和性解决了第一和第三个问题你可以用nodeAffinity指定 Pod 必须或尽量调度到打了特定 label 的节点比如gputrue的节点、zoneaz1的节点。Pod 间亲和性解决第二个问题的另一面让 frontend 和 backend 尽量落在同一个拓扑域里减少跨节点网络开销。反亲和性则是解决多副本不能挤在一起。典型配置affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: my-service topologyKey: kubernetes.io/hostname这段的意思是在调度时尽量避免让app: my-service的多个副本落在同一个 hostname物理节点上。注意这里用了preferredDuringScheduling...它是尽量满足但非强制因为如果你强制每个节点只能一个副本而节点总数少于副本数时多余副本永远 Pending。还有一个和资源限制强相关的点反亲和性必须配合资源 requests 合理设置才能发挥作用。你想把一个 stateful 服务的三个副本分散到三台机器但每台机器上其他 Pod 已经占了大量 requests三台机器都只剩不到一个副本所需的资源配额那调度器要么把三个副本都塞到一台机器要么让部分副本 Pending。所以做高可用部署时节点空闲资源必须留出至少一个副本容量。4.3 污点与容忍给节点设置准入规则污点taint和容忍toleration是调度里的准入控制。它不像资源限制那样直接约束容器能消耗多少资源而是决定容器能不能进这台节点。举个例子集群里有几台机器是高性能型比如带 NVMe 磁盘、大内存你希望只有关键数据库类 Pod 能上去普通业务不要占用。可以给高性能节点打一个污点kubectl taint nodes perf-node dedicateddb:NoSchedule然后在数据库 Pod 上配置容忍tolerations: - key: dedicated operator: Equal value: db effect: NoSchedule这样普通 Pod 上没有对应的容忍调度器根本不会把它们放到 perf-node 上数据库 Pod 带有容忍可以进入。这里面有个易混淆的效应NoSchedule只影响未来调度不会驱逐已经运行在该节点上的 PodNoExecute会立即驱逐所有不容忍的 Pod。如果你要对存量 Pod 生效只能用NoExecute或者先排空节点再重新调度。污点与容忍和资源限制的配合在混部场景尤为重要。混部在线业务和离线任务跑在同一批机器上的核心是在线业务用污点把离线任务挡住离线任务靠容忍进入然后离线任务设置较低的 requests 来充分利用空闲资源但它的 limits 必须受到严格约束否则内存泄漏或 CPU 毛刺会反过来影响在线业务。我在生产环境见过一个很典型的组合在线服务设置requests.cpu2、limits.cpu4离线批量任务设置requests.cpu0.5、limits.cpu8同时给离线任务一个低优先级 PriorityClass。这样调度器把离线任务塞满节点但一旦在线服务扩展副本节点资源紧张高优先级在线服务的抢占机制会把低优先级离线任务挤掉。这套逻辑跑得很稳但前提是离线任务必须能容忍被抢占和重启不然数据一致性会出大问题。5. 实战排查资源限制和调度叠加时最容易踩的坑5.1 现象一容器 OOMKilled 到底是谁杀的收到告警说某个 Pod 被 OOMKilled第一反应往往是去容器里看日志增加内存 limit。但同样的现象背后有三种可能cgroup 内存超限容器进程被内核作为 cgroup 超额的首犯杀掉。宿主机物理内存本身耗尽全局 OOM Killer 启动挑了一个进程杀。这个进程可能不是使用内存最多的而是 oom_score 最高的。容器内应用自己崩溃、panic被 kubelet 判定为内存超限重启。判断方法是看内核日志。cgroup v1 时用dmesg -T | grep -i oomv2 时可以查 BPF 或 events。如果日志里出现Memory cgroup out of memory那是容器自身超限如果是全局 OOM会看到Out of memory: Kill process ...且受害进程的 cgroup 路径通常不是规规矩矩的容器路径。经验之谈当 Pod 的内存 limit 是 4GB但容器内 Java 进程的堆设了 5GB或者堆没设 XmxJVM 默认按宿主机内存比例分配那必然反复 OOMKilled。我在排查过几次后凡是容器化 Java 应用第一件事就是确认 JVM 参数是否感知到了 cgroup 限制。JDK 8u191 和 JDK 10 已经支持容器感知默认-XX:UseContainerSupport会自动根据容器内存限制调整默认堆大小老版本 JDK 会把宿主机内存当成可用堆大小直接打爆限制。这类问题不用改代码换基础镜像就能解纯属踩坑经验。5.2 现象二CPU Limit 设了但容器还是把节点打满还有一个高频现象给容器设了limits.cpu2按理说不可能超过 2 核但节点整体 CPU 使用率还是 100%而且top显示某个容器进程 CPU 占用很高。这不是限制没生效而是你限制的是一个容器内的所有进程总和如果这个容器里起了 8 个线程它们加在一起不能超过 2 核但如果容器的 PID namespace 里有多个进程其中一个进程自身就能跑满 2 核另一个就完全没 CPU 可用。看起来就是某个进程把节点打满。更隐蔽的是 CPU 管理器CPU Manager的静态策略。如果你在 kubelet 开启了--cpu-manager-policystatic并且 Pod 是 Guaranteed QoSrequests limits 且都是整数核kubelet 会把该 Pod 的容器绑定到独占的 CPU 核心上。这种模式下其他容器无法使用这些核心即使它们是空闲的。节点上其他 Pod 会看起来明明有 CPU 余量但调度就是失败。排查时请先看这个 Pod 是否处于 Guaranteed QoS再检查 kubelet 的 CPU manager 策略。5.3 现象三节点明明有剩余资源Pod 却调度不上我之前遇到过最让人头疼的一类问题是kubectl describe node显示剩余资源还够但新 Pod 始终 Pending提示资源不足。排查后发现两个原因叠加kube-scheduler 看在节点的allocatable资源时扣掉的是所有已存在 Pod 的 requests而不是实际使用量。如果一个节点上有一堆未知来源的静态 Podstatic pod或者之前残留的裸 Pod 已经消失但 sandbox 没清干净它们的 requests 会一直占坑。节点被打了污点或者有node.kubernetes.io/unschedulable: NoSchedule的标记没有去掉。很多人排障时只看kubectl get node的状态是 Ready没注意到SchedulingDisabled。实操中我一般按这个顺序排查# 查看节点状态和污点 kubectl describe node node-name | grep -A5 Taints # 查看节点已分配的资源 kubectl describe node node-name | grep -A7 Allocated resources # 查看节点上的 Pod 是否都是活跃的 kubectl get pods --all-namespaces --field-selector spec.nodeNamenode-name如果你发现节点上残留大量 Terminating 状态的 Pod它们占用的 requests 会被继续计入直到真正删除。这时需要确认容器运行时挂掉了或 kubelet 卡死把 sandbox 清掉问题立刻缓解。5.4 现象四GPU 节点上的资源竞争烫过几次之后我专门把 GPU 调度单独拎出来。GPU 资源不像 CPU 那样可无限分片它默认是整卡分配。如果你给一个 Pod 声明nvidia.com/gpu: 1那这张卡就整张归它其他 Pod 不能用哪怕它在上面只做很轻的推理。于是出现了两种典型问题节点有 8 张卡但其中一张因显存问题被系统做成了nvidia.com/gpu: 7调度器按 7 张卡分配剩下 1 张卡的资源永远不会被调度因为设备插件上报的计数就是 7。多个 Pod 共享同一张卡时通过 MIG 切片或 vGPU 方案你需要额外声明每个 Pod 的显存和算力需求否则调度器依旧按整卡粒度决策资源利用率不高。GPU 限制这块本质上调度器只管分配到的整数资源是否够而不管显存会不会 OOM。运行起来后如果显存超了驱动会直接把进程杀掉表现形式和普通 OOMKilled 很像但原因完全不同——你查 cgroup 内存的指标可能一切正常。所以给 GPU 工作负载做监控时一定要把 NVIDIA 的nvidia-smi显存指标采集进去不能用系统内存指标代替。6. 我的资源限制与调度策略清单讲了这么多机制和事故最后给一份可以直接抄作业的配置思路。不同团队的基础设施不一样但以下几条通用原则我几乎所有项目里都会贯彻。6.1 资源限制设置原则在线服务 requests 不要小于峰值期间实际使用量的中位数否则高峰段 CPU 时间片分配不足请求延迟指数级上升。内存的 limits 必须大于 JVM 堆 堆外内存 线程栈 直接内存。无论是 Java 还是 Go运行时都有不少非堆内存。Java 服务我通常把-Xmx设置为容器内存 limit 的 60% 到 70%给 MetaSpace 和直接内存留足空间。CPU limits 不要设成永远打不满的值。我见过有人给所有服务设limits.cpu8跑在 2 核的节点上——首先调度器会直接拒绝因为 limits 不影响调度但 kubelet 的 cgroup 配置会按 8 核设 quota如果节点上其他 Pod 不停增加这个服务不会被限流吗会但实际被限的节点层面却在和其他 Pod 争抢。设 limits 前一定要确认节点物理 CPU 总量。磁盘 IO 限制能设就设尤其日志型容器和临时文件密集型服务比如代理缓存。--device-read-bps、--device-write-bps在 Docker 下可以直接设Kubernetes 里可以通过 device plugin 和 CSI 实现复杂度略高但值得做。6.2 调度配置原则集群节点分组是基础中的基础。用 label 区分通用节点、CPU 密集型节点、内存型节点、GPU 节点并在工作负载上通过 nodeAffinity 或污点容忍控制流向。核心工作负载用PodDisruptionBudgetPDB保护起来。PDB 的作用不是调度而是防止 Kubernetes 在节点维护时把过多副本同时下线。配合反亲和性可以让一个节点挂了这件事的影响面最小化。定期用descheduler或手工巡检处理资源碎片。节点被大量小 requests 的 Pod 占满后可能每个 Pod 都只用了不到 20% 的 CPU但调度器再也塞不进一个需要 1 核资源的 Pod。这时候需要重新调度把 Pod 往其他节点上迁移。启用PriorityClass并设计好优先级分类。不要只给在线、离线各一个优先级建议按系统关键组件 有状态核心业务 无状态在线服务 批处理离线任务 可丢弃的临时任务划分至少五个档位抢占行为才可控。6.3 可观测性没有数据一切策略都是瞎猜资源限制和调度如果没有可观测性支撑基本等于盲飞。我最小化的一套监控配置包含节点维度CPU 使用率、CPU steal、内存使用率、内存压力、磁盘 IO 延迟、网络丢包率。容器维度cgroup 内的 CPU 使用量、限流时间throttled time、内存使用量、OOM 事件次数。调度维度调度耗时分位数、Pending Pod 数量、调度失败原因分布resource insufficient / taints / affinity 不满足、节点资源碎片率。有一组关键指标让我无数次在故障发生前发现问题container_cpu_cfs_throttled_periods和container_memory_working_set_bytes。前者一旦显著增长说明 CPU limit 即将成为瓶颈后者持续逼近 limits则预示 OOM 风险。如果你还在裸跑 Docker 而没有这些指标强烈建议接上 cAdvisor 或 Prometheus 的容器指标采集。最后再说一个个人经验资源限制和调度策略的每一次调整都要在灰度环境里先观察至少一个业务周期。CPU limits 调小哪怕 0.5 核都可能让一批延迟敏感型服务的 P99 从 50ms 涨到 500ms。不要在大版本发布当天顺手调配置我吃过这个亏——线上做了一次听起来无害的 limits 收敛中午流量高峰直接触发大面积 CPU 节流接口超时告警刷了一下午。改配置和改代码一样要有变更流程、有回滚预案、有灰度验证。这套思路放到今天依然实用。
阅读完成 · 觉得有帮助?
咨询建站