2. 用量事件建模
用量计量的第一步不是写聚合 SQL,也不是配置配额阈值,而是把业务世界里发生的“动作”翻译成一条条可被系统稳定处理的用量事件。用量事件是计量系统的原子事实:它描述“谁、在什么时间、对什么资源、做了多少次/多少量、属于哪个维度”。如果事件模型设计得含糊,后续的聚合、计费、配额扣减都会在边界情况上反复出错;如果设计得清晰,聚合逻辑往往只是简单的分组求和。
本节围绕六个关键决策展开:可计量事件的定义、事件属性、维度、粒度、幂等键与时间语义,最后给出事件契约与版本演进策略。
一、什么算“可计量事件”
可计量事件必须满足三个条件:可观测(系统能捕获到它发生)、可归属(能确定它属于哪个租户/账户/资源)、可量化(能给出一个数值与单位)。例如“用户点击按钮”通常不可计量,因为它没有稳定的归属和量化语义;而“API 网关完成一次请求”是可计量的,因为它有租户标识、时间戳、请求路径和响应状态。
常见的可计量事件包括:API 调用、消息投递、存储写入字节、计算任务执行秒数、导出文件生成次数、活跃用户心跳等。每类事件都应有一个稳定的 event_type,例如 api.request.completed、storage.bytes.written、compute.seconds.consumed。命名建议采用“资源.动作.状态”的三段式,避免使用模糊的 usage 或 activity。
二、事件属性:最小充分集
一条用量事件不需要携带全部业务字段,但必须包含计量与追溯所需的最小充分集。典型属性如下:
event_id:全局唯一标识,用于去重与追踪。event_type:事件类型,决定后续聚合规则。tenant_id/account_id:归属主体,计费与配额的核心维度。resource_id:被消耗的资源实例,如 API 路由、存储桶、计算集群。quantity与unit:数值与单位,如1次、2048字节、3.5秒。occurred_at:事件在业务上发生的时间。recorded_at:事件被计量系统接收的时间。idempotency_key:幂等键,用于防止重复计量。dimensions:附加维度,如region、plan、api_version。
属性设计要避免两个极端:字段太少导致无法按客户、按区域出账;字段太多导致事件体积膨胀、隐私风险上升。一个实用的判断标准是:任何未来可能用于分组、过滤或对账的字段,都应在事件契约中显式声明,而不是塞进自由文本。
三、维度与粒度:决定聚合的灵活度
维度是聚合时的分组键,粒度是事件的时间精度与数量精度。维度设计直接决定“能不能按客户+区域+API 版本出账”。常见维度包括租户、账户、资源、区域、套餐、环境(prod/staging)、API 版本。粒度则包括:
- 时间粒度:秒级、分钟级、小时级。计费通常按小时或天聚合,但事件本身建议保留秒级时间戳。
- 数量粒度:每次调用记 1 次,还是按请求体大小记字节;是记原始值还是预聚合值。
- 资源粒度:按单个实例记,还是按资源组记。
粒度越细,聚合越灵活,但存储与处理成本越高。工程上常见的折中是:事件层保留细粒度原始事实,聚合层按小时或天生成汇总表。这样既支持实时配额扣减,也支持离线对账。
四、幂等键与时间语义
分布式系统中,事件重复投递几乎不可避免。幂等键的作用是让同一条业务事实即使被多次写入,也只计量一次。幂等键的构造应满足:同一业务事实的多次重试得到相同键,不同事实得到不同键。例如:
idempotency_key = sha256(
tenant_id + "|" +
event_type + "|" +
resource_id + "|" +
business_trace_id
)
其中 business_trace_id 是业务侧生成的唯一请求标识。若业务侧无法提供,可退化为 tenant_id + event_type + resource_id + occurred_at 截断到秒,但要接受同一秒内同资源多次调用可能被误合并的风险。
时间语义需要区分三个时间:业务发生时间(occurred_at)、系统记录时间(recorded_at)、聚合归属时间(billing_period)。计费通常按 occurred_at 归属账期,但若事件延迟到达,可能需要按 recorded_at 做补录。建议在事件契约中同时保留两者,并在聚合规则中明确以哪个为准。
五、事件契约与版本演进
事件契约是事件源与计量系统之间的接口约定。它应明确字段名、类型、是否必填、取值范围和示例。契约一旦发布,就不能随意改字段名或删字段,否则下游聚合会静默出错。版本演进建议遵循以下原则:
- 只增不改:新增字段是兼容的,修改字段语义或类型是不兼容的。
- 双写过渡:从 v1 到 v2 时,事件源同时发送两个版本,计量系统按版本路由,确认 v2 稳定后再下线 v1。
- 版本字段显式化:在事件中携带
schema_version,聚合层根据版本选择解析规则。 - 弃用有窗口:旧版本至少保留一个完整账期,避免跨月对账时数据缺失。
六、一个可参考的事件契约示例
下面给出一个 API 调用事件的契约片段,字段均为示例,实际项目可按需裁剪:
{
"event_id": "evt_9f3a2c1b",
"event_type": "api.request.completed",
"schema_version": "2.1",
"tenant_id": "t_10086",
"account_id": "acc_5521",
"resource_id": "route:/v1/orders",
"quantity": 1,
"unit": "request",
"occurred_at": "2025-03-12T08:15:30Z",
"recorded_at": "2025-03-12T08:15:31Z",
"idempotency_key": "sha256:t_10086|api.request.completed|route:/v1/orders|trace_abc123",
"dimensions": {
"region": "cn-east-1",
"plan": "pro",
"api_version": "v1",
"status_code": 200
}
}
聚合时,按 tenant_id + billing_period + dimensions.region 分组,对 quantity 求和,即可得到“某租户某账期某区域的 API 调用次数”。若需要按状态码区分成功与失败,可在 dimensions 中保留 status_code,聚合时过滤 status_code < 400 只计成功调用。
七、常见建模陷阱
- 把事件当日志:日志可以随意增删字段,事件契约不能。事件是计费依据,必须稳定。
- 幂等键用随机 UUID:随机 UUID 无法识别重试,等于没有幂等。幂等键必须由业务事实决定。
- 时间语义混用:用
recorded_at归属账期,会导致延迟事件跨月错账。建议默认用occurred_at,补录场景显式标记。 - 维度爆炸:把用户 IP、请求 ID 等高基数字段放进
dimensions,会让聚合表膨胀。高基数信息应放在事件明细中,不参与分组。
用量事件建模的最终检验标准是:给定任意一条事件,你能回答“它属于谁、发生在何时、消耗了多少、按什么维度聚合、重复投递会不会多算”。如果这五个问题都有明确答案,事件模型就具备了支撑计量、配额与计费的基础能力。