尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

K8s资源治理实战:Namespace、ResourceQuota与DownwardAPI详解

K8s资源治理实战:Namespace、ResourceQuota与DownwardAPI详解 先聊一个我真实碰到过的场景。早两年有个朋友负责的测试集群突然卡成幻灯片kubectl top nodes一敲某个节点 CPU 直接干到 99%但诡异的是他跑在上面的应用明明没多少流量。排查了半天才发现是另一个团队的同事给 CI 跑了十几个批处理任务全都调度到了同一个节点上而且这些 Pod 的 YAML 里压根没写resources字段。这个事让我印象特别深也直接引出了今天想聊的话题为什么在 K8s 里命名空间的资源治理和Pod 的资源声明从来都是一块儿出现的再加上DownwardAPI这个从 Pod 内部感知自身元数据的机制它们其实是把资源管控和应用自治这两件事串起来的完整链路。这篇文章就围绕这三个点展开适合正在学 K8s 的运维、开发也适合那些已经上了 K8s 但觉得资源老是被莫名其妙占满、或者想知道 Pod 内部容器怎么读取自身信息的同学。不说废话直接进干货。1. 从一次资源被“抢”光的故障说起Namespace 的隔离价值1.1 Namespace 到底是什么很多人刚接触 K8s 的时候会把 Namespace 理解成用来分开环境的东西——比如 dev、test、prod 各一个。这个说法没错但只看到了表面。Namespace 本质上是集群内部的逻辑隔离边界它隔离的是资源对象本身不是网络、不是存储、更不是内核级别的安全边界。换句话说Namespace 管的是谁能在这个目录下创建什么东西而不是谁能访问谁。这就引出一个关键点如果你在一个集群里不做任何 Namespace 规划所有团队的所有应用全堆在default命名空间里那从资源调度的视角看大家的 Pod 是一锅炖的。调度器默认情况下只要节点还有资源就会把 Pod 放上去完全不关心这个 Pod 是哪个团队、哪个业务线的。一旦出现资源争抢凭什么是你的测试任务把我的生产任务挤掉了这种争吵就只是时间问题。所以 Namespace 的第一个价值不是技术层面的而是管理层面的秩序。它把共享集群变成了多个逻辑上独立的小集群。这里我再补充一个很多人忽略的细节Namespace 的隔离能力是可以扩展的。配合 RBAC角色权限控制可以做到某个用户只能在指定 Namespace 里操作配合 NetworkPolicy 可以做 Namespace 级别的网络访问控制。但注意默认情况下 Namespace 之间是完全互通的不存在所谓的自动隔离这一点务必记牢。1.2 创建和使用 Namespace 的实操命令创建 Namespace 非常简单# 方法一直接用命令创建 kubectl create namespace app-prod # 方法二通过 YAML 声明式创建 cat EOF | kubectl apply -f - apiVersion: v1 kind: Namespace metadata: name: app-prod EOF但是创建一个 Namespace 之后你马上会遇到实际问题你创建的 Deployment 如果没写 Namespace它还是会跑到 default 里去。所以更稳妥的做法是在创建其他资源时就指定 Namespace或者在kubectl命令后用-n参数指定上下文kubectl apply -f deployment.yaml -n app-prod # 查看该命名空间下的所有资源 kubectl get all -n app-prod # 切换默认命名空间推荐安装 kubens 工具 kubens app-prod操作层面就这些真正常被忽略的是资源对象的命名空间归属意识。比如 Service、ConfigMap、Secret 这些资源都是有命名空间属性的你在kubectl get secrets的时候不指定-n是看不到其他 Namespace 里的 Secret 的。这个看不见很容易让人误以为不存在结果就是排查问题的时候绕了很远的路。我建议在任何团队协作场景下养成每一条命令都带-n的习惯或者干脆安装 kubens 切换默认上下文。1.3 用 ResourceQuota 给命名空间上锁有了 Namespace 之后下一步就是给它设置资源上限。这就是ResourceQuota做的事。打个比方如果 Namespace 是一间办公室ResourceQuota 就是这间办公室的电卡额度——你可以在办公室里随便用电但总数不能超过额定功率否则就跳闸。ResourceQuota 可以限制的资源种类很多常见的有这么几类计算资源requests.cpu、requests.memory、limits.cpu、limits.memory存储资源requests.storage、persistentvolumeclaims对象数量pods、services、configmaps、secrets等一个典型的 ResourceQuota 配置长这样apiVersion: v1 kind: ResourceQuota metadata: name: app-prod-quota namespace: app-prod spec: hard: requests.cpu: 20 requests.memory: 40Gi limits.cpu: 40 limits.memory: 80Gi pods: 100 persistentvolumeclaims: 50 configmaps: 100 secrets: 100看到这里你可能会想到一个问题如果 ResourceQuota 硬性要求requests.cpu和limits.cpu而集群里刚好有一个 Deployment 的 Pod 没写resources字段会发生什么答案很残酷创建会直接失败。K8s 的准入控制Admission Control会在资源进入 etcd 之前就拦截掉这个不满足配额要求的请求并给出一个类似exceeded quota的错误信息。这个设计在第一次遇到的时候会觉得有点不近人情但它是故意的——防止任何绕过配额控制的资源悄悄溜进来。实操中的血泪经验是在一个已经存在大量无资源声明的旧集群里直接创建 ResourceQuota 有可能导致存量 Pod 的重新调度无法进行因为新 Pod 过来一看配额不够了。所以如果你负责治理一个历史悠久的集群建议先在测试 Namespace 试运行一段时间确认所有应用都补上了resources字段再上生产。这个顺序一旦乱了那真的是给自己挖坑。2. 给 Pod 定好“预算”Requests 和 Limits 的差异怎么用2.1 为什么很多团队没写 resources集群也没崩我敢打赌任何一个跑了一段时间的 K8s 集群里default命名空间下没有声明resources的 Deployment 一抓一大把。而且它们还跑得好好的。这是不是说明resources字段就没那么重要不是不重要是你在透支稳定性。K8s 调度器在调度 Pod 的时候通常默认给未声明资源的 Pod 塞一个零值这个零值意味着调度器认为该 Pod 对 CPU 和内存的需求为 0于是任何节点它都可能被调度上去。同一时刻只要节点上的实际资源被这些零需求的 Pod 耗光节点就会进入资源压力状态然后 kubelet 开始按优先级驱逐 Pod。被驱逐的往往是那些真正声明了 requests 但优先级不够高的应用反而是零需求的 Pod 因为没声明资源、QoS 等级最低往往会被最先杀掉下文会展开讲。所以不写resources不代表没问题只是问题被推迟到了节点资源临界点才集中爆发。到时候整个集群的行为会变得完全不可预测。你回想一下那些莫名其妙 Pod 被杀的 issue十有八九跟资源声明缺失有直接关系。2.2 Requests 与 Limits 的行为差异requests和limits这俩字段很容易被简单理解成软限制和硬限制但它们在 CPU 和内存上的行为差异是巨大的资源类型requests 的作用limits 的作用超限后的行为CPU调度依据决定 Pod 放在哪个节点运行期通过 CFS 配额限制 CPU 使用上限被限流throttled进程不会死内存调度依据同时是 kubelet 驱逐决策的参考运行期由 cgroup 限制内存上限触发 OOM Killer容器被杀这个表格值得反复看几遍。CPU 是可压缩资源Limit 超了就限流内存是不可压缩资源Limit 超了就是被杀。举一个具体场景你给某个容器设了requests.memory: 256Milimits.memory: 512Mi。正常情况该容器内存稳定在 300Mi 左右但某次请求量突增冲到 600Mi。会发生什么容器直接被 OOMKilledkubelet 会重启它。如果重启后再次 OOM就会进入 CrashLoopBackOff。整个过程 kubelet 不会提前给你任何警告因为在你设的 512Mi limit 被突破前Pod 的状态可能一直是 Running。监控面板上kubectl top pod显示内存接近 limit但没人注意然后某天突然变成 OOMKilled——这种故障我实在见过太多次了。那是不是 limits 不设就完事了也不是。如果只设 requests 不设 limits那么当节点内存充足时容器可以随便用甚至把整台节点的内存都吃掉最终导致节点内存压力殃及同节点其他 Pod。所以生产环境通常两者都设。只有个别场景比如你想让某个批处理作业尽量利用空闲资源才会考虑只设 requests 不设 limits这种弹性利用需要配合对应的优先级和驱逐策略来做不适合默认配置。2.3 三类 QoS 等级节点资源紧张时谁先牺牲K8s 会根据 Pod 的 requests 和 limits 声明情况自动给每个 Pod 划分QoS 等级而 QoS 等级直接决定了当节点资源不足时 Pod 被驱逐的先后顺序GuaranteedPod 中每个容器都设置了requests和limits并且相等或者只设了 limitsK8s 会把它当作 requests。这类 Pod 优先级最高最后一个被驱逐。Burstable至少有一个容器设置了 requests 或 limits但没达到 Guaranteed 的严格一致。这类 Pod 在节点资源压力中等时可能被驱逐。BestEffort所有容器都没有设置任何 requests 和 limits。这类 Pod 最惨节点一紧张第一个被清理。你可能会问那我只想给 Pod 设置一个名义上限不想让它跟 requests 绑太紧该怎么处理K8s 的规则就是如果你只设置了 limitsrequests 会默认等于 limits所以它自动就成了 Guaranteed。这意味着你不小心更安全了但也意味着调度器会按 limits 来预留资源实际上会把资源白白锁死一部分。实操建议很简单尽量把 requests 设成你观察到的正常水位比如 70% 分位把 limits 设成你愿意为这个容器承担的最大波动比如正常水位的 1.5~2 倍。别听网上那些requestslimits的一刀切建议那是为了获得 Guaranteed 等级、避免被驱逐采取的保守策略对资源利用率要求高的业务并不友好。根据业务容忍度去权衡才是真正负责的做法。下面是一个带完整资源声明的 Deployment 示例apiVersion: apps/v1 kind: Deployment metadata: name: web-server namespace: app-prod spec: replicas: 3 selector: matchLabels: app: web-server template: metadata: labels: app: web-server spec: containers: - name: app image: nginx:1.25 resources: requests: cpu: 250m memory: 256Mi limits: cpu: 1 memory: 512Mi这里的250m表示 0.25 个 CPU 核心1表示 1 个核心。注意limits.cpu最好写成字符串避免 YAML 解析器把它当成数字 1 而不是字符串 1 带来的歧义。3. LimitRange让命名空间里的人没法“裸奔”3.1 为什么有了 ResourceQuota 还不够ResourceQuota 能从总量上控制一个 Namespace 的资源消耗但它管不住个体。举个例子你在app-prod空间设了 CPU requests 总上限 20 核结果有个同事提交了一个需要 1核 的 Deployment完全合法但如果他提交的是需要 30核 的 Deployment超过了单核配额预期qouta 会拦下来。可如果他只是把 20 个 Deployment 每个都要 1核qouta 也放行——因为总量没超过。但问题是你没法保证每个人创建的 Pod 都写了 resources。ResourceQuota 也只能干瞪眼因为配额已经被总量卡住了但 Pod 本身不带 requests 的话调度器不会给它预留任何资源。为了避免这种一个人裸奔全集群遭殃的情况就需要LimitRange这层兜底。LimitRange 做的事很直白在Namespace 级别给 Pod 或者容器设置一个默认的资源声明范围。凡是没写resources的容器LimitRange 会自动帮你补上一个默认的 requests/limits让你不至于裸奔凡是写的值超出 LimitRange 设定的最大最小值直接拒绝创建。它本质上是给创建规格定的边界规则跟 ResourceQuota 的总量控制恰好互补。这么一配合你就能理解了ResourceQuota 管总量LimitRange 管个体两者都是通过 Admission Controller 实现的。前者防止一个团队把整个池子掏空后者防止个别人给集群埋雷。3.2 LimitRange 配置示例按容器维度还是按 Pod 维度LimitRange 可以作用在多个维度上最常用的是Pod和Container两个 scope。Container scope 负责给单个容器设置默认值Pod scope 负责给整个 Pod 的容器集合设置最小最大限制。下面是一个 Container scope 的示例apiVersion: v1 kind: LimitRange metadata: name: container-limit-range namespace: app-prod spec: limits: - type: Container max: cpu: 2 memory: 2Gi min: cpu: 50m memory: 64Mi default: cpu: 250m memory: 256Mi defaultRequest: cpu: 100m memory: 128Mi配置说明max/min单个容器允许的最大/最小值。超过就会拒绝创建。default如果容器只设置了requests没设置limits则用这里的值填充limits。defaultRequest如果容器只设置了limits没设置requests则用这里的值填充requests。如果default和defaultRequest都没设置而容器又没写资源声明Pod 会被拒绝。实操中你会发现一个有趣的组合效应LimitRange 给定默认值之后即便你的 YAML 里什么都不写最终创建的 Pod 也会自动带上 requests 和 limits。而且注意一点Pod 的 QoS 等级在这个情况下通常是 Burstable因为 requests 和 limits 数值不同不是 Guaranteed。如果业务对 QoS 等级有严格要求还是需要显式声明。3.3 实测一个容易栽的坑LimitRange 与 ResourceQuota 的顺序问题很多人搞不清这两个组件到底谁先执行。实际上Kubernetes 的准入控制器是按固定顺序执行的。在默认安装中LimitRanger和ResourceQuota各自作为 Admission Controller 插件LimitRanger 会先执行ResourceQuota 后执行。也就是说当一个 Pod 被创建时LimitRange 先检查并给缺省字段补上默认值ResourceQuota 再检查补完后的 Pod 加上已存在对象的总量是否超出配额。这个顺序非常关键但也很容易踩坑。有一种情况是你先创建了 ResourceQuota要求所有 Pod 必须有requests.cpu。然后你给某个应用提交了一个没写resources的 Deployment。按 LimitRanger 的规则它会补一个默认值进去所以 ResourceQuota 校验时是能看到这个补上的值的创建会成功。但如果你的 LimitRange 配置里的default值小于 Quota 要求的最低规格或者干脆没有配 LimitRange创建就会因为没写 requests被 Quota 拦截。这个坑我早年真实踩过一次。当时我新建了一个 Namespace先套了 ResourceQuota然后去部署一个老应用没写 resources。结果报错信息是failed quota: app-prod-quota must specify requests.cpu。我一开始以为是自己旧的 Deployment 里某个字段写错了后来才意识到是 LimitRange 没配缺省值没补上。从那次以后我给自己定了个操作习惯在创建 ResourceQuota 之前必须保证 LimitRange 已经就位或者所有老应用的 YAML 都已经声明全了 resources。这个顺序问题建议你看完这篇文章就记下来。4. DownwardAPI让容器“认识自己”的轻量注入通道4.1 为什么要用 DownwardAPI先从一个痛点聊起。你有一个 Pod 跑在集群里业务代码需要知道自己是哪个 Pod、所在节点 IP、所属命名空间或者要把 Pod 的 labels 注入到日志里做关联分析。这些信息在 Pod 外部你通过kubectl get pod -o yaml随时都能看到但容器内部怎么获取呢有人第一反应是挂 ConfigMap把 Pod 名、节点 IP 这些值填进去。但这里有个致命问题Pod 名、Pod IP 是在 Pod 创建之后才生成的你在部署 Deployment 时根本不知道最终 Pod 叫什么名字ConfigMap 没法提前写。所以我们需要一种机制能在 Pod 创建时把这些运行时才确定的信息动态注入给容器。这就是 DownwardAPI 的核心价值。DownwardAPI 不是什么神秘组件它是 K8s API 的一部分允许你把 Pod 的元数据metadata和部分 spec/status 字段暴露给容器。暴露方式有两种环境变量和卷文件。它没有独立的 API 对象而是通过 Pod YAML 里的特定字段来声明。4.2 方式一通过环境变量注入环境变量的方式最直观。在容器定义里使用valueFrom.fieldRef就可以引用 Pod 的元数据apiVersion: v1 kind: Pod metadata: name: downward-demo namespace: app-prod labels: app: demo version: v1 spec: containers: - name: demo image: busybox:1.36 command: [sh, -c, sleep 3600] env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: POD_SERVICE_ACCOUNT valueFrom: fieldRef: fieldPath: spec.serviceAccountName - name: MEM_LIMIT valueFrom: resourceFieldRef: containerName: demo resource: limits.memory重点解释几个容易出错的细节status.podIP只在 Pod 已经获得 IP 之后才存在。如果容器启动时需要 POD_IP而 Pod IP 尚未分配环境变量会被设置为空字符串。官方建议在容器启动脚本里重试或判断空值不要假设它在启动时一定可用。spec.nodeName在调度之前是空的。只有当 Pod 被成功调度到某个节点后nodeName 才会被填充。所以如果你的容器启动时就想读 NODE_NAME同样要考虑时序问题。resourceFieldRef可以获取容器的 requests/limits 资源数值但只支持 CPU 和内存且单位是 K8s 内部的整数格式比如内存是字节数不是你以为的 Mi 或 Gi。如果需要换算成人类可读的 Mi需要业务代码自行除以 1024*1024或者用卷方式挂载后配合脚本处理。这里有个关键约束fieldRef只能引用当前 Pod 自身的字段不能引用其他 Pod、Service、ConfigMap 等任何其他对象。它的定位很纯粹告诉你自己是谁而不是帮你去查别人。4.3 方式二通过卷挂载支持运行期更新环境变量的方式有两大局限一是只在容器启动时注入一次如果 Pod 的 labels 或 annotations 后续被修改环境变量不会跟着变二是注入的信息量比较零散内容稍多就显得杂乱。这时就该用卷挂载了。DownwardAPI 卷可以把 Pod 的元数据以文件形式挂载进容器并且每当这些元数据变化时文件内容会自动更新有轻微延迟容器内的进程可以动态读取而不需要重启。apiVersion: v1 kind: Pod metadata: name: downward-volume-demo namespace: app-prod labels: app: demo version: v1 annotations: owner: platform-team spec: containers: - name: demo image: busybox:1.36 command: [sh, -c, sleep 3600] volumeMounts: - name: podinfo mountPath: /etc/podinfo volumes: - name: podinfo downwardAPI: items: - path: pod_name fieldRef: fieldPath: metadata.name - path: namespace fieldRef: fieldPath: metadata.namespace - path: labels fieldRef: fieldPath: metadata.labels - path: annotations fieldRef: fieldPath: metadata.annotations容器启动后在/etc/podinfo目录下会看到四个文件pod_name、namespace、labels、annotations每个文件的内容就是对应的字段值。其中 labels 和 annotations 会以类似keyvalue的格式逐行输出适合脚本解析。这种方式在哪些场景特别有用呢举一个常见例子你的应用是配置中心Pod 注册自己时需要附带 Pod 的 labels 或 annotations同事后续在线上改了 labels比如加了azzone-a配置中心要能感知到这个变化并刷新注册信息。用环境变量的方式做不到动态更新用卷挂载就可以。4.4 与 ConfigMap 的使用边界别用错我在不少团队的 YAML 里看到过这样的写法把 Pod 名称、节点 IP 写进 ConfigMap然后用configMapKeyRef注入环境变量。这种做法能跑但隐患很大Pod 名是运行时才能确定的你写成 ConfigMap 时用的是 Deployment 名还是上一个 Pod 的名字服务多实例时所有副本注入的值都一样完全失去区分度。ConfigMap 一旦创建改动需要重新发布 Pod 才会生效无法反映当前 Pod 的运行时状态比如 IP 变化后重启容器之前 env 里的 IP 还是旧的。ConfigMap 本质上适合放业务配置DownwardAPI 适合放运行时元数据。两者混用一旦配置项多了后续维护会特别混乱。什么时候用 ConfigMap域名、开关、超时参数、外部依赖地址这些相对静态的内容。什么时候用 DownwardAPIPod 名、IP、命名空间、labels、annotations、资源用量这类对象自身的运行时信息。个人建议的优先级是能通过 DownwardAPI 拿到的信息优先用 DownwardAPI需要跟业务配置统一管理的静态内容才放 ConfigMap。这样你的 Pod 声明会更有自洽性排查问题时也更清晰。5. 多容器 Pod 的坑一个容器起不来真的会影响其他容器聊了这么多资源限制和 DownwardAPI最后想顺带把热搜里那个问题说透一个 Pod 里如果有多个容器其中一个没有成功启动会影响其他容器吗答案是会而且影响方式比很多人以为的更复杂。5.1 多容器 Pod 的资源分配模型首先要建立正确的模型在 K8s 里资源限制最终是作用在 Pod 层面的。你在容器 A 里写了requests.cpu: 100m在容器 B 里写了requests.cpu: 200m并不代表这两个容器各自独立拥有 100m 和 200m而是整个 Pod 的 CPU requests 总量是 300m。调度器按 Pod 总需求量把 Pod 调度到节点上之后才由 kubelet 通过 cgroup 为各个容器做细分。这意味着同一 Pod 内多个容器是共享资源池的。如果容器 A 的 CPU 用量超过了它自己的 limits但整个 Pod 还没超过总 limits容器 A 的行为取决于 cgroup 层级和 CFS 配额的具体配置——在常见的 containerd 运行时下它会受到自身 container 级 cgroup 的限制不会无限挤占 B。但容器的内存则要特别小心如果 Pod 总内存接近 limits一个容器内存暴涨很可能导致同 Pod 其他容器被连带杀死因为 K8s 杀容器时并不总能精准定位到罪魁祸首尤其在 Pod 级 cgroup 接近上限时OOM Killer 可能随机选中进程。5.2 多容器 Pod 没有成功启动容器时对其他容器的真实影响默认的 Pod 启动策略下其中一个容器启动失败会触发 Pod 的重启策略。如果restartPolicy: Alwayskubelet 会反复重启失败的容器而另一个本来正常的容器也会因为 Pod 进入等待状态而无法进入Ready条件。注意这里的 Ready 是 Pod 级别的状态不是单容器状态。也就是说即使容器 B 内部的进程一直在跑只要容器 A 没起来整个 Pod 也不会被 Service 的 Endpoints 纳入流量转发。这就引出多容器 Pod 的一个非常经典的坑sidecar 容器和主容器是共生的主容器起不来sidecar 也白搭sidecar 起不来主容器即使进程在跑也无法对外提供服务。举一个我见过多次的例子一个团队在 Pod 里放了一个用于日志采集的 sidecar 容器这个采集器在启动时要读取一个配置文件。他们只更新了 ConfigMap没注意到配置格式有误导致 sidecar 启动失败。结果所有 Pod 都处于CrashLoopBackOff状态主业务容器其实没崩但 Service 已经不把流量转发到这些 Pod 上了。整条业务链路的 QPS 掉到 0排查花了不少时间才定位到是 sidecar 的问题。多容器 Pod 的资源限制做法里我的建议是把主容器和 sidecar 容器的资源声明分开写清楚。sidecar 通常很小但也要给足 requests不能让主容器把 Pod 的总额度全占了。否则某个瞬时高峰sidecar 因为拿不到 CPU日志堆积反过来拖慢主进程。5.3 用 DownwardAPI 配合多容器场景做自治多容器 Pod 还有一个细节DownwardAPI 的resourceFieldRef在环境变量方式下可以引用的是某个指定容器的资源值。所以如果你在 Pod A 里同时跑主容器和 sidecarsidecar 想感知主容器的 memory limit 做自我保护可以这么写env: - name: MAIN_CONTAINER_MEM_LIMIT valueFrom: resourceFieldRef: containerName: main-app resource: limits.memory这在日志采集器、监控 sidecar 的场景下很实用。比如 sidecar 可以根据主容器的 limit 动态调整自身的日志批量发送阈值避免在主容器内存高位时再去扩充缓冲占用额外内存。如果你在同一个 Pod 里用卷挂载方式暴露 DownwardAPI 信息还要注意一点这个卷是 Pod 级别的所有容器都可以读取同一个挂载点。这在设计上其实是一个便利——你可以把 Pod 元数据写到共享目录让多个容器共享同一份身份信息而不需要为每个容器单独注入环境变量。不过多容器共享同一个 DownwardAPI 卷时要小心文件权限不同容器运行的用户不同如果其中某个容器没有读取挂载目录的权限启动时会报permission denied这个坑在安全上下文开启runAsNonRoot之后更为明显。6. 配合 Prometheus 与压测场景高并发服务的资源配置实战6.1 从热搜里的若依微服务迁移场景说起资源规划为什么是第一步热搜词里有一条是单节点 k8s 上的若依微服务整套环境准不停服、不丢数据地迁移到阿里云 ecs迁移完成后由压测人员peseman使用配套的 jmeter 脚本做高并发测试验证云上环境的承载能力。这个场景非常典型——它把迁移和压测两件事绑在了一起而这两件事的共同地基就是资源声明与配额设计。假设你在本地单节点 K8s 上跑着一个若依微服务里面往往有 gateway、system、auth、file 等一二十个服务。迁移到阿里云 ECS 后如果不提前给每个服务定义好 requests/limits压测一启动Pod 会大批量重启或 OOMKilled根本谈不上高并发验证。我见过不少团队在压测前不整理资源声明结果压测一开始节点 CPU 被打满、Pod 被驱逐完全分不清是代码瓶颈还是资源配额配置不合理。所以这类场景下的第一步永远是把所有 Deployment 的资源声明补齐再套 ResourceQuota 和 LimitRange。具体做法是先跑一轮业务回归用kubectl top pod --containers -n 你的命名空间统计每个服务在正常和压测流量下的 CPU/内存水位记录分位数。按 70%~80% 分位设置 requests按 1.5~2 倍或观察到的峰值上限设置 limits。根据所有服务 requests 的总量决定 ECS 实例规格和数量留 20%~30% 的 buffer 给突发流量和调度抖动。在目标 Namespace 上创建 ResourceQuota总量按节点可分配资源的 80%~90% 设置留一部分给系统组件。创建 LimitRange把 max/min/default 设好防止有人新增服务时忘写 resources。这是一个非常务实的流程能极大降低你在压测时被资源层故障干扰的概率。6.2 构建一套可观测的占位保证压测时不掉链子除了资源声明压测场景还特别依赖观测能力。你的 Pod 里有没有暴露 metrics资源使用数据有没有被 Prometheus 采集这两个问题不解决压测结论没有任何参考意义。因为压测不只是顶不顶得住还包括顶不住的时候是哪个环节先出问题。推荐的做法是在业务容器旁边加一个 prometheus 专用 sidecar或直接在主容器里暴露/metrics用 ServiceMonitor 或 PodMonitor 自动采集 Pod 的指标配合 Prometheus 里的 kubelet 指标观测每个节点的资源水位设置关键告警Pod 频繁重启、节点内存水位 85%、CPU throttling 比例 20% 等。跟前面的多容器 Pod 生命周期结合起来看你会发现压测时的容器崩溃往往遵循一条路径——业务容器内存接近 limit - OOMKilled - 重启 - 同 Pod 其他容器受影响 - Service 摘除该 Pod - 流量转移到其他副本 - 其他副本压力加大 - 连锁故障。如果你的资源声明和监控告警没提前做好这条链路从开始到雪崩可能只要几分钟连手动介入的时间都不够。6.3 迁移后压测的实战经验最后分享几个迁移 压测场景里我从实际项目中攒下来的经验第一个Pod 的requests和节点的实际可分配资源要区分开。K8s 节点的Allocatable不等于节点的总内存/CPU因为系统组件kubelet、容器运行时、操作系统也要占用资源。你在算集群容量的时候直接用kubectl describe node看Allocatable而不是看云厂商控制台里的 CPU/内存规格。第二个压测开始前先做一次资源请求的合理性审计。用下面这条命令快速找出哪些容器的资源声明明显过度或缺失kubectl get pods -A -o custom-columnsNAMESPACE:.metadata.namespace,POD:.metadata.name,CONTAINER:.spec.containers[*].name,REQUESTS_CPU:.spec.containers[*].resources.requests.cpu,LIMITS_CPU:.spec.containers[*].resources.limits.cpu,REQUESTS_MEM:.spec.containers[*].resources.requests.memory,LIMITS_MEM:.spec.containers[*].resources.limits.memory看到缺失的地方一屏扫过去就清楚了。第三个也是我吃过亏的limits.memory不是设得越高越好。如果你给业务容器设了特别高的内存 limit比如 4Gi 但实际只用到 200Mi调度器按 requests 调度所以实际不会占太多但一旦该容器发生内存泄漏它会把节点内存慢慢吃完直到触发节点级驱逐。设置一个合理偏紧的 limit 反而是保护能让问题早暴露、早重启、早恢复。第四个压测时观察 CPU throttling。如果容器 CPU 使用率经常撞到 limit指标里会出现明显的 CPU 限流throttle表现为请求延迟周期性变高。你可以通过 PromQL 查container_cpu_cfs_throttled_seconds_total来判断。这个指标常常被忽略但它能解释很多压测结果看起来 CPU 都没打满但响应时间却很高的反常现象。7. 常见问题排查思路和我的经验总结7.1 遇到 Pod 创建失败先看这几个字段很多新手以及一些老手在 Pod 创建失败时第一反应就是去看kubectl describe pod里底部的 Events。这没错但如果错误涉及 ResourceQuota 或 LimitRangeEvents 里往往只会给一个干巴巴的 exceeded quota。我建议按这个顺序排查确认错误类型kubectl get events -n ns --sort-by.lastTimestamp | tail -20看最新的几条。区分是配额错误还是 LimitRange 错误。配额错误一般带exceeded quotaLimitRange 错误一般带minimum cpu usage per Container is或maximum cpu usage per Container is。核对自己的 resources 声明。没有声明就加声明声明了但超出 LimitRange 范围就改范围或改值。检查 ResourceQuota 当前用量kubectl get resourcequota -n ns -o yaml看status.used是不是已经把hard顶满了。有一次我排查一个始终无法扩容的服务kubectl get deploy显示期望副本数是 5但一直只有 3 个 Ready。原因是该 Namespace 的 ResourceQuota 里limits.memory已经被现有 Pod 占满第 4、第 5 个 Pod 一直创建失败但 Deployment 本身没有明确报错。这种情况只看 Deployment 状态绝对会一头雾水必须把 ResourceQuota 的 used/hard 亮出来才能看到全貌。7.2 DownwardAPI 常见踩坑记录DownwardAPI 本身不复杂但出错的地方往往很低级我列两个最常见的问题。第一fieldPath 写错。比如想拿 Pod 名字写成metadata.name是对的但有人会写成metadata.labels[app]这类带索引的写法在 fieldPath 里并不被支持至少在环境变量方式下不行要单独拆开。另外status.podIP必须写status.podIP不是spec.clusterIP这种概念。第二环境变量方式的时序问题。前面说过status.podIP和spec.nodeName在 Pod 启动初期可能是空值。如果业务启动后立刻读这些环境变量并做了非空校验很容易直接报错退出。我建议在启动脚本里加一段等待逻辑比如循环判断POD_IP是否为空最多等 30 秒而不是启动第一帧就去读。卷挂载方式则没有这个问题文件是实时更新的推荐关键场景用卷而不是环境变量。7.3 资源配额 DownwardAPI 多容器如何组合出自我保护型 Pod如果把前面所有知识点串起来你会得到一个很实用的自我保护型 Pod 设计思路让容器知道自己被分到了多少资源然后根据这个信息动态调整自己的行为。比如一个 Java 应用它可以在启动时读取resourceFieldRef注入的容器内存 limit然后自己计算堆内存大小比如设置为 limit 的 60%而不是硬编码-Xmx512m。这样当你在不同环境用不同资源规格部署同一个镜像时不需要改配置应用自己就能适应。这是一个非常典型的 DownwardAPI 资源限制的组合玩法。再配上前面的多容器场景主容器是 Java 应用sidecar 是日志采集器。sidecar 读到主容器的内存 limit 后可以按比例设置自己的缓冲区上限、批量发送阈值。主容器内存压力大的时候sidecar 自己先降级而不是加剧内存争抢。这种感知资源 - 调整自我的模式是我在运维比较复杂的微服务集群时特别推荐的一种设计理念。它不复杂但需要你在写 YAML 的时候多想一步容器需要知道自己的资源边界吗如果需要用 DownwardAPI 注入如果不需要那也要保证资源声明清晰让调度器有据可依。这两个动作做到了你的集群稳定性和人效都会有明显提升。篇幅有限这期的内容就到这里。对于命名空间资源限制和 DownwardAPI 这三个话题如果你在实际配置中碰到任何拿不准的报错信息按照上面几节给的排查路径走一遍大概率能找到答案。
返回列表