· 阅读约 3 分钟
在广告仍在投放时拦截无效点击
ClickFortify 如何将广告点击流转化为 Google Ads 和 Meta 中的排除规则,以及为什么要基于 NATS 构建事件驱动服务。
机器翻译。 阅读原文

广告主为每一次点击付费。其中一部分点击来自机器人、VPN、代理和数据中心,而几周后才送达的报告只能记录下已经发生的损失。ClickFortify 是我独立设计并运维的项目,它的任务是在广告仍在消耗预算时发现无效流量并将其排除。
问题的形态
几乎所有设计决策都由三个特性驱动:
- 它从不停歇。点击持续不断地涌入,全天候、跨时区。
- 每个广告主都是一个租户。每个广告主都有自己的账户、规则和数据,彼此之间绝不能泄露。
- 动作发生在别处。检测到无效点击只完成了一半工作。排除规则必须通过 API 写回广告主的 Google Ads 或 Meta 广告账户,而这些 API 有配额限制,偶尔还会失败。
刻意选择事件驱动
采集点击、为点击评分和同步排除规则,三者的负载特征截然不同。采集流量突发性强,必须保持快速;评分是 CPU 密集型;同步则受制于别人的 API。把它们放在同一个进程里,会让最慢的环节决定所有环节的节奏。
因此,每个关注点都是独立的服务,通过 NATS 上的事件相互连接。点击被采集后发布出去;评分服务订阅、做出判定并发布判定结果;同步服务接收需要排除的判定。每个服务都能独立扩展,缓慢的广告平台 API 也永远不会拖慢采集。
为点击评分
没有任何单一信号能证明一次点击无效,因此评分会综合多种信号:行为信号、设备指纹,以及对 VPN、代理和数据中心流量的过滤。将评分与采集分离,意味着规则可以随时调整,而不必触碰那个必须始终在线的部分。
写回时不冲击 API
写回是最容易以各种“有趣”方式出错的环节。排除规则通过 Redis Bull 任务队列下发,这样每次同步都带有退避重试,一阵突发的判定也会变成平稳的 API 调用流,而不是一个尖峰。
// Illustrative: one queued exclusion per tenant, platform and IP
await exclusions.add(
'exclude-ip',
{tenantId, platform, ip},
{
jobId: `${tenantId}:${platform}:${ip}`, // idempotent: the same IP is never queued twice
attempts: 5,
backoff: {type: 'exponential', delay: 30_000},
removeOnComplete: true
}
);稳定的任务 ID 让这个操作具备幂等性:即使同一个 IP 在一分钟内被标记十次,也只会同步一次。
多租户与一次在线迁移
由于每个广告主都有独立的隔离租户数据库,大规模变更可以逐个租户地规划,而不是一次性进行高风险的切换。正是这样,我在近乎零停机的情况下完成了 500+ 个租户数据库的线上生产数据库迁移。
给构建同类系统的人的建议
- 把写回广告平台视为真正的难点,而不是检测本身。
- 按负载特征拆分服务,而不是按名词拆分。
- 在需要重试之前,先让每一个外部副作用都具备幂等性。