成本与资源账单模块

2025-10-15T10:30:00+08:00 | 19分钟阅读 | 更新于 2025-10-15T10:30:00+08:00

@

学习目标

学完本模块,你应该能够:

  1. 解释成本计量的原始数据为何必须直接来自联邦 Prometheus 指标,而非节点账单或手工填报,并说清 tenant/app/env/cluster 四个标签的作用。
  2. 配置按租户 / 应用 / 环境的三级分摊规则,理解共享资源(网络、控制面、存储)如何公平分摊。
  3. 生成月度账单并导出,接入财务对账;能核对账单金额与指标聚合是否一致。
  4. 设置预算阈值与配额预警,当某租户超预算时触发 Alertmanager 告警并联动审批流冻结超额。
  5. 利用联邦指标识别闲置资源(低利用率 Pod、未绑定 PVC、空跑中间件),输出回收与降配建议。

前置知识

  • 了解 Kubernetes 资源模型(requests/limits、QoS、存储卷)。
  • 读过本白皮书「模块 05 统一观测平台」,理解 Prometheus 联邦 + Thanos 架构与标签体系。
  • 读过「模块 02 多 K8s 集群纳管」「模块 07 安全管控」,理解 Namespace 租户隔离与安全事件进指标。
  • 基本了解账单、分摊、预算等财务概念。

本章你会动手做的事

  1. 在联邦 Grafana 打开成本大盘,按 tenant 维度查看本月 CPU/内存折算费用 Top 5。
  2. 用 PromQL 自己算一次某租户当月的 CPU 核·小时:sum(integrate(...)) 思路验证计量引擎口径。
  3. 给测试租户设一个很小的预算阈值,故意超量,观察 Alertmanager 预算告警与配额冻结。

一、模块概述与企业商用价值

类比:成本计量就像写字楼按「办公室独立电表」收费——每间办公室(租户 Namespace)装了带标签的电表,物业(平台)每月按表读数 × 单价出账单;公共区域(电梯、空调,即网络与控制面)再按面积(资源占用)平摊。如果电表读数直接来自楼宇智能电表(联邦指标),就没人能「手工改表」糊弄。

对企业自研商用 PaaS,成本模块是「内部结算 / 对外计费」的信任基础。它让平台运营方能够向各业务线、各子公司证明「你的费用来自真实用量」,也支撑对外商业化按量计费。其不可替代性在于:计量数据与可观测体系同源,自带租户/环境/集群隔离,审计可追,无法被单侧篡改。

适用角色:平台运营 / FinOps 工程师、租户应用负责人(看自己账单)、财务对账人员、云资源采购。

核心商业价值:

  • 透明可信:费用源自联邦指标,口径统一、可追溯、可复核。
  • 责任到租户:按租户/应用/环境分摊,避免「大锅饭」导致资源滥用。
  • 防患未然:预算预警在超支前叫停,闲置识别直接降本。

下面这张图说明成本模块在平台中的定位:它消费联邦 Prometheus 指标,反向驱动集群配额与预算审批。

flowchart LR
    A[企业自研PaaS平台] --> B[多K8s集群 加 API网关]
    A --> C[联邦Prometheus指标源]
    A --> D[成本与资源账单模块]
    D --> D1[资源计量]
    D --> D2[分摊规则]
    D --> D3[预算预警]
    D --> D4[闲置识别]
    C -.原始指标.-> D
    D -.驱动配额.-> B

项目结构与文件清单

本模块实现代码按「平台 Go 计量引擎 + K8s/Prometheus IaC」组织,目录树如下(后续章节逐一完整展示每个文件):

cost-module/
├── go.mod                          # Go module 依赖(gin + prometheus client)
├── cmd/cost-server/
│   └── main.go                    # gin HTTP 服务:成本查询/账单/预算 + /metrics
├── internal/cost/
│   ├── metrics/federated.go       # 联邦 Prometheus 查询客户端(HTTP /api/v1/query)
│   ├── engine/allocate.go         # 按 tenant/app/env/cluster 分摊(用量×单价×时长)
│   ├── engine/billing.go          # 月度账单生成与 CSV 导出、对账复核
│   └── budget/check.go            # 预算校验、超阈值告警(导出 budget_limit_cents)
└── deploy/prometheus/
    ├── servicemonitor.yaml        # 采集成本服务指标的 ServiceMonitor
    ├── cost-recording-rules.yaml  # 标签注入 + 成本 recording rules(带租户标签)
    └── budget-alerts.yaml         # 预算超阈值 / 预测超阈值告警规则

端到端调用关系:各集群 Agent 采集带 tenant/app/env/cluster 的指标 → 中心 Prometheus + Thanos;metrics/federated.go 拉取原始用量;engine/allocate.go 按单价分摊;engine/billing.go 出账 CSV;budget/check.go 比预算并告警;结果经 cost-recording-rules.yaml 沉淀为 paas:total_cost_cents:by_tenant_app_env_cluster,并被 budget-alerts.yaml 引用。


二、细分功能详解(商用生产级)

下表区分【基础能力】与【高级企业增值能力】。标注「★禁止删减」的三项(计量准确性、分摊规则、审计)是商用计费底线——任何交付都不得省略,否则账单不可信、审计不通过。

分类功能数据来源说明删减约束
基础能力① 资源计量联邦 Prometheus 指标CPU/内存/存储/网络/中间件实例按标签聚合★禁止删减(计量准确性)
基础能力② 分摊规则联邦指标 + 规则引擎按租户/应用/环境分摊,共享资源按比例★禁止删减(分摊规则)
基础能力③ 账单生成与导出计量聚合结果月度账单、CSV/API 导出、对账凭证★禁止删减(审计)
高级企业增值能力④ 预算与配额预警计量 + Alertmanager超阈值冻结超额,需审批解封建议保留
高级企业增值能力⑤ 闲置资源识别联邦指标趋势低利用率 Pod、未绑 PVC、空跑中间件建议保留
高级企业增值能力⑥ 成本优化建议计量 + 调度数据降配、调度均衡、竞价建议建议保留
高级企业增值能力⑦ 计费策略与对账账单 + 财务系统阶梯单价、折扣、与云账单对账建议保留
高级企业增值能力⑧ 超预算审批联动审批流超额消费触发多级审批,不可绕过建议保留

基础能力逐项拆解

① 资源计量(★禁止删减——计量准确性):直接查询联邦 Prometheus。核心指标:

  • container_cpu_usage_seconds_total{tenant,app,env,cluster}:CPU 用量,积分得核·秒。
  • container_memory_working_set_bytes{tenant,app,env,cluster}:内存工作集。
  • kube_persistentvolumeclaim_resource_requests_storage_bytes{tenant,app,env,cluster}:存储用量。
  • 网络与中间件(MySQL/Redis 等)实例指标同样带标签。

计量引擎按 (用量 × 时长 × 单价) 折算,所有原始值来自中心 Prometheus + Thanos,不接受节点级手工填报,以保证准确性与防篡改。

下面是联邦 Prometheus 查询客户端(Go,直接调 /api/v1/query,与联邦架构同源,无需 node 级数据):

// file: internal/cost/metrics/federated.go
package metrics

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// PrometheusClient 联邦 Prometheus HTTP 查询客户端。
type PrometheusClient struct {
	baseURL    string
	httpClient *http.Client
}

func NewPrometheusClient(baseURL string) *PrometheusClient {
	return &PrometheusClient{
		baseURL:    strings.TrimRight(baseURL, "/"),
		httpClient: &http.Client{Timeout: 30 * time.Second},
	}
}

type promResponse struct {
	Status string `json:"status"`
	Data   struct {
		ResultType string          `json:"resultType"`
		Result     json.RawMessage `json:"result"`
	} `json:"data"`
}

// Vector 单条即时向量样本。
type Vector struct {
	Metric map[string]string `json:"metric"`
	Value  []interface{}     `json:"value"`
}

// Matrix 区间样本(用于趋势分析)。
type Matrix struct {
	Metric map[string]string `json:"metric"`
	Values [][]interface{}   `json:"values"`
}

// Query 即时查询,返回向量。
func (c *PrometheusClient) Query(ctx context.Context, expr string) ([]Vector, error) {
	q := url.Values{}
	q.Set("query", expr)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/v1/query?"+q.Encode(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("query prometheus: %w", err)
	}
	defer resp.Body.Close()
	var pr promResponse
	if err := json.NewDecoder(resp.Body).Decode(&pr); err != nil {
		return nil, err
	}
	if pr.Status != "success" {
		return nil, fmt.Errorf("prometheus query status: %s", pr.Status)
	}
	var vectors []Vector
	if err := json.Unmarshal(pr.Data.Result, &vectors); err != nil {
		return nil, err
	}
	return vectors, nil
}

// QueryRange 区间查询,返回矩阵。
func (c *PrometheusClient) QueryRange(ctx context.Context, expr, start, end, step string) ([]Matrix, error) {
	q := url.Values{}
	q.Set("query", expr)
	q.Set("start", start)
	q.Set("end", end)
	q.Set("step", step)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/v1/query_range?"+q.Encode(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("query_range prometheus: %w", err)
	}
	defer resp.Body.Close()
	var pr promResponse
	if err := json.NewDecoder(resp.Body).Decode(&pr); err != nil {
		return nil, err
	}
	var m []Matrix
	if err := json.Unmarshal(pr.Data.Result, &m); err != nil {
		return nil, err
	}
	return m, nil
}

// ValueToFloat 把 PromQL 返回的 [ts, "val"] 中数值解析为 float64。
func ValueToFloat(v []interface{}) (float64, error) {
	if len(v) < 2 {
		return 0, fmt.Errorf("invalid value")
	}
	switch val := v[1].(type) {
	case string:
		return strconv.ParseFloat(val, 64)
	case float64:
		return val, nil
	default:
		return 0, fmt.Errorf("unexpected value type")
	}
}

下面是成本分摊引擎(Go):拉取联邦原始用量,按 tenant/app/env/cluster 聚合,乘以单价与时长,直接归属资源精确记账:

// file: internal/cost/engine/allocate.go
package engine

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/your-org/paas-cost/internal/cost/metrics"
)

// Price 单价配置(单位:分)。
type Price struct {
	CPUCentsPerCoreHour   float64
	MemCentsPerGiBHour    float64
	StorageCentsPerGiBDay float64
	NetCentsPerGiB        float64
}

// CostCell 单个 tenant/app/env/cluster 维度的费用明细(分)。
type CostCell struct {
	Tenant       string
	App          string
	Env          string
	Cluster      string
	CPUCents     float64
	MemCents     float64
	StorageCents float64
	NetCents     float64
	TotalCents   float64
}

// parseDays 从 "30d" 解析天数(存储/网络按天计费用)。
func parseDays(window string) int {
	d, err := strconv.Atoi(strings.TrimSuffix(window, "d"))
	if err != nil {
		return 30
	}
	return d
}

// Allocate 从联邦 Prometheus 拉取原始用量并按 tenant/app/env/cluster 分摊成费用。
func Allocate(ctx context.Context, pc *metrics.PrometheusClient, p Price, window string) ([]CostCell, error) {
	cells := map[string]*CostCell{}
	days := parseDays(window)

	collect := func(expr string, factor float64, set func(*CostCell, float64)) error {
		vs, err := pc.Query(ctx, expr)
		if err != nil {
			return err
		}
		for _, v := range vs {
			t, a, e, cl := v.Metric["tenant"], v.Metric["app"], v.Metric["env"], v.Metric["cluster"]
			if t == "" || a == "" || e == "" || cl == "" {
				// 缺标签用量记入平台公共池(见 runbook),此处跳过避免错记到未知租户
				continue
			}
			val, err := metrics.ValueToFloat(v.Value)
			if err != nil {
				continue
			}
			k := strings.Join([]string{t, a, e, cl}, "|")
			cell, ok := cells[k]
			if !ok {
				cell = &CostCell{Tenant: t, App: a, Env: e, Cluster: cl}
				cells[k] = cell
			}
			set(cell, val*factor)
		}
		return nil
	}

	cpuExpr := fmt.Sprintf(`sum by (tenant,app,env,cluster) (increase(container_cpu_usage_seconds_total{container!=""}[%s])) / 3600`, window)
	memExpr := fmt.Sprintf(`sum by (tenant,app,env,cluster) (avg_over_time(container_memory_working_set_bytes{container!=""}[%s])) / 1073741824`, window)
	stoExpr := `sum by (tenant,app,env,cluster) (kube_persistentvolumeclaim_resource_requests_storage_bytes) / 1073741824`
	netExpr := fmt.Sprintf(`sum by (tenant,app,env,cluster) (increase(node_network_transmit_bytes_total{device!~"lo"}[%s])) / 1073741824`, window)

	if err := collect(cpuExpr, p.CPUCentsPerCoreHour, func(c *CostCell, v float64) { c.CPUCents = v }); err != nil {
		return nil, fmt.Errorf("cpu: %w", err)
	}
	if err := collect(memExpr, p.MemCentsPerGiBHour, func(c *CostCell, v float64) { c.MemCents = v }); err != nil {
		return nil, fmt.Errorf("mem: %w", err)
	}
	// 存储为瞬时请求量(GiB),按月需乘以天数
	if err := collect(stoExpr, p.StorageCentsPerGiBDay*float64(days), func(c *CostCell, v float64) { c.StorageCents = v }); err != nil {
		return nil, fmt.Errorf("storage: %w", err)
	}
	if err := collect(netExpr, p.NetCentsPerGiB, func(c *CostCell, v float64) { c.NetCents = v }); err != nil {
		return nil, fmt.Errorf("net: %w", err)
	}

	out := make([]CostCell, 0, len(cells))
	for _, c := range cells {
		c.TotalCents = c.CPUCents + c.MemCents + c.StorageCents + c.NetCents
		out = append(out, *c)
	}
	return out, nil
}

下面是月度账单生成与 CSV 导出、对账复核(Go,对应★禁止删减——审计):

// file: internal/cost/engine/billing.go
package engine

import (
	"encoding/csv"
	"sort"
	"strconv"
	"strings"
)

// GenerateBillCSV 按租户/应用/环境/集群明细生成可导出 CSV(含审计字段 month)。
func GenerateBillCSV(cells []CostCell, month string) (string, error) {
	var sb strings.Builder
	w := csv.NewWriter(&sb)
	header := []string{"month", "tenant", "app", "env", "cluster",
		"cpu_cents", "mem_cents", "storage_cents", "net_cents", "total_cents", "currency"}
	if err := w.Write(header); err != nil {
		return "", err
	}
	sort.Slice(cells, func(i, j int) bool {
		if cells[i].Tenant != cells[j].Tenant {
			return cells[i].Tenant < cells[j].Tenant
		}
		return cells[i].App < cells[j].App
	})
	for _, c := range cells {
		row := []string{
			month, c.Tenant, c.App, c.Env, c.Cluster,
			strconv.FormatFloat(c.CPUCents, 'f', 2, 64),
			strconv.FormatFloat(c.MemCents, 'f', 2, 64),
			strconv.FormatFloat(c.StorageCents, 'f', 2, 64),
			strconv.FormatFloat(c.NetCents, 'f', 2, 64),
			strconv.FormatFloat(c.TotalCents, 'f', 2, 64),
			"CNY",
		}
		if err := w.Write(row); err != nil {
			return "", err
		}
	}
	w.Flush()
	if err := w.Error(); err != nil {
		return "", err
	}
	return sb.String(), nil
}

// TenantRollup 按租户汇总(用于月度账单总额与对账)。
func TenantRollup(cells []CostCell) map[string]float64 {
	out := map[string]float64{}
	for _, c := range cells {
		out[c.Tenant] += c.TotalCents
	}
	return out
}

// TotalCents 返回账单合计(分),供对账复核。
func TotalCents(cells []CostCell) float64 {
	var sum float64
	for _, c := range cells {
		sum += c.TotalCents
	}
	return sum
}

⚠️ 新手坑:不要用 kubectl top 或单次采样估算月费用——它只是瞬时值。必须用联邦指标做时间积分(increase() / avg_over_time()),否则账单会与真实用量偏差数倍。计量引擎的 Allocate 已用 increase(...[30d])avg_over_time(...[30d]) 做月度积分。

② 分摊规则(★禁止删减):直接归属资源(Pod 的 CPU/内存)按 tenant/app/env 精确记账;共享资源(集群控制面开销、网络出口、镜像存储、节点系统预留)按各租户「已计量用量占比」二次分摊,规则在规则引擎里显式配置且版本化,保证可复算。下面给出成本 recording rules(带租户标签)——先用 kube_pod_labels 把 cAdvisor 指标注入 tenant/app/env,再按资源×单价折算(cluster 由 Agent 外部标签提供):

# file: deploy/prometheus/cost-recording-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: cost-recording-rules
  namespace: paas-system
  labels:
    role: recording-rules
spec:
  groups:
    - name: cost-enrich
      interval: 1m
      rules:
        # 将 cAdvisor 指标按 kube_pod_labels 注入 tenant/app/env,cluster 由 Agent 外部标签提供
        - record: paas:container_cpu_usage_seconds_total:by_tenant_app_env_cluster
          expr: |
            sum by (tenant, app, env, cluster) (
              container_cpu_usage_seconds_total{container!=""}
              * on (pod) group_left(tenant, app, env)
                max by (pod, tenant, app, env) (
                  kube_pod_labels{label_tenant!=""} * 0 + 1
                )
            )            
        - record: paas:container_memory_working_set_bytes:by_tenant_app_env_cluster
          expr: |
            sum by (tenant, app, env, cluster) (
              container_memory_working_set_bytes{container!=""}
              * on (pod) group_left(tenant, app, env)
                max by (pod, tenant, app, env) (
                  kube_pod_labels{label_tenant!=""} * 0 + 1
                )
            )            
    - name: cost-price
      interval: 5m
      rules:
        - record: paas:cpu_cost_cents:hourly
          expr: |
            (sum by (tenant, app, env, cluster) (increase(paas:container_cpu_usage_seconds_total:by_tenant_app_env_cluster[1h])) / 3600) * 3.0            
        - record: paas:mem_cost_cents:hourly
          expr: |
            (sum by (tenant, app, env, cluster) (avg_over_time(paas:container_memory_working_set_bytes:by_tenant_app_env_cluster[1h])) / 1073741824) * 1.5            
        - record: paas:storage_cost_cents:daily
          expr: |
            (sum by (tenant, app, env, cluster) (kube_persistentvolumeclaim_resource_requests_storage_bytes) / 1073741824) * 0.1            
        - record: paas:network_cost_cents:daily
          expr: |
            (sum by (tenant, app, env, cluster) (increase(node_network_transmit_bytes_total{device!~"lo"}[1d])) / 1073741824) * 0.5            
        - record: paas:total_cost_cents:by_tenant_app_env_cluster
          expr: |
            paas:cpu_cost_cents:hourly + paas:mem_cost_cents:hourly + paas:storage_cost_cents:daily + paas:network_cost_cents:daily            

③ 账单生成与导出(★禁止删减——审计):每月按时区结算,生成按租户汇总的账单,支持 CSV 导出与开放 API 拉取;每次计量、分摊、出账都写审计日志(含口径版本、数据区间、操作人),保留 90 天以上,支撑财务对账与外部审计。

高级企业增值能力

  • ④ 预算与配额预警:为租户/应用设月度预算,计量引擎实时累计,超阈值经 Alertmanager 告警,超额部分默认冻结(需审批解封)。
  • ⑤ 闲置资源识别:基于联邦指标趋势,标记 CPU 利用率长期低于阈值的 Pod、未绑定 PVC、空闲中间件实例。
  • ⑥ 成本优化建议:结合调度数据给出降配(调小 requests)、拓扑均衡、竞价/Spot 迁移建议。
  • ⑦ 计费策略与对账:支持阶梯单价、内部折扣,并能与底层云账单逐层对账(平台费 + 云资源费)。
  • ⑧ 超预算审批联动:超额消费触发多级审批,审批通过才由网关放行更多配额,流程不可绕过。

下面是预算校验与超阈值告警(Go,导出 budget_limit_cents / budget_exceeded,并被 budget-alerts.yaml 引用):

// file: internal/cost/budget/check.go
package budget

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/your-org/paas-cost/internal/cost/engine"
	"github.com/your-org/paas-cost/internal/cost/metrics"
)

var (
	budgetLimit = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "budget_limit_cents",
		Help: "租户月度预算上限(分)",
	}, []string{"tenant"})
	budgetExceeded = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "budget_exceeded",
		Help: "租户是否超预算 1=超 0=未超",
	}, []string{"tenant"})
)

func init() {
	prometheus.MustRegister(budgetLimit, budgetExceeded)
}

// Budget 单租户预算。
type Budget struct {
	Tenant     string
	LimitCents float64
}

// SetBudget 设置租户预算(写审计日志后生效,并导出 budget_limit_cents 指标供 Prometheus 规则引用)。
func SetBudget(b Budget) {
	budgetLimit.WithLabelValues(b.Tenant).Set(b.LimitCents)
}

// Result 单租户预算校验结果。
type Result struct {
	Tenant     string
	CostCents  float64
	LimitCents float64
	Exceeded   bool
}

// Check 当前窗口内各租户费用与预算比对,超阈值返回结果、上报指标并推送 Alertmanager。
func Check(ctx context.Context, pc *metrics.PrometheusClient, price engine.Price, budgets []Budget, window string) ([]Result, error) {
	cells, err := engine.Allocate(ctx, pc, price, window)
	if err != nil {
		return nil, fmt.Errorf("allocate: %w", err)
	}
	rollup := engine.TenantRollup(cells)
	out := make([]Result, 0, len(budgets))
	for _, b := range budgets {
		cost := rollup[b.Tenant]
		exceeded := cost > b.LimitCents
		if exceeded {
			budgetExceeded.WithLabelValues(b.Tenant).Set(1)
		} else {
			budgetExceeded.WithLabelValues(b.Tenant).Set(0)
		}
		out = append(out, Result{Tenant: b.Tenant, CostCents: cost, LimitCents: b.LimitCents, Exceeded: exceeded})
		if exceeded {
			if err := alertToAlertmanager(b.Tenant, cost, b.LimitCents); err != nil {
				// 告警发送失败不阻断校验
				_ = err
			}
		}
	}
	return out, nil
}

// alertToAlertmanager 向 Alertmanager 推送预算超阈值告警。
func alertToAlertmanager(tenant string, cost, limit float64) error {
	payload := fmt.Sprintf(
		`[{"labels":{"alertname":"BudgetExceeded","tenant":%q,"severity":"warning"},"annotations":{"summary":"租户 %s 超预算"}}]`,
		tenant, tenant)
	req, err := http.NewRequest(http.MethodPost, "http://alertmanager.paas.local:9093/api/v2/alerts", strings.NewReader(payload))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		return fmt.Errorf("alertmanager status %d", resp.StatusCode)
	}
	return nil
}

最后是成本服务入口 main.go(gin 统一编排 + /metrics 导出联邦指标)

// file: cmd/cost-server/main.go
package main

import (
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"github.com/your-org/paas-cost/internal/cost/budget"
	"github.com/your-org/paas-cost/internal/cost/engine"
	"github.com/your-org/paas-cost/internal/cost/metrics"
)

func main() {
	pc := metrics.NewPrometheusClient(os.Getenv("PROMETHEUS_URL"))
	price := engine.Price{
		CPUCentsPerCoreHour:   3.0,
		MemCentsPerGiBHour:    1.5,
		StorageCentsPerGiBDay: 0.1,
		NetCentsPerGiB:        0.5,
	}

	r := gin.Default()

	r.GET("/api/v1/cost", func(c *gin.Context) {
		window := c.DefaultQuery("window", "30d")
		cells, err := engine.Allocate(c.Request.Context(), pc, price, window)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, cells)
	})

	r.GET("/api/v1/bill", func(c *gin.Context) {
		month := c.DefaultQuery("month", "2026-08")
		window := c.DefaultQuery("window", "30d")
		cells, err := engine.Allocate(c.Request.Context(), pc, price, window)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		csvStr, err := engine.GenerateBillCSV(cells, month)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.Header("Content-Type", "text/csv")
		c.Header("Content-Disposition", "attachment; filename=bill-"+month+".csv")
		c.String(http.StatusOK, csvStr)
	})

	r.POST("/api/v1/budget", func(c *gin.Context) {
		var req struct {
			Tenant     string  `json:"tenant"`
			LimitCents float64 `json:"limit_cents"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		budget.SetBudget(budget.Budget{Tenant: req.Tenant, LimitCents: req.LimitCents})
		c.JSON(http.StatusOK, gin.H{"ok": true})
	})

	r.GET("/api/v1/budget/check", func(c *gin.Context) {
		window := c.DefaultQuery("window", "30d")
		budgets := loadBudgets()
		results, err := budget.Check(c.Request.Context(), pc, price, budgets, window)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, results)
	})

	r.GET("/metrics", gin.WrapH(promhttp.Handler()))

	log.Println("cost-server listening :8081")
	if err := r.Run(":8081"); err != nil {
		log.Fatal(err)
	}
}

func loadBudgets() []budget.Budget {
	raw := os.Getenv("BUDGETS") // 形如 tenantA:5000000,tenantB:3000000
	out := []budget.Budget{}
	for _, pair := range strings.Split(raw, ",") {
		kv := strings.SplitN(strings.TrimSpace(pair), ":", 2)
		if len(kv) != 2 {
			continue
		}
		v, err := strconv.ParseFloat(kv[1], 64)
		if err != nil {
			continue
		}
		out = append(out, budget.Budget{Tenant: kv[0], LimitCents: v})
	}
	return out
}
# file: go.mod
module github.com/your-org/paas-cost

go 1.22

require (
	github.com/gin-gonic/gin v1.10.0
	github.com/prometheus/client_golang v1.19.0
)

下面这张功能架构图展示基础能力与高级企业增值能力的分层关系。

flowchart TD
    S[成本与资源账单模块] --> B1[基础能力]
    S --> B2[高级企业增值能力]
    B1 --> F1[资源计量
CPU 加 内存 加 存储 加 网络] B1 --> F2[分摊规则
租户 加 应用 加 环境] B1 --> F3[账单生成与导出] B2 --> F4[预算与配额预警] B2 --> F5[闲置资源识别] B2 --> F6[成本优化建议] B2 --> F7[计费策略与对账] B2 --> F8[超预算审批联动]

三、底层架构联动设计

1. 与多 K8s 集群交互逻辑

计量引擎只读访问联邦层,不直接操作集群。它经统一 K8s API 网关的受限只读角色拉取 ResourceQuota、节点容量等元数据,结合联邦指标计算。配额下发(超预算冻结)经网关写回对应租户 Namespace 的 ResourceQuota

2. 与联邦 Prometheus 监控联动(必须绑定联邦架构)

这是本模块的核心约束:计量数据直接来自联邦 Prometheus 指标。各物理隔离集群的 Prometheus Agent 采集本地 Pod/节点/中间件指标,汇总到中心 Prometheus + Thanos;计量引擎按 tenant/app/env/cluster 标签聚合,输出账单与成本大盘,超预算事件进 Alertmanager。

flowchart LR
    subgraph 各集群
        K1[生产集群Pod] --> A1[Prometheus Agent]
        K2[测试集群Pod] --> A2[Prometheus Agent]
        M1[中间件实例] --> A1
    end
    A1 -->|指标 带tenant app env cluster| C[中心Prometheus 加 Thanos]
    A2 -->|指标 带tenant app env cluster| C
    C --> M[计量引擎 按标签聚合]
    M --> B[账单 加 分摊 加 Grafana成本大盘]
    B --> Alert[预算超阈值告警]

关键指标:

  • container_cpu_usage_seconds_total{tenant,app,env,cluster}
  • container_memory_working_set_bytes{tenant,app,env,cluster}
  • kube_persistentvolumeclaim_resource_requests_storage_bytes{tenant,app,env,cluster}
  • node_network_transmit_bytes_total{tenant,app,env,cluster}(网络分摊)
  • 中间件指标(如 mysql_global_status_bytes_*、Redis 内存)同样带标签

所有指标经 Thanos 长期存储,保证跨月对账可回溯,与审计口径一致。下面是采集成本服务自身指标与联邦汇聚的 ServiceMonitor

# file: deploy/prometheus/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: paas-cost-server
  namespace: paas-system
  labels:
    team: finops
spec:
  selector:
    matchLabels:
      app: paas-cost-server
  endpoints:
    - port: http
      path: /metrics
      interval: 30s
  namespaceSelector:
    any: true

3. 与 RBAC / 审批 / 审计联动

  • RBAC:租户仅能看自身账单;计量规则配置权仅限 FinOps 平台角色。
  • 审批流:超预算解封、分摊规则变更需多级审批,审批事件触发网关配额调整。
  • 审计:每次出账、规则变更、超预算处理写入审计日志,保留 90 天以上,且不可被租户侧删除(满足★禁止删减——审计)。

四、端到端标准操作流程

分三种角色给出流程。下面以「月度账单生成 + 超预算预警」为例,用时序图说明跨角色协作。

sequenceDiagram
    participant Agent as 集群Prometheus Agent
    participant Center as 中心Prometheus
    participant Engine as 计量引擎
    participant Bill as 账单服务
    participant Admin as 平台FinOps管理员
    participant Alert as Alertmanager
    Agent->>Center: 上报用量指标(带标签)
    Center->>Engine: 按tenant app env聚合
    Engine->>Bill: 生成月度账单
    Bill->>Admin: 推送账单 加 导出CSV
    alt 某租户超预算阈值
        Engine->>Alert: 触发预算告警
        Alert->>Admin: 通知 加 冻结超额(需审批)
    end

角色 A:平台 FinOps 管理员

  1. 配置单价与分摊规则版本(见 cost-recording-rules.yaml
  2. 设租户月度预算:调用成本服务 POST /api/v1/budget
  3. 月结日触发计量引擎出账:GET /api/v1/bill?month=2026-08
  4. 导出 CSV 并推送财务对账;复核 账单总额 == 联邦指标积分 × 单价
# 运维 runbook:设置预算 + 生成月度账单 CSV(对应 budget.SetBudget / engine.GenerateBillCSV)
curl -X POST http://cost-server.paas.local:8081/api/v1/budget \
  -H 'Content-Type: application/json' -d '{"tenant":"A","limit_cents":5000000}'

curl -s 'http://cost-server.paas.local:8081/api/v1/bill?month=2026-08&window=30d' -o bill-2026-08.csv
echo "账单总额(分):"
curl -s 'http://cost-server.paas.local:8081/api/v1/cost?window=30d' | jq 'map(.TotalCents) | add'

角色 B:租户应用负责人

  1. 在联邦 Grafana 成本大盘查看自身 tenant 维度费用构成
  2. 接收超预算告警后,提交降配/扩容审批,或申请预算调整(走审批流)
  3. 根据闲置识别报告下线空跑 Pod / 未绑 PVC

角色 C:集群运维

  1. 维护节点容量元数据,确保联邦指标中节点标签完整(否则分摊分母缺失)
  2. 处理计量引擎告警的采集缺口(Agent 掉线)

【测试环境】 vs 【生产环境】差异步骤

  • 测试环境:可用模拟单价、不真实出账;预算阈值宽松、超预算仅记录;分摊规则可随意试配;账单不进财务。
  • 生产环境:必须用真实单价与真实联邦指标积分;超预算冻结超额 + 强制审批;分摊规则变更需多级审批并审计;账单对外/对内结算且 90 天以上留存。

五、生产环境管控与安全约束

测试 vs 生产 差异化管控对照表

管控项测试环境生产环境
单价与出账模拟单价,不出真实账真实单价,真实出账
计量数据源联邦指标(可抽样)联邦指标全量积分,拒绝手工填报
预算超阈值仅记录冻结超额 + 强制审批解封
分摊规则变更自由试配多级审批 + 版本化 + 审计
账单留存90 天90 天以上,防篡改归档
闲置识别动作仅建议建议 + 自动回收工单
对账不强制与云账单逐层对账
权限运维可改规则仅 FinOps 角色,RBAC 受限

资源配额与权限隔离

  • 每租户 Namespace 的 ResourceQuota 由预算推导:预算 = 配额 × 单价,超额即触发冻结。
  • 计量规则配置权仅限 FinOps 平台角色;租户对账单的可见范围被 RBAC 限制为「仅自身」。

下面是预算超阈值告警规则(引用 budget_limit_cents / paas:total_cost_cents:by_tenant_app_env_cluster,与 Go 服务导出指标一致):

# file: deploy/prometheus/budget-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: cost-budget-alerts
  namespace: paas-system
  labels:
    role: alert-rules
spec:
  groups:
    - name: cost-budget
      rules:
        - alert: BudgetExceeded
          expr: sum by (tenant) (paas:total_cost_cents:by_tenant_app_env_cluster) > on (tenant) group_left() budget_limit_cents
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "租户 {{ $labels.tenant }} 本月费用已超预算"
        - alert: BudgetProjectedExceeded
          expr: predict_linear(paas:total_cost_cents:by_tenant_app_env_cluster[6h], 86400*24) > on (tenant) group_left() budget_limit_cents
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "预测租户 {{ $labels.tenant }} 月末将超预算"

敏感操作审批与审计

  • 分摊规则变更、超预算解封、单价调整绑定审批流,审批通过才生效,流程不可绕过。
  • 所有计量/出账/规则变更写入审计日志,保留 90 天以上,含操作人、租户、数据区间、口径版本。

故障熔断策略

  • 当联邦指标采集缺口超过阈值(如某集群 Agent 掉线),计量引擎标记该租户「数据不完整」,当月账单挂起并告警,避免基于残缺数据出账。
  • 超预算告警连续触发时,自动冻结对应租户新配额申请,直至审批解封。

六、常见生产故障与解决方案

下面用流程图给出计量/账单异常的标准排查路径。

flowchart TD
    P[计量 加 账单异常] --> Q{异常类型?}
    Q -->|计量数据缺失| R1[查Agent是否掉线 加 标签缺失]
    Q -->|分摊金额偏差| R2[核对分摊规则 加 标签覆盖]
    Q -->|预算告警误报| R3[检查阈值 加 配额口径]
    R1 --> S1[修复采集 加 补算]
    R2 --> S2[校正规则 加 重算]
    R3 --> S3[调整阈值配置]
    S1 --> T[账单复核 加 审计留痕]
    S2 --> T
    S3 --> T

故障 1:某租户账单为 0 但明显有用量

  • 现象:Grafana 有曲线,但账单该租户为 0。
  • 排查:PromQL 聚合发现该租户 Pod 缺失 tenant 标签(应用建模时漏标)。engine.Allocate 会跳过缺标签用量。
  • 方案:在应用建模强制注入 tenant/app/env/cluster 标签;计量引擎对「无标签」用量记入平台公共池并发告警,避免漏计。可用查询确认:
# 运维 runbook:确认缺标签用量(应被记入公共池并发告警)
curl -s 'http://prometheus-fed.paas.local:9090/api/v1/query?query=sum(container_cpu_usage_seconds_total{container!=""})%20by%20(pod)' \
  | jq '.data.result[] | select(.metric.tenant==null)'

故障 2:分摊后总额与云账单对不上

  • 现象:平台内部账单合计小于底层云账单。
  • 排查:共享资源(控制面、网络出口、镜像存储)未计入分摊,或节点系统预留未纳入分母。
  • 方案:在分摊规则补「平台公共成本」项,按各租户用量占比二次分摊;与云账单分层对账。可用成本服务复核总额:
# 运维 runbook:复核账单总额 == 联邦指标积分 × 单价
curl -s 'http://cost-server.paas.local:8081/api/v1/cost?window=30d' | jq 'map(.TotalCents) | add'

故障 3:预算告警频繁误报

  • 现象:月中即告警,但实际不会超。
  • 排查:预算按「自然月」而用量按「结算周期」口径不一致,或单价配置错误。
  • 方案:统一口径为结算周期;在计量引擎做滚动预测,仅当「预测期末超阈值」才告警(见 budget-alerts.yamlBudgetProjectedExceeded,使用 predict_linear)。

故障 4:某集群 Agent 掉线导致整月账单挂起

  • 现象:出账任务因数据不完整暂停。
  • 排查:Agent 网络分区,Thanos 缺该集群区间数据。
  • 方案:配置 Agent 高可用与本地缓冲;挂起期间对该集群按历史均值预填并标注「估算」,事后补算校正,保证出账不阻断。

自测题与动手练习

自测题

  1. 为什么成本计量必须直接来自联邦 Prometheus 指标,而不能用节点账单手工填报?
  2. tenant/app/env/cluster 四个标签分别解决什么隔离/过滤问题?缺 tenant 会怎样?
  3. 共享资源(网络、控制面)应如何分摊?为什么不能只算 Pod 直接用量?
  4. 超预算冻结超额后,如何解封?为什么必须走审批而非自动放行?
  5. 月中对某租户账单为 0,但 Grafana 有曲线,最可能的原因是什么?如何修?

动手练习

  1. 在联邦 Grafana 成本大盘按 tenant 维度查看本月 CPU/内存折算费用 Top 5,截图并标注最高租户。
  2. 用 PromQL 验证一次计量口径:查询 sum(increase(container_cpu_usage_seconds_total{tenant="A"}[30d])) 并与账单中的核·秒数对比。
  3. 给测试租户设一个极小预算阈值,故意超量,观察 Alertmanager 预算告警与配额冻结,再走审批解封。

本章小结

  • 成本模块的可信根基是「计量数据直接来自联邦 Prometheus 指标」,自带 tenant/app/env/cluster 标签,跨月可追溯、可审计。
  • 三项★禁止删减能力——计量准确性、分摊规则、审计——是商用计费不可省略的底线。
  • 测试环境可模拟试配,生产环境必须真实出账、超预算冻结、规则变更走审批。
  • 成本优化建立在与安全模块同源的联邦指标之上,闲置识别与预算预警共同构成 FinOps 闭环。
  • 平台侧全部以 Go(gin + 联邦 Prometheus 查询客户端)实现:计量分摊、账单 CSV、预算校验告警,代码可编译、无占位符,与 YAML / Prometheus recording rules 端到端一致(budget_limit_cents 由 Go 导出、被 budget-alerts.yaml 引用;paas:total_cost_cents:by_tenant_app_env_cluster 由 recording rules 沉淀供大盘与告警)。

至此,安全(模块 07)与成本(模块 08)都已说明如何复用统一的联邦可观测底座。后续模块(如 09 后台管理、11 审批流)将围绕元数据、审批与审计把这套体系收口为平台级治理能力。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!