客户买的是「用了多少」,不是「你收了几条日志」。
一、十一块六的账单
一个客户这个月上报了 108,000 条事件,账单上写着 11.60 美元。他问:每千条 0.20,为什么不是 21.60?
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
因为前 50,000 条免费。超过的 58,000 条按每千 0.20 计,58 × 0.20 = 11.60。
按量计费(usage-based billing)看起来是「数事件 × 单价」,实际是一条四步链路。 原始事件要变成用量记录,再聚合成桶,最后才落成账单行;中间任何一步出错,账单就是错的。
12 个按量客户,每月 1,296,000 条事件。 这不是一个大数字,但它分布在 8,640 个五分钟桶里,平均每桶 150 条。
事件数不等于计费单位。 客户重试一次、网络重投一次,都会多出一条事件;如果直接按事件行数收费,收的是传输次数,不是用量。
账单必须能逐行解释。 客户拿着 11.60 来问,你要能拆成免费额度、超出条数、单价与取整四段;拆不开,就说明计量层和账单层之间断了。
108,000 条大约是每分钟 2.5 条。 按 30 天摊,每个客户每天 3,600 条;这个量级决定桶不能按分钟切,否则桶数会涨五倍,存储先撑不住。
三个数字要先分开:事件、用量、账单。 事件是输入,用量是去重后的计量,账单是金额;三者对不上时,先确认对方说的是哪一个,再决定查哪一层。
二、四步:从一条事件到一行账单
| 步骤 | 输入 | 输出 | 关键约束 |
|---|---|---|---|
| 1 原始事件 | 客户端上报 | 追加写的事件行 | 带幂等键,可重放 |
| 2 用量记录 | 事件 | 按客户与日期汇总 | 去重后只增不减 |
| 3 聚合桶 | 用量记录 | 5 分钟桶 | 可重算,不覆盖历史 |
| 4 账单行 | 桶 + 阶梯价 | 一张发票的一行 | 与桶的合计可对账 |
原始事件只追加,不改写。 收到就写,至少带 customer_id、event_id、usage_date 三个字段;重投靠幂等键挡掉,不靠客户端自觉。
用量记录是去重后的结果,不是原始条数。 迟到与重复都在这层处理,下游只看这一层;这样换聚合方式时不用回头重扫事件表。
桶要可重算。 结算窗口内允许重算,窗口关闭后冻结;冻结的桶是账单的唯一依据,改历史必须走冲正。
账单行必须能追回桶。 一行账单对应一组桶,桶对应一组用量记录;三层都能反查,才有对账的可能。
四步之间要有明确的时间边界。 事件写入、聚合、出账各有一个截止点;边界不清,同一批事件会被算两次或整批漏掉。
聚合要按客户分片。 不同客户的桶不能混算,否则一个客户的重投会抬高另一个客户的账单,这种错最难解释。
重算必须幂等。 同一个窗口重算十次,结果要逐分相同;做不到,就说明聚合里混进了「当前时间」这类不稳定输入。
写入失败要能补。 客户端断网时先本地排队,恢复后重放;服务端只认幂等键,不认「这是第一次」,否则补写会被当成新用量。
聚合产物要带版本。 桶的计算逻辑改了,旧桶不能悄悄重算,要标版本;否则同一个客户两个月账单用了两种口径,谁也解释不清。
账单行要存快照。 单价、免费额度、阶梯区间在出账时写进行内,之后改价不影响已出账单,也不用回头重算历史。
三、阶梯价与三道去重
| 区间 | 单价 | 单客户 108,000 条的计费 |
|---|---|---|
| 0–50,000 | 免费 | 0 |
| 50,001–500,000 | 0.20/千 | 58 × 0.20 = 11.60 |
| 500,001 以上 | 0.10/千 | 0 |
阶梯价按区间累加,不是整单套用一档。 108,000 条只落在第二档,所以是 58 个千 × 0.20;如果误用整单单价,结果会差出几倍。
第一道去重是幂等键。 customer_id + event_id + usage_date;客户端重试、网络重投、代理重发都靠它挡,键一变历史就断。
第二道去重是结算窗口。 24 小时内到达的算本期,超出的计入下期;迟到 0.8% 就是 10,368 条,不设窗口就会月月改上期账单。
第三道去重是客户端与服务端的计数偏差。 0.3% = 3,888 条 = 0.78 美元;以服务端为准,客户端只做展示,不能两边都算。
重复上报 0.9% = 11,664 条。 这些是同一个 event_id 被写了两次;不去重,账单会随客户端重试次数波动,客户自己数一遍就会发现。
上限只告警不断服务。 2 倍平均 = 216,000 条/客户/月;超限发提醒,让客户自己决定,而不是直接停掉,按量客户最怕的就是突然不可用。
免费额度是定价的一部分,不是漏洞。 50,000 条免费让客户敢接入;把它当成要严防的死角,会做出没人敢用的产品。
阶梯价在出账时重算一次。 用量记录只存条数,价格在账单行计算;把价格写死在采集端,改价就要回填历史,回填就会出错。
窗口关闭要有明确信号。 冻结动作要落一条记录,迟到事件据此判断进本期还是下期,不能靠查询当前时间来决定归属。
去重率要每月看趋势。 0.8% 与 0.9% 单月看不出问题,连续三个月上涨说明客户端重试逻辑在恶化,越晚修越贵。
四、把一百二十九万六千条算回一张账单
复算:12 个客户按同一阶梯价计算,迟到、重复与计数偏差分别按比例列示,上限取平均用量的两倍。
12 × 108,000 = 1,296,000 条/月
30 × 288 = 8,640 桶;1,296,000 ÷ 8,640 = 150 条/桶
(108,000 − 50,000) ÷ 1,000 × 0.20 = 11.60/客户
12 × 11.60 = 139.2/月;× 12 = 1,670.4/年
迟到 1,296,000 × 0.8% = 10,368;重复 × 0.9% = 11,664;偏差 × 0.3% = 3,888 = 0.78 美元
上限 108,000 × 2 = 216,000
139.2 美元/月是 12 个客户的按量收入。 年化 1,670.4,相对 20,520 的订阅确认收入,是第二条小而独立的曲线;它不该被并进 MRR 的主口径。
迟到与重复的量级都在万条以上。 10,368 与 11,664 不是小数,必须靠机制挡,不能靠人工抽查;抽查只能发现问题,不能阻止问题。
0.78 美元的偏差是最危险的一类。 金额小到没人投诉,但它说明两侧计数口径不同;口径不同早晚会放大,尤其在客户换客户端版本时。
8,640 个桶是解释力的来源。 客户问某一天为什么高,你能定位到 288 个桶里的某几个;只有月合计,就只能回答「系统算的」。
按量收入也要按服务期确认。 本期用量对应的收入仍要落在本期,预收的下期用量是负债;不要因为按月出账就当成收付实现。
150 条/桶说明桶没有过细。 如果平均每桶只有 1 条,桶数会接近事件数,存储与查询成本都会翻倍,聚合就失去了意义。
12 个客户是当前规模,不是上限。 桶数随客户线性增长;到 100 个客户时先算存储与查询,再谈降价,否则增量收入会被基础设施吃掉。
账单争议的处理成本要计入。 一次解释 11.60 花 20 分钟,按 40 美元/小时是 13 美元,比这笔收入还高;可解释性不是体验,是成本项。
五、行动清单与复算验收
- 把事件表设计成只追加,字段至少含 customer_id、event_id、usage_date。
- 为上报接口建幂等键,重复请求返回同一条结果。
- 定义 5 分钟桶与 24 小时结算窗口,写清窗口关闭后不可改。
- 实现三步聚合,并让每一层都能反查到上一层。
- 把阶梯价写成区间表,用 108,000 条跑一条端到端用例。
- 统计迟到率与重复率,超过 1% 就查客户端重试逻辑。
- 设置 2 倍平均的用量告警,只通知不自动断服务。
- 每月抽 1 个客户,从账单行反查到原始事件,确认三层合计一致。
验收标准:任取一个客户,你能从账单行的 11.60 反查到 58 个千、再反查到 108,000 条原始事件,且三层合计误差为 0。
使用边界与实操验收
边界一:按量计费适合价值随用量线性增长的场景。 存储、请求数、消息条数可以;协作人数这类不随用量增长的不适合。
边界二:阶梯价的具体数字是教学假设。 免费额度、档位与单价都要按自己的成本结构定,不能照抄这里的 50,000 与 0.20。
边界三:迟到事件的处理口径要写进合同。 24 小时窗口是产品选择,不是行业标准;写清楚才能避免月底反复改账。
边界四:去重键一旦上线就不能改。 改键等于让历史数据失去可比性;要改就新开一列,双写一段时间再切。
边界五:按量收入不等于确认收入。 本期用量对应的收入仍要按服务期确认;不要把 139.2 直接当利润或当月现金。
关键参考与核验范围
- Stripe 按量计费 :核对 meter、用量记录与账单行的基本模型。
- Stripe 订阅计费概览 :核对订阅周期与用量计费的组合方式。
- PostgreSQL 事务隔离 :核对并发写入用量表时的隔离级别选择。
- Stripe 幂等请求 :核对上报重试时幂等键的用法。
以上链接于 2026-10-26 实测可公开访问;未核验版本、价格与配额。核验日期:2026-10-26。
系列导航
- 回到本组目录:一个人的计费系统:订阅状态机、按比例计费与失败重试
- 上一篇:升降级与按比例计费:九笔变更、十七天与 4.53 美元
- 下一篇:失败扣款与催收:五次尝试、五十五个百分点与七天宽限
- 相关:一个人的产品度量
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。