第二章:广告请求链路——从用户点击到广告展示的完整旅程
大家好,我是你们的老朋友。今天我们来聊聊广告系统里最核心、也最刺激的部分——广告请求链路。
说白了,就是当你在某个App里刷到一条广告时,背后到底发生了什么?
我刚开始带团队时,有个新人问我:“不就是发个请求、返回个广告吗?能有多复杂?”
嗯,我当时笑了笑,带他走了一遍完整的链路。他看完后沉默了半小时。
今天,我就把这条链路掰开揉碎讲给你听。
2.1 完整请求链路:从用户到广告的七步曲
一条广告请求,从用户点击媒体开始,到广告展示结束,大致经历七个关键节点。我习惯把它画成一条“请求流水线”:
- 用户访问媒体 —— 用户打开App或网页
- 媒体SDK发起请求 —— 媒体客户端向广告SDK请求广告
- 广告SDK封装请求 —— SDK收集设备信息、上下文信息,打包成标准请求
- ADX/SSP接收请求 —— 广告交易平台或供应方平台收到请求
- DSP竞价 —— 需求方平台参与实时竞价
- 广告返回与渲染 —— 胜出的广告素材返回给媒体
- 广告展示与监测 —— 广告展示,同时触发曝光监测
你看,从用户手指点到广告出现在屏幕上,中间要经过这么多环节。每个环节都可能出问题。
核心要点: 整个链路必须在100-200毫秒内完成。超过这个时间,用户可能已经划走了。
2.2 各模块交互时序:谁先谁后,谁等谁
我习惯用“时序图”来理解模块间的交互。这里我简化一下,给你看关键步骤:
用户 → 媒体App → 广告SDK → ADX → DSP1, DSP2, DSP3...
↓ ↓ ↓ ↓
展示广告 ← 返回素材 ← 竞价结果 ← 出价
具体来说:
- 媒体App 先向 广告SDK 发起请求,携带用户信息
- 广告SDK 把请求转发给 ADX,同时开始计时
- ADX 把请求广播给多个 DSP,等待出价
- DSP 在限定时间内(通常是50-80ms)返回出价和广告素材
- ADX 选出最高出价者,返回给SDK
- SDK 渲染广告,展示给用户
这里有个细节:DSP的返回时间必须严格控制在ADX设定的超时阈值内。我记得有一次,某个DSP的响应时间平均在120ms,而我们的ADX超时设置是100ms。结果呢?那个DSP几乎从未胜出过。
个人经验: 我建议把DSP的超时阈值设置为“硬超时”和“软超时”两层。软超时给一个警告日志,硬超时直接丢弃响应。这样既能监控异常,又不会拖慢整体链路。
2.3 超时与降级策略:别让一个环节拖垮整个系统
说到超时,这是广告系统里最头疼的问题之一。你想想看,如果某个DSP响应慢了,整个广告请求都得等它。用户可没耐心等。
我经历过一次线上事故:某个DSP因为内部故障,响应时间从50ms飙升到500ms。结果整个广告系统的平均响应时间从120ms涨到了350ms。用户反馈“广告加载太慢”,媒体方直接投诉。
从那以后,我设计了一套三级降级策略:
| 降级级别 | 触发条件 | 处理方式 |
|---|---|---|
| L1 轻度降级 | 单个DSP超时率 > 5% | 将该DSP的请求并发数减半,增加超时容忍度 |
| L2 中度降级 | 多个DSP超时率 > 10% | 启用本地缓存广告,减少实时竞价请求 |
| L3 重度降级 | 整体超时率 > 20% | 直接返回兜底广告,跳过竞价环节 |
这套策略的核心思想是:宁可展示一条低价值的广告,也不要让用户等太久。
避坑指南: 我曾经犯过一个错误——把降级阈值设得太低。结果某个DSP稍微波动一下,系统就频繁降级,导致广告收入大幅下降。后来我把阈值调高到10%才稳定下来。记住:降级是保底手段,不是常规操作。
2.4 超时处理的代码示例
给你看一段我常用的超时处理逻辑,用Go语言写的:
func fetchAdFromDSP(ctx context.Context, dsp *DSPClient) (*AdResponse, error) {
// 设置超时上下文,硬超时100ms
timeoutCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
// 创建一个带缓冲的channel,防止goroutine泄漏
resultCh := make(chan *AdResponse, 1)
errCh := make(chan error, 1)
go func() {
resp, err := dsp.RequestAd(timeoutCtx)
if err != nil {
errCh <- err
return
}
resultCh <- resp
}()
select {
case resp := <-resultCh:
return resp, nil
case err := <-errCh:
return nil, err
case <-timeoutCtx.Done():
// 超时了,记录日志并返回降级广告
log.Warnf("DSP %s timeout", dsp.Name)
return getFallbackAd(), nil
}
}
这段代码的核心是context.WithTimeout和select多路复用。说白了,就是给每个DSP请求一个“死线”,到了时间还没返回,就直接走降级逻辑。
关键点: 注意defer cancel()。如果不调用cancel,goroutine可能会一直阻塞,造成内存泄漏。我见过不少团队踩过这个坑。
2.5 链路监控:没有监控,一切都是盲人摸象
最后,我想聊聊监控。没有监控,你根本不知道链路里哪个环节出了问题。
我习惯在每个模块的入口和出口都埋点,记录:
- 请求量 —— 每个模块接收了多少请求
- 响应时间 —— P50、P95、P99三个分位值
- 错误率 —— 超时、失败、降级的比例
- 吞吐量 —— 每秒处理的请求数
举个例子,如果发现ADX到DSP的P99响应时间突然从80ms涨到200ms,那就要立刻排查是哪个DSP出了问题。
我的习惯: 我会在监控面板上设置“红黄绿”三色预警。绿色表示正常,黄色表示需要关注,红色表示立即处理。这样值班的同学一眼就能看出问题。
好了,这一章的内容就到这里。广告请求链路看似简单,但每个细节都藏着坑。下一章我会讲广告检索与定向,到时候我们聊聊怎么从海量广告里快速找到最合适的那一条。
记住一句话:广告系统的核心,就是在有限的时间内,做出最优的决策。