4、日志采集架构:客户端埋点、服务端日志、Nginx日志、Kafka接入层设计
日志采集,说白了就是广告系统的「眼睛」和「耳朵」。
没有日志,你根本不知道系统在干什么。用户点了没?广告展示了没?转化了没?全凭日志说话。我见过不少团队,上线第一天就跑得欢,结果第二天发现日志全丢了——那叫一个惨。
今天咱们就聊聊日志采集的四个关键环节:客户端埋点、服务端日志、Nginx日志,还有Kafka接入层。嗯,一个一个来。
4.1 客户端埋点:离用户最近的一环
客户端埋点,就是在前端页面或App里埋下「探针」。用户每次点击、曝光、滑动,都会触发一个事件上报。
我个人习惯把埋点分成两类:
- 曝光埋点:广告出现在用户视野中时触发。注意,不是页面加载就触发,得等元素真正可见。
- 点击埋点:用户点击广告时触发。这个相对简单,但要注意防重复点击。
举个例子,一个典型的曝光埋点代码长这样:
// 曝光埋点示例
function trackImpression(adId, slotId) {
const data = {
event: 'impression',
ad_id: adId,
slot_id: slotId,
timestamp: Date.now(),
user_id: getUserId(),
page_url: window.location.href
};
// 使用Beacon API发送,页面关闭也不丢
navigator.sendBeacon('/log/impression', JSON.stringify(data));
}
这里我用了 navigator.sendBeacon,而不是传统的 XMLHttpRequest。为什么?因为用户关闭页面时,XHR请求可能被中断,而Beacon能保证数据送达。我在项目中遇到过,用XHR埋点,结果用户快速关闭页面,曝光数据丢了30%以上。换成Beacon后,丢包率降到1%以下。
4.2 服务端日志:最可靠的日志来源
服务端日志,是广告系统的「定海神针」。
你想想看,客户端可能丢数据,网络可能出问题,但服务端日志是系统内部产生的,只要服务器没宕机,日志就跑不了。
服务端日志主要记录三类事件:
- 请求日志:每次广告请求的完整信息,包括用户ID、设备信息、请求参数、返回的广告列表等。
- 竞价日志:DSP(需求方平台)的竞价记录,包括出价、胜出/失败、价格等。
- 转化日志:用户完成转化(如注册、购买)的回调记录。
我建议服务端日志采用「异步写」的方式。什么意思?就是业务线程只管处理请求,日志交给后台线程去写。否则,写日志会拖慢接口响应时间。我曾经见过一个项目,因为同步写日志,接口延迟从10ms飙升到200ms——这谁受得了?
一个典型的服务端日志配置(以Java的Logback为例):
<!-- 异步Appender配置 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE" />
<queueSize>1024</queueSize> <!-- 队列大小 -->
<discardingThreshold>0</discardingThreshold> <!-- 不丢弃日志 -->
</appender>
4.3 Nginx日志:流量入口的「黑匣子」
Nginx日志,是广告系统的「黑匣子」。
所有外部请求,第一站就是Nginx。它记录了最原始的请求信息:来源IP、User-Agent、请求URL、响应状态码、响应时间等。
Nginx日志的格式可以自定义。我个人习惯把广告系统需要的字段都加进去:
log_format ad_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time '
'$http_x_forwarded_for';
这里有个坑:$request_time 和 $upstream_response_time 的区别。前者是Nginx处理请求的总时间,包括与上游服务器的通信时间;后者是上游服务器(比如广告引擎)的处理时间。如果两者差距很大,说明网络或Nginx本身成了瓶颈。
我曾经排查过一个线上问题:广告响应偶尔超时。查了Nginx日志,发现 $request_time 正常,但 $upstream_response_time 偶尔飙到5秒以上。最后定位到是广告引擎的某个线程池满了,请求在排队。嗯,没有Nginx日志,这个问题根本无从下手。
buffer=0,虽然性能会差一点,但数据完整性更重要。
4.4 Kafka接入层:日志的「高速公路」
日志采集上来之后,不能直接写到数据库。为什么?因为广告系统的日志量太大了——每天几十亿甚至上百亿条。直接写数据库,数据库会直接崩溃。
这时候就需要Kafka。Kafka就像一个「消息高速公路」,所有日志先发到Kafka,再由下游的消费者(比如实时计算、离线数仓)去消费。
Kafka接入层的设计,我总结三个要点:
- Topic划分:按日志类型分Topic。比如
ad_impression、ad_click、ad_conversion。不要把所有日志塞到一个Topic里,否则下游消费会互相影响。 - 分区策略:按广告ID或用户ID哈希分区。这样同一个广告或同一个用户的所有日志,都会落到同一个分区,方便下游做聚合计算。
- 数据压缩:日志量大的时候,带宽是瓶颈。我建议开启Kafka的压缩功能,比如
compression.type=snappy。压缩率能达到3:1到5:1,效果很明显。
一个典型的Kafka生产者配置(以Java为例):
Properties props = new Properties();
props.put("bootstrap.servers", "kafka1:9092,kafka2:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("acks", "all"); // 所有副本都确认,保证不丢数据
props.put("compression.type", "snappy");
props.put("batch.size", 16384); // 批量发送,提高吞吐量
props.put("linger.ms", 10); // 等待10ms,凑够一批再发
acks 设为 0,觉得性能高。结果某次Kafka集群抖动,日志丢了整整10分钟。从那以后,我再也不敢用 acks=0 了。广告系统的日志,一条都不能丢。
4.5 整体架构:从埋点到Kafka的完整链路
把上面四个环节串起来,完整的日志采集架构是这样的:
| 环节 | 数据源 | 传输方式 | 可靠性 | 延迟 |
|---|---|---|---|---|
| 客户端埋点 | 浏览器/App | HTTP/Beacon | 低(可能被屏蔽) | 秒级 |
| 服务端日志 | 广告引擎 | 异步写文件 | 高 | 毫秒级 |
| Nginx日志 | 反向代理 | 实时写文件 | 高 | 毫秒级 |
| Kafka接入 | 日志采集Agent | 批量发送 | 极高(可配置) | 秒级 |
实际部署时,我会在每台服务器上部署一个日志采集Agent(比如Filebeat或Flume),负责把服务端日志和Nginx日志实时发送到Kafka。客户端埋点则直接通过HTTP接口发送到Kafka的REST代理层。
嗯,到这里,日志采集架构的核心内容就讲完了。说白了,就是四个字:可靠、实时。可靠保证数据不丢,实时保证数据能用。两者缺一不可。