文章目录
- 1. 基本概念
- 2.基本配置信息、执行命令
- 2.1 配置文件
- 2.2 启动kafka
- 2.3 创建topic
- 2.4 发送消息
- 2.5 消费消息
- 2.6 单播消息
- 2.7 多播消息
- 2.8 查看消费组及信息
- 3.主题、分区的概念
- 3.1 主题topic
- 3.2 partition分区
- 3.2.1 创建多分区的主题
- 3.3 kafka中消息日志文件中保存的内容
- 4.kafka集群、副本
- 4.1 搭建
- 4.2 副本的概念
- 4.3 kafka集群消息的发送
- 4.4 kafka集群消息的消费
- 5 kafka的Java客户端 生产者基本实现
- 5.1 基本实现
- 5.2 同步发送
- 5.2 异步发送
- 5.3 参数部分
- 6 Java客户端 消费者的实现细节
- 6.1 消费者的基本实现
- 6.2 消费者的自动提交和手动提交offset
- 6.3 长轮询poll消息
- 6.4 消费者的健康状态检查
- 7 spring boot中使用kafka
- 8 kafka集群controller、rebalance和hw
- 8.1 controller
- 8.2 rebalance
- 8.3 HW和LEO
- 9 kafka线上问题优化
- 9.1 如何防止消息丢失
- 9.2 如何防止消息重复消费
- 9.3 如何做到顺序消费
- 9.4 解决消息积压问题
- 9.5 延迟队列
1. 基本概念
名称 | 解释 |
broker | 消息中间件处理节点,一个kafka节点就是一个broker,一个或多个broker可以组成一个kafka集群 |
topic | kafka根据topic对消息进行归类,发布到kafka集群的每条消息都需要指定一个topic |
producer | 消息生产者,向broker发送消息的客户端 |
consumer | 消息消费者,从broker读取消息的客户端 |
consumer group | 每个consumer属于一个特定的consumer group,一条消息可以被多个不同的consumer group消费,但是一个consumer group中只有一个consumer能够消费该消息 |
partition | 物理上的概念,一个topic可以分为多个partition,每个partition内部消息是有序的 |
2.基本配置信息、执行命令
2.1 配置文件
config目录下server.properties文件:
- broker.id kafka集群下 每个broker id是不一样的
- listeners 放服务器主机地址,后面的9092是kafka提供对外服务的默认端口
- log.dirs 消息存储文件存放地址
- zookeeper.connect zk地址
2.2 启动kafka
启动kafka,进入bin目录下:
./kafka-server-start.sh -daemon ../config/server.properties
可以进入zk看节点是否成功启动
ls /brokers/ids
2.3 创建topic
执行以下命令创建名为"test"的topic,这个topic只有一个partition,并且备份因子也设置为1:
./kafka-topics.sh --create --zookeeper (zk地址):2181 --replication-factor 1 --partitions 1 --topic test
–replication-factor 1表示一个副本 --partitions 1表示一个分区
查看当前kafka内有哪些topic:
./kafka-topics.sh --list --zookeeper (zk地址):2181
2.4 发送消息
发送消息 命令行模式(存在log.dirs中topic开头文件夹里面的.log
文件中),生产者将消息发送给broker,broker会将消息保存在本地的日志文件中,消息的保存是有序的,通过offset偏移量来描述消息的有序性:
./kafka-console-producer.sh --broker-list (kafka地址):9092 --topic test
2.5 消费消息
从最后一条消息的偏移量+1开始消费,其实就是不消费历史消息,只消费最新的,即消费者消费消息时也是通过offset来描述当前要消费的那条消息位置:
./kafka-console-consumer.sh --bootstrap-server (kafka地址):9092 --topic test
从头开始消费:
./kafka-console-consumer.sh --bootstrap-server (kafka地址):9092 --from-beginning --topic test
2.6 单播消息
如果多个消费者在同一个消费组,那么只有一个消费者可以收到订阅的同一个topic中的消息,即同一个消费组中只能有一个消费者收到一个topic中的消息,并且一直是这个消费者,不会来回切换;
./kafka-console-consumer.sh --bootstrap-server (kafka地址):9092 --consumer-property group.id=testGroup --topic test
2.7 多播消息
不同的消费组订阅同一个topic,那么不同的消费组中只有一个消费者能收到消息(单播),但是多个消费组中的消费者收到了同一个消息;
单播放保证的是同一个消费者组只有一个消费者可以消费消息,如果需要还想获取该消息,需要再起一个消费者组去消费;
2.8 查看消费组及信息
查看当前主题下有哪些消费者组
./kafka-consumer-groups.sh --bootstrap-server (kafka地址):9092 --list
查看消费组的详细信息:
./kafka-consumer-groups.sh --bootstrap-server (kafka地址):9092 --describe --group testGroup
current-offset:最后被消费的消息的偏移量,即最后消费到哪里了
log-end-offset:消息总量,即最后一条消息的偏移量
lag:积压了多少条消息,未被消费的量
3.主题、分区的概念
3.1 主题topic
类别名称,逻辑划分,kafka通过topic对消息进行分类,不同的topic会被订阅该topic的消费者消费。
但是有一个问题,如果说这个topic中的消息非常多,需要几个T来保存,因为消息是会被保存到log日志文件中的,为了解决文件过大的问题,kafka提出了partition分区的概念;
3.2 partition分区
一个主题中的消息量是非常大的,因此可以通过分区的设置,来分布式存储这些消息,比如一个topic创建了3个分区,那么topic中的消息就会分别存放在这三个分区中;
- 分区存储,可以解决统一存储文件过大问题
- 提高了读写的吞吐量,即读和写可以同时在多个分区中进行
3.2.1 创建多分区的主题
为一个主题创建分区:
./kafka-topics.sh --create --zookeeper (zk地址):2181 --replication-factor 1 --partitions 2 --topic test1
查看topic的分区信息
./kafka-topics.sh --describe --zookeeper (zk地址):2181 --topic test1
3.3 kafka中消息日志文件中保存的内容
- 00000.log:这个文件中保存的就是消息
- _consumer_offsets-49:kafka内部自己创建了_consumer_offsets主题包含了50个分区,这个主题用来存放消费者消费某个主题的偏移量;因为每个消费者都会自己维护消费的主题的偏移量,也就是说每个消费者会把消费的主题的偏移量自主上报给kafka中的默认主题_consumer_offsets,即某一个消费者挂了,启用另一个消费者也知道从何处继续接着消费;kafka为了提升这个主题的并发性,默认设置50个分区,这个可以设置。
- 提交到那个分区,通过hash函数:hash(consumerGroupId)%_consumer_offsets主题的分区数
- 提交到该主题中的内容是:key是consumerGroupId+topic+分区号,value就是当前offset的值
- 文件中保存的消息默认保存7天,7天后被删除
4.kafka集群、副本
4.1 搭建
以一台服务器为例,创建多个配置文件,修改broker.id,listeners,log.dir;
4.2 副本的概念
副本是对分区的备份,在集群中,不同的副本会被部署在不同的broker上,多个副本在kafka集群的多个broker中,会有一个副本作为leader,其他是follower,只有一个节点创建副本没有意义。
创建一个主题,2个分区,3个副本:
./kafka-topic.sh --create --zookeeper (zk地址):2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic
leader的作用是读和写,副本是leader会将数据同步过来;
生产者向两个leader发消息,消费者消费两个leader;
- isr:可以同步和已经同步的节点会被存入到isr集合中,如果isr中节点性能较差,会被踢出isr集合;
集群中有多个broker,创建主题时可以指明主题有多少个分区,把消息拆分到不同分区中存储,可以为分区创建多个副本,不同的副本存放在不同的broker里;
4.3 kafka集群消息的发送
./kafka-console-producer.sh --broker-list (节点1),(节点2),(节点3) --topic my-replicated-topic
4.4 kafka集群消息的消费
./kafka-console-consumer.sh --bootstrap-server (节点1),(节点2),(节点3) --from-beginning --topic my-replicated-topic
消费时带上消费者组
./kafka-console-consumer.sh --bootstrap-server (节点1),(节点2),(节点3) --from-beginning --consumer-property group.id=testGroup --topic my-replicated-topic
使用消费组组消费,通过设置消费组组,当前消费者组不会消费其他消费组组的消息
./kafka-console-consumer.sh --bootstrap-server 地址 --topic test_topic --group test2
向topic内发送3条消息,可见 test1消费了消息,无挤压,test2没有消费消息,消息挤压了。
两个broker,每个broker中有多个partition,一个partition只能被一个消费组里的某一个消费者消费,从而保证消费顺序,kafka只在patition范围内保证消息消费的局部顺序性,不能在同一个topic中的多个partition中保证总的消费顺序性,一个消费者可以消费多个partition;
消费组中的消费者的数量不能比一个topic中的partition数量多,否则多出来的消费者消费不到消息,partition的数量决定了消费组中消费者的数量,建议同一个消费组中消费者的数量不要超过partition的数量,否则多的消费者消费不到消息,如果消费者挂了,那么会触发rebalance机制,会让其他消费者来消费;
5 kafka的Java客户端 生产者基本实现
5.1 基本实现
package kafka;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class MyProducer {
public final static String TOPIC_NAME = "my_test_topic";
public static void main(String[] args) throws InterruptedException {
Properties props = new Properties();
//集群用逗号隔开
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "172.16.253.21:9093,172.16.253.21:9094");
/**
*前提是同步发送:发出消息的,持久化机制参数
* acks=0,表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息,性能最高,但是容易丢消息
* ack=1,表示至少要等待leader已经成功将数据写入到本地log,但是不需要等待所有follower是否写入成功,就可以继续发送下一条消息了,
* 这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,数据会丢失
* ack=-1或all,需要等待min,insync.replicas(默认为1,为1其实和ack=1一样,为2则表示leader和一个follower同步完成之后,返回返回ack给生产者,
* 表示有多少个partition已经收到并写入了消息)这个参数配置的副本个数都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据,
* 这是最强的数据保证,一般除非是金融级别等场景才需要使用这种配置;
*/
props.put(ProducerConfig.ACKS_CONFIG, "1");
/**
*没有收到ack:
* 发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造成消息的重复发送,比如网络抖动,所以需要在接受者这边做好
* 消息接收的幂等性处理
*/
props.put(ProducerConfig.RETRIES_CONFIG, 3);
//重试间隔设置
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
//设置发送消息的本地缓冲区,如果设置了改缓冲区,消息会先发送到本地缓冲区,可以提高消息发送的性能,默认值是33554432,即32MB
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
/**
*kafka本地线程会从缓冲区取数据,批量发送到broker,设置批量发送消息的大小,默认是16384,即16kb,
* 就是说一个branch满了16kb就发送出去
*/
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
/**
*默认值是0,意思就是消息必须立刻被发送,但这样会影响性能,一般设置10ms左右,即消息发送完后会进入本地的一个batch,如果10ms内,
* 这个batch满了16kb就会随batch一起被发送出去,没满,也会发送出去,不能让消息发送延迟时间太长
*/
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
//把发送的key从字符串序列化为字节数组
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//把发送的value从字符串序列化为字节数组
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//发消息的客户端
Producer<String, String> producer = new KafkaProducer<>(props);
int msgNum = 5;
final CountDownLatch countDownLatch = new CountDownLatch(msgNum);
for (int i = 0; i < 10000000000l; i++) {
Order order = new Order((long) i, i);
//指定发送分区
// ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, 0,
// order.getOrderId().toString(), JSON.toJSONString(order));
//未指定发送分区,具体发送的分区计算公式,hash(key)%partitionNum,order.getOrderId().toString()是key,
// key的作用是指明往哪个分区发
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME,
order.getOrderId().toString(), JSON.toJSONString(order));
//同步发送消息
try {
RecordMetadata recordMetadata = producer.send(producerRecord).get();
//阻塞
System.out.println("topic" + recordMetadata.topic());
System.out.println("partition" + recordMetadata.partition());
System.out.println("offset" + recordMetadata.offset());
} catch (InterruptedException e) {
e.printStackTrace();
//日志 人工处理等
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
//异步回调发送发送消息
producer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (Objects.nonNull(e)) {
System.out.println("消息发送失败");
}
if (Objects.nonNull(recordMetadata)) {
System.out.println("topic" + recordMetadata.topic());
System.out.println("partition" + recordMetadata.partition());
System.out.println("offset" + recordMetadata.offset());
}
countDownLatch.countDown();
}
});
}
countDownLatch.await(5, TimeUnit.SECONDS);
producer.close();
}
}
@Data
@AllArgsConstructor
class Order {
private Long orderId;
private int count;
}
5.2 同步发送
如果生产者发送消息没有收到ack,生产者会阻塞,阻塞3s,如果还没有收到ack,会进行重试,重试的次数3次;
5.2 异步发送
异步发送,生产者发送完消息后就可以执行之后的业务,broker在收到消息后异步调用生产者提供的callback回调方法。
5.3 参数部分
6 Java客户端 消费者的实现细节
6.1 消费者的基本实现
package kafka;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
public class MyConsumer {
public final static String TOPIC_NAME = "my_test_topic";
public final static String CONSUMER_GROUP_NAME = "testGroup";
public static void main(String[] args) {
Properties props = new Properties();
//集群用逗号隔开
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "172.16.253.21:9093,172.16.253.21:9094");
//消费分组名
props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
//是否自动提交offset,默认就是true
// props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
//自动提交offset时间间隔
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
//手动提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
/**
* 当消费主题的是一个新的消费组,或者指定offset的消费发送,offset不存在,那么应该如何消费
* latest(默认):只消费自己启动之后发送到主题的消息
* earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都开始从头消费)
*/
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
//consumer给broker发送心跳的时间间隔
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
//kafka如果超过10秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
//一次poll最大拉取消息的条数,可以根据消费速度的快慢来设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
//如果两次poll的时间超出了30s的时间间隔,kafka会认为其消费能力过弱,将其踢出消费组,将分区分配给其他消费者
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
//消费者订阅主题列表,这里可以订阅多个主题
consumer.subscribe(Arrays.asList(TOPIC_NAME));
//指定分区消费
// consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
//消息回溯消费
// consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
// consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
//指定offset消费
// consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
// consumer.seek(new TopicPartition(TOPIC_NAME, 0),10);
//从指定的时间点开始消费
//拿到主题下所有的分区
List<PartitionInfo> partitionInfos = consumer.partitionsFor(TOPIC_NAME);
//从一小时前开始消费
long fetchDateTime = new Date().getTime() - 1000 * 60 * 60;
HashMap<TopicPartition, Long> map = Maps.newHashMap();
for (PartitionInfo par : partitionInfos) {
map.put(new TopicPartition(TOPIC_NAME, par.partition()), fetchDateTime);
}
//根据时间拿到偏移量
Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
//partition
TopicPartition key = entry.getKey();
// offset
OffsetAndTimestamp value = entry.getValue();
if (Objects.isNull(key) || Objects.isNull(value)) {
continue;
}
long offset = value.offset();
if (Objects.nonNull(value)) {
consumer.assign(Arrays.asList(key));
consumer.seek(key, offset);
}
}
while (true) {
//poll() API 是拉取消息的长轮询
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.offset());
System.out.println(record.partition());
System.out.println(record.key());
System.out.println(record.value());
}
//所有的消息已消费完
if (records.count() > 0) { //有消息
//手动提交offset,当前线程会阻塞直到offset提交成功,一般使用同步提交,因为提交之后也没有什么逻辑代码了
//方法1 手动同步提交 这里会阻塞 直到broker返回ack
// consumer.commitSync();
//方法2 异步提交offset 不会阻塞 kafka集群调用callback方法
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
if (Objects.nonNull(e)){
System.out.println(map);
}
}
});
}
}
}
}
6.2 消费者的自动提交和手动提交offset
消费者无论是自动提交还是手动提交,都需要把所属的消费组+消费的某个主题+消费的某个分组及消费的偏移量,这样的信息提交到集群的_consumer_offsets主题里面。
自动提交:消费者poll消息下来以后就会自动提交offset,注意:自动提交会丢消息,因为消费者在消费前提交offset,也可能提交完后还没有消费时消费者挂了;
手动提交:同步:在消费完消息后调用同步提交的方法,当集群返回ack前一直阻塞,返回ack后表示提交成功,执行之后的逻辑;
异步:在消息消费完之后提交,不需要等到集群ack,直接执行之后的逻辑,可以设置一个回调方法,供给集群调用;
6.3 长轮询poll消息
默认情况下,消费者一次会poll 500条消息,代码中设置了长轮询的时间是1000毫秒,意味着:1.如果一次poll到500条,就直接执行for循环;2.如果这一次没有poll到500条,且时间在1s内,那么长轮询继续poll,要么到500条,要么到1s;3.如果多次poll都没达到500条,且1s时间达到了,那么直接执行for循环;
如果两次poll的时间间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者被踢出消费组,触发rebalance机制,rebalance机制会造成性能开销,可以通过设置这个参数,让一次poll的消息条数少一点。
6.4 消费者的健康状态检查
消费者每隔1s向kafka集群发送心跳,集群发现如果有超过10s没有续约的消费者,将被踢出消费组,触发该消费者的rebalance机制,将该分区交给消费组里的其他消费者进行消费;
7 spring boot中使用kafka
pom依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.8.0</version>
</dependency>
consumer
@Component
public class consumer {
@KafkaListener(topics = "my_test_topic", groupId = "group1")
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println(value);
System.out.println(record);
//手动提交offset 不手动提交消息会被重复消费
ack.acknowledge();
}
@KafkaListener(topics = "my_test_topic", groupId = "group2")
public void listenGroups(ConsumerRecords<String, String> records, Acknowledgment ack) {
//需要对records进行遍历
//手动提交offset
ack.acknowledge();
}
@KafkaListener(groupId = "group3", topicPartitions = {
@TopicPartition(topic = "topic1", partitions = {"0", "1"}),
@TopicPartition(topic = "topic2", partitions = "0",
partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100")) //消费1号分区从100偏移量开始
}, concurrency = "3") //concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数
public void listenGroup1(ConsumerRecords<String, String> records, Acknowledgment ack) {
//需要对records进行遍历
//手动提交offset
ack.acknowledge();
}
}
producer
@RestController
@RequestMapping("/msg")
@RequiredArgsConstructor
public class KafkaController {
private final static String TOPIC_NAME = "";
private final KafkaTemplate<String, String> kafkaTemplate;
@RequestMapping("/send")
public String sendMessage() {
kafkaTemplate.send(TOPIC_NAME, 0, "key", "message");
return "success";
}
}
手动提交配置
spring:
kafka:
ack-mode:MANUAL_IMMEDIATE #手动提交调用ack.acknowledge()之后立即提交,一般用这种 还有别的配置,可自行查阅
8 kafka集群controller、rebalance和hw
8.1 controller
集群中谁来充当controller:每个broker启动时会向zk创建一个临时序号节点,获得的序号最小的那个broker将会作为集群中的controller,负责这么几件事:
- 当集群中有一个副本的leader挂掉,需要在集群中选举出一个新的leader,选举的规则是从isr集合中最左边获得;
- 当集群中有broker新增或减少,controller会同步信息给其他broker
- 当集群中有分区新增或减少,controller会同步信息给其他broker
8.2 rebalance
前提:消费组中的消费者没有指明分区来消费
触发的条件:当消费组中的消费者和分区的关系发生变化的时候
分区分配的策略:在rebalance之前,分区的怎么分配会有这么三种策略
- range:根据公式计算得到每个消费者消费哪几个分区,前面的消费者是分区总数/消费者数量+1,之后的消费者是分区总数/消费者数量
- 轮询:大家轮着来
- sticky:粘合策略,如果需要rebalance,会在之前已分配的基础上调整,不会改变之前的分配情况,如果这个策略没有开,那么就要进行全部的重新分配,建议开启。
8.3 HW和LEO
LEO是某个副本最后消息的消息位置,log_end_offset;
HW是已完成同步的位置,消息在写入broker时,且每个broker完成这条消息的同步后,hw才会变化,在这之前消费者是消费不到这条消息的,在同步完成之后,hw更新之后,消费者才能消费到这条消息,这样的目的是防止消息的丢失;
hw相当于一条线,等所有的leo到达这条线的时候,hw才下来,可以消费;
9 kafka线上问题优化
9.1 如何防止消息丢失
生产者:
- 使用同步发送
- 把ack设置成1或者all,并且设置同步的分区数>=2
消费者: - 把自动提交改成手动提交
broker:设置多个副本
9.2 如何防止消息重复消费
在防止消息丢失的方案中,如果生产者发送完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了,此时生产者会进行重试,于是broker就会收到多条相同的消息,从而造成消息的重复消费。
解决方法:
- 生产者关闭重试:会造成丢消息,不建议使用
- 消费者端解决非幂等性消费问题:在数据库中创建联合主键,防止相同的主键 创建出多条记录;使用分布式锁,以业务id为锁,保证只有一条记录能够创建成功;
9.3 如何做到顺序消费
生产者:保证消息顺序消费,且消息不丢失——使用同步发送,ack设置成非0的值
消费者:主题只能设置一个分区,消费组中只能有一个消费组
kafka的顺序消费使用场景不多,因为牺牲掉了性能,但是比如rocketmq在这一块有专门的功能已设计好;
9.4 解决消息积压问题
原因:消息的消费者的消费速度远赶不上生产者的生产消息速度,导致kafka中有大量的数据没有被消费,随着没有被消费的数据堆积越多,消费者寻址的性能也会越来越差,最后导致整个kafka对外提供服务的性能越来越差,从而造成其他服务的访问速度变慢,造成服务雪崩;
解决:
- 在消费者中使用多线程,充分利用机器的性能进行消费消息;
- 创建多个消费组,多个消费者,部署到其他机器上,一起消费,提高消费者的速度;
9.5 延迟队列
应用场景:订单创建后,超过30分钟没有支付,则需要取消订单,这种场景可以通过延时队列来实现;
具体方案:
- kafka中创建相应的主题,:topic_30ms
- 消费者消费该主题的消息,通过轮询的方式
- 消费者消费消息时判断消息的创建时间和当前时间有没有超过30ms,前提时订单没支付,是则更改订单状态,否则记录当前的offset,并且不再继续消费消息,等待一分钟后,再次向kafka拉取该offset及之后的消息,继续进行判断,以此反复;