第3章:日志传输与缓冲:Kafka在日志链路中的角色

聊到日志系统,很多人第一反应就是ELK。但说实话,如果没有Kafka在中间撑着,你那套ELK大概率撑不过三个月。我见过太多团队,日志一多直接把Elasticsearch写挂了,然后整个集群雪崩。

Kafka在日志链路里扮演什么角色?说白了就三个字:缓冲层。它像一个大水池,上游的日志生产者只管往里倒水,下游的消费者按自己的节奏慢慢喝。这样谁都不会被冲垮。

3.1 Kafka在日志链路中的核心定位

先看一个典型的日志链路:

应用服务 → Filebeat/Logstash → Kafka → Logstash → Elasticsearch → Kibana

Kafka夹在中间,干了几件大事:

  • 削峰填谷:业务高峰期日志量暴增,Kafka帮你扛住,下游慢慢消费
  • 解耦:日志生产和消费不再强依赖,生产者挂了不影响消费者
  • 多路复用:一份日志可以同时被多个消费者消费,比如实时告警和离线分析
  • 持久化:日志不会丢,哪怕下游全挂了,Kafka里还存着

核心观点:没有Kafka的日志系统,就像没有缓冲区的管道,压力一大就崩。

我记得有一次帮客户排查问题,他们的日志系统一到双十一就挂。我一看架构,Filebeat直连Elasticsearch。嗯,这不挂才怪。后来加了Kafka,稳如老狗。

3.2 Topic设计原则

Topic是Kafka里最基础的概念。设计得好不好,直接影响整个系统的性能和可维护性。

3.2.1 按业务域划分

我个人习惯,Topic的命名规则是:{业务线}.{服务名}.{日志类型}。比如:

order.payment.access
order.payment.error
user.login.behavior
user.register.metric

为什么要这么分?你想想看,如果所有日志都塞到一个Topic里,下游消费的时候还得做过滤,性能差不说,维护起来也头疼。

3.2.2 控制Topic数量

Topic不是越多越好。我见过有人一个服务建了50个Topic,结果运维起来想死。一般来说:

  • 一个服务3-5个Topic就够了
  • 按日志级别分:info、warn、error
  • 按业务类型分:access、behavior、metric

小技巧:如果日志量特别大,可以按时间维度分Topic,比如每天一个。但别分太细,否则消费者管理起来很麻烦。

3.2.3 合理设置分区数

分区数决定了Kafka的并行处理能力。但也不是越大越好。

分区数 优点 缺点
太少(1-3) 管理简单 吞吐量低,消费者并行度不够
适中(6-12) 吞吐量和性能平衡 需要合理规划消费者数量
太多(>50) 理论上吞吐量高 文件句柄多,Leader选举慢,运维复杂

我建议:分区数 = 消费者线程数 × 2。这样既能充分利用消费者,又留有余量。

3.3 分区与副本策略

分区和副本,是Kafka高可用和高性能的基石。这块坑不少,我踩过。

3.3.1 分区策略

消息怎么分配到分区?默认是轮询。但日志场景下,我建议用key-based策略。

// 按服务实例ID分区,保证同一实例的日志有序
producer.send(new ProducerRecord<>("order.log", instanceId, logMessage));

为什么要这样做?因为日志分析经常需要按时间线看一个实例的行为。如果分散到不同分区,排序就麻烦了。

3.3.2 副本策略

副本数一般设2或3。多了浪费存储,少了不安全。

  • 副本数=2:能容忍1个Broker挂掉
  • 副本数=3:能容忍2个Broker挂掉,但存储成本高50%

注意:副本数不能超过Broker数量。比如你有3台Broker,副本数最多设3。我曾经见过有人设了5个副本,结果只有3台机器,副本永远同步不完。

3.3.3 ISR机制

ISR(In-Sync Replicas)是Kafka保证数据一致性的关键。只有ISR里的副本才能参与Leader选举。

配置参数:

# 最小ISR数量,建议设为副本数-1
min.insync.replicas=2

# 生产者的ACK策略,建议设为all
acks=all

我曾经踩过一个坑:副本数设了3,但min.insync.replicas设了1。结果一个Broker挂了,数据写入成功,但实际只有1个副本有数据。后来另一个Broker也挂了,数据直接丢了。嗯,从那以后我再也不敢偷懒了。

3.4 数据可靠性保证

日志丢了,问题排查就无从谈起。所以数据可靠性是Kafka配置的重中之重。

3.4.1 生产者端

生产者配置直接影响数据是否丢失:

# 关键配置
acks=all                    # 等待所有副本确认
retries=3                   # 重试次数
max.in.flight.requests.per.connection=1  # 保证顺序
enable.idempotence=true     # 开启幂等性,防止重复

为什么acks要设all?因为默认的acks=1只保证Leader写成功,如果Leader挂了,数据就丢了。设成all,所有ISR副本都写成功才返回,虽然慢一点,但安全。

3.4.2 Broker端

Broker端的配置同样重要:

# 关键配置
default.replication.factor=3      # 默认副本数
min.insync.replicas=2             # 最小ISR数量
unclean.leader.election.enable=false  # 不允许非ISR副本成为Leader

重点unclean.leader.election.enable一定要设为false。否则当所有ISR副本都挂了,Kafka会选一个数据不全的副本当Leader,导致数据丢失。宁可不可用,也不能丢数据。

3.4.3 消费者端

消费者端也要注意:

# 关键配置
enable.auto.commit=false    # 手动提交偏移量
auto.offset.reset=earliest  # 从最早开始消费

我建议手动提交偏移量。自动提交虽然方便,但万一处理完还没提交就挂了,重启后会重复消费。手动提交可以精确控制:

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理日志
        processLog(record);
    }
    // 处理完再提交
    consumer.commitSync();
}

3.5 实战避坑指南

最后分享几个我踩过的坑:

  • 分区数不能减少:Kafka不支持减少分区数,设计时一定要留余量
  • 消息体不要太大:单条消息超过1MB会影响性能,日志一般控制在100KB以内
  • 监控磁盘使用率:Kafka日志默认保留7天,磁盘满了会直接挂掉
  • 合理设置保留策略:按时间和大小双重限制,比如保留7天或100GB

我的习惯:每个Topic都设置retention.ms=604800000(7天)和retention.bytes=107374182400(100GB),哪个先到就删。这样既保证有足够时间处理,又不会撑爆磁盘。

好了,Kafka在日志链路中的角色就聊到这。下一章我们讲日志采集层的实战,到时候会手把手教你配Filebeat和Logstash。