广告技术系列(六):计费与预算控制

目录

运动鞋品牌给新品活动设置了一万元预算。多个竞价节点同时看到“余额充足”,各自继续投放;有些点击稍后才到达,有些转化第二天才确认。只靠每次扣费时执行一次原子减法,无法保证这些尚未到达的费用都在预算之内。

前五篇把广告请求连接到了交付和效果,本篇讨论钱如何进入这条链路。计费系统要同时维护事实和约束:真实发生的费用必须可追溯,尚未结算的承诺不能被其他请求重复使用。 出价、预算控速、账务和结算各有职责,不能用一个余额变量包办。

计费事件与优化目标是两个维度

按时间、展示、点击和转化付费,是不同的合同约定,并非后一种必然替代前一种的年代序列。

模型 合同约定的计费依据 计算时需要确定
CPT / CPD 时段或天数 资源范围、交付要求、补偿条款
CPM 合格展示次数 展示定义、成交千次价和无效事件处理
CPC 合格点击次数 点击判定、去重规则与实际成交点击价
CPA / CPL 合格转化或线索 转化定义、归因、验证与撤销条件
CPS 销售相关结果 按单或按比例、支付/退款口径、佣金规则

固定时段也可以附带交付保障;按转化付费也存在数据质量、归因和商品经营风险。媒体对落地页控制较少,不代表对最终转化完全没有影响。

费用按合同确定的成交价计算,不能一律用报价。固定 CPM 成交价为 20 元、合格展示为 1500 次时,费用为 30 元;若每次成交价不同,应按记录逐项累计。报价是交易输入,实际扣费规则可能与其不同。

oCPM、oCPC 等通常同时涉及优化目标和原有计费事件:以转化为目标优化,并不意味着都在展示时扣费,也不意味着单次 CPA 得到保证。名称需结合具体产品定义。Google 对目标 CPA 的说明也区分了目标平均成本与单次转化成本。Google 出价说明

价值换算必须对应所购买的机会

若目标转化成本为 t,模型预测点击后的转化概率 pCVR,则理想化的点击价值为 t × pCVR。若参与展示机会竞价,还需要点击概率:

预测千次展示价值 = 1000 × t × pCTR × pCVR

例如 t 为 100 元,pCTR 为 2%,点击后 pCVR 为 3%,则点击价值为 3 元、千次展示价值为 60 元。若模型直接输出展示后转化概率,就不能重复乘 pCTR。实际策略还会考虑预算、竞争、校准和目标,不应将这条说明性换算写成某平台完整算法。

拍卖规则与计费事件也应分开。GSP 多广告位机制不普遍保证真实出价;按质量加权排序时,支付阈值不能简单写成“第二名报价加一分钱”。第二篇已说明不同机制的前提,本篇只把交易确认的价格与版本带入账务。

胜出、展示与计费分别记录

竞价胜出通知证明的是交易结果,不自动证明素材已经展示。OpenRTB 的胜出通知 nurl 与可计费通知 burl 有不同语义,具体计费仍依据双方合同及实际事件。OpenRTB 2.6

CPM、CPC、CPA 的事件可能乱序、重复或迟到。每条记录应带来源、事件类型、业务标识、发生时间、接收时间、币种、价格及规则版本。CPC 不能默认只按 impression ID 去重:同一次展示下哪些点击算重复,需要明确规则;转化和退款也应具有各自的业务事件标识。

分布式时钟有偏差,需要规定账期与时间来源,但不必给整个广告系统建立一个全局计费顺序。通常更重要的是同一账户和同一业务操作的版本、一致提交与因果关联。

预算、余额和节奏各自约束什么

资金余额或授信限制支付能力,活动总预算限制投放规模,日限额限制指定账期内的承担,pacing 决定在时间上如何使用额度。它们不能互相替代。

首先要确认产品中的“日预算”含义。有的系统提供硬上限,有的提供平均日预算与另行规定的每日、月度收费上限;例如 Google Ads 的公开说明就将这些概念分开。将平均目标当成不可超过的日限额,会误读正常投放结果。Google 预算说明

在本文设计中,硬预算使用以下约束:

已确认消耗 + 未结算预留 + 新增预留 ≤ 预算上限

所有值属于同一币种、账户和预算周期。活动、日限额及资金账户共同限制投放时,必须在一致的授权边界内检查,不能分别读到通过结果后再逐个扣减。

预算先预留,计费事件经幂等提交进入账本,未知结果保留待核对状态

预留需要覆盖可能发生的责任

如果一项机会最多可能产生 5 元费用,按 0.1 元期望成本预留只能控制统计风险,不能证明不超预算。严格限制需要可信的费用上界、限制未决数量或其他能够约束最大责任的机制。

对于可能发生多次计费动作、较长时间后转化的机会,需要定义责任截止和最大数量。若合同无法给出可执行的上界,就应明确采用估算、风险缓冲与平台承担规则,而非承诺数学意义上的零超投。

预留也不能只因本地请求超时就立即释放。远端可能已经交付,回调还在路上;只有确认未产生责任,或达到合同允许释放的条件,才能恢复可用额度。超过时间窗口的回调如何核对,应提前约定。

幂等和扣费必须共同提交

“先写去重标记,再扣余额”存在崩溃窗口:标记成功、扣费失败,重试便可能永久跳过费用。反过来先扣费再写标记,也可能重复扣费。

下面是事务语义的伪代码,不是可直接复制运行的数据库程序:

BEGIN
  按账户与操作范围锁定预留记录
  按固定顺序锁定涉及的资金账户与预算周期
  查找来源 + 事件类型 + 业务事件 ID
  若已处理:
    核对载荷指纹,相同则返回已存结果,不同则报冲突
  校验事件关联、价格、币种、规则版本与可计费资格
  写入唯一事件记录和不可变计费流水
  更新预留状态、已用额度及相关账务余额
  写入后续通知的 outbox 记录
COMMIT

锁顺序和唯一约束用于处理竞争,实际隔离级别、死锁重试与存储分片需要结合实现验证。原子提交成功但响应丢失时,调用者使用原事件 ID 重试并得到原结果;不能生成新 ID 把同一事件变成第二笔费用。

实际费用大于预留时,仍要保存真实费用和差异,并按合同记录平台承担、追加责任或争议处理。不能把真实金额截断到预留值来伪造账平,也不能未经约定直接向广告主多收。

调整和退款通过关联原流水的冲正或调整记录表达,保留原因、版本与审批证据。修改原记录的金额会破坏后续审计和对账。

Redis 原子性不等于账务一致性

DECR 本身原子,但“检查余额是否足够,然后再 DECR”并非一个原子条件扣减;它也不会替其他预算、业务流水和数据库写入提供事务。

即便把条件判断写进 Lua,Redis 与数据库之间仍存在双写边界。故障切换还涉及持久化与复制保证,不能因为使用 WAIT 就声称系统成为强一致存储。Redis WAIT 文档

一种可解释的职责划分是:持久账本和额度授权作为权威,缓存支持查询或受约束的局部决策。若 Redis 参与授权关键路径,应明确故障时如何冻结、恢复和防止额度重复使用,而不是依赖事后周期同步自动补齐所有差异。

分配互不重叠的额度,减少中心通信

分布式决策不必让每台机器缓存同一份全局可用余额。中心可以为节点分配互不重叠的额度块,节点只在自己的块内预留和计费。

例如总额 100 元,节点 A 获得 30 元、B 获得 20 元,中心保留 50 元。节点之间不能直接花对方的额度;中心已分配未归还的部分不能再次发放。只要责任和转移有一致记录,可以减少通信,同时维持预算约束。

难点在回收与失联恢复:A 失联时不能立刻把其剩余额度发给 C,因为 A 可能仍在旧网络分区继续使用。需要让旧授权失效,并由实际接收支出的一方检查新旧代次;仅在中心提高 fencing token、下游却不验证,仍不能隔离旧持有者。

额度块过大会降低资金利用率,过小会增加中心通信。这是可测量的工程取舍,不是“分布式必然允许 2% 超投”的理由。若业务接受超投,金额、责任和补偿应来自明确合同与策略。

控速限制何时花,不创造新预算

令牌桶可以控制突发,但如果初始桶中装入全天预算,又按全天预算持续补充,最多可释放的量会超过一天的预算。时间单位混用还会让补充速率进一步失真。pacing 必须同时受硬预算约束。

下面给出一个线性累计放量的纯函数示例。金额为同一种最小记账单位的整数;elapsed 是预算周期开始后的秒数,duration 是该周期的实际秒数,burst 是允许提前使用的额度。

def pacing_room(budget: int, elapsed: int, duration: int,
                spent: int, reserved: int, burst: int = 0) -> int:
    values = (budget, elapsed, duration, spent, reserved, burst)
    if any(type(value) is not int for value in values):
        raise TypeError("integer units and seconds required")
    if duration <= 0 or any(
        value < 0 for value in (budget, elapsed, spent, reserved, burst)
    ):
        raise ValueError("invalid budget or time")
    released = min(budget, burst + budget * min(elapsed, duration) // duration)
    return max(0, released - spent - reserved)

预算 10000 单位、周期 1000 秒、burst 为 0,在第 250 秒累计放量 2500;已经花费 1800、预留 400 时,本次节奏余量为 300。周期结束也最多释放 10000,未使用部分不会自动保证花完。

这个函数只计算快照下的节奏余量,不完成并发预留、数据库提交或跨日重置。多个请求同时得到 300 后,仍必须通过前述一致性边界竞争额度。生产策略也可按预计流量或转化价值使用非线性曲线,而非强制每分钟花同样多的钱。

预算周期需明确时区和实际起止时间;跨日时为新周期创建独立账,不删除旧周期的未决责任。迟到事件依据约定的归属规则进入原周期或调整账,不能只看处理当天。

从计费流水到结算账单

计费确认业务责任,结算依据合同汇总应收应付,再连接实际资金支付。预付减少部分信用风险,但仍可能有退款、拒付和未决费用;后付需要授信与收款管理。不能把某种支付方式描述为零风险。

金额内部使用足够细的整数或定点精度,保留币种和舍入规则。每次 CPM 展示金额可能不足一分,若逐次舍入为零再求和就会丢失收入;应按合同规定的粒度累计或分配舍入差额。

对账至少区分事件数量、计价、无效流量调整、分成、税费、汇率和已付款项。差异先追到具体记录及版本,再按合同处理;不能预设任何平台账单永远正确,也不能以一个通用百分比决定可否申诉。

无效流量不等于所有非人类流量,异常 IP 或点击间隔也不能直接证明欺诈。可见性、归因和计费资格各有定义:不是所有展示合同都按可见曝光收费,转化归因窗口改变也不必然改变 CPM 或 CPC 账单。

归因模型说明哪些转化记到哪次广告上,合同说明其中哪些转化可收费。扩大窗口可能改变候选和分配,但不自动证明产生了更多增量业务。回调撤销、退款和无效流量复核应有可重放记录,而非在报表里无痕覆盖。

广告计费的可靠性来自清楚的责任边界:出价决定愿意支付多少,预算授权限制尚可承担多少,事件确认实际发生什么,账本保存事实,结算完成合同约定的支付。把这些状态连接起来,才能在延迟、重试和并发之中解释每一笔钱的去向。

评论