跳转至

经典案例:广告点击计数与流式反作弊系统 (Ad Click Aggregator & Anti-Fraud - 类似 Google Ads)

广告是几乎所有互联网巨头(Google、Meta、字节跳动、腾讯、阿里巴巴)最核心的盈利支柱。在按点击付费(CPC,Cost Per Click)模式下,用户的每一次有效点击都直接对应着真实的资金扣减。

初学者往往最容易低估这个系统的难度:统计点击次数不就是 count++ 吗? 现实场景极其残酷:如果系统因为宕机重试多统计了一次点击,就是向广告主重复收费(财务欺诈与法律风险);如果少统计了,平台就会白白损失真金白银。与此同时,竞争对手的水军、黑客的刷量脚本每秒都在发起数万次虚假点击,系统必须在毫秒级内识别并剔除作弊流量!


1. 初学者视角:普通计数器为什么无法用于广告计费?

graph TD
    subgraph "传统 Web 计数的致命问题"
    A[用户点击广告] --> B[应用服务器执行计数]
    B --> C[(Redis INCR / 数据库累加)]
    C --> D1["1. 丢数据风险: 服务器断电或崩溃, 内存数据丢失, 平台少赚钱!"]
    C --> D2["2. 重复计费风险: 网络抖动导致客户端重试, 同一次点击被统计两次, 广告主投诉!"]
    C --> D3["3. 恶意作弊羊毛党: 黑客用 Python 脚本 1 秒点击 1 万次, 瞬间把广告主的预算刷光!"]
    end

2. 需求分析与系统范围 (Requirements & Scope)

2.1 功能性需求

  1. 精准点击聚合 (Click Aggregation):按广告 ID(ad_id)以分钟为单位(如每 1 分钟)聚合有效点击次数;
  2. 实时流式反作弊 (Anti-Fraud):毫秒级过滤机器人、脚本刷量、恶意高频点击等虚假流量;
  3. 曝光与点击双流对齐 (Impression-Click Join):必须校验该点击之前是否发生过真实的广告曝光;
  4. 多维度报表查询:广告主能实时查看过去 24 小时各广告的点击量、消耗金额与转化率。

2.2 非功能性需求

  1. 金融级精准一次 (End-to-End Exactly-Once):数据不重不漏,绝不重复计费,绝不丢失数据;
  2. 极高写入吞吐:支撑每秒数十万次(\(100,000+\text{ QPS}\))的点击事件流;
  3. 低延迟 (Low Latency):点击发生后,广告主在 1 分钟内即可看到聚合后的计费数据。

3. 容量估算 (Back-of-the-Envelope Estimation)

  • 每日点击总量:设系统每天处理 10 亿次广告点击(约 \(10^9\) 次);
  • 平均每秒点击量 (QPS):
\[ \text{Avg QPS} = \frac{10^9}{86,400\text{ s}} \approx 11,500\text{ QPS} \]

大促高峰期(按 5 倍计算)可达 60,000 QPS。

  • 每日点击日志原始数据量:
  • 单条点击事件(click_id, ad_id, user_id, ip, timestamp 等)约 \(200\text{ Bytes}\);
  • 每日日志数据量:\(10^9 \times 200\text{ B} = 200\text{ GB}\)/天。

4. 核心流处理架构:端到端 Exactly-Once 保证

要保证计费数据的绝对精确,必须构建端到端精准一次(End-to-End Exactly-Once)的数据处理管道:

graph TD
    Client[用户前端点击] --> Gateway["API 网关 (生成唯一 click_id)"]
    Gateway --> Kafka[(消息队列 Kafka: 事务型 Producer)]

    subgraph "实时流计算引擎 (Apache Flink)"
    Kafka --> FlinkSource[Flink Source: 消费点击事件]
    FlinkSource --> AntiFraud["实时反作弊算子 (黑名单 + 规则过滤)"]
    AntiFraud --> WindowAgg["1 分钟翻滚窗口聚合 (Tumbling Window)"]
    end

    subgraph "存储与计费 (两阶段提交 2PC)"
    WindowAgg --> FlinkSink[Flink 2PC Sink: 两阶段提交]
    FlinkSink --> OLAP[(ClickHouse / MySQL: 广告分钟级计费报表)]
    end
  1. Source 端(Kafka 事务与 Offset):
  2. Flink 实时记录消费的 Kafka Offset;
  3. 内部计算状态(Chandy-Lamport Checkpoint 机制):
  4. Flink 定期向数据流中注入 Checkpoint Barrier(屏障);
  5. 当屏障流经各个算子时,算子将当前的聚合中间状态(如某广告当前已累加 52 次)异步快照存储到持久化存储(如 HDFS / S3);
  6. Sink 端(两阶段提交 2PC):
  7. 采用 XA 事务或两阶段提交协议 写入下游数据库;
  8. 只有当整个 Checkpoint 成功完成时,下游数据库的事务才真正执行 COMMIT;
  9. 即使 Flink 节点突然崩溃重启,系统也能从上一次成功的快照完全恢复,不多算一次,也不少算一次!

5. 核心反作弊引擎:规则过滤与双流对齐

5.1 实时反作弊常见特征与规则

  1. IP / 设备指纹频次限制:同一个 IP 或设备在 1 分钟内对同一广告点击超过 5 次,超出部分直接判定为恶意刷量,不予计费;
  2. CTR 异常突增检测:某个广告的历史点击率(CTR)通常为 2%,若突增至 60%,系统自动触发风控拦截并告警。

5.2 核心硬核防御:曝光与点击双流对齐 (Dual-Stream Join)

很多恶意脚本甚至根本不打开网页,直接通过 HTTP 接口伪造点击请求发给服务器。 工业界最坚固的防线是:曝光与点击双流对齐验证!

sequenceDiagram
    participant Web as 浏览器 / App
    participant Flink as Flink 实时双流 Join 引擎
    participant Valid as 有效计费池

    Note over Web: 1. 用户刷出网页, 广告展示在屏幕上
    Web->>Flink: 发送曝光事件 (imp_id = 9988, ad_101, 10:00:00)
    Note over Flink: 将曝光事件存入 Flink 状态缓存 (保留 15 分钟)

    Note over Web: 2. 用户在 3 秒后点击了该广告
    Web->>Flink: 发送点击事件 (imp_id = 9988, click_id = 1122, 10:00:03)

    Note over Flink: 3. 双流对齐核验: 在状态缓存中寻找 imp_id = 9988?
    alt 找到了曝光事件且时间间隔合理 (0.5秒 ~ 15分钟)
        Flink->>Valid: 核验通过! 计入正常扣费统计
    else 未找到曝光事件 (幽灵点击 / 脚本伪造)
        Note over Flink: 判定为恶意虚假点击, 直接丢弃并拉黑该 IP!
    end
  • 时间合理性校验:
  • 曝光到点击的时间间隔如果在 0.1 秒以内,人类生理反应不可能这么快,必定是自动化机器人脚本;
  • 若超过 15 分钟,说明会话早已过期,同样不予计费。

6. 面试深挖:迟到数据与乱序处理 (Watermark 水位线)

移动端用户在地铁进隧道时,可能点击了广告,但由于断网,网络请求延迟了 2 分钟才真正发送到服务器。 此时该分钟的统计窗口可能已经关闭并输出了,怎么办?

  • Flink Watermark(水位线机制):
  • 水位线代表“系统认为当前时间戳小于等于 \(T\) 的数据已经全部到达”;
  • 允许设置容忍延迟阈值(如允许迟到 30 秒):
\[ \text{Watermark} = \text{MaxEventTime} - 30\text{ s} \]
  • 严重迟到数据的最终兜底(Side Output 侧输出流):
  • 如果数据迟到了数十分钟(超出了水位线容忍范围),窗口已经彻底关闭;
  • Flink 将这些极端迟到数据输出到专门的侧输出流(Side Output);
  • 触发差额修正流程(Billing Correction Pipeline),在次日对账时以退款(Refund)或补扣的形式体现在最终财务账单中。