PAYATHON 2026

为什么不能使用 Prometheus 作为计费系统?

支付阿杰

结论

Prometheus 的数值计算并不是“不准确”。它是为监控场景设计的时序系统,并不是记录每笔业务交易的账本。

在告警、趋势分析和容量规划中,偶尔缺少几个采样点通常可以接受。按请求计费则不同,每个请求都要可靠记录,还要支持去重、追溯和重算。Prometheus 的采集链路无法保证这些条件,因此不适合作为计费系统的唯一数据源。

这里的 100% accuracy 更准确的含义是“计费事件必须完整,并且只能计算一次”,而不是浮点数的计算精度。

为什么监控数据可能不完整

Prometheus 采集的是快照,不是请求明细

Prometheus 通常按固定间隔抓取目标的 /metrics。假设抓取间隔是 15 秒,在这段时间内发生的请求不会被逐条保存,而是汇总到 Counter、Histogram 等指标中。

例如:

http_requests_total

这个 Counter 可以表示累计请求数,但无法回答:

  • 哪个客户发起了请求;
  • 每个请求对应哪个订单;
  • 请求是否已经计费;
  • 某次增长具体包含哪些请求;
  • 重复数据应该如何去重。

如果计费只依赖总数,发生异常后也很难逐笔核对。

抓取可能失败

Prometheus 使用 pull 模式。网络中断、超时、服务发现延迟、Prometheus 重启或目标短暂不可用,都可能导致某次抓取失败。

只要目标进程持续运行,Counter 就能跨过抓取空档保留累计值。例如,先抓到 100,下一次抓到 130,仍可推断中间增加了 30。但如果目标在下一次成功抓取前重启,内存中的 Counter 会归零,尚未采集的增量就可能永久丢失。

例如:

成功抓取:requests_total = 100
随后处理了 20 个请求
进程重启,Counter 归零
再次抓取:requests_total = 3

Prometheus 可以识别 Counter reset,却无法根据这些样本恢复重启前没有抓取到的 20 个请求。

短生命周期实例容易产生缺口

批处理任务、Serverless 实例和临时容器可能在 Prometheus 下一次抓取前退出。即使实例已经处理了请求,只要它没有被成功发现和抓取,相应指标就不会进入 Prometheus。

Pushgateway 可以缓解部分短任务的指标暴露问题,但它仍不是具有事务保证的计费事件存储,也不会自动解决重复提交、事件身份和账单审计等问题。

指标标签不是无限明细存储

理论上可以给指标添加 customer_id、request_id 等标签,但不应把每个请求 ID 都写进 Prometheus 标签:

billing_requests_total{
  customer_id="customer-123",
  request_id="request-7f9c..."
}

request_id 几乎每次都不同,这会产生大量时间序列,造成高基数问题,明显增加内存、存储和查询压力。Prometheus 适合维度有限的聚合指标,不适合保存每个请求的唯一明细。

监控链路没有账本语义

可靠的计费系统通常还要具备以下能力:

  • 每个事件都有稳定的唯一 ID;
  • 重试时可以幂等写入;
  • 重复事件可以去重;
  • 计费记录不会被普通的指标更新覆盖;
  • 可以按客户、订单和时间范围审计;
  • 计价规则变更后可以重算;
  • 可以证明某笔费用来自哪些原始事件。

Prometheus 的时间序列样本不具备完整的交易账本语义。Recording rule、remote write 和长期存储可以改善查询效率、数据保留时间或系统可用性,但无法自动把监控数据变成可审计的逐笔计费记录。

推荐的解决方案

处理请求时,应将计费事件写入持久化系统,并为每个事件分配唯一 ID。Prometheus 则继续用于监控计费链路的运行状态。

一种常见的数据结构如下:

CREATE TABLE billing_events (
    event_id        UUID PRIMARY KEY,
    customer_id     TEXT NOT NULL,
    request_id      TEXT NOT NULL,
    event_type      TEXT NOT NULL,
    quantity        NUMERIC NOT NULL,
    occurred_at     TIMESTAMPTZ NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (customer_id, request_id, event_type)
);

唯一约束可以避免同一个请求因重试而被重复计费:

INSERT INTO billing_events (
    event_id,
    customer_id,
    request_id,
    event_type,
    quantity,
    occurred_at
)
VALUES (
    :event_id,
    :customer_id,
    :request_id,
    'api_request',
    1,
    :occurred_at
)
ON CONFLICT (customer_id, request_id, event_type) DO NOTHING;

如果请求处理和计费事件发布必须保持一致,可以使用事务性 outbox,把业务变更和待发送事件放在同一个数据库事务中提交:

BEGIN;

UPDATE requests
SET status = 'completed'
WHERE request_id = :request_id;

INSERT INTO billing_outbox (
    event_id,
    request_id,
    customer_id,
    payload
)
VALUES (
    :event_id,
    :request_id,
    :customer_id,
    :payload
)
ON CONFLICT (event_id) DO NOTHING;

COMMIT;

后台任务随后将 outbox 事件发送到消息队列或计费服务。由于消息投递通常会重试,消费者仍需根据 event_id 实现幂等处理。

同时,可以将汇总结果暴露给 Prometheus:

billing_events_processed_total{status="success"} 125340
billing_events_processed_total{status="duplicate"} 27
billing_events_processed_total{status="failed"} 3
billing_event_processing_seconds_bucket{le="0.1"} 124000

这些指标适合判断计费服务是否出现积压或失败。最终账单仍应根据持久化的计费事件计算,不能从 billing_events_processed_total 反推。

注意事项

Prometheus 并非完全不能用于计费。如果业务允许估算,例如内部成本分摊、容量费用趋势分析,或用于非结算用途的用量统计,那么 Prometheus 的聚合指标可能已经足够。

如果金额需要对外结算,应将计费事件库、事务日志,或具备去重和审计能力的消息链路作为事实来源。Prometheus 可以用于交叉核对,比如比较账本事件数和服务端请求数,及时发现明显偏差,但不应作为唯一账本。

备注:内容仅供参考。