kafka 概述
kafka 是一个分布式基于发布/订阅模式的消息队列。
使用消息队列的好处:
-
解耦:消息队列可以使得不同的系统协同处理一些需求。允许独立扩展或修改两边的处理过程;
-
可恢复性:某个系统失效或者宕机之后,不会影响到整个系统。重新加入队列中的消息仍然可以在系统恢复后被处理。
-
缓冲:当某一个流程处理复杂,生产消息和消费消息处理速度不一致时,会有大量消息或者事件堆积。消息队列可以处理消费和生产速度不太一致的情况。
-
流量削峰:当访问量激增的时候,使用消息队列能够使得关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
-
异步通信:消息队列提供了异步处理机制,允许用户 把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要 的时候再去处理它们。
kafka 架构
producer
消息生产者,向 kafka broker 发送消息的客户端。
consumer
消费者,从 kafka broker 消费消息的客户端。
consumer group
有多个 consumer 组成,一个消费者消费一到多个分区的消息。组内消费者之间互不影响。所以消费者组的数量不要超过分区的数量。
broker
一个 kafka 服务器就是一个 broker。
topic
一个队列,生产者和消费者面向的都是 topic。
partition
topic 是消息逻辑的划分,partition 是消息物理的划分。当消息比较多时,存储在单个 broker 上很容易遇到瓶颈问题。将一个大的 topic 分成几个 partition 存储在不同的机器上,可以实现 topic 的可扩展性。每个 partition 都是有序的。
replication 副本
kafka 的副本机制保证了当 kafka 某个节点宕机之后,该节点上 partition 的数据不丢失。在 kafka 中一个 topic 每个分区都会有若干个副本,其中一个 leader 和若干个 follower。
leader
topic 的每个 partition 有若干个副本,这些副本中只有一个对外提供读写功能,也就是消息的生成和消费。
follower
实时从 leader 中同步数据,当 leader 挂掉之后。多个 follower 重新选举 leader。
zookeeper
kafka 集群依赖 zookeeper。其中 zookeeper 只保存 broker 和 consumer。因为 producer 生产数据时可以指定往哪个 broker 里面写入。consumer 需要依赖 zookeeper 完成负载均衡。当个别节点不是特别稳定时,会实现 Rebalance 操作。
kafka 工作机制
kafka 文件存储机制
kafka 中生产者生产消息,消费者消费消息都是面向 topic 的。topic 是逻辑概念,partition 是物理概念,每个 partition 对应一个 log 文件,存储的是 producer 生产的数据,每条消息都是被追加到 log 文件的末尾,并且有自己的 offset。消费者组中每个消费者都会记录自己消费的 offset。
为了防止大文件的产生,进而影响消费速度。每个 partition 又划分成若干个 segment。segment 也是一个逻辑概念,在每个 partition 里,看不到具体划分的依据,而是在配置文件中进行配置的。默认的是 “.log” 文件大小超过 500m(不确定),划分一个 segment。
在一个 segment 文件中,至少包含 “xxx.log”(存储的是具体的消息),"xxx.index"(存储的是 offset 和消息在 .log 文件中的索引信息),“xxx.timeindex”(存储的是时间戳和消息在 .log 文件中的索引信息)等文件,除此之外还有“leader-epoch-checkpoint”,“xxx.snapshoot” 等文件。其中 "xxx" 指的是存储在 log 文件中第一条消息的 offset 。
生产者
分区策略
创建 topic 时需要指定相应的分区。将 topic 进行分区可以提高并发,以 partition 进行读写,除此之外可以方便在集群中的扩展能力。
以 Java 为例看一下 kafka 的分区策略。
我们将要发送的数据封装成一个 ProducerRecord 对象,这个对象有很多重载方法。可以指定 partition,key,timestamp,Iterable 等参数。
-
指定 partition 时,直接将数据写入该 partition;
-
没有指定 partition 但指定 key,将 key 的 hash 值与 topic 的 partition 个数进行取余得到 partition 的值。
-
都没有指定的时候,第一次调用的时候会随机生成一个整数,然后每次调用都会递增1,将这个值与 partition 个数取余得到 partition 的值。
生成者数据可靠性策略
producer 发送数据到 partition 之后,需要收到 partition 发送的 ack(确认机制),如果没有收到 ack,则重新发送数据。
ack 发送策略
acks 参数配置
| 参数值 | 特点 |
|---|---|
| 0 | producer 不需要 broker 返回的 ack,当 broker 故障会造成数据的丢失。 |
| 1 | partition 的 leader 写入磁盘之后返回 ack,当 follower 同步完成之前 leader 故障,会造成数据丢失。 |
| -1 | partition 的 broker 和 follower 都落入磁盘成功之后才返回 ack。如在 follower 同步完成之后,producer 收到 ack 之前,leader 发生故障,会造成数据重复 |
ISR 机制
为了避免某些 follower 因为一些故障迟迟不和 leader 进行数据同步,进而影响 ack 机制。leader 动态维护了一个和 leader 保持数据同步的 follower 集合(ISR)。当配置了所有的副本同步完成之后才会发送 ack 时,只需要保证 ISR 里面的副本同步完成之后即可。
故障处理
LEO:每个副本的最后一个 offset
HW:所有副本(ISR)中最小的 LEO,HW 之前的数据才对 consumer 可见。
follower 故障
follower 故障之后会被踢出 ISR,等故障恢复之后,该 follower 会读取上次记录的 HW,并将高于 HW 的截取掉(例如上图中的第三个 follower 故障,恢复之后截取高于 HW 的数据后,变成和第二个 follower 一样),然后从 leader 同步数据,当该 follower 的 LEO 大于等于该 partition 的 HW 之后就可以重新加入 ISR。
leader 故障
leader 发生故障之后,会从 ISR 中选出一个新的 leader,之后,为保证多个副本之间的数据一致性,其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader 同步数据。
Exactly Once 语义
At Least Once 语义:ack 设置为 -1,能保证生成数据不丢失,但是不能保证数据不重复。
At Most Once 语义:ack 设置为 0,能保证数据不重复,但是不能保证不丢失。
kafka 引入了幂等性,producer 不论 向 broker 发送多少次重复数据,broker 端都只会持久化一条。
At Least Once + 幂等性 = Exactly Once
消费者
消费方式
consumer 采用 pull 拉取模式从 broker 中读取数据,这个模式可以根据 consumer 的消费能力消费处理消费速度。当没有数据的时候可以传入一个一个参数 timeout,如果当前没有 数据可供消费,consumer 会等待一段时间之后再返回,这段时长即为 timeout。
offset 的维护
由于 consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故 障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢 复后继续消费。
Kafka 0.9 版本之前,consumer 默认将 offset 保存在 Zookeeper 中,从 0.9 版本开始, consumer 默认将 offset 保存在 Kafka 一个内置的 topic 中,该 topic 为__consumer_offsets。
kafka 事务
Producer 事务
为了实现跨分区跨会话的事务,需要引入一个全局唯一的 Transaction ID,并将 Producer 获得的 PID 和 Transaction ID 绑定。这样当 Producer 重启后就可以通过正在进行的 Transaction ID 获得原来的 PID。
为了管理 Transaction,Kafka 引入了一个新的组件 Transaction Coordinator。Producer 就 是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于 事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
Consumer 事务
上述事务机制主要是从 Producer 方面考虑,对于 Consumer 而言,事务的保证就会相对 较弱,尤其时无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过 offset 访 问任意信息,而且不同的 Segment File 生命周期不同,同一事务的消息可能会出现重启后被 删除的情况。
kafka API
producer API
消息发送流程
producer 发送消息是异步发送。主要涉及两个线程
main 线程:main 线程将消息发送给 RecordAccumulator
Sender 线程:Sender 线程不断的从 RecordAccumulator 拉取数据。
RecordAccumulator 是一个线程共享变量。
代码
public class CustomProducer {
private static final Properties PROPERTIES = new Properties();
static {
// kafka 集群,broker-list
PROPERTIES.put("bootstrap.servers", "hadoop102:9092");
PROPERTIES.put("acks", "all");
//重试次数
PROPERTIES.put("retries", 1);
//批次大小
PROPERTIES.put("batch.size", 16384);
//等待时间
PROPERTIES.put("linger.ms", 1);
//RecordAccumulator 缓冲区大小
PROPERTIES.put("buffer.memory", 33554432);
// 序列化机制
PROPERTIES.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
PROPERTIES.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
}
public void producer01() {
// 不带回调函数
Producer<String, String> producer = new KafkaProducer<>(PROPERTIES);
for (int i = 0; i < 100; i++) {
producer.send(new ProducerRecord<>("first", Integer.toString(i), Integer.toString(i)));
}
producer.close();
}
public void producer02() {
// 带有回调函数
Producer<String, String> producer = new KafkaProducer<>(PROPERTIES);
for (int i = 0; i < 100; i++) {
producer.send(new ProducerRecord<>("first", Integer.toString(i), Integer.toString(i)),
(recordMetadata, e) -> {
// 该方法会在 Producer 收到 ack 时调用,为异步调用
// 如果 e == null 说明发送成功,否则失败
if (Objects.isNull(e)) {
System.out.println(recordMetadata.offset());
} else {
e.printStackTrace();
}
});
}
producer.close();
}
public void producer03() throws ExecutionException, InterruptedException {
// 不带回调函数
Producer<String, String> producer = new KafkaProducer<>(PROPERTIES);
for (int i = 0; i < 100; i++) {
// 一条消息发送之后,会阻塞当前线程,直至返回 ack。
producer.send(new ProducerRecord<>("first", Integer.toString(i), Integer.toString(i))).get();
}
producer.close();
}
}
consumer API
offset 提交
consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故 障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢 复后继续消费。所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。
自动提交
自动提交需要设置两个参数:
-
enable.auto.commit:是否开启自动提交 offset 功能
-
auto.commit.interval.ms:自动提交 offset 的时间间隔
手动提交
自动提交需要设置时间,但是这个时间很难把握。所以 kafka 也支持手动提交。手动提交 offset 的方法有两种:commitSync(同步提交)和 commitAsync(异步提交)
commitSync vs commitAsync
相同点:
会将本次 poll 的一批数据最高的偏移量提交。
不同点:
commitSync:阻塞当前线程,直到提交成功。失败会自动重试。
commitAsync:没有失败重试机制。
代码
public class CustomConsumer {
private static final Properties PROPS = new Properties();
static {
PROPS.put("bootstrap.servers", "hadoop102:9092");
PROPS.put("group.id", "test");
PROPS.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
PROPS.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
}
public static void consumer01() {
// 自动提交
PROPS.put("enable.auto.commit", "true");
// 自动提交时长
PROPS.put("auto.commit.interval.ms", "1000");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(PROPS);
consumer.subscribe(Collections.singletonList("first"));
while (true) {
ConsumerRecords<String, String> polls = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : polls) {
System.out.print(record.toString());
}
}
}
public static void consumer02() {
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(PROPS);
consumer.subscribe(Collections.singletonList("first"));
while (true) {
ConsumerRecords<String, String> polls = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : polls) {
System.out.print(record.toString());
}
// 同步提交,当前线程会阻塞直到 offset 提交成功
consumer.commitSync();
}
}
public static void consumer03() {
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(PROPS);
consumer.subscribe(Collections.singletonList("first"));
while (true) {
ConsumerRecords<String, String> polls = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : polls) {
System.out.print(record.toString());
}
// 同步提交
consumer.commitAsync((map, e) -> {
if (Objects.nonNull(e)) {
System.out.println("失败");
}
});
}
}
}
无论是同步提交还是异步提交都有可能会造成数据漏消费或者重复消费。
Rebalance
当有新的消费者加入消费者组、已有的消费者推出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程叫做 Rebalance。 消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费。要实现自定义存储 offset,需要借助 ConsumerRebalanceListener。