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%以下。

注意:客户端埋点有个天然缺陷——用户可能禁用JavaScript,或者使用广告屏蔽插件。所以,客户端埋点只能作为辅助,不能完全依赖。

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>
小技巧:服务端日志一定要加「traceId」。每个请求生成一个唯一ID,贯穿整个调用链。这样排查问题时,你才能把客户端、服务端、Nginx的日志串起来。我习惯用UUID,简单粗暴。

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日志,这个问题根本无从下手。

核心要点:Nginx日志一定要开启「实时写入」,不要缓冲。否则服务器突然宕机,最后几秒的日志就丢了。我一般设置 buffer=0,虽然性能会差一点,但数据完整性更重要。

4.4 Kafka接入层:日志的「高速公路」

日志采集上来之后,不能直接写到数据库。为什么?因为广告系统的日志量太大了——每天几十亿甚至上百亿条。直接写数据库,数据库会直接崩溃。

这时候就需要Kafka。Kafka就像一个「消息高速公路」,所有日志先发到Kafka,再由下游的消费者(比如实时计算、离线数仓)去消费。

Kafka接入层的设计,我总结三个要点:

  1. Topic划分:按日志类型分Topic。比如 ad_impressionad_clickad_conversion。不要把所有日志塞到一个Topic里,否则下游消费会互相影响。
  2. 分区策略:按广告ID或用户ID哈希分区。这样同一个广告或同一个用户的所有日志,都会落到同一个分区,方便下游做聚合计算。
  3. 数据压缩:日志量大的时候,带宽是瓶颈。我建议开启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代理层。

嗯,到这里,日志采集架构的核心内容就讲完了。说白了,就是四个字:可靠、实时。可靠保证数据不丢,实时保证数据能用。两者缺一不可。

最后提醒一句:日志采集架构搭建好之后,一定要做「压测」。模拟线上流量,看看Kafka能不能扛住,看看Agent会不会OOM。我见过太多系统,上线第一天日志就崩了——就是因为没做压测。别踩这个坑。