我在消峰的时候会用到 kafka,比如企微回调,量非常大不能同步处理完成,或是各种聊天监听、聊天消息转发。
今天记录下使用 kafka 的一些心得,和原理方面的解释。
使用起来非常简单,Maven 中引用如下代码。
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
配置文件添加如下代码
spring:
# Kafka 配置项,对应 KafkaProperties 配置类
kafka:
bootstrap-servers: localhost:9092,localhost:9093,localhost:9094 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
# Kafka Producer 配置项
producer:
acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
retries: 3 # 发送失败时,重试发送的次数
key-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息的 key 的序列化
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
# Kafka Consumer 配置项
consumer:
auto-offset-reset: latest # 在广播订阅下,一般情况下,无需消费历史的消息,而是从订阅的 Topic 的队列的尾部开始消费即可
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring:
json:
trusted:
packages: com.demo.*
# Kafka Consumer Listener 监听器配置
listener:
missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
logging:
level:
org:
springframework:
kafka: INFO # spring-kafka
apache:
kafka: INFO # kafka
新建发送者类
@Component
@Slf4j
public class CallKafkaProducer {
@Resource
private KafkaTemplate<Object, Object> kafkaTemplate;
@Autowired
private AsyncEventBus asyncEventBus;
@Async
public CompletableFuture<SendResult<Object, Object>> send(CallbackKafkaDto reqDto) {
if (ObjectUtil.isEmpty(reqDto) || StrUtil.isBlank(reqDto.getBody())) {
log.warn("CallKafkaProducer Kafka 数据为空");
return null;
}
String orderId = JSONUtil.parseObj(reqDto.getBody()).getStr("orderId");
CompletableFuture<SendResult<Object, Object>> future = kafkaTemplate.send(Constant.CALLBACK_TOPIC, orderId, JSON.toJSONString(reqDto));
log.info("CallKafkaProducer 发送到 Kafka,topic={},orderNo={}", Constant.CALLBACK_TOPIC, JSON.toJSONString(reqDto));
future.whenComplete(((result, e) -> {
if (ObjectUtil.isNotEmpty(result)) {
log.info("CallKafkaProducer 发送到 Kafka 成功,topic={},orderNo={},result:{}", Constant.CALLBACK_TOPIC, JSON.toJSONString(reqDto), result);
} else {
log.error("CallKafkaProducer 发送到 Kafka 失败,topic={},orderNo={}", Constant.CALLBACK_TOPIC, JSON.toJSONString(reqDto), e);
asyncEventBus.post(FlyEventListener.FlyEventDto.builder()
.url(Constant.ORDER_FLY_ALARM)
.message(StrUtil.format("CallKafkaProducer 发送到 Kafka 失败,topic={},orderNo={}", Constant.CALLBACK_TOPIC, JSON.toJSONString(reqDto), e))
.build());
}
}));
return future;
}
}
新建接受者类
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;
@Service
public class ManualCommitKafkaConsumer {
@KafkaListener(topics = "your-topic", groupId = "your-consumer-group")
public void listen(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
try {
// 处理消息
System.out.printf("Received message: key=%s, value=%s, partition=%d, offset=%d%n",
record.key(), record.value(), record.partition(), record.offset());
// 根据业务逻辑决定是否提交偏移量
boolean shouldCommit = processMessage(record);
if (shouldCommit) {
// 手动提交偏移量
acknowledgment.acknowledge();
System.out.println("Offset committed");
} else {
// 不提交偏移量,可以选择记录日志或采取其他措施
System.out.println("Offset not committed");
}
} catch (Exception e) {
// 异常处理,确保应用不会因为未处理的异常而中断
System.err.println("Error processing message: " + e.getMessage());
// 可以选择不提交偏移量,让消息在下次消费时重新处理
}
}
}
为了能看懂配置文件的含义,先介绍下 kafka 的专业名词。
kafka 的专业名词及解释
先画个图方便理解。
broker
把 kafka 安装在电脑上,电脑就是 kafka 的 broker,同样把 Kafka 安装到服务器,服务器就是 broker。
那你要是好奇我在电脑上装两个 Kafka 呢?那电脑上就有两个 broker。
就好比在手机上双开微信,手机上就有两个微信。但两个微信的安装位置不能一样,Kafka 的 broker 也是这个概念,在电脑上装两个 Kafka,安装的目录也得不一样。
Kafka 运行的系统 + 安装目录 = broker。
controller
一个集群有多个 broker,群龙无首可不行,得有个管事的,管事的那个叫 controller。
controller 是 ZooKeeper/KRaft 根据策略挑选的,他额外负责集群元数据管理,比如分区的 Leader 选举、副本分配等,他就是众 broker 的老大。
topic
把同一类型的消息分个类,这个类别就是主题。比如订单的消息放到订单主题,用户的消息放到用户主题。这是跟业务相关的概念,Kafka 对主题没有要求。
partition
分区,这是 topic 存消息的地方。比如发送者往订单 topic 发消息,消息存订单 topic 的 partition 里。
从磁盘上理解 partition 是 broker 安装目录下的一个文件夹。
一个 topic 有多个 partition,每个 partition 分散在不同的 broker 上,每个 broker 都有个安装目标 /kafka,order_topic 所在的目录就是 /kafka/order_topic。
每台机器上的 /kafka/order_topic 目录都是 partition,所有的 partition 合在一起,就是 topic 的内容。
注意每个 broker 上 /kafka/order_topic 文件夹下的内容是不一样的。
Kafka 为什么这样设计?
最开始 topic 里所有的消息都放到一个 broker 的 /kafka/order_topic。但这样如果数据量大了,IO 成了瓶颈。
所有 Kafka 把一个 topic 里的消息,均匀的分布在不同的 broker 的 /kafka/order_topic,这叫高并发、负载均衡。
上面说的 controller 负责分配这个事。
partition 的数量是可配置的,一般在创建 topic 时候指定,后续可以新增但是不能减少,分区越多,并发越高,但也会增加管理成本。
segment
partition 文件夹里面的文件就是 segment,这是存储消息的文件。
一个 segment 包含 .log .index .timeindex 文件,当 .log 文件大于 1G 的时候,会生成一套新的 segment,.log 文件的文件名表示了开始的 offset,比如第一个是 000000000.log,第二个是 0123452.log。
.log 文件可以想象成一个超大的数组(实际上 kafka 对消息做了层包装),.index 是 .log 文件的索引。
leader 和 replica/follower
但这样万一哪台 broker 宕机或是没网了,topic 的消息岂不是丢了?
实际上每台 broker 的 partition 不止一份。比如 broker1 的 /kafka/order_topic,在 broker2 上有备份。
网上找个图大概这样,像 partition0 除了在 broker1 上有,在 broker2 上也有备份,这样万一 broker1 宕机了,broker2 也能顶上。
broker1 和 broker2 上的 partition0 谁当 leader,这也是 controller 决定的。
partition leader 负责接收 producer 消息写入进来,也负责 consumer 移动 offset,其他 replica 从 leader 拉取消息内容。
如果 replica 拉取消息及时,和 leader 的消息量差距也不大,他们就在 ISR (In-Sync Replicas) 中。
ISR 是一种状态,表面 replica 和 leader 差不多,万一 leader 宕机,就会从 ISR 中选个 replica 当 leader,这也是 controller 做的。
如果哪个 replica 由于比如网络问题和 leader 没同步上,就会踢出 ISR,直到赶上 leader 的进度,则重新放入 ISR 中。
副本在生产中一般配置 3 个,1 个 leader 2 个 replica。
有点绕就对了,再回头看看我画的图吧。
producer
消息的生产者
consumer 和 consumer group
消息的接受者,consumer 必须在 consumer group 里面,一个 consumer group 内可以有多个 consumer,一个consumer group 接受的消息不会重复,因为每个 partition 只会分给 consumer group 的一个成员,
offset
消费者的偏移量,表示消费到哪里了,可以理解为 .log 文件的数组下标。
配置文件解释
知道了上面的关键词,再看配置文件就好理解了。挑几个不容易理解的说说。
acks retries
acks 决定发送者什么情况才算发送成功,retries 表示发送失败重试多少次。
0 表示只发不管 partition 有没有写入成功。
1 表示 leader 写入成功就算成功,后续 ISR 同步给其他 follower。
all 表示所有 partition 都吸入成功才算成功。
auto-offset-reset
表示消费者从哪个 offset 开始消费。earliest 最早,latest 最晚,none 手动指定。
trusted.packages
这主要是因为 JackSon 在反序列号的时候权限比较大,可以直接反序列号指定的类,比如
{ "@type": "com.malicious.AttackPayload", "data": "恶意数据" }
假设这个类的构造器、static 块有恶意代码,就会执行。
这里一般配置项目的包名主路径。
enable-auto-commit
是否自动提交,默认值是 true,可选 false。
发送消息、消费消息的时候,不是一个个消费的,是累积到一定量一起发送的。
消费消息的时候,如果抛异常了 spring 默认消费失败,按道理应该是一致卡在那个地方,实际上消费失败几次还是会自动跳过,因为开启了自动提交,每 5s 自动提交当前的 offset,所以即使失败,在失败中提交了 offset 还是会到下一条。
额外提醒
kafka 发送消息是异步的,有可能失败,虽然概率非常低,但还是做好预警处理。
在比如异步结算的场景,需要保证每个消息都必须消费成功,要配置 acks = all,enable-auto-commit = false,手动提交 offset。
为了防止消费失败一致卡在一个 offset,可以设置重试,或死信队列。