Ari的小跟班

  :: :: 博问 :: 闪存 :: :: :: :: 管理 ::
  65 随笔 :: 1 文章 :: 1 评论 :: 15008 阅读

参考尚硅谷的18_尚硅谷_Kafka_生产者_数据可靠_哔哩哔哩_bilibili,感谢尚硅谷!

安装使用Kafka

​ 在安装并且部署完ZooKeeper集群后,再进行Kafka的安装。这里我以kafka_2.12-3.0.0为例(下载链接,下载下来之后将后缀名rar改成tgz,链接:https://wwrj.lanzoue.com/iRiNC0tfgtni)。

​ 我们的机器部署情况如下所示,每台机器都安装了ZooKeeper和Kafka:

hadoop1 hadoop2 hadoop3
ZooKeeper ZooKeeper ZooKeeper
Kafka Kafka Kafka

安装Kafka步骤:

  1. 解压kafka_2.12-3.0.0.tgz/opt/module下,并且重命名文件夹为kafka
  2. 和ZooKeeper一样,数据不能存储在临时目录目录下,其默认存储数据的目录为/tmp/kafka-logs
    1. server.properties(在kafka/config目录下)配置文件中,将修改为/opt/module/kafka/datas,并且创建该文件夹。
  3. 因为我们是kafka集群,所以server.properties中的broker.id属性不能重复(最重要的!!),这里我让hadoop1的broker.id为1,hadoop2的为2,hadoop3的为3.
  4. 还是在server.properties中,将zookeeper.connect属性修改为192.168.117.128:2181,192.168.117.129:2181,192.168.117.130:2181/kafka,也分别是hadoop1,2,3的ip地址。
  5. server.properties中添加listeners=PLAINTEXT://192.168.117.129:9092(以192.168.117.129的机器hadoop2为例),依次在三台虚拟机中都添加上这个listeners属性

至此,kafka安装就已经完成了。依次使用命令bin/kafka-server-start.sh -daemon config/server.properties启动kafka,随后用jps查看,若出现以下结果,则表明kafka正常启动成功:

kafka启动所需的命令

启动kafka要先启动zookeeper,zookeeper学习可以参考bilibili的尚硅谷的教程:07_尚硅谷_zk_本地_安装_哔哩哔哩_bilibili

启动kafka需要执行命令,进入到/opt/module/zookeeper-3.5.7路径下,执行

bin/zkServer.sh start

将start改为status可以查看zookeeper的状态。

kafka启动命令:进入到kafka目录中,/opt/module/kafka。使用命令起到kafka:

bin/kafka-server-start.sh -daemon config/server.properties

停止kafka命令

bin/kafka-server-stop.sh

kafka的构成

​ 设计理念概括:

  1. 为方便扩展,并提高吞吐量,一个topic分为多个partition
  2. 配合分区的设计,提出消费者组的概念,组内每个消费者并行消费
  3. 为提高可用性,为每个partition增加若干副本,类似NameNode。
  4. ZK中记录谁是leader,Kafka2.8.0以后也可以配置不采用ZK(从Kafka 2.8.0开始,Kafka可以在无需配置ZooKeeper的情况下运行。这种模式被称为KRaft模式(Kafka Raft Metadata mode),在此模式下,Kafka将使用内置的Raft协议来处理元数据。在KRaft模式下,Kafka可以摆脱对ZooKeeper的依赖,简化部署和运维,同时提高了系统的可扩展性、稳定性和性能。)

​ Kafka中的一些术语解释:

  1. Producer:消息生产者,就是向 Kafka broker 发消息的客户端
  2. Consumer:消息消费者,向 Kafka broker 取消息的客户端
  3. Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
  4. Broker一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个broker 可以容纳多个 topic。
  5. Topic:可以理解为一个队列,生产者和消费者面向的都是一个 topic。
  6. Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列
  7. Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个Follower。
  8. Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 Leader。注意Leader是针对分区中的副本而言的。
  9. Follower:每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader

Kafka命令行模式

​ 图示如下:

主题命令行操作

  1. 查看操作主题命令参数:bin/kafka-topics.sh

    参数 描述
    --bootstrap-server <String: server toconnect to> 连接的 Kafka Broker 主机名称和端口号。
    --topic <String: topic> 操作的 topic 名称。
    --create 创建主题。
    --delete 删除主题。
    --alter 修改主题。
    --list 查看所有主题。
    --describe 查看主题详细描述。
    --partitions <Integer: # of partitions> 设置分区数。
    --replication-factor<Integer: replication factor> 设置分区副本。
    --config <String: name=value> 更新系统默认的配置。
  2. 创建 first topic:

    命令:bin/kafka-topics.sh --bootstrap-server hadoop2:9092 --create --partitions 1 --replication-factor 3 --topic first;

    选项说明:--topic 定义 topic 名;--replication-factor 定义副本数;--partitions 定义分区数;以上命令创建了一个名为first的topic,该topic有一个分区,每个分区有3个副本。

  3. 修改分区数(注意:分区数只能增加,不能减少):bin/kafka-topics.sh --bootstrap-server hadoop1:9092 --alter --topic first --partitions 3

  4. 查看 first 主题的详情:bin/kafka-topics.sh --bootstrap-server hadoop1:9092 --describe --topic first.

    修改分区前查看的结果为(可以看到只有一个分区,该分区号为0):

    Topic: first TopicId: P1r81zugTNqar7Bk62kG0w PartitionCount: 1 ReplicationFactor: 3 Configs: segment.bytes=1073741824
    Topic: first Partition: 0 Leader: 3 Replicas: 2,3,1 Isr: 3,1,2

    修改分区为3之后的结果为:

    Topic: first TopicId: P1r81zugTNqar7Bk62kG0w PartitionCount: 3 ReplicationFactor: 3 Configs: segment.bytes=1073741824
    Topic: first Partition: 0 Leader: 3 Replicas: 2,3,1 Isr: 3,1,2
    Topic: first Partition: 1 Leader: 3 Replicas: 3,2,1 Isr: 3,2,1
    Topic: first Partition: 2 Leader: 1 Replicas: 1,3,2 Isr: 1,3,2

    可以看到有三个分区,每个分区有三个副本,Leader:3表示三个副本中,brokerId = 3的副本为Leader,ISR会在之后介绍。

  5. 删除 topic(仅做命令展示,可以不用执行):bin/kafka-topics.sh --bootstrap-server hadoop1:9092 --delete --topic first

生产者命令行操作

  1. 查看操作生产者命令参数: bin/kafka-console-producer.sh

    参数 描述
    --bootstrap-server <String: server toconnect to> 连接的 Kafka Broker 主机名称和端口号。
    --topic <String: topic> 操作的 topic 名称。
  2. 发送消息

    bin/kafka-console-producer.sh --bootstrap-server hadoop1:9092 --topic first
    >hello world
    >atguigu atguigu

消费者命令行操作

  1. 查看操作消费者命令参数: bin/kafka-console-consumer.sh

    参数 描述
    --bootstrap-server <String: server toconnect to> 连接的 Kafka Broker 主机名称和端口号。
    --topic <String: topic> 操作的 topic 名称。
    --from-beginning 从头开始消费。
    --group <String: consumer group id> 指定消费者组名称。
  2. 消费消息:

    1. 消费first主题中的数据:bin/kafka-console-consumer.sh --bootstrap-server hadoop1:9092 --topic first
    2. 把主题中所有的数据都读取出来(包括历史数据):bin/kafka-console-consumer.sh --bootstrap-server hadoop1:9092 --from-beginning --topic first

​ Q1:我们在进行命令行操作时,只需要写一个Broker地址就可以了,而且写任意一个Broker的地址,得到的结果都是一致的,这是什么原因呢?

​ 虽然连接到了不同的 broker,但是实际上访问的是同一个 Kafka 集群,因此不同的Broker命令返回的会是同样的结果。Kafka 集群将处理 broker 之间的通信,以确保数据在集群中的分布和复制。当您连接到任何一个 broker 时,它将根据集群的元数据返回关于主题的信息。

​ 可以将 --bootstrap-server 参数视为指示客户端如何与 Kafka 集群建立连接的方法。这个参数的目的是为客户端提供一个启动点,以便找到集群中的其他 broker。一旦客户端连接到一个 broker,它将自动从集群中获取其他 broker 的信息。因此,连接到集群中的任何一个 broker 都可以获得相同的结果。

Kafka生产者

消息发送原理

​ 在消息发送的过程中,涉及到了两个线程——main线程Sender线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker。

​ 主要流程如下:

  1. 首先创建一个main线程,即KafkaProducer对象,调用KafkaProducer对象的send(ProducerRecord对象)方法,就会进入消息发送流程。

  2. 依次进入拦截器Interceptors,序列化器Serializer,分区器Partitioner分区器。

    1. 在生产环境中拦截器使用的不多,一般使用的都是Flume的拦截器。
    2. 虽然有Java自己的序列化器,但是我们一般不使用(Java序列化器序列化后的数据太重了),我们较多使用的是StringSerializer序列化器,因为传输的内容大部分都是String
    3. 分区器用来判断一条数据最终要发送到哪个分区,随后添加到RecordAccumulator中。在RecordAccumulator 中,会为每个分区维护一个队列,当一个消息要被添加到 RecordAccumulator(总大小默认为32MB) 时,会调用 Partition 的 append 方法来将消息添加到对应分区的队列中。在 append 方法中,会先将消息加入到 RecordBatch(批次,多个消息可以作为一个批次,RecordBatch默认大小为16K)中,并尝试将 RecordBatch 加入到 RecordAccumulator 的待发送队列中。
  3. sender发送数据到Broker之前会判断条件,只有满足以下两个条件中任一个条件时,才会发送到Broker中:

    1. batch.size:RecordBatch的大小,只有数据积累到batch.size之后,sender才会发送数据。默认16k。
    2. linger.ms:如果数据迟迟未达到batch.size,sender等待linger.ms设置的时间到了之后就会发送数据。单位ms,默认值是0ms,表示没有延迟,0ms表示数据过来一条就马上发过去一条,即batch,size这个参数就没用了。
    3. 在实际生产环境种,这两个参数都需要进行调整
  4. 我们知道,每个分区都有可能有多个副本,副本存在的Broker可能有多个(副本中有一个leader和若干follower),那么具体来说是存放到哪个Broker上呢?实际上,sender会把队列(对应一个分区)中的数据发送到分区(leader身份)所在的Broker上

    1. 并且sender发送时是以Broker为维度的,可能不同topic的不同分区的leader都在一个broker上,那么当他们ready后,sender会一起发送过去。

  5. 这里发送到Broker时需要通过网络(具体使用Selector进行的)进行,会有一个应答机制,和计算机网络的滑动窗口类似,默认每个Broker结点最多缓存5个请求(窗口大小为5)。这里Broker的应答机制有三种,分别是

    1. acks:0 表示生产者发送过来的数据,不需要等数据落盘应答
    2. acks:1 表示生产者发送过来的数据,Leader收到数据后应答。
    3. acks:-1(all) 表示生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。-1和all等价。
  6. 如果发送成功了,就会移除RecordAccumulator中对应队列的消息,并且请求缓存数也会减一;若发送失败了,则会重试(默认重试次数为Integer的最大值)

总结如下:

生产者参数解释:

参数名称 描述
bootstrap.servers 生产者连接集群所需的 broker 地 址 清 单 。 例如 hadoop1:9092,hadoop2:9092,hadoop3:9092,可以设置1个或者多个,中间用逗号隔开。注意这里并非需要所有的 broker 地址,因为生产者从给定的 broker里查找到其他 broker 信息。
key.serializer 和 value.serializer 指定发送消息的 key 和 value 的序列化类型。一定要写全类名,可以用StringSerializer.class.getName()获得全类名
buffer.memory RecordAccumulator 缓冲区总大小,默认32M
batch.size 缓冲区一批数据最大值,默认16K。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。
linger.ms 如果数据迟迟未达到batch.size,sender等待 linger.time 之后就会发送数据。单位 ms,默认值是 0ms,表示没有延迟。生产环境建议该值大小为 5-100ms 之间。
acks 0:生产者发送过来的数据,不需要等数据落盘应答。
1:生产者发送过来的数据,Leader 收到数据后应答。
-1(all):生产者发送过来的数据,Leader+和 isr 队列里面的所有节点收齐数据后应答。默认值是-1,-1 和all 是等价的。
max.in.flight.requests.per.connection 允许最多没有返回 ack 的次数,默认为 5,开启幂等性要保证该值是 1-5 的数字。
retries 当消息发送出现错误的时候,系统会重发消息。retries表示重试次数。默认是 int 最大值,2147483647。

如果设置了重试,还想保证消息的有序性,需要设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1否则在重试此失败消息的时候,其他的消息可能发送成功了
retry.backoff.ms 两次重试之间的时间间隔,默认是 100ms
enable.idempotence 是否开启幂等性,默认 true,开启幂等性。
compression.type 生产者发送的所有数据的压缩方式。默认是 none,也就是不压缩。
支持压缩类型:none、gzip、snappy、lz4 和 zstd。

Q1:RecordAccumulator中每个队列对应一个分区,那么生产者如何知道这个分区的leader所在的broker呢?

答:

​ Kafka生产者在初始化时,会从一个或多个broker(在producer配置中指定)请求元数据(metadata)信息。元数据信息包含了集群中所有broker的地址,以及每个主题和分区的信息(包括每个分区的所有副本和当前leader副本),并将这些元数据缓存在本地。

​ 请注意,生产者本地缓存的元数据信息可能会过时。例如,当分区的leader发生故障或者发生重平衡时,元数据中的leader副本信息可能会发生变化。在这种情况下,当生产者尝试发送消息时,它可能会收到一个错误,告诉它现在的leader信息不正确。生产者会自动从broker请求更新的元数据信息,并使用新的元数据重新尝试发送消息。

​ 生产者也可以定期自动更新元数据信息,以防止使用过时的缓存信息。这可以通过配置metadata.max.age.ms参数来实现,该参数指定了生产者在向broker请求新的元数据之间的最大时间间隔。默认情况下,此值为5分钟(300000毫秒)。

异步发送

​ 异步发送是Kafka生产者默认的发送方式。在这种模式下,生产者不会等待broker的ack响应,而是将消息迅速添加到本地缓冲区并立即返回。异步发送的主要优点是提高了吞吐量和性能,因为生产者在发送消息时不会被阻塞。然而,这种方式的一个缺点是,在某些情况下,可能会导致消息丢失。例如,在发送消息到broker之前,生产者发生故障导致缓冲区中的消息丢失。

不带回调

​ 创建 Kafka 生产者,采用异步的方式发送到 Kafka Broker。

  1. 首先创建maven工程,添加以下依赖:

    <dependencies>
    <dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.0.0</version>
    </dependency>
    </dependencies>
  2. 创建包名:com.atguigu.kafka.producer

  3. 编写不带回调函数的代码:

package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class CustomProducer {
public static void main(String[] args) {
//0创建配置
Properties properties = new Properties();
//添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG
,"192.168.117.128:9092");
//给key和value添加序列化器
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG
, StringSerializer.class.getName());//这一步是不是也是一种反射的体现?
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG
,StringSerializer.class.getName());
//1、创建kafka生产者对象;因为KafkaProducer实现了closable接口
try(KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);){
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new
ProducerRecord<>("first","atguigu3 " + i));
}
}
}
}

带回调函数

​ 回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata)和异常信息(Exception),如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。

package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Objects;
import java.util.Properties;
public class CustomProducerWithCallback {
public static void main(String[] args) {
//0创建配置
Properties properties = new Properties();
//添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG
,"192.168.117.128:9092");
//给key和value添加序列化器
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG
, StringSerializer.class.getName());//这一步是不是也是一种反射的体现?
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG
,StringSerializer.class.getName());
//1、创建kafka生产者对象;因为KafkaProducer实现了closable接口
try(KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);){
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new ProducerRecord<>("first","atguigu,with callback " + i),new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (Objects.isNull(e)){
System.out.println("主题"+recordMetadata.topic()+" 分区为"+recordMetadata.partition());
}
}
});
}
}
}
}

​ 这里我们可以通过回调函数知道发送的主题以及实际发送到的分区:

同步发送

​ 在同步发送模式下,生产者会等待broker发送ack响应,确认它已经成功地接收并处理了消息。这种方式的主要优点是提高了数据可靠性,因为生产者会确保消息被成功地发送到broker。然而,同步发送会降低吞吐量和性能,因为生产者需要等待每个消息的确认响应。同步发送可以通过设置max.block.ms参数来实现,它指定了生产者等待发送操作完成的最长时间。

​ 只需在异步发送的基础上,再调用一下 get()方法即可,并处理一下异常(抛出或者捕获)。kafkaProducer.send(new ProducerRecord<>("first","kafka" + i)).get();

分区相关操作

分区的好处

  1. 便于合理使用存储资源:每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
  2. 提高并行度:生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据

默认分区策略

  1. 指明partition的情况下,直接将指明的值作为partition值;例如partition=0,所有数据写入分区0
  2. 没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值;例如:key1的hash值=5, key2的hash值=6 ,topic的partition数=2,那么key1 对应的value1写入1号分区,key2对应的value2写入0号分区。
  3. 既没有partition值又没有key值的情况下,Kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,Kafka再随机一个分区进行使用(和上一次的分区不同)。例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k)或者linger.ms设置的时间到, Kafka再随机一个分区进行使用(如果还是0会继续随机)。

​ 我们可以在send()方法中指明key和partition值。

自定义分区器

​ 如果研发人员可以根据企业需求,自己重新实现分区器。例如我们实现一个分区器实现,发送过来的数据中如果包含 atguigu,就发往 0 号分区,不包含 atguigu,就发往 1 号分区。

​ 我们可以自定义一个类然后实现 Partitioner接口,再时间其中的partition()方法:

package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
/**
* 1. 实现接口 Partitioner
* 2. 实现 3 个方法:partition,close,configure
* 3. 编写 partition 方法,返回分区号
*/
public class MyPartitioner implements Partitioner {
/**
* 返回信息对应的分区
* @param topic 主题
* @param key 消息的 key
* @param keyBytes 消息的 key 序列化后的字节数组
* @param value 消息的 value
* @param valueBytes 消息的 value 序列化后的字节数组
* @param cluster 集群元数据可以查看分区信息
* @return
*/
@Override
public int partition(String topic, Object key, byte[]
keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 获取消息
String msgValue = value.toString();
// 创建 partition
int partition;
// 判断消息是否包含 atguigu
if (msgValue.contains("atguigu")){
partition = 0;
}else {
partition = 1;
}
// 返回分区号
return partition;
}
// 关闭资源
@Override
public void close() {
}
// 配置方法
@Override
public void configure(Map<String, ?> configs) {
}
}

​ 随后在KafkaProducer里配置这个对应的分区器就可以了:properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,MyPartitioner.class.getName())

生产者提高吞吐量

​ 我们可以通过修改batch,size,linger.ms,compression.type和RecordAccumulator缓冲区的大小来实现。

  1. batch.size:properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);,默认是16K

  2. linger.ms:默认是0,在默认情况下,batch.size就没用了。properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);将其设置为1

  3. RecordAccumulator:缓冲区大小,默认 32M:buffer.memory:properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);

  4. compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd。properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");

    这几种压缩方式有什么区别吗?

    1. GZIP(GNU Zip):
      • 优点:GZIP具有较高的压缩率,通常可以有效地减小数据传输和存储的开销
      • 缺点:GZIP压缩和解压缩速度相对较慢,同时对CPU资源的占用较高
    2. Snappy:
      • 优点:Snappy压缩速度非常快,对CPU资源的占用相对较低。此外,Snappy解压速度也非常快,可以更快地进行消息处理。
      • 缺点:Snappy的压缩率相对较低,因此存储和传输开销节省的可能不如GZIP明显。
    3. LZ4:
      • 优点:LZ4压缩速度很快,同时具有相对较好的压缩率。LZ4解压速度也很快,对CPU资源的占用相对较低。
      • 缺点:LZ4的压缩率可能不如GZIP,但通常优于Snappy。
    4. Zstd(Zstandard):
      • 优点:Zstd是一种新型压缩算法,具有很高的压缩率,同时压缩和解压缩速度相对较快,对CPU资源的占用适中。
      • 缺点:由于Zstd是较新的算法,它在某些环境中的兼容性可能不如其他更成熟的压缩算法

数据可靠性——ack应答机制

​ 我们可以通过properties.put(ProducerConfig.ACKS_CONFIG, "all");来设置acks的值。

​ 先看以下图示,当ack为0或者1时,都有可能丢失数据:

​ Leader收到数据,所有Follower都开始同步数据,但有一个Follower,因为某种故障,迟迟不能与Leader进行同步,那这个问题怎么解决呢?

答: Leader维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)。

如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认30s。例如2超时,(leader:0, isr:0,1)。

​ 这样就不用等长期联系不上或者已经故障的节点。

数据可靠性分析:如果分区副本设置为1个,或 者ISR里应答的最小副本数量( min.insync.replicas 默认为1,就是ISR里面的broker的数量,包括自己)设置为1,和ack=1的效果是一样的,仍然有丢数的风险(leader:0,isr:0)。

所以:数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2

​ 可靠性总结:

  • acks=0,生产者发送过来数据就不管了,可靠性差,效率高;
  • acks=1,生产者发送过来数据Leader应答,可靠性中等,效率中等;
  • acks=-1,生产者发送过来数据Leader和ISR队列里面所有Follwer应答,可靠性高,效率低;
  • 在生产环境中,acks=0很少使用;acks=1,一般用于传输普通日志,允许丢个别数据;acks=-1,一般用于传输和钱相关的数据,对可靠性要求比较高的场景。

​ 但还是有个数据重复的问题,假设leader已经给所有follower同步完数据并且落盘了,正准备返回ack时,leader挂了,由于client没有收到ack,他会重试,发送新的数据请求给新的leader,这个时候leader又会同步一次数据到follower并且落盘,导致数据重复。

数据去重

​ 首先我们先确认一下数据传递的语义:

  • 至少一次(At Least Once)= ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2;可以保证数据不丢失,但是不能保证数据不重复。
  • 最多一次(At Most Once)= ACK级别设置为0;可以保证数据不重复,但是不能保证数据不丢失。
  • 精确一次(Exactly Once):对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失;

可以参考Kafka设计解析(八):Kafka事务机制与Exactly Once语义实现原理_架构_郭俊_InfoQ精选文章

​ 如何实现精确一次呢?Kafka 0.11版本以后,引入了一项重大特性:幂等性事务

幂等性

​ 幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复

​ 那么同样内容的一条消息,Kafka是如何判断是否重复呢?标准如下:

​ 重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID指的是Producer ID,是Kafka为每个生产者分配的唯一标识符。当生产者启动时,它会与事务协调器(一个特定的Kafka broker)建立联系并获取PID。PID用于跟踪生产者发送的消息,并确保它们的顺序。Partition 表示分区号;Sequence Number是单调自增的。

​ 所以幂等性只能保证的是在单分区单会话内不重复。如果开了多个会话,即KafkaProducer初始化多次(比如第一个掉了之后,启动了第二个KafkaProducer对象来继续发送消息),那么消息可能是会重复的。

如何开启幂等性呢?答:幂等性通过开启参数enable.idempotence ,它的默认值为 true,false 关闭。即幂等性是默认开启的。

​ 但是幂等性只能保证单分区单会话不重复,试想这样的例子:

​ 假设你有一个在线购物应用,用户在结账时会生成订单并扣除相应的库存。订单和库存分别对应两个不同的主题(如ordersinventory)。当一个新的订单创建时,需要向orders主题写入订单信息,同时扣除inventory主题中对应商品的库存。

​ 在这种情况下,你需要确保这两个操作是原子性的。如果仅仅使用幂等性,你无法确保同时更新两个主题。例如,如果orders主题的写入成功,而inventory主题的写入失败,就会导致数据不一致。

​ 这个时候就需要使用生产者事务了。

生产者事务

​ 首先,开启事务,必须开启幂等性!

其执行流程如下图所示:

​ 最关键的是:用户需要指定一个独一无二的transcational id给事务协调器,为了保证在整个Kafka集群中,正在运行的所有生产者(具有事务功能)的transactional ID都必须是不同的

​ 在Java中,其相关的API有5个,如下所示:

  1. 初始化事务:void initTransactions();
  2. 开启事务:void beginTransaction() throws ProducerFencedException;
  3. 在事务内提交已经消费的偏移量(主要用于消费者):void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,String consumerGroupId) throws ProducerFencedException;
  4. 提交事务:void commitTransaction() throws ProducerFencedException;
  5. 放弃事务(类似于回滚):void abortTransaction() throws ProducerFencedException;

代码如下:

package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Objects;
import java.util.Properties;
public class CustomProducerTranscations {
public static void main(String[] args) {
//0创建配置
Properties properties = new Properties();
//添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG
,"192.168.117.128:9092");
//给key和value添加序列化器
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG
, StringSerializer.class.getName());//这一步是不是也是一种反射的体现?
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG
,StringSerializer.class.getName());
//设置事务id(必须),id可以任意,但是不能在运行事务的生产者中有重复
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction_id_0");
//1、创建kafka生产者对象;
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
try{
// 初始化事务
kafkaProducer.initTransactions();
// 开启事务
kafkaProducer.beginTransaction();
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new ProducerRecord<>("first","atguigu Transaction " + i),new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (Objects.isNull(e)){
System.out.println("主题"+recordMetadata.topic()+" 分区为"+recordMetadata.partition());
}
}
});
}
kafkaProducer.commitTransaction();
}catch (Exception e){
kafkaProducer.abortTransaction();
}finally {
kafkaProducer.close();
}
}
}

数据有序

​ 我们知道Kafka默认保留5个没有应答的request,假设这5个request没有按顺序到达broker,那么broker落盘的时候就可能乱序。

​ 在kafka中,一般来说,单分区内可以保证有序(有一定条件),多分区之间是没办法保证有序的

​ 单分区内保证有序的条件如下:

  1. kafka在1.x版本之前保证数据单分区有序,条件如下:

    max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)。

  2. kafka在1.x及以后版本保证数据单分区有序,条件如下:

    1. 未开启幂等性

      max.in.flight.requests.per.connection需要设置为1。

    2. 开启了幂等性

      max.in.flight.requests.per.connection需要设置小于等于5。

      原因说明:因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。

​ 举个例子,假设kafka集群收到的1,2,4,5,3;但是正确的顺序应该是1,2,3,4,5;当个max.in.flight.requests.per.connection设置为5时,kafka集群如何保证有序:

Kafka Broker

ZooKeeper中存储的Kafka信息

​ 在未启用Kraft模式时,ZooKeeper中会存储Broker相关的信息。我们可以使用PrettyZoo软件方便的查看ZooKeeper中的信息,new一个服务器后,进入到kafka目录查看信息,可以看到kafka目录下有很多文件夹:

​ 我们重点介绍一下几个文件夹以及其中存储的信息:

1./kafka/brokers/ids下会存储有哪些正在工作的broker:比如我目前三个台Broker(broker id分别是1 2 3)

2./kafka/brokers/topics/first/partitions/0/state下会记录哪个Broker是Leader(如果是多副本的话),有哪些Broker是可以用的。

其内容为:

{"controller_epoch":14,"leader":1,"version":1,"leader_epoch":24,"isr":[1,2,3]}

​ 表示这个分区的ISR有[1,2,3],其中1是leader;

​ (1)controller_epoch:这个字段表示当前的控制器(Controller)的纪元。控制器是Kafka集群中的一个特殊角色,负责管理分区的状态、副本迁移和故障恢复等。当控制器发生故障或被重新选举时,controller_epoch会增加,以便识别不同的控制器周期。

​ (2)leader_epoch:这个字段表示leader副本的纪元。当分区的leader副本发生变化(如故障或迁移等原因)时,leader_epoch会递增。这可以帮助避免某些过时信息造成的问题。

​ (3)version:这个字段表示分区状态信息的版本。Kafka可能会在未来的版本中对分区状态的格式进行修改,此字段用于区分不同的格式版本。

3./kafka/consumers下会记录有关消费者的信息,0.9版本之后offset就存储在kafka主题中了。

4./kafka/controller用于辅助选举leader。具体来说,在kafka节点上线后,谁先去controller中成功注册了信息,谁就是leader。比如现在我的leader是1,那么ZooKeeper中controller信息就是:{"version":1,"brokerid":1,"timestamp":"1682220957091"}

总结如下:

Broker工作流程

  1. 每台Broker启动之后,都会主动向ZooKeeper中进行注册;相应的ZooKeeper中的/kafka/brokers/ids里的内容也会改变或者增加。
  2. 随后,ZooKeeper中的controller信息,各个Broker会去抢占,谁先抢到,谁就是主事(即用于辅助选举Leader)的controller。
  3. 因为每个broker中都有controller信息,那么由成为leader的broker去监听ZooKeeper中/kafka/brokers/ids的变化。具体来说会做以下工作:
    1. 如果有新的Broker加入集群,Controller会识别新加入的Broker并将其纳入集群管理之中。新Broker可以用来存储新的副本以及负载均衡等操作。
    2. 如果某个Broker宕机了,Controller会检查所有受影响的分区,并为那些失去Leader的分区重新选举新的Leader。它会按照先前提到的规则(先选择ISR列表中的副本,然后根据AR列表的顺序选举)在存活的副本中选举新的Leader。此外,Controller还会更新ISR列表,将失去联系的副本从ISR中移除。
    3. 如果Controller自身所在的Broker宕机了,Kafka集群会自动选举一个新的Controller来接管原Controller的工作。
  4. 由该主事的controller来主持Leader选举,其选举规则为:
    1. 首先,只有在ISR中存活的结点才可以参与选举。
    2. 按照AR(Kafka分区中所有副本的统称,比如[1,0,2])中的顺序,排在前面的越优先。比如AR[1,0,2],ISR[0,1,2],那么会按照1->0->2进行Leader的选举。
  5. 选举出来之后,会把leader结点信息上传到ZooKeeper中,即/kafka/brokers/topics/first/partitions/0/state(以first主题为例)中的信息。
  6. 其他Broker中的Controller从ZooKeeper中同步相关信息。
  7. 生产者发送消息给Leader,随后Leader会同步信息给各个Follower。在Kafka集群中,信息是以LOG方式存储的,具体来说就是一个一个Segment,每个Segment最大为1G,由.log文件和.index组成(为了在1G中更快地找到想要的信息)
  8. 假设Leader挂了,那么直观体现就是在ZooKeeper中的brokers/id会少一个Leader的id。
    1. 一开始选举出来的Controller会监听到brokers的变化
    2. 该Controller会从Zook中拉取/kafka/brokers/topics/first/partitions/0/state(以first主题为例)中的信息,按照之前讲的选举规则选举出新的leader。
    3. 选举出新的leader后,更新ZooKeeper中的相关信息(主要是Leader和ISR)。并且各个Broker会去主动的拉取ZooKeeper中更新后的信息

图解如下:


Q1:leader是相对于分区副本而言的,如果不创建topic,那么是不是就没有leader了?

​ 答:是的,如果你没有创建任何主题,那么ZooKeeper中就不会有关于Leader的信息。实际上,在没有创建任何主题的情况下,Kafka集群不会有分区和副本,因此也就不存在Leader的概念。

Q2:那kafka中每个broker之间是不是就没有leader关系,他们的关系是平等的?

​ 答:是的,对于Kafka集群中的Broker(例如hadoop1,hadoop2和hadoop3),它们在集群中的地位是平等的。Kafka集群中的每个Broker都可以扮演多个角色,包括分区的Leader副本、Follower副本和Controller。

  1. 分区的Leader副本:负责处理生产者和消费者的读写请求。一个主题的不同分区的Leader副本可以分布在不同的Broker上。
  2. 分区的Follower副本:主要用于同步Leader副本的数据。在Leader副本失效时,Follower副本之一将被选为新的Leader。
  3. Controller:Kafka集群中的一个特殊Broker,负责管理集群的元数据信息以及副本的Leader选举等。每个Kafka集群只有一个Controller。

​ 这种分布式架构确保了Kafka集群的高可用性、可扩展性和负载均衡。每个Broker在整个集群中都有一定的职责,但没有一个Broker具有绝对的领导地位。这有助于防止单点故障并提高系统的整体性能。

Q3:若Controller所在的broker挂了并且分区副本所在的broker也挂了,这个时候集群会怎么办?

​ 答:ZooKeeper通过心跳机制来检测Broker是否宕机。每个Broker都会定期向ZooKeeper发送心跳信号,表明它们还处于活动状态。如果某个Broker在一定时间内没有发送心跳信号,ZooKeeper会认为该Broker已经宕机。当Controller所在的Broker宕机时,ZooKeeper会触发其他存活的Broker参与新一轮的Controller选举(这个过程中,各个Broker会通过向ZooKeeper请求"/controller"节点的锁来争取成为新的Controller。哪个Broker成功获取到锁,就成为新的Controller)。选举出来之后,去监听/brokers/ids中的内容,发现leader掉了,那么就会进行新的一轮副本leader的选举。

Q4:AR的顺序是如何确定的?因为在通常情况下,AR中的第一个就是leader副本;

​ 答:kafka会采取RoundRobinAssignment(循环分配)策略。

​ 这是RoundRobinAssignment策略的大致步骤:

  1. 将所有Broker排序(通常是从小到大排序,具体执行时可能会进行动态调整)。
  2. 对于每个分区,从上一分区的第一个副本所在的Broker之后的下一个Broker开始,依次为当前分区的每个副本选择一个Broker。
  3. 当选择一个副本的Broker时,如果当前Broker已经达到了其应该承载的副本数,则跳过这个Broker并继续查找下一个可用的Broker。
  4. 对于每个分区,第一个副本(AR列表中的第一个Broker)成为Leader副本,其他副本成为Follower副本。

​ 假设以创建一个新的有三个分区每个分区有三个副本的topic(second)为例,kafka根据实际情况排序出来的顺序为[2,3,1],那么0号分区的leader会是2,1----3,2----1.

​ 实际情况也是这样的:

0号分区:

1号分区:

2号分区:

Q5:Controller是如何监听Zookeeper中"/kafka/brokers/ids"的变化?

答:在ZooKeeper中,Watcher是一种监听机制允许客户端在指定的ZooKeeper节点(如"/kafka/brokers/ids")上接收通知。这些通知可以包括节点数据的更改、节点删除或子节点的更改等事件。这种机制使客户端能够快速地了解ZooKeeper中节点的变化情况,并作出相应的响应。

​ Kafka中的Controller会注册一个Watcher来监听"/kafka/brokers/ids"节点的变化。这是通过使用ZooKeeper客户端API完成的。以下是Kafka Controller注册Watcher的概要步骤:

  1. Kafka Controller首先使用ZooKeeper客户端API建立与ZooKeeper服务器的连接。
  2. 然后,Controller在"/kafka/brokers/ids"节点上注册一个Watcher。这是通过调用getChildren()方法并传入Watcher对象完成的。此时,Watcher会附加到"/kafka/brokers/ids"节点上。
  3. 一旦"/kafka/brokers/ids"节点上发生变化(如子节点的添加或删除),Watcher会收到一个通知事件。这个通知事件包含了事件类型(如节点创建、节点删除、节点数据更改等)以及触发事件的节点路径。
  4. 在收到通知事件后,Watcher回调方法会被触发。Kafka Controller在此方法中处理事件,例如更新内部状态、重新选举分区的Leader等。
  5. 请注意,Watcher是一次性的,意味着它在触发一次后需要重新注册。因此,Kafka Controller需要在每次处理完事件后重新为"/kafka/brokers/ids"节点设置一个新的Watcher。

新增结点和退役结点

新增结点后进行负载均衡

​ 如果我们对一个已经正在运行的kafka集群新增了Broker后,想让这个Broker参与之前创建的主题,可以使用负载均衡,将新的Broker配置进去,具体步骤为(假设新增的Broker的id是0,我们要负载均衡的主题为first主题):

  1. 创建一个topics-to-move.json文件,内容为:

    {
    "topics": [
    {"topic": "first"}
    ],
    "version": 1
    }
  2. 生成一个负载均衡的计划,使用命令(假设json文件就在这当前文件夹下)bin/kafka-reassign-partitions.sh --bootstrap-server hadoop1:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate

    结果会显示:

    Current partition replica assignment
    {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]}]}
    Proposed partition reassignment configuration
    {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
  3. 使用Proposed partition reassignment configuration的配置,创建副本存储计划(所有副本存储在 broker0、broker1、broker2、broker3 中)。假设创建的存储计划为increase-replication-factor.json;

    它的内容就是

    Proposed partition reassignment configuration
    {"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}
  4. 执行副本存储计划。bin/kafka-reassign-partitions.sh --bootstrap-server hadoop1:9092 --reassignment-json-file increase-replication-factor.json --execute

  5. 验证证副本存储计划。bin/kafka-reassign-partitions.sh --bootstrap-server hadoop1:9092 --reassignment-json-file increase-replication-factor.json --verify

    验证结果为:

    Status of partition reassignment:
    Reassignment of partition first-0 is complete.
    Reassignment of partition first-1 is complete.
    Reassignment of partition first-2 is complete.
    Clearing broker-level throttles on brokers 0,1,2,3
    Clearing topic-level throttles on topic first

    退役结点

    ​ 同样的,对该结点涉及的主题,进行负载均衡,方式还是和上面一致,将该broker中从主题中移除。我们可以使用其他工具进行方便的退役结点。

Connect相关

kafka的connect尝试

同步文件

尝试使用connect自带的文件source和sink测试文件的同步更新。

  1. standalone模式启动connect:

​ 其中的一些配置文件说明如下,首先是config目录下的connect-standalone.properties配置文件:

#设置需要连接到的Kafka节点
bootstrap.servers=localhost:9092
# key和value的转换器
key.converter=org.apache.kafka.connect.json.JsonConverter
value.converter=org.apache.kafka.connect.json.JsonConverter
# 是否也转换schema,如果设置为false的话,就只会转换payload
key.converter.schemas.enable=true
value.converter.schemas.enable=true
# offset保存于的文件
offset.storage.file.filename=/tmp/connect.offsets
# 自动刷新Offset的时间,单位时毫秒
offset.flush.interval.ms=10000

​ 因为我们要测试文件的同步,所以我们先编辑source的配置文件connect-file-source.properties,内容如下:

# Source的名字
name=local-file-source
# Source对应的类型
connector.class=FileStreamSource
# Task的数量,因为我们是standalone模式,所以只能是1个。在分布式部署时,可以设置为多个
tasks.max=1
# 需要读取的文件
file=/root/access.log
# 读取消息后存入的主题,这个主题最好提前创建出来,可以提前规划好分区和副本因子
topic=connect-test

​ 对应的sink的配置文件是connect-file-sink.properties

# Sink的名字
name=local-file-sink
# Sink的类型,因为我们是文件的connect,所以这个类不能动
connector.class=FileStreamSink
# Task的数量
tasks.max=1
# sink输出的文件
file=/root/access2.log.sink
# 消费的主题,必须与source的主题一致
topics=connect-test

connect-test主题的创建,如果我们不自己创建的话,connect是会自动创建的,我们就默认由connect自动创建。接着启动connect,使用如下命令进行启动:

bin/connect-standalone.sh -daemon config/connect-standalone.properties config/connect-file-source.properties config/connect-file-sink.properties

​ 对应的三个配置文件分别就是我们刚刚介绍的三个properties文件,运行命令后,查看jps命令,可以看到多了一个ConnectStandalone进程出来。

​ 这就是一个Worker了。下面我测试是否可以进行同步,多开一个ssh选项卡,使用tail -f /root/access2.log.sink 命令追踪该文件内容,可以发现,/root/access2.log.sink文件自动创建出来了,但是source的文件/root/access.log并没有自动创建,下面我们创建该文件touch /root/access.log,并向其中追加内容。通过echo命令新增内容。

​ 如果我修改,将“123”改成“321”的话:

access2.log.sink的内容不会修改。所以该方式是适合日志同步的,因为日志是追加编辑的,并且一般来说不会修改已经写入的内容。

Distributed模式尝试connect

​ 也就是分布式模式,在这种模式下,Connect可以以集群的形式运行在不同的节点上。不同节点上的Worker需要具有相同的group.id,并且这个group.id不能与消费者组的名字起冲突。在这种集群模式下的Kafka Connect具有良好的拓展性(相比于Standalone模式,单机模式只能启动一个Worker)和容错性。如果一个新的Worker上线,或者一个现有的Worker宕机,则其他的Worker会自动地分配Worker内运行的Connector与Task,以动态平衡集群的压力。

一、启动命令

​ 需要先编辑一下connect-distributed.properties配置文件,具体如下:

# A list of host/port pairs to use for establishing the initial connection to the Kafka cluster.
# 设置活跃的broker
bootstrap.servers=kafka1:9092,kafka2:9092,kafka3:9092
# unique name for the cluster, used in forming the Connect cluster group. Note that this must not conflict with consumer group IDs
# Kafka Connect集群的ID,相同的ID的Worker自动组成集群
group.id=connect-cluster
# The converters specify the format of data in Kafka and how to translate it into Connect data. Every Connect user will
# need to configure these based on the format they want their data in when loaded from or stored into Kafka
# key和value都是用Json转换器
key.converter=org.apache.kafka.connect.json.JsonConverter
value.converter=org.apache.kafka.connect.json.JsonConverter
# Converter-specific settings can be passed in by prefixing the Converter's setting with the converter we want to apply
# it to
key.converter.schemas.enable=true
value.converter.schemas.enable=true
# Topic to use for storing offsets. This topic should have many partitions and be replicated and compacted.
# Kafka Connect will attempt to create the topic automatically when needed, but you can always manually create
# the topic before starting Kafka Connect if a specific topic configuration is needed.
# Most users will want to use the built-in default replication factor of 3 or in some cases even specify a larger value.
# Since this means there must be at least as many brokers as the maximum replication factor used, we'd like to be able
# to run this example on a single-broker cluster and so here we instead set the replication factor to 1.
# 在单机模式下,offset的维护是写在一个文件里的,而在集群模式下,offset的维护是使用的一个topic进行维护的。
offset.storage.topic=connect-offsets
offset.storage.replication.factor=1
#offset.storage.partitions=25
# Topic to use for storing connector and task configurations; note that this should be a single partition, highly replicated,
# and compacted topic. Kafka Connect will attempt to create the topic automatically when needed, but you can always manually create
# the topic before starting Kafka Connect if a specific topic configuration is needed.
# Most users will want to use the built-in default replication factor of 3 or in some cases even specify a larger value.
# Since this means there must be at least as many brokers as the maximum replication factor used, we'd like to be able
# to run this example on a single-broker cluster and so here we instead set the replication factor to 1.
# 在集群模式下,config的维护也是使用一个主题。
config.storage.topic=connect-configs
config.storage.replication.factor=1
# Topic to use for storing statuses. This topic can have multiple partitions and should be replicated and compacted.
# Kafka Connect will attempt to create the topic automatically when needed, but you can always manually create
# the topic before starting Kafka Connect if a specific topic configuration is needed.
# Most users will want to use the built-in default replication factor of 3 or in some cases even specify a larger value.
# Since this means there must be at least as many brokers as the maximum replication factor used, we'd like to be able
# to run this example on a single-broker cluster and so here we instead set the replication factor to 1.
# 维护connect状态的主题
status.storage.topic=connect-status
status.storage.replication.factor=1
#status.storage.partitions=5
# Flush much faster than normal, which is useful for testing/debugging
# offset刷新的间隔
offset.flush.interval.ms=10000
# 下面可以看到,Kafka Connect集群监听REST API的端口是8083
# List of comma-separated URIs the REST API will listen on. The supported protocols are HTTP and HTTPS.
# Specify hostname as 0.0.0.0 to bind to all interfaces.
# Leave hostname empty to bind to default interface.
# Examples of legal listener lists: HTTP://myhost:8083,HTTPS://myhost:8084"
#listeners=HTTP://:8083
# The Hostname & Port that will be given out to other workers to connect to i.e. URLs that are routable from other servers.
# If not set, it uses the value for "listeners" if configured.
#rest.advertised.host.name=
#rest.advertised.port=
#rest.advertised.listener=
# Set to a list of filesystem paths separated by commas (,) to enable class loading isolation for plugins
# (connectors, converters, transformations). The list should consist of top level directories that include
# any combination of:
# a) directories immediately containing jars with plugins and their dependencies
# b) uber-jars with plugins and their dependencies
# c) directories immediately containing the package directory structure of classes of plugins and their dependencies
# Examples:
# plugin.path=/usr/local/share/java,/usr/local/share/kafka/plugins,/opt/connectors,
# 插件的位置,这个比较重要,因为kafka connect自带的只有FileStreamSink和FileStreamSource,如果我们要转换jdbc或者ES等,就需要到confluent官网上下载插件,然后放到这个目录下
plugin.path=/opt/module/kafka/plugins

​ 接着把这个编辑好的connect-distributed.properties配置文件分发到其他kafka Connect节点上,因为是可以动态上线的,只要保证配置文件的关键信息一致(bootstrap.serversgroup.id),那么谁先启动谁后启动就不重要了。

​ 使用下面命令以集群模式启动Kafka Connect,接着使用jps查看进程会发现ConnectDistributed进程已启动。

bin/connect-distributed.sh -daemon config/connect-distributed.properties

REST API

​ Kafka Connect在集群模式下的启动,并没有设置输入、输出的属性,而这些Connector都是需要我们在后面手动添加和维护的。集群模式的Kafka Connect可以通过Rest风格的API以http请求的方式调用。

1.创建Connector

​ 比如我们以集群模式,启动一个读取文件的Worker。

curl -X POST -H 'Content-Type:application/json' -i 'http://kafka1:8083/connectors' \
--data \
'{
"name":"test-file-source",
"config":{
"connector.class":"FileStreamSource",
"tasks.max":3,
"file":"/root/access.log",
"topic":"connect-distributed-test"
}
}'

​ 其实发送的json内容与standalone模式下的connect-file-source.properties的内容是对应的。

2.查看所有的Connector
curl -X GET 'http://kafka1:8083/connectors'

3.删除指定的Connector
curl -X DELETE 'http://kafka1:8083/connectors/test-file-source'
4.查看指定Connector的运行状态
curl -X GET 'http://kafka1:8083/connectors/test-file-source/status'

5.暂停指定的Connector
curl -X PUT 'http://kafka1:8083/connectors/test-file-source/pause'

6.恢复指定的Connector
curl -X PUT 'http://kafka1:8083/connectors/test-file-source/resume'

​ 因为我们还没配sink,所以我们可以通过consumer进行消费,消费命令如下:

bin/kafka-console-consumer.sh --bootstrap-server kafka1:9092,kafka2:9092,kafka3:9092 --topic connect-distributed-test --from-beginning

​ 显示结果如下(会把文件的所有内容消费出来,但是sink只会消费or同步新数据):


​ 以同样的方式创建sink,不过要注意connector.classtopics还有file与source不同,而且connector的名字也要不同。

curl -X POST -H 'Content-Type:application/json' -i 'http://kafka1:8083/connectors' \
--data \
'{
"name":"test-file-sink",
"config":{
"connector.class":"FileStreamSink",
"tasks.max":3,
"file":"/root/access.log.sink",
"topics":"connect-distributed-test"
}
}'

7.重启指定的Connector
curl -X POST 'http://kafka1:8083/connectors/test-file-source/restart'
8.获取所有的Task
curl -X GET 'http://kafka1:8083/connectors/test-file-source/tasks'
9.查看指定Tasks的运行状态
curl -X GET 'http://kafka1:8083/connectors/test-file-source/tasks/0/status'
10.获取所有插件
curl -X GET 'http://kafka1:8083/connector-plugins'

同步数据库

同步步骤

一、下载好对应的JDBC plugins和Jar包

​ 在confluent上下载plugins(confluentinc-kafka-connect-jdbc-10.6.0.zip),JDBC Connector (Source and Sink) | Confluent Hub,之后解压后放到之前配置的路径下/opt/module/kafka/plugins,一些参数和配置说明可以查看JDBC Source Connector Configuration Properties | Confluent Documentation。准备好plugins后还要准备好对应的连接数据库的JAR包,因为我用的是Mysql 8.0.31版本,所以使用mysql-connector-j-8.0.31.jar文件,将它放到kafka/libs文件夹下。

二、创建对应的Source的Connector

​ mode采用的是增量方式,监控id字段。有许多source的方式,具体可以查看官方文档。

curl -X POST -H 'Content-Type:application/json' -i 'http://kafka1:8083/connectors' \
--data \
'{
"name":"jdbc-source-connector2",
"config":{
"connector.class":"io.confluent.connect.jdbc.JdbcSourceConnector",
"connection.url":"jdbc:mysql://kafka1:3306/test1",
"connection.user":"root",
"connection.password":"root",
"mode":"incrementing",
"incrementing.column.name":"id",
"topic.prefix":"mysql-",
"table.whitelist":"student_1"
}
}'

​ 如果数据库没有数据的话,topic不会自动创建。数据库中有数据的话,会自动创建topic为mysql-student_1

​ 如果我们直接消费这个topic的数据(前提是配置的表里已经存在有数据),那么会直接读到表的内容:

二、创建对应的Sink的Connector

​ 将test1数据库里的student_1同步至test2数据库的student_2表中,如果student_2表不存在的话就自动创建。

curl -X POST -H 'Content-Type:application/json' -i 'http://kafka1:8083/connectors' \
--data \
'{
"name":"jdbc-sink-connector2",
"config":{
"connector.class":"io.confluent.connect.jdbc.JdbcSinkConnector",
"connection.url":"jdbc:mysql://kafka1:3306/test2",
"connection.user":"root",
"connection.password":"root",
"topics":"mysql-student_1",
"insert.mode":"upsert",
"pk.mode":"record_value",
"pk.fields":"id",
"table.name.format":"student_2",
"auto.create":"true"
}
}'

​ 结果如下:

​ 增量方式比较适合主键严格自增,因为他是根据id来判断是否增量同步的。

参数说明

Source参数说明

摘自:JDBC Source Connector Configuration Properties | Confluent Documentation

一、mode参数:

​ 1.mode表示每次轮询时更新表的模式。类型为string, 选项包括:"","bulk","timestap","incrementing","timestamp+incrementing"。

  • bulk:每次轮询时执行整个表的批量加载。每次都会拉取整个表,适合会定时删除数据的表,若表不会定时删除数据,则使用bulk模式会导致数据重复的情况。
  • incrementing:在每个表上使用严格递增的列以仅检测新行。 请注意,这不会检测现有行的修改或删除。适合只会增加数据的表,并且该表有一个严格递增的列(最好是主键,比如是id)。
  • timestamp:使用时间戳(或类似时间戳)列来检测新行和修改后的行。 这假设列随着每次写入而更新,并且值是单调递增的,但不一定是唯一的。
  • timestamp+incrementing:使用两列,一个用于检测新行修改行的时间戳列,以及一个为更新提供全局唯一 ID 的严格递增列,因此可以为每一行分配一个唯一的流偏移量。

​ 2.重要程度:较高!

​ 3.Mode参数依赖于incrementing.column.name,timestamp.column.name,validate.non.null

​ 4.参数项解释:

​ (1)incrementing.column.name:用于检测新行的严格递增列的名称。 任何空值表示应通过查找自动递增列来自动检测该列。 此列不能为空。类型为String,默认为"",重要程度中等。

​ (2)timestamp.column.name:一个或多个时间戳列的逗号分隔列表,用于使用 COALESCE SQL 函数检测新的或修改的行。 每次轮询都会发现第一个非空时间戳值大于先前看到的最大时间戳值的行。 至少一列不应为空。类型为String,默认为"",重要程度中等。

​ (3)timestamp.initial:用于使用时间戳条件的初始查询的纪元时间戳。 使用 -1 以使用当前时间。 如果未指定,将检索所有数据。类型为long,默认为null,重要程度低。

​ (4)validate.non.null:默认情况下,JDBC 连接器将验证所有递增表和时间戳表都没有为用作其 ID/时间戳的列设置 NOT NULL(即ID/时间戳都不能为空)。 如果表不存在,JDBC 连接器将无法启动。 将此设置为 false 将禁用这些检查。类型为boolean,默认为true,重要程度低。

​ (5)query:如果指定,query可以选择新的或更新的行。 如果您想要连接表、选择表中列的子集或筛选数据,请使用此设置。 如果使用,连接器将使用此查询复制数据,并且禁用全表复制。

​ (6)quote.sql.identifiers:何时在 SQL 语句中引用表名、列名和其他标识符。 为了向后兼容,默认值为always。类型为string,默认为"always",重要程度中等。

​ (7)query.suffix:要附加在生成的查询末尾的后缀。类型为string,默认为“”,重要程度低。

​ (8)transaction.isolation.mode:在对数据库运行查询时控制使用哪个事务隔离级别的模式。 默认情况下,没有设置显式事务隔离模式。SQL_SERVER_SNAPSHOT 仅适用于配置为写入 SQL Server 的连接器。选项包括:DEFAULT,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE以及 SQL_SERVER_SNAPSHOT.

二、Connection参数(1~6也是Sink通用的)

  1. connection.attempts:尝试获取有效 JDBC 连接的最大次数。 该值必须是正整数。默认值为3.
  2. connection.backoff.ms:连接尝试之间的退避(间隔)时间(以毫秒为单位)。类型为long,默认值为10000(即10s)。
  3. connection.url:JDBC连接的URL。例如jdbc:oracle:thin:@localhost:1521:orclpdb1, jdbc:mysql://localhost/db_name, jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=db_name。类型为string,重要程度高。
  4. connection.user:JDBC连接时的用户名。重要程度高。
  5. connection.password:JDBC连接时的密码。重要程度高。
  6. dialect.name:应该用于此连接器的数据库方言的名称。 默认情况下这是空的,连接器会根据 JDBC 连接 URL 自动确定方言。 如果您想覆盖该行为并使用特定方言,请使用此选项。 可以使用 JDBC 连接器插件中所有正确打包的方言。类型为string,默认为"",可选值:[, Db2DatabaseDialect, MySqlDatabaseDialect, SybaseDatabaseDialect, GenericDatabaseDialect, OracleDatabaseDialect, SqlServerDatabaseDialect, PostgreSqlDatabaseDialect, SqliteDatabaseDialect, DerbyDatabaseDialect, SapHanaDatabaseDialect, MockDatabaseDialect, VerticaDatabaseDialect]。其实就是对应着你连接的数据库的类型。
  7. catalog.pattern:从数据库中获取表元数据的目录模式。
  8. table.whitelist:要包含在复制中的表列表。 如果指定,则不要设置 table.blacklist。 使用逗号分隔的列表指定多个表(例如,table.whitelist: "User, Address, Email")。
  9. table.blacklist:要从复制中排除的表列表。 如果指定,则不要设置 table.whitelist。 使用逗号分隔的列表指定多个表(例如,table.blacklist: "User, Address, Email")。
  10. schema.pattern:从数据库中获取表元数据的模式模式。类型为string,默认为null,若为""则表示不采用模式来获取元数据。null(默认值)表示不使用架构名称来缩小搜索范围,并且无论架构如何,都会提取所有表元数据。重要程度高!如果将其保留为默认的 null 设置,连接器可能会超时并因接收到大量表元数据而失败。 确保为大型数据库设置此参数。

三、Connector参数

  1. table.types:默认情况下,JDBC 连接器只会从源数据库中检测类型为 TABLE 的表。 此配置允许提取以逗号分隔的表类型列表。类型为list,可选值:TABLE,VIEW ,SYSTEM TABLE ,GLOBAL TEMPORARY, LOCAL TEMPORARY, ALIAS, SYNONYM。在大多数情况下,只有 TABLEVIEW 才有意义。默认值为TABLE
  2. poll.interval.ms:轮询每个表中新数据的频率(以毫秒为单位)。类型为int,默认为5000(即5s),重要程度高。
  3. batch.max.rows:轮询新数据时要包含在单个批次中的最大行数此设置可用于限制连接器内部缓冲的数据量。类型为int,默认为100,重要程度为低。
  4. table.poll.interval.ms:轮询新表或删除表的频率(以毫秒为单位),这可能会导致更新任务配置以开始轮询添加表中的数据或停止轮询已删除表中的数据。
  5. topic.prefix:添加到表名的前缀以生成要将数据发布到的 Apache Kafka® 主题的名称,或者在自定义查询的情况下,生成要发布到的主题的全名。主题就是:前缀+表名。
  6. timestamp.delay.interval.ms:在我们将其包含在结果中之前,在具有特定时间戳的行出现后等待多长时间。 您可以选择添加一些延迟以允许具有较早时间戳的事务完成。 第一次执行将获取所有可用记录(即从时间戳 0 开始),直到当前时间减去延迟。 接下来的每次执行都将获取从我们上次获取到当前时间减去延迟的数据。类型为long,默认为0,重要程度为高。
  7. db.timezone:使用基于时间的条件进行查询时连接器中使用的 JDBC 时区的名称。 默认为 UTC。
  8. timestamp.granularity:定义时间戳列的粒度。 选项包括:Valid Values: [connect_logical, nanos_long, nanos_string, nanos_iso_datetime_string]
    • connect_logical (default):使用 Kafka Connect 的内置表示表示时间戳值
    • nanos_long:represents timestamp values as nanos since epoch
    • nanos_string:represents timestamp values as nanos since epoch in string
    • nanos_iso_datetime_string:使用 iso 格式‘yyyy-MM-dd’T’HH:mm:ss.n’

Sink参数说明

摘自:JDBC Sink Connector Configuration Properties | Confluent Documentation

一、Writes

  1. insert.mode:要使用的插入模式。类型为string,默认为insert,可选值:[insert, upsert, update],重要程度高!

    • insert:使用标准 SQL INSERT 语句。
    • upsert:如果连接器支持,则对目标数据库使用适当的更新插入语义——例如,INSERT OR IGNORE。 使用 upsert 模式时,您必须在连接器配置中添加和定义 pk.modepk.fields 属性。 例如:
    {
    ...
    "pk.mode": "record_value",
    "pk.fields": "id"
    ...
    }

    ​ 在前面的示例中,pk.fields 应该包含您的主键。

    • update:如果连接器支持,则对目标数据库使用适当的更新语义——例如,UPDATE
  2. batch.size:在可能的情况下,指定尝试将多少记录一起批处理以插入到目标表中。类型为int,默认值为3000,重要程度中等。

  3. delete.enabled:是否将null记录值视为删除。 要求 pk.mode record_key。类型为boolean,默认为false,重要程度中等。

二、Data Mapping

  1. table.name.format目标表名称的格式字符串,其中可能包含“${topic}”作为原始主题名称的占位符。例如,主题“orders”的 kafka_${topic} 将映射到表名“kafka_orders”。例如我刚刚的例子的sink,sink对应的表名为student_2。
  2. pk.mode:主键模式,交互也参考pk.fields的说明(在下一项中)。重要程度高。 支持的模式有:
    • none:没有主键使用。
    • kafka:Apache Kafka® 坐标用作主键。
    • record_key:使用记录键中的字段,该字段可以是原始字段或结构(字段组合)。
    • record_value:使用记录值中的字段,它必须是一个结构。

尝试切换mode

一、Source

​ 使用timestamp+incrementing

curl -X POST -H 'Content-Type:application/json' -i 'http://kafka1:8083/connectors' \
--data \
'{
"name":"jdbc-source-withTime",
"config":{
"connector.class":"io.confluent.connect.jdbc.JdbcSourceConnector",
"connection.url":"jdbc:mysql://kafka1:3306/test1",
"connection.user":"root",
"connection.password":"root",
"mode":"timestamp+incrementing",
"incrementing.column.name":"id",
"timestamp.column.name":"updateTime",
"topic.prefix":"mysql-",
"table.whitelist":"employee_with_time"
}
}'

​ 我的表结构为

-- test1.employee_with_time definition
CREATE TABLE `employee_with_time` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`location` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`updateTime` timestamp NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

​ 使用id+updateTime作为自增列和更新的时间,如果数据库里的updateTime更新了(比上一次最新的要大,无论这条记录的其他字段是否有修改),那么这条记录就会再次读到主题里。

​ 其中的更新逻辑为,因为updateTimetimestamp.column.name指定的列。就以上图为例,只有updateTime大于“2022-11-25 13:23:33”才会检测到。例如我把id=1的记录的updateTime改为“2022-11-25 10:24:33”,该时间比"2022-11-25 10:22:33"要大,但是比“2022-11-25 13:23:33”要小,那本次修改就不会被记录。

​ 如果改成以下记录,改成比“2022-11-25 13:23:33”要大的时间记录,则会被source检测到,并且生产进topic中。

二、Sink

​ 那么以此为思路再写一个sink,想达到的需求就是,不仅可以把新增的行通不过去,也可以把修改的行同步过去。

curl -X POST -H 'Content-Type:application/json' -i 'http://kafka1:8083/connectors' \
--data \
'{
"name":"jdbc-sink-withTime1",
"config":{
"connector.class":"io.confluent.connect.jdbc.JdbcSinkConnector",
"connection.url":"jdbc:mysql://kafka1:3306/test2",
"connection.user":"root",
"connection.password":"root",
"topics":"mysql-employee_with_time",
"insert.mode":"upsert",
"pk.mode":"record_value",
"pk.fields":"id",
"table.name.format":"employee_with_time"
}
}'

​ 使用upsert模式,理论上是可以更新修改后的数据,并且把新增的记录也同步过去。经过测试,只要updateTime大于全局最大的updateTime,就会同步过去,实际上这也是符合逻辑的,因为我们修改数据时的时间点应该是大于显存的所有的updateTime时间戳,新增记录同样也是检测这个updateTime

​ Sink到的数据库建议是提前创建出来(直接按照源表复制过去)的,官方文档也建议先由自己提前创建好Sink到的表。

工作中遇到的问题

时间转换出错

​ 在从Mysql导出数据到Pgsql时,会出现时间会快8个小时的情况。最后排查发现Mysql使用的版本为:5.7.35,但是驱动也用的5.0版本,这个版本的渠道取时间时会出现bug,驱动换成8.0的驱动即可解决这个问题。

posted on   Ari的小跟班  阅读(314)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示