Kubernetes 成本不该只按节点看:用 OpenCost 把闲置容量与 Namespace 账单拆开
Kubernetes 集群的资源监控通常从节点开始:CPU 使用率、内存使用率、Pod 数量和磁盘容量。但这些指标很难直接回答财务和平台团队更关心的问题:某个团队本周实际占用了多少成本?集群里又有多少预算消耗在尚未被工作负载使用的容量上?
把节点账单平均除以 Namespace 数量很省事,却会掩盖两个问题。第一,业务的 requests、实际使用量和节点规格并不相同;第二,扩容余量、调度碎片、最低节点数等形成的闲置成本,既不能悄悄消失,也不宜随意归给某一个业务团队。
OpenCost 是一个 Apache-2.0 许可的开源 Kubernetes 成本监控项目。它读取 Prometheus 中的集群指标和定价信息,将成本映射到 cluster、node、namespace、controller、service、pod 等 Kubernetes 维度。本文不把它当作“账单真相机”,而是建立两种可以并存的成本口径:一份用于识别闲置容量,另一份用于内部成本归属(showback)。
先把两份报表的目的分开
成本分析最容易犯的错误,是只保留一个“按团队成本”数字。实践中至少应保留以下两种视图。
- 未分摊闲置成本的视图:业务工作负载成本照常归属,同时保留
__idle__项。这份视图回答“集群中到底有多少容量还没有被业务有效占用”,适合平台团队排查节点池、requests 和自动扩缩容策略。 - 已分摊闲置成本的视图:将闲置成本分摊给非闲置的分配项。这份视图适合预算报告、团队趋势比较和内部 showback,但必须明确分摊规则,不能把它误称为某个服务的直接云账单。
因此,闲置成本不是需要被隐藏的异常值,而是平台效率的观测信号。若 __idle__ 长期较高,可能是节点池最小副本过多、业务 requests 长期高估、HPA 下限过大,或因亲和性、污点和资源规格导致可调度容量被切碎。
安装前检查:让 OpenCost 连接正确的 Prometheus
OpenCost 需要访问 Prometheus。先确认运行中的 Prometheus Service 名称,而不是照抄某个发行版的默认值:
kubectl -n monitoring get svc
下面假设 Service 名为 kube-prometheus-stack-prometheus,位于 monitoring Namespace,端口是 9090。若你的实际名称不同,只替换对应三个参数。官方项目建议通过 Helm Chart 安装:
helm repo add opencost https://opencost.github.io/opencost-helm-chart helm repo update helm upgrade --install opencost opencost/opencost \ --namespace opencost \ --create-namespace \ --set opencost.prometheus.internal.enabled=true \ --set opencost.prometheus.internal.namespaceName=monitoring \ --set opencost.prometheus.internal.serviceName=kube-prometheus-stack-prometheus \ --set opencost.prometheus.internal.port=9090 \ --set opencost.mcp.enabled=false
最后一项显式关闭 MCP:本文只使用成本 API,先缩小部署后的暴露面。部署后等待 Deployment 就绪,并通过本地端口转发访问 API;不要把成本接口直接公开到互联网。
kubectl -n opencost rollout status deployment/opencost kubectl -n opencost port-forward svc/opencost 9003:9003
另开一个终端确认服务存活:
curl -fsS http://127.0.0.1:9003/healthz
如果 Prometheus 是 HA、分片或长周期存储架构,连接单个 Prometheus 实例可能得到不完整或间歇性的结果。此时应让 OpenCost 指向 Thanos Query、Cortex 或 Mimir 这类全局查询端点,并先在测试集群比对数据口径。
第一份查询:把闲置容量单独留下
下面的请求查询最近七天、按 Namespace 聚合的分配成本。includeIdle=true 会让响应中保留闲置分配,shareIdle=false 则避免把闲置成本提前混入业务 Namespace。
curl -G 'http://127.0.0.1:9003/allocation' \ --data-urlencode 'window=7d' \ --data-urlencode 'aggregate=namespace' \ --data-urlencode 'includeIdle=true' \ --data-urlencode 'shareIdle=false' \ | jq .
先保留完整 JSON,再决定如何导出字段。这样做比一开始就把结果塞进报表可靠,因为集群定价配置、时间窗口和 OpenCost 版本都会影响响应内容。确认字段后,可以用 jq 输出每个分配项的总成本以及 CPU、内存、持久卷成本:
curl -sG 'http://127.0.0.1:9003/allocation' \
--data-urlencode 'window=7d' \
--data-urlencode 'aggregate=namespace' \
--data-urlencode 'includeIdle=true' \
--data-urlencode 'shareIdle=false' \
| jq '
.data[0]
| to_entries[]
| {
allocation: .key,
total_cost: .value.totalCost,
cpu_cost: .value.cpuCost,
ram_cost: .value.ramCost,
pv_cost: .value.pvCost
}
'
观察时不要只盯最高的 Namespace。__idle__ 的绝对值和占比更值得持续记录:它升高时,先看节点池容量与 requests;它降低但某个 Namespace 的成本骤升时,再看副本数、持久卷和工作负载标签。
第二份查询:为团队报表分摊闲置成本
内部预算沟通常希望每个 Namespace 都有一个总额。可以关闭闲置项输出,并打开 shareIdle=true:
curl -G 'http://127.0.0.1:9003/allocation' \ --data-urlencode 'window=7d' \ --data-urlencode 'aggregate=namespace' \ --data-urlencode 'includeIdle=false' \ --data-urlencode 'shareIdle=true' \ | jq .
这不是替换第一份报表,而是补充它。直接成本适合回答“这个 Namespace 部署了什么、消耗了什么”;分摊后的成本适合回答“在当前集群经营方式下,这个团队应承担多少共同容量”。把二者放在同一周报中,团队就能区分业务增长和平台闲置,而不是围绕一个混合数字争论。
让数字可解释:标签、时间窗口与数据质量
按 Namespace 聚合只是起点。一个 Namespace 往往同时包含多个服务、定时任务和环境,因此它适合平台级月报,却未必能直接作为产品线结算单位。更稳妥的做法是先制定少量且稳定的归属标签,例如 owner、environment 和 cost-center,并要求应用团队在 Namespace 或工作负载层面补齐这些信息。标签治理的目标不是追求无限细粒度,而是避免出现“成本看得见、负责人找不到”的无主分配项。
时间窗口也要固定。排查突发事件可以用 window=24h,周度成本复盘使用 window=7d,月度预算则应统一采用自然月或明确的滚动窗口。不要把今天的 24 小时数据与上周七天数据直接比较;请求量、工作日分布和扩缩容节奏都会让这种比较产生误导。将查询参数、导出时间和定价配置版本与原始 JSON 一同保存,后续出现争议时才能复算。
另外,成本分配依赖 Prometheus 指标的完整性。若 kube-state-metrics、节点指标或持久卷相关指标缺失,响应仍可能返回数据,却不能代表完整成本。部署初期可选择一个业务量稳定的 Namespace,将 OpenCost 的结果与其 requests、节点池容量和云厂商账单周期交叉核对;确认口径后再接入预算看板。对于预留实例、Savings Plans、税费、共享 NAT 或集中采购折扣,应该在组织层面的 FinOps 规则中说明如何处理,而不是期望一次 API 查询自动完成财务核算。
一个可复现的周度复盘流程
可以把成本复盘拆成四步,而不是只在月末打开仪表盘。第一步,执行未分摊查询,记录 __idle__ 与各 Namespace 的直接成本;第二步,执行分摊查询,生成面向团队的归属总额;第三步,将两份结果和前一周期相比,筛选出绝对值或变化率异常的项目;第四步,回到 Kubernetes 对象与扩缩容配置查原因。
例如某个 payments Namespace 的分摊后成本上涨,并不应立刻要求团队降本。先确认订单量是否增长,再查看 Deployment 副本、HPA 的最小/最大副本、容器 requests,以及是否新建了高成本持久卷。反过来,如果业务量稳定而 __idle__ 上升,优先排查节点池最小规模和资源规格是否造成碎片。这样的顺序可以避免把平台容量策略错误地归咎于业务团队。
在自动化导出时,建议将 API 原始响应写入受访问控制的对象存储或制品库,只把已聚合的结果送入 BI 和告警系统。成本信息通常不含业务秘密,但它可能暴露服务名称、部署规模和内部组织结构。与监控指标一样,应限制 API 访问范围,并让导出任务使用最小权限的 Kubernetes ServiceAccount。
从看见成本到治理成本
成本 API 的价值不在于生成漂亮图表,而在于建立可复现的排查路径。可以把上述两个请求放进受控的 CronJob 或 CI 导出任务,每周保存原始 JSON,并按 Namespace、环境和业务归属标签汇总。随后按以下顺序处理异常:
__idle__高:检查节点池最小规模、过度预留、不可调度碎片和低利用率规格。- 单一 Namespace 的直接成本高:检查 requests/limits、异常副本数、GPU 或持久卷成本,以及是否误把批处理留在常驻节点池。
- 成本无法归属:补齐 Namespace 和工作负载的 owner、environment、cost-center 标签;没有归属标签的账单无法变成可行动的预算信号。
- 报表与云账单不一致:核对时间窗口、折扣/预留实例、税费、共享网络服务和定价配置。OpenCost 的分配视图不能自动覆盖所有财务核销口径。
最终目标并非把每个 Namespace 压到最低成本,而是让容量余量、可靠性需求和成本归属都能被清楚解释。先把 __idle__ 与业务分配拆开,团队才有机会在不牺牲稳定性的前提下,持续收紧 Kubernetes 的成本反馈环。