Kafka 基础
安装
zookeeper 安装参照:Dubbo 基础
zookeeper 基本使用参照:Zookeeper 基础
- 解压
# 安装 /app/kafka_2.12-1.0.2.tgz
tar -zxf kafka_2.12-1.0.2.tgz
- 配置环境变量
vim /etc/profile
# 在文件中插入如下
export KAFKA_HOME=/app/kafka_2.12-1.0.2
export PATH=$PATH:KAFKA_HOME/bin
# 使配置文件生效
./etc/profile
- 设置 kafka 持久化消息的文件位置
cd /app/kafka_2.12-1.0.2/config
vim server.properties
# 设置 kafka 持久化消息的文件位置
log.dirs=/app/kafka-logs
# 设置 kafka 在 zookeeper 里面的根节点位置
zookeeper.connect=localhost:2181/kafka-msg
# 创建持久化消息的文件夹
mkdir -p /app/kafka-logs
- 启动
kafka-server-start.sh /app/kafka_2.12-1.0.2/config/server.properties
- 登录 zookeepr 可以看见
ls /kafka-msg
>[cluster, controller, controller_epoch, brokers, admin, isr_change_notification, consumers, log_dir_event_notification, latest_producer_id_block, config]
后台方式启动:
kafka-server-start.sh -daemon /app/kafka_2.12-1.0.2/config/server.properties
命令行基本使用
主题
kafka-topics.sh 用于管理主题
列出所有主题 --list
kafka-topics.sh --list --zookeeper localhost:2181/kafka-msg
创建主题 --create
partitions:指定分区;支持横向扩展。多个主题会被 kafka 尽可能平均到不同的服务器上。
replication-factor:指定一个分区有几个副本;提供高可用。
kafka-topics.sh --create --topic topic1 --partitions 1 --replication-factor 1 --zookeeper localhost:2181/kafka-msg
创建主题时指定配置
kafka-topics.sh --create --topic topic2 --partitions 1 --replication-factor 1 --zookeeper localhost:2181/kafka-msg --config cleanup.policy=delete --config compression.type=gzip
- 查询重写了配置的主题
kafka-topics.sh --zookeeper localhost:2181/kafka-msg --topics-with-overrides --describe
查询主题信息 --describe
kafka-topics.sh --describe --topic topic1 --zookeeper localhost:2181/kafka-msg
- PartitionCount:有多少个分区
- ReplicationFactor:有多少个副本
- Partition:分区在几号分区上
- Leader;Replicas;Isr 当前分区的这些信息在哪个服务器上
修改主题 --alter
修改主题的配置
# 过时的方法
# kafka-topics.sh --zookeeper localhost:2181/kafka-msg --alter --topic topic2 --config max.message.bytes=1048576
kafka-configs.sh --zookeeper localhost:2181/kafka-msg --alter --entity-type topics -entity-name topic2 --add-config max.message.bytes=1048576
删除主题的配置
# 过时的方法
# kafka-topics.sh --zookeeper localhost:2181/kafka-msg --alter --delete-config max.message.bytes --topic topic2
kafka-configs.sh --zookeeper localhost:2181/kafka-msg --alter --entity-type topics -entity-name topic2 --delete-config max.message.bytes
删除主题 --delete
kafka-topics.sh --delete --topic topic1 --zookeeper localhost:2181/kafka-msg
生产者
kafka-console-producer.sh 用于生产消息
开启生产者
# 给哪个服务器的 哪个主题生产消息
kafka-console-producer.sh --broker-list localhost:9092 --topic topic1
消费者
kafka-console-consumer.sh 用于消费消息
开启消费者
# 从 哪个服务器 的哪个主题消费消息
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic topic1
# 从头开始消费消息 接着发消息也能收到
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic topic1 --from-beginning
偏移量
kafka-consumer-groups.sh 用于查询各消费者的偏移量
列出正在消费的消费组
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
查询指定消费者的消费情况
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group group1
TOPIC | PARTITION | CURRENT-OFFSET | LOG-END-OFFSET | LAG | CONSUMER-ID | HOST | CLIENT-ID
topic1 | 0 | 9 | 9 | 0 | consumer-1-150d1897-3037-XXX | 192.168.83.208 | consumer-1
- TOPIC:主题
- PARTITION:分区
- CURRENT-OFFSET:当前已经消费的偏移量
- LOG-END-OFFSET:总数据量
- LAG:未消费的数据量
- CONSUMER-ID:消费者id
- HOST:消费者主机ip
- CLIENT-ID:客户端id
将偏移量设置为最早
# 下述语句只是查看分配后的偏移量 并不执行;若要执行需要添加 --execute
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --reset-offsets --group group1 --to-earliest --topic topic1
将偏移量设置为最新
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --reset-offsets --group group1 --to-latest --topic topic1 --execute
指定偏移量
# --shift-by 可以是正负数;添加 --execute 执行;topic1:0 => 0号分区
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --reset-offsets --group group1 --shift-by -2 --topic topic1:0
注意:仅当组处于非活动状态,但当前状态稳定时,才能重置分配。
Java 客户端操作 Kafka
远程连接 kafka 需要配置:vim conf/server.properties
listeners=PLAINTEXT://127.0.0.1:9092 # 私网 ip
advertised.listeners=PLAINTEXT://127.0.0.1:9092 # 公网 ip
生产者 KafkaProducer
创建生产者
Map<String, Object> configs = new HashMap<>();
// 配置生产者如何与broker建立连接
configs.put("bootstrap.servers", "8.142.69.204:9092");
// 要发送信息的key数据的序列化类
configs.put("key.serializer", IntegerSerializer.class);
// 要发送消息的value数据的序列化类
configs.put("value.serializer", StringSerializer.class);
// acks = 1:表示消息只需要写到主分区即可,然后就响应客户端,而不等待副本分区的确认。
// 在该情形下,如果主分区收到消息确认之后就宕机了,而副本分区还没来得及同步该消息,则该消息丢失
// acks = 0:生产者不等待broker对消息的确认,只要将消息放到缓冲区,就认为消息已经发送完成。
// 该情形不能保证broker是否真的收到了消息,retries配置也不会生效。发送的消息的返回的消息偏移量永远是 -1。
// acks = all:首领分区会等待所有的ISR副本分区确认记录。该处理保证了只要有一个ISR副本分区存活,消息就不会丢失。
// 这是Kafka最强的可靠性保证,等效于 acks=-1
configs.put("acks", "1");
// 重试次数;MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 保证消息的有序性;
// 否则在重试此失败消息的时候,其他的消息可能发送成功了
// configs.put("retries", "1");
KafkaProducer<Integer, String> producer = new KafkaProducer<>(configs);
封装消息
// 主题;分区;key;value
ProducerRecord<Integer, String> producerRecord = new ProducerRecord<>("topic1", 0, 1001, "hello kafka");
消息的确认
// 消息的同步确认
Future<RecordMetadata> future = producer.send(producerRecord);
// 消息的异步确认
producer.send(producerRecord, (recordMetadata, e) -> {});
消费者 KafkaConsumer
创建消费者
Map<String, Object> configs = new HashMap<>();
// 配置生产者如何与broker建立连接
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "8.142.69.204:9092");
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// 消费组
configs.put(ConsumerConfig.GROUP_ID_CONFIG, "group1");
// 从哪个偏移量开始消费
// earliest:自动重置偏移量到最老的偏移量
// latest:自动重置偏移量到最新的偏移量
configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(configs);
订阅主题
consumer.subscribe(Collections.singletonList("topic1"));
每隔 3S 循环拉取消息
while (true){
// 拉取消息
ConsumerRecords<Integer, String> consumerRecords = consumer.poll(3000);
for (ConsumerRecord<Integer, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
主题 KafkaAdminClient
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.84.145:9092");
KafkaAdminClient kafkaAdminClient = (KafkaAdminClient) KafkaAdminClient.create(configs);
查询主题
ListTopicsResult listTopicsResult = kafkaAdminClient.listTopics();
Collection<TopicListing> topicListings = listTopicsResult.listings().get();
for (TopicListing topicListing : topicListings) {
// 主题名称;是否是内部主题
System.out.println(topicListing);// (name=topic1, internal=false) (name=topic2, internal=false)
}
ListTopicsOptions listTopicsOptions = new ListTopicsOptions();
// 设置超时时间 单位毫秒
listTopicsOptions.timeoutMs(500);
// 列出内部主题 (name=__consumer_offsets, internal=true)
listTopicsOptions.listInternal(true);
ListTopicsResult listTopicsResult = kafkaAdminClient.listTopics(listTopicsOptions);
Collection<TopicListing> topicListings = listTopicsResult.listings().get();
for (TopicListing topicListing : topicListings) {
System.out.println(topicListing);
}
创建主题
// topicName;partition;replication
NewTopic topic3 = new NewTopic("topic3", 1, (short) 1);
CreateTopicsResult topics = kafkaAdminClient.createTopics(Collections.singleton(topic3));
topics.values().forEach((name, voidKafkaFuture) -> System.out.println(name));
删除主题
DeleteTopicsResult topic3 = kafkaAdminClient.deleteTopics(Collections.singleton("topic3"));
topic3.values().forEach((name, voidKafkaFuture) -> System.out.println(name));
更新主题配置信息
ConfigEntry configEntry = new ConfigEntry("cleanup.policy", "compact");
Map<ConfigResource, Config> alterConfigs = new HashMap<>();
Config config = new Config(Collections.singleton(configEntry));
ConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC, "topic2");
alterConfigs.put(configResource, config);
AlterConfigsResult alterConfigsResult = kafkaAdminClient.alterConfigs(alterConfigs);
alterConfigsResult.values().forEach((configResource1, voidKafkaFuture) -> System.out.println(configResource1.toString()));
增加分区
Map<String, NewPartitions> newPartitions = new HashMap<>();
NewPartitions newPartition = NewPartitions.increaseTo(5);
newPartitions.put("topic4", newPartition);
CreatePartitionsResult createPartitionsResult = kafkaAdminClient.createPartitions(newPartitions);
Map<String, KafkaFuture<Void>> values = createPartitionsResult.values();
Set<Map.Entry<String, KafkaFuture<Void>>> entries = values.entrySet();
for (Map.Entry<String, KafkaFuture<Void>> entry : entries) {
System.out.println(entry.getKey());
}
描述主题
DescribeTopicsResult describeTopicsResult = kafkaAdminClient.describeTopics(Collections.singleton("topic2"));
Map<String, KafkaFuture<TopicDescription>> values = describeTopicsResult.values();
Set<Map.Entry<String, KafkaFuture<TopicDescription>>> entries = values.entrySet();
for (Map.Entry<String, KafkaFuture<TopicDescription>> entry : entries) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().get().toString());
}
注意:记得关闭 KafkaAdminClient 否则创建和更新会没有效果
kafkaAdminClient.close();
SpringBoot Kafka 基本使用
pom.xml
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
生产者
同步确认消息
ProducerRecord<Integer, String> producerRecord = new ProducerRecord<>("topic1", 0, 1001, "hello kafka");
ListenableFuture future = kafkaTemplate.send(producerRecord);
ListenableFuture<SendResult<Integer, String>> future = kafkaTemplate.send("topic1", 0, 1001, "hello kafka");
RecordMetadata recordMetadata = future.get().getRecordMetadata();
异步确认消息
ListenableFuture<SendResult<Integer, String>> future = kafkaTemplate.send("topic1", 0, 1001, "hello kafka");
future.addCallback(new ListenableFutureCallback<>() {
@Override
public void onFailure(Throwable ex) {
System.out.println("消息发送失败:" + ex.getMessage());
}
@Override
public void onSuccess(SendResult<Integer, String> result) {
System.out.println("消息发送成功");
RecordMetadata recordMetadata = result.getRecordMetadata();
System.out.println(recordMetadata);
}
});
消费者
@KafkaListener(topics = "topic1")
public void onMessage(ConsumerRecord<Integer, String> record){
System.out.println("收到消息:" + record);
}
生产者发送消息的时候会默认创建主题
高级特性
生产者
参数配置
参数名称 | 描述 |
---|---|
batch.size | 每当多个记录被发送到同一分区时,生产者将尝试将记录批处理到一起,以减少请求。 此配置控制以字节为单位的默认批处理大小。 |
acks | =0: 生产者根本不会等待 broker 的任何确认。该记录将立即添加到套接字缓冲区,并被视为已发送。 这种情况下,无法保证 broker 已收到记录,重试配置不生效(因为客户端通常不知道任何故障)。 每条记录返回的偏移量将始终设置为 -1。 =1: Leader 把记录写入其本地日志,但不等待所有 Flower 的完全确认的情况下做出响应。 在这种情况下,如果 Leader 在确认记录后立即失败,但在 Flower 复制记录之前,记录将丢失。 = -1: Leader 将等待所有同步副本确认记录。 这保证了只要至少有一个同步副本保持活动状态,记录就不会丢失。 |
linger.ms | 生产者不会立即发送一条记录,而是等待给定的延迟,以允许发送其他记录,从而可以将发送的记录批处理在一起。此设置默认为0(即无延迟)。 |
client.id | 生产者发送请求的时候传递给 broker 的 id 字符串。 |
compression.type | 生产者生成的所有数据的压缩类型。默认值为 none(即无压缩)。 有效值为:none、gzip、snappy、lz4 。 压缩是对整批数据的压缩,因此批处理的效果也会影响压缩率(批处理越多,压缩效果越好)。 |
retries | 如果记录发送失败,设置一个大于零的值将导致客户端重新发送记录。 |
max.in.flight.requests.per.connection | 在阻塞之前,客户端在单个连接上发送的最大未确认请求数。 如果此设置设置为大于1,并且存在发送失败的情况,则存在由于重试而导致消息重新排序的风险(即启用了重试)。 |
interceptor.classes | 配置拦截器类的列表。 即实现了 org.apache.kafka.clients.producer.ProducerInterceptor 接口的类。 默认情况下,没有拦截器。 |
key.serializer | 实现了 org.apache.kafka.common.serialization.Serializer 接口的 key 序列化类。 |
value.serializer | 实现了 org.apache.kafka.common.serialization.Serializer 接口的 value 序列化类。 |
拦截器
// 拦截器 多个拦截器拿逗号隔开
configs.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.demo.config.Interceptor1");
public class Interceptor1 implements ProducerInterceptor {
@Override
public ProducerRecord onSend(ProducerRecord producerRecord) {
System.out.println("消息确认前1...");
return producerRecord;
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
System.out.println("消息确认后1...");
}
@Override
public void close() {
System.out.println("关闭 Interceptor1 ...");
}
@Override
public void configure(Map<String, ?> map) {
System.out.println("获取一些自定义配置1..." + map.get("batch.size"));
}
}
序列化器
// 定义实体类
public class User implements Serializable {}
// 自定义序列化器
public class UserSerializer implements Serializer<User> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
System.out.println(configs);
System.out.println(isKey);
}
@Override
public byte[] serialize(String topic, User data) {
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(data);
oos.flush();
bytes = bos.toByteArray();
oos.close();
bos.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return bytes;
}
@Override
public void close() {
System.out.println("close...");
}
}
// 配置自定义序列化器
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, UserSerializer.class);
KafkaProducer<Integer, User> producer = new KafkaProducer<>(configs);
// 使用自定义序列化器发消息
User user = new User();
user.setId(1);
user.setUsername("张三");
ProducerRecord<Integer, User> producerRecord = new ProducerRecord<>("topic1", 0, 1001, user);
分区器
org.apache.kafka.clients.producer.Partitioner 接口
默认实现类:org.apache.kafka.clients.producer.internals.DefaultPartitioner
- 如果在 ProducerRecord 中指定了分区,则使用指定的分区号。
- 如果未指定分区,但存在 key ,根据 key 的哈希值选择一个分区。
- 如果没有分区和 key,则以循环方式选择一个分区。
// 自定义分区器
public class Partitioner1 implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 指定分区 ID
return 0;
}
@Override
public void close() {
System.out.println("Partitioner1 close...");
}
@Override
public void configure(Map<String, ?> configs) {
System.out.println(configs);
}
}
// 使用自定义分区器
configs.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.demo.config.Partitioner1");
消费者
参数配置
参数名称 | 描述 |
---|---|
key.deserializer | 实现了 org.apache.kafka.common.serialization.Deserializer 接口的 key 反序列化类。 |
value.deserializer | 实现了 org.apache.kafka.common.serialization.Deserializer 接口的 value 反序列化类。 |
client.id | 当从服务器消费消息的时候向服务器发送的id字符串。 |
group.id | 用于唯一标识当前消费者所属的消费组的字符串。 |
partition.assignment.strategy | 当使用消费组的时候,分区分配策略的类名。 |
auto.offset.reset | 当 kafka 中没有初始偏移量或当前偏移量在服务器中不存在时的处理方式: 1)earliest:自动重置偏移量到最早的偏移量; 2)latest:自动重置偏移量为最新的偏移量; 3)none:如果消费组原来的(previous)偏移量不存在,则向消费者抛异常; 4)anything:向消费者抛异常。 |
enable.auto.commit | 默认true。是否自动周期性地向服务器提交偏移量。 |
auto.commit.interval.ms | 默认 5000 ms。周期性提交的时间。 |
fetch.min.bytes | 服务器获取请求返回的最小数据量。 如果没有达到参数值会让请求等待,以让更多数据累计,达到参数值后响应请求。 单位字节。 |
fetch.max.wait.ms | 如果数据量达不到 fetch.min.bytes。服务端最大的阻塞时长。 |
session.timeout.ms | 当使用 Kafka 的消费组的时候,消费者周期性地向 broker 发送心跳数表明自己的存在。如果经过该超时时间还没有收到消费者的心跳则 broker 将消费者从消费组移除,并启动再平衡。 |
heartbeat.interval.ms | 当使用消费组的时候,该条目指定消费者向消费者协调器发送心跳的时间间隔。 该条目的值必须小于 session.timeout.ms,也不应该高于 session.timeout.ms 的1/3。 |
max.poll.interval.ms | 消费者调用 poll() 方法的最大时间间隔。 如果在此时间内消费者没有调用 poll() 方法,则 broker 认为消费者失败,触发再平衡。 |
max.poll.records | 一次调用 poll() 方法返回的记录最大数量。 |
interceptor.classes | 拦截器类的列表。该拦截器需要实现 org.apache.kafka.clients.consumer.ConsumerInterceptor。 |
retry.backoff.ms | 在发生失败的时候如果需要重试,则该配置表示客户端等待多长时间再发起重试。 |
request.timeout.ms | 客户端等待服务端响应的最大时间。如果该时间超时,则客户端会重新发起请求,如果重试耗尽,请求失败。 |
reconnect.backoff.ms | 重新连接主机的等待时间。 |
reconnect.backoff.max.ms | 重新连接到反复连接失败的broker时要等待的最长时间。 |
拦截器
// 消费者方拦截器 多个拦截器拿逗号隔开
configs.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.demo.config.Interceptor3,com.demo.config.Interceptor4");
public class Interceptor3 implements ConsumerInterceptor {
@Override
public ConsumerRecords onConsume(ConsumerRecords records) {
System.out.println("poll 方法返回结果之前...");
System.out.println("消费者拦截器 onConsume 3...");
System.out.println(records);
// 消息不做处理直接返回
return records;
}
@Override
public void close() {
System.out.println("关闭 Interceptor3 ...");
}
@Override
public void onCommit(Map map) {
System.out.println("消费者提交偏移量的时候提交该方法...");
System.out.println("消费者拦截器 onCommit 3...");
System.out.println(map);
}
@Override
public void configure(Map<String, ?> map) {
System.out.println("获取一些自定义配置3..." + map);
}
}
反序列化器
// 自定义反序列化器
public class UserDeSerializer implements Deserializer<User> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
System.out.println(configs);
System.out.println(isKey);
}
@Override
public User deserialize(String topic, byte[] data) {
User user = new User();
ByteArrayInputStream bis = new ByteArrayInputStream(data);
try {
ObjectInputStream ois = new ObjectInputStream(bis);
user = (User) ois.readObject();
} catch (Exception ex) {
ex.printStackTrace();
}
return user;
}
@Override
public void close() {
System.out.println("close...");
}
}
// 配置自定义反序列化器
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDeSerializer.class);
KafkaConsumer<Integer, User> consumer = new KafkaConsumer<>(configs);
// 使用自定义反序列化器拉消息
while (true) {
ConsumerRecords<Integer, User> consumerRecords = consumer.poll(3000);
for (ConsumerRecord<Integer, User> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
System.out.println(consumerRecord.value());
}
}
位移提交
给当前消费者手动分配一系列主题分区。指定当前消费者消费 topic1 主题的 0 号分区消息
consumer.assign(Collections.singletonList(new TopicPartition("topic1", 0)));
获取给当前消费者分配的分区集合。一个消费者可以消费多个分区的消息。
Set<TopicPartition> assignment = consumer.assignment();
获取对用户授权的所有主题分区元数据。key:主题名。value:当前主题的分区信息。
Map<String, List<PartitionInfo>> listTopics = consumer.listTopics();
listTopics.forEach((key, value) -> {
System.out.println(key);
System.out.println(value);
});
获取指定主题的分区元数据。
List<PartitionInfo> topic1 = consumer.partitionsFor("topic1");
检查指定主题分区的消费偏移量。
long offset = consumer.position(new TopicPartition("topic1", 0));
对于给定的主题分区,列出它们第一个消息的偏移量。value:偏移量。
Map<TopicPartition, Long> topicOffset = consumer.beginningOffsets(Collections.singletonList(new TopicPartition("topic1", 0)));
topicOffset.forEach((key, value) -> {
System.out.println(key);
System.out.println(value);
});
将偏移量移动到每个给定分区的最后一个。
consumer.seekToBeginning(Collections.singletonList(new TopicPartition("topic1", 0)));
将给定每个分区的消费者偏移量移动到它们的起始偏移。
consumer.seekToEnd(Collections.singletonList(new TopicPartition("topic1", 0)));
将给定主题分区的消费偏移量移动到指定的偏移量。
consumer.seek(new TopicPartition("topic1", 0), 20);
kafka 中 __consumer_offsets 主题保存了每个消费组某一时刻提交的 offset 信息。
kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server 192.168.84.145:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --consumer.config /app/kafka_2.11-1.0.1/config/consumer.properties --from-beginning | head
# 消费组:console-consumer-49092;主题:ch10;分区:4;偏移量:536151
> [console-consumer-49092,ch10,4]::[OffsetMetadata[536151,NO_METADATA],CommitTime 1650266553875,ExpirationTime 1650352953875]
再均衡
一个主题可以有多个分区。一个分区只能由消费组里的一个消费者进行消费。如果主题数、分区数、消费者数发生变化。消费者消费的分区会进行再均衡。
触发再均衡的三个条件:
- 消费组内成员发生变更,这个变更包括的消费者的增加或减少。例如:消费者宕机退出消费组。
- 主题的分区数发生变更,kafka 目前只支持增加分区。
- 订阅的主题发生变更,当消费组使用正则表达式订阅主题,而恰好又新建了对应的主题。
再均衡过程中,消费者无法从 kafka 中消费消息。如果 kafka 集群节点过多,再均衡过程可能达到数分钟或数小时。
如何尽可能避免再均衡的发生:(即 kafka 错误的认为一个正常的消费者宕机了)
- session.timout.ms:控制心跳超时的时间。默认45s,该属性指定了消费者在被认为死亡之前可以与服务器断开连接的时间。
- heartbeat.interval.ms:控制心跳发送频率。每隔一段时间向服务端发送一个心跳证明自己还活着。
- max.poll.interval.ms:控制 poll 的间隔。消费者 poll 数据后,需要一些处理,再进行拉取。如果两次拉取时间间隔超过这个参数设置的值,那么消费者就会被踢出消费者组。
谁来执行再均衡和消费组管理
- kafka 提供了一个角色:Group Coordinator(消费组协调器)。
当消费组的第一个消费者启动的时候,消费者会和 kafka broker 确定谁是当前消费组的协调器。之后该消费组的所有消费者和该协调器通信。
- 如何确定 Coordinator
- 确定消费组位移信息写入 __consumer_offsets 的哪个分区。
- 该分区的 leader 所在的 broker 就是组协调器。
主题
参数配置
参数名称 | 默认值 | 服务器默认属性 | 描述 |
---|---|---|---|
cleanup.policy | delete | log.cleanup.policy | 日志清理策略(delete|compact) delete:当回收时间或尺寸达到限制会被丢弃。 compact:会被压缩。 |
retention.ms | 7 天 (168 小时) | log.retention.(hours|minutes) | 超过这个时间会根据 policy 处理日志数据。 |
retention.bytes | none | log.retention.bytes | delete 策略下日志所能达到的最大尺寸。 默认情况下没有尺寸限制只有时间限制。 |
segment.bytes | 1GB | log.segment.bytes | kafka 中 log 日志是分成一块块存储的,此配置是指 log 日志划分成块的大小。 |
segment.ms | 7 天 | log.roll.hours | 即使log的分块文件没有达到需要删除、压缩的大小。 一旦 log 的时间达到上限,会强制刷新建一个 log 分块文件。 |
主题增加分区
主题只能增加分区,不能减少分区:
ERROR org.apache.kafka.common.errors.InvalidPartitionsException: The number of partitions for a topic can only be increased. Topic topic2 currently has 2 partitions, 1 would not be an increase.(kafka.admin.TopicCommand$)
kafka-topics.sh --zookeeper localhost:2181/kafka-msg --alter --topic topic2 --partitions 2
主题重新分配分区
kafka-reassign-partitions.sh
- 创建一个包含 5 个分区 的主题 topic3。
kafka-topics.sh --create --topic topic3 --zookeeper localhost:2181/kafka-msg --partitions 5 --replication-factor 1
- 创建 json 文件:描述需要重新分配的分区。/app/topics-to-move.json
{"topics":[{"topic":"topic3"}],"version":1}
- 利用 json 文件让 kafka 生成分区建议。
kafka-reassign-partitions.sh --zookeeper localhost:2181/kafka-msg --topics-to-move-json-file /app/topics-to-move.json --generate --broker-list "1,2"
- 将分区建议复制 生成要执行的 json 文件。/app/topics-to-execute.json
此处可根据需要自定义。
{"version":1,"partitions":[{"topic":"topic3","partition":3,"replicas":[2],"log_dirs":["any"]},{"topic":"topic3","partition":1,"replicas":[2],"log_dirs":["any"]},{"topic":"topic3","partition":4,"replicas":[1],"log_dirs":["any"]},{"topic":"topic3","partition":2,"replicas":[1],"log_dirs":["any"]},{"topic":"topic3","partition":0,"replicas":[1],"log_dirs":["any"]}]}
- 根据 json 文件重新分配分区。
kafka-reassign-partitions.sh --zookeeper localhost:2181/kafka-msg -reassignment-json-file /app/topics-to-execute.json --execute
- 查看重新分配分区是否完成。
kafka-reassign-partitions.sh -zookeeper localhost:2181/kafka-msg --reassignment-json-file /app/topics-to-execute.json --verify
主题重新分配副本
kafka-reassign-partitions.sh
- 创建 json文件:描述主题、分区、副本信息。/app/increment-replication-factor.json
{"version":1,"partitions":[{"topic":"topic6","partition":0,"replicas":[1,2,3]},{"topic":"topic6","partition":1,"replicas":[1,2,3]},{"topic":"topic6","partition":2,"replicas":[1,2,3]}]}
- 执行分配
kafka-reassign-partitions.sh --zookeeper localhost:2181/kafka-msg --reassignment-json-file /app/increment-replication-factor.json --execute
副本自动再均衡
kafka-preferred-replica-election.sh
- 创建一个包含 5 个分区 2个副本 的主题 topic3。
kafka-topics.sh --create --topic topic5 --partitions 5 --replication-factor 2 --zookeeper localhost:2181/kafka-msg
也可以在创建时直接指定 Leader 和 Follower
kafka-topics.sh --create --topic topic6 --replica-assignment "1:2,2:1,1:2,2:1,1:2" --zookeeper localhost:2181/kafka-msg
描述主题后结果:
Topic:topic6 PartitionCount:5 ReplicationFactor:2 Configs:
Topic: topic6 Partition: 0 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: topic6 Partition: 1 Leader: 2 Replicas: 2,1 Isr: 2,1
Topic: topic6 Partition: 2 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: topic6 Partition: 3 Leader: 2 Replicas: 2,1 Isr: 2,1
Topic: topic6 Partition: 4 Leader: 1 Replicas: 1,2 Isr: 1,2
- 此时 2号 broker 宕机
描述主题后结果:
Topic:topic5 PartitionCount:5 ReplicationFactor:2 Configs:
Topic: topic6 Partition: 0 Leader: 1 Replicas: 1,2 Isr: 1
Topic: topic6 Partition: 1 Leader: 1 Replicas: 2,1 Isr: 1
Topic: topic6 Partition: 2 Leader: 1 Replicas: 1,2 Isr: 1
Topic: topic6 Partition: 3 Leader: 1 Replicas: 2,1 Isr: 1
Topic: topic6 Partition: 4 Leader: 1 Replicas: 1,2 Isr: 1
- 此时 2号 broker 恢复服务
描述后的主题:
Topic:topic5 PartitionCount:5 ReplicationFactor:2 Configs:
Topic: topic6 Partition: 0 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: topic6 Partition: 1 Leader: 1 Replicas: 2,1 Isr: 1,2
Topic: topic6 Partition: 2 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: topic6 Partition: 3 Leader: 1 Replicas: 2,1 Isr: 1,2
Topic: topic6 Partition: 4 Leader: 1 Replicas: 1,2 Isr: 1,2
broker 恢复了 但是 Leader 的分配并没有变化,还是处于 Leader 切换后的分配情况。
- 自动再均衡所有 topic
kafka-preferred-replica-election.sh --zookeeper localhost:2181/kafka-msg
-
指定 topic 再均衡
- 创建 json 文件:描述需要再均衡的 topic 和 分区。/app/preferred-replica.json
{"partitions":[{"topic":"topic6","partition":0},{"topic":"topic6","partition":1},{"topic":"topic6","partition":2},{"topic":"topic6","partition":4}]}
- 执行操作
kafka-preferred-replica-election.sh --zookeeper localhost:2181/kafka-msg --path-to-json-file /app/preferred-replica.json
操作完成后会恢复到最初的 Leader 分配状态。
分区分配策略
实现 org.apache.kafka.clients.consumer.internals.PartitionAssignor 接口
-
RangeAssignor 范围分配
- 对每个主题内的分区按照 id 排序。
- 对消费者进行字典排序。
- 尽量均衡的将每个主题内的分区分配给消费者。
字典序靠前的消费者分到的分区多。
-
RoundRobinAssignor 轮询分配
- 对每个主题内的分区按照 id 排序。
- 对消费者进行字典排序。
- 将每个主题内的分区依次分配给消费者。
在两个消费者订阅的主题相同时可以尽量分配均衡,但当订阅的主题不相同时,订阅多的消费者分到的分区多。
-
StickyAssignor 粘性分配
无论是 RangeAssignor 还是 RoundRobinAssignor 都没有考虑上一次的分配结果
- 分区的分配尽量的均衡
- 每一次重分配仅仅分配需要修改的部分
物理存储
kafka 的消息以主题为单位进行分类,各个主题之间相互独立互不影响。每个主题可以分为一个或多个分区,每个分区各自存在一个记录消息的数据日志文件。
# cd/app/kafka/kafka-logs-1/ch10-0
# 查询 ch10 主题;0号分区的日志文件
-rw-rw-r-- 1 work work 648 Mar 14 15:22 00000000000000643964.index
-rw-rw-r-- 1 work work 347370 Mar 4 16:55 00000000000000643964.log
-rw-rw-r-- 1 work work 24 Mar 14 15:22 00000000000000643964.timeindex
-rw-rw-r-- 1 work work 10485760 Mar 14 15:24 00000000000000644948.index
-rw-rw-r-- 1 work work 222502 Mar 11 14:34 00000000000000644948.log
-rw-rw-r-- 1 work work 10 Mar 7 15:14 00000000000000644948.snapshot
-rw-rw-r-- 1 work work 10485756 Mar 14 15:24 00000000000000644948.timeindex
-rw-rw-r-- 1 work work 10 Mar 14 15:24 00000000000000645607.snapshot
后缀名 | 说明 |
---|---|
.index | 偏移量索引文件 |
.timeindex | 时间戳索引文件 |
.log | 日志文件 |
.snapshot | 快照文件 |
存储特点
- 文件名一致的文件集合称为一个 LogSegment。
- Kafka 日志追加是顺序写入的。
- 文件名表示当前 LogSegment 中第一条消息的偏移量。
- 索引文件会根据 log.index.size.max.bytes 值进行预先分配空间,即文件创建的时候就是最大值。当真正的进行索引文件切分的时候,才会将其裁剪到实际数据大小的文件。
- 在偏移量索引文件中,每个索引项共占用 8 个字节,并分为两部分。相对偏移量占 4 个字节。物理地址占 4 个字节。
索引文件
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000643964.index --print-data-log | head
# 可以看到并不是每个偏移量都有索引
offset: 643976 position: 4373
offset: 643988 position: 8711
offset: 644000 position: 13081
offset: 644012 position: 17487
offset: 644024 position: 21829
offset: 644036 position: 26061
offset: 644048 position: 30289
offset: 644060 position: 34550
offset: 644072 position: 38806
-
偏移量索引文件用于记录消息偏移量与物理地址之间的映射关系。物理地址(4byte)+相对offset(4byte)。
-
时间戳索引文件用于记录消息偏移量与时间戳之间的映射关系。时间戳(8byte)+相对offset(4byte)。
-
kafka 中的索引文件是以稀疏索引的方式构造消息的索引,并不保证每一个消息在索引文件中都有对应的索引项。
-
每当写入一定量的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项。
-
通过修改 log.index.interval.bytes 的值,改变索引项的密度。
切分文件
-
当前日志分段文件的大小超过了 broker 端参数 log.segment.bytes 配置的值。
-
当前日志分段中消息的最大时间戳与当前系统的时间戳的差值大于 log.roll.ms 或 log.roll.hours 参数配置的值。log.roll.ms 的优先级高。
-
偏移量索引文件或时间戳索引文件的大小达到 broker 端参数 log.index.size.max.bytes 配置的值。
-
追加的消息的偏移量与当前日志分段的偏移量之间的差值大于 Integer.MAX_VALUE ,即要追加的消息的偏移量不能转变为相对偏移量。
4 个字节刚好对应 Integer.MAX_VALUE 。
日志文件
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000643964.log --print-data-log | head
Starting offset: 643964
baseOffset: 643964 lastOffset: 643964 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 91 isTransactional: false position: 0 CreateTime: -1 isvalid: true size: 367 magic: 2 compresscodec: NONE crc: 915595156
baseOffset: 643965 lastOffset: 643965 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 91 isTransactional: false position: 367 CreateTime: -1 isvalid: true size: 336 magic: 2 compresscodec: NONE crc: 559393333
baseOffset: 643966 lastOffset: 643966 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 91 isTransactional: false position: 703 CreateTime: -1 isvalid: true size: 367 magic: 2 compresscodec: NONE crc: 326902282
字段 | 说明 |
---|---|
*offset | 逐渐增加的整数,每个offset对应一个消息的偏移量 |
position | 消息批字节数,用于计算物理地址 |
CreateTime | 时间戳 |
compresscodec | 压缩类型 |
crc | 对所有字段进行校验后的crc值 |
事务
只有生产者生产消息
Map<String, Object> configs = new HashMap<>();
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// client id
configs.put(ProducerConfig.CLIENT_ID_CONFIG, "trans_client_id");
// transaction id
configs.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "trans_tx_id");
// ISR 需要全部确认消息
configs.put(ProducerConfig.ACKS_CONFIG, "-1");
// 创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<>(configs);
// 初始化事务
producer.initTransactions();
// 开启事务
producer.beginTransaction();
try {
// 逻辑处理
int i = 1/0;
// 封装消息
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("topic1", 0, "key", "hello kafka");
// 发送消息
producer.send(producerRecord);
// 提交事务
producer.commitTransaction();
} catch (Exception e) {
// 终止事务
producer.abortTransaction();
} finally {
// 关闭生产者
producer.close();
}
消费消息和生产消息并存
生产者
Map<String, Object> configs = new HashMap<>();
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configs.put(ProducerConfig.CLIENT_ID_CONFIG, "trans_client_id2");
configs.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "trans_tx_id2");
configs.put(ProducerConfig.ACKS_CONFIG, "-1");
// 启用幂等性
configs.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
return new KafkaProducer<>(configs);
消费者
Map<String, Object> configs = new HashMap<>();
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configs.put(ConsumerConfig.GROUP_ID_CONFIG, "group1");
// configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// 关闭自动提交偏移量
configs.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
事务处理
// 创建消费者
KafkaConsumer<String, String> consumer = getConsumer();
// 订阅主题
consumer.subscribe(Collections.singletonList("topic4"));
// 创建生产者
KafkaProducer<String, String> producer = getProducer();
// 事务初始化
producer.initTransactions();
while (true) {
// 开启事务
producer.beginTransaction();
// 拉取消息
ConsumerRecords<String, String> consumerRecords = consumer.poll(3000);
try {
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
// 根据拉取到的消息 处理后 再次发送
System.out.println("拉取到的消息:" + consumerRecord);
// 逻辑处理
int i = 1 / 0;
// 将 topic4 的消息 逻辑处理后再次发送到topic5
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("topic5", 0, consumerRecord.key(), consumerRecord.value());
producer.send(producerRecord);
// 设置偏移量
TopicPartition topicPartition = new TopicPartition(consumerRecord.topic(), consumerRecord.partition());
OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(consumerRecord.offset() + 1);
offsets.put(topicPartition, offsetAndMetadata);
}
// 提交偏移量
producer.sendOffsetsToTransaction(offsets, "group1");
// 提交事务
producer.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
// 回滚事务
producer.abortTransaction();
} finally {
// 关闭资源
// consumer.close();
// producer.close();
}
}
Kafka 图形管理工具 Kafka Eagle
- Kafka 开启 JMX
vim /app/kafka_2.12-1.0.2/kafka-server-start.sh
# 开启 JMX
export JMX_PORT=9581
-
下载 Kafka Eagle:https://github.com/smartloli/kafka-eagle-bin/tags
-
配置环境变量
vim /etc/profile
# 添加环境变量
export KE_HOME=/app/kafka-eagle-web-2.0.5
export PATH=$PATH:$KE_HOME/bin
- 修改 Kafka Eagle 配置文件
vim /app/kafka-eagle-web-2.0.5/conf/system-config.properties
# kafka 集群别名
kafka.eagle.zk.cluster.alias=cluster1
# kafka 集群 zookeeper 地址
cluster1.zk.list=localhost:2181/kafka-msg
# kakfa eagle 存储
kafka.eagle.url=jdbc:sqlite:/app/kafka-eagle-web-2.0.5/db/ke.db
- 启动 Kafka Eagle
ke.sh start
参考git:https://gitee.com/zhangyizhou/learning-kafka-demo.git