网络、存储与负载均衡成本:从对象生命周期追到账单行
一个团队把工作负载 requests 下调并缩掉节点,计算成本如期下降,云账单总额却几乎不动。追到账单行才发现:跨可用区访问数据库持续增长,已删除应用留下 Retain 数据盘,测试 Service 创建的外部负载均衡器仍在计费,出口流量还经过 NAT。只优化 CPU 和内存,相当于只看见资源图的一角。
三条证据链必须分别成立
网络成本从流量路径开始:源 workload、源节点与可用区、目的类型、目的区域、字节数、计价方向和价格规则。相同字节量在同节点、同可用区、跨可用区、跨区域和互联网出口上的含义不同;“Ingress 免费”或“内网不收费”都不能当成跨云通用规则,实际费率与豁免必须从目标供应商的账单和价格入口确认。
存储成本从声明与资产生命周期开始:PVC 请求容量和 StorageClass 决定 Kubernetes 期望,PV 的 spec.csi.volumeHandle 常用于连接真实磁盘,云账单还可能包含预置 IOPS、吞吐、快照、备份和跨区复制。OpenCost 规范以 PVC 请求容量及 PV/磁盘价格计算持久存储分配成本,这不是所有供应商账单维度的完整替代。
负载均衡成本从 Service 派生资源开始:Service UID、type: LoadBalancer、loadBalancerClass、provider annotation 与 status.loadBalancer 只描述 Kubernetes 一侧;云侧可能创建 LB、监听器、规则、后端组、公网 IP、健康检查和数据处理项。NAT gateway、固定公网 IP 和 DNS 通常又是独立资产。Service 删除了,不代表每个派生对象都已结清。
因此统一账本应保留两组关系,而不是只保留展示名称:
Kubernetes object identity
cluster_id + object_uid + kind + namespace + observed_window
Provider asset identity
provider_resource_id + asset_type + region/zone + billing_record_id
Evidence edge
object_uid -> provider_resource_id -> billing_record_id名称可以重用,UID 和 provider resource ID 才能避免新旧对象串账。映射失败的账单行进入待对账队列,不得因为 dashboard 无法展示就丢弃。
先建立只读资产快照
采集端需要 kubectl,以及读取 Node、Service、EndpointSlice、StorageClass、PV、PVC、Pod 与 controller 的权限。先确认权限,再导出不含 Secret 的快照:
kubectl auth can-i list nodes
kubectl auth can-i list services --all-namespaces
kubectl auth can-i list persistentvolumes
kubectl auth can-i list persistentvolumeclaims --all-namespaces
kubectl get nodes -o 'custom-columns=NAME:.metadata.name,PROVIDER:.spec.providerID,ZONE:.metadata.labels.topology\.kubernetes\.io/zone'
kubectl get service -A -o 'custom-columns=NS:.metadata.namespace,NAME:.metadata.name,UID:.metadata.uid,TYPE:.spec.type,CLASS:.spec.loadBalancerClass,INGRESS:.status.loadBalancer.ingress[*].hostname'
kubectl get pv -o 'custom-columns=NAME:.metadata.name,UID:.metadata.uid,SC:.spec.storageClassName,RECLAIM:.spec.persistentVolumeReclaimPolicy,HANDLE:.spec.csi.volumeHandle,CLAIM_NS:.spec.claimRef.namespace,CLAIM:.spec.claimRef.name'
kubectl get pvc -A -o 'custom-columns=NS:.metadata.namespace,NAME:.metadata.name,UID:.metadata.uid,SC:.spec.storageClassName,REQUEST:.spec.resources.requests.storage,VOLUME:.spec.volumeName,PHASE:.status.phase'预期 Node 能给出 provider ID 和 zone,LoadBalancer Service 能给出 UID 与云入口,绑定 PVC 能连到 PV 和 volume handle。某些托管平台会隐藏或改写 provider ID;这时必须使用平台提供的资产清单或标签映射,不能靠名称模糊匹配。
快照文件包含内部拓扑和资产 ID,应写入受控证据目录、设置短保留期,不提交 Git。使用云账单导出时,读取身份只授予目标 dataset、bucket 或报表的查询权限;Kubernetes 只读身份与云账单身份分开,避免一个 Pod 同时获得全集群元数据和整个组织账单。
配置字段会改变成本与清理语义
StorageClass 的 reclaimPolicy 决定 PVC 删除后动态供应 PV 的回收方向。Delete 通常请求 provisioner 删除底层卷,Retain 则保留 PV 和外部资产以等待人工处理;它适合需要取证或防误删的数据,却会让“应用已删”与“磁盘仍计费”同时成立。volumeBindingMode: WaitForFirstConsumer 可以让卷在 Pod 调度拓扑确定后再供应,减少卷和节点落在不同 zone 的失败与闲置。allowVolumeExpansion 允许扩容,不提供自动缩容,扩大的账单不会因文件删除自行下降。
Service 的 type: LoadBalancer 请求外部实现供应 LB。loadBalancerClass 选择实现方,provider annotations 可能改变内外网、规格、IP 与后端模式;这些字段具有供应商语义,升级控制器前要用目标版本文档和 server-side dry-run 检查。externalTrafficPolicy: Local 可能保留客户端源地址并改变跨节点流量路径,但也会影响后端分布和健康检查,不能作为单纯降本开关。
网络采集也有代价。Kubecost Network Costs 通过每节点 DaemonSet、hostNetwork 和宿主机 conntrack 等信息细化 Pod 流量归属,需要特权能力;GKE Autopilot 或受限 Pod Security 环境可能不允许。不能为了成本可见性放宽全集群安全基线。OpenCost 和云原生成本导出可提供另一部分证据,但网络为零可能表示数据源未启用、路径未覆盖或账单尚未到达,而不是没有费用。
正向实验:把临时对象追到清理完成
实验应在可销毁、已设置预算告警的非生产集群进行。目标集群必须有默认动态卷供应器和 LoadBalancer 实现;没有云集成的本地集群可验证 Kubernetes 生命周期,但不会产生真实 LB 或云盘账单。下面的对象使用很小的声明,仍可能产生费用:
apiVersion: v1
kind: Namespace
metadata:
name: cost-path-lab
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: evidence-volume
namespace: cost-path-lab
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: evidence-app
namespace: cost-path-lab
spec:
replicas: 1
selector:
matchLabels: { app: evidence-app }
template:
metadata:
labels: { app: evidence-app }
spec:
containers:
- name: web
image: registry.k8s.io/pause:3.10
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: evidence-volume
---
apiVersion: v1
kind: Service
metadata:
name: evidence-entry
namespace: cost-path-lab
spec:
type: LoadBalancer
selector: { app: evidence-app }
ports:
- port: 80
targetPort: 80先用 server-side dry-run 检查准入,再创建并记录 UID:
kubectl apply --server-side --dry-run=server -f cost-path-lab.yaml
kubectl apply -f cost-path-lab.yaml
kubectl -n cost-path-lab rollout status deploy/evidence-app --timeout=180s
kubectl -n cost-path-lab wait --for=jsonpath='{.status.phase}'=Bound pvc/evidence-volume --timeout=180s
kubectl -n cost-path-lab get service evidence-entry -w
kubectl -n cost-path-lab get service evidence-entry -o yaml
kubectl -n cost-path-lab get pvc evidence-volume -o yaml
kubectl get pv "$(kubectl -n cost-path-lab get pvc evidence-volume -o jsonpath='{.spec.volumeName}')" -o yaml预期 PVC 为 Bound,PV 能看到 reclaim policy 和 CSI volume handle;云集群中的 Service 最终出现 ingress hostname 或 IP。随后在只读云资产清单中用 volume handle、LB hostname/IP、Service UID 标签或控制器标签定位 provider resource ID,再用同一 ID 和覆盖资源存在期的窗口查询账单导出。账单有延迟,资源 Ready 不能证明账单已经到达。
清理时先保存对象与 provider ID,再删除 namespace,最后观察外部资产消失:
kubectl -n cost-path-lab get service,pvc -o yaml > cost-path-k8s-evidence.yaml
kubectl delete namespace cost-path-lab --wait=true
kubectl get pv
kubectl get namespace cost-path-lab验收不是“namespace 不存在”,而是 Kubernetes Service/PVC 已删除、相关 PV 按 policy 收敛、云 LB/磁盘/IP/后端组不再处于活动状态,后续稳定账单窗口不再新增对应使用量。证据文件含资源 ID,复核完成后按保留策略销毁。
反向实验:用 Retain 暴露孤儿卷
不要在云端故意制造昂贵资源。可先在隔离集群检查一个专用测试 StorageClass 的回收行为;若它是 Delete,复制成测试类并明确设置 Retain,不得修改团队默认 StorageClass。正向实验已经清理时,重新创建 namespace,并让一个最小 PVC 显式引用该测试类:
export LAB_STORAGE_CLASS='cost-retain-lab'
kubectl get storageclass "$LAB_STORAGE_CLASS" -o yaml
kubectl create namespace cost-path-lab
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: evidence-volume
namespace: cost-path-lab
spec:
storageClassName: ${LAB_STORAGE_CLASS}
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: evidence-volume-consumer
namespace: cost-path-lab
spec:
containers:
- name: hold
image: registry.k8s.io/pause:3.10
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: evidence-volume
EOF
kubectl -n cost-path-lab wait --for=jsonpath='{.status.phase}'=Bound pvc/evidence-volume --timeout=180s
export PV_NAME="$(kubectl -n cost-path-lab get pvc evidence-volume -o jsonpath='{.spec.volumeName}')"
kubectl get pv "$PV_NAME" -o 'custom-columns=NAME:.metadata.name,RECLAIM:.spec.persistentVolumeReclaimPolicy,HANDLE:.spec.csi.volumeHandle,PHASE:.status.phase'
kubectl -n cost-path-lab delete pod evidence-volume-consumer --wait=true
kubectl -n cost-path-lab delete pvc evidence-volume
kubectl get pv "$PV_NAME" -o wide在 Retain 路径中,预期 PV 进入 Released,底层 volume handle 仍存在。这是保护数据的设计结果,不是控制器故障。若成本系统把它显示为 __unmounted__ 或资产成本,而 namespace allocation 中看不到它,两边都可能正确。必须由存储 owner 判断归档、重新绑定还是销毁;未经确认直接删除云盘可能造成不可恢复的数据损失。
确认实验卷没有保留价值后,由存储 owner 按 CSI 与云平台流程删除底层资产,再删除残留 PV 和 namespace;只执行 kubectl delete pv "$PV_NAME" 可能移除 Kubernetes 记录,却没有证明外部磁盘已经销毁。最后在云资产清单和后续稳定账单窗口复核 volume handle 不再产生新增计量。
LB 的安全反例不需要再创建第二个资源。删除 Service 后若云 LB 长时间仍存在,先看 Service finalizer、云控制器日志和 provider 事件;若 Kubernetes 对象已经消失,再按保存的 provider ID 检查是否是控制器外创建、共享 LB 或删除失败。不能仅凭名称相似删除云资产。
网络归属要从路径而不是端点猜测
对每条成本记录至少保存 source_zone、destination_class、destination_zone_or_region、bytes、direction 和 pricing_rule_id。Pod 调用一个内部域名,不代表链路只在集群内:DNS 可能解析到公网入口,Service 可能经过跨区 LB,数据库副本可能在另一 zone,NAT 还可能为私网出口单独计费。
排查从流量事实开始,再匹配账单。观测系统负责给出源、目的、字节与路径证据;成本层负责把路径映射到费率和账单行,不重复搭建抓包与指标平台。抽样一条非零流量,验证源 Pod 到节点 zone、目的 endpoint、LB/NAT 资产和账单计价方向能闭环。若采集器只看到 Pod IP 而看不到 SNAT 后地址,记录覆盖缺口,不用平均分摊伪装精确。
跨区费用突增时,对齐 Deployment rollout、拓扑分布、Service traffic policy、数据库 failover 和节点池变化。流量字节没变而费用上升,优先检查路径分类或价格规则;字节与费用同时上升,再检查副本位置、重试、缓存失效和数据搬迁。网络费率会变化,报表保存 pricing rule revision,不把单价硬编码进业务仓库。
数据质量门禁不能只看总额
资产覆盖率至少分三类计算:有 Kubernetes 对象且有 provider asset;有 provider asset 但对象已消失;有账单行但两侧都无法映射。总额对得上,也可能把一块孤儿盘分给错误团队。每条 evidence edge 保存发现方式、置信度和最近验证窗口,名称匹配只能是低置信候选。
生命周期门禁围绕状态转换:Service 创建后在供应预算内出现 provider asset;删除后外部资产进入删除并停止新增计量;PVC/PV 根据 reclaim policy 进入预期状态;Released、Available、无 claimRef 的 PV 和无对象映射的磁盘进入待办;公网 IP、NAT、快照和备份即使没有 Pod 也继续纳入资产扫描。
金额门禁同时保留 Allocation、Assets 与云账单。Allocation 回答工作负载归属,Assets 回答节点、磁盘、LB 等底层资产,Cloud Cost 或供应商导出回答最终账单;三者可以在相同窗口内核对,但不能互相替代。最近窗口可能尚未稳定,差异要带数据新鲜度和账单修订状态。
权限、容量与架构取舍
只读采集器不应获得删除 Service、PV 或云资产的权限。清理执行身份按资产类型拆分,并要求 provider resource ID、owner、备份状态和变更单;共享 LB、共享卷和 NAT 不能由单一 namespace owner 自行销毁。Network Costs 这类宿主机采集器还要审查 privileged、hostNetwork、/proc 访问、镜像供应链与日志中的地址信息。
高基数网络流会迅速增加 DaemonSet CPU、内存、Prometheus 序列和存储。容量取决于节点数、连接数、采样粒度、保留期和聚合维度;先按 zone、namespace、workload 建立可解释基线,再为争议流量保留短期细粒度,不默认永久保存每条连接。PV 与 LB 清单则应低频全量扫描加事件增量,避免只依赖易丢失的删除事件。
架构选型按证据需要决定:只做资产审计时,Kubernetes inventory 加云账单足够;要做 namespace/workload 分摊时,需要 OpenCost、Kubecost 或云原生容器分摊;要细化 Pod 网络路径时,再引入具备相应权限的数据源。工具越深入,采集特权、运行成本和平台限制越高。零值只有在数据源健康、路径覆盖和账单稳定后才有意义。
升级、回滚与退出要保住映射链
升级 CSI、云控制器或成本平台前,保存 StorageClass、Service、PV/PVC 样本及其 provider 映射,在测试集群验证新旧对象字段、标签和删除 finalizer。平行运行新旧采集链时用同一对象 UID、provider ID 和窗口比较覆盖率,避免两个采集器把同一账单行重复导入。回滚不仅看 Pod Ready,还要看 PV、LB、network 分项和孤儿资产队列是否恢复。
退出采集工具前导出对象到资产的映射、价格规则版本、未结清账单、孤儿队列和保留政策。先让替代链覆盖一个完整稳定窗口,再撤销 Kubernetes ClusterRole、宿主机特权、云账单查询身份和对象存储权限。最后卸载组件并检查其 Service、PVC、LB、IP 与保留卷;卸载成功不等于派生资产已经停止计费。
