Kafka学习笔记

1、服务搭建

服务启动流程

wget https://downloads.apache.org/kafka/2.8.0/kafka_2.13-2.8.0.tgz
tar -xzf kafka_2.13-2.8.0.tgz
mv kafka_2.13-2.8.0 /usr/local/kafka
sh /usr/local/kafka/bin/zookeeper-server-start.sh config/zookeeper.properties
sh /usr/local/kafka/bin/kafka-server-start.sh config/server.properties

1、配置属性

Kafka的配置文件在/conf/server.properties

# 每个broker在集群中的唯一标识
broker.id=0
# 服务监听的地址和端口,如果不填则将localhost注册到zookeeper上,会引起找不到broker的问题
listeners=PLAINTEXT://192.168.0.29:9092
# 用于请求网络请求的线程数
num.network.threads=3
# 用于处理磁盘io的线程数
num.io.threads=8
# 发送数据的缓冲区大小
socket.send.buffer.bytes=102400
# 接受数据的缓存区大小
socket.receive.buffer.bytes=102400
# 运行接受的最大数据包,反之OOM
socket.request.max.bytes=104857600
# 数据存放地址,注意:日志地址的配置在log4j.properties中
log.dirs=/data/kafka/logs
# 新创建的topic默认的分区数
num.partitions=3
# 消息保存时间,默认为7天
log.retention.hours=168
# 配置每个分区中段的大小,默认为1G
log.segment.bytes=1073741824
# 检查过期周期
log.retention.check.interval.ms=300000
# Zookeeper地址
zookeeper.connect=192.168.0.29:2181,192.168.0.195:2181,192.168.0.27:2181
# zk链接超时时间
zookeeper.connection.timeout.ms=6000
# 延迟重平衡的时间
group.initial.rebalance.delay.ms=0
# 自动创建topic
auto.create.topics.enable=true
# 是否物理删除topic
delete.topics.enable=true

2、常用操作

1、所有topic

 ./bin/kafka-topics.sh --zookeeper localhost:2181 --list 

2、topic详细信息

./bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic testTopic

3、删除topic

./bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic testTopic

4、发送消息

./bin/kafka-console-producer.sh  --broker-list 192.168.0.29:9092 --topic testTopic   

5、消费消息

./bin/kafka-console-consumer.sh --bootstrap-server 192.168.0.29:9092 --topic testTopic --from-beginning    

2、应用

1、使用场景

  • 消息系统,最普遍的用法是用作消息队列和RocketMQ一样,Kafka的高吞吐量,备份冗余分布等特性很适合做消息系统
  • 日志系统:配合ElasticSearch,LogStash,FileBeat,Kibana,搭建一套ELK系统,查看服务的日志
  • 网站用户行为追踪:前端卖点将用户行为轨迹数据放入Kafka中,后面通过Hadoop,Spark等大数据分析工具对用户进行分析
  • 流处理:将已收集的流数据提供给其他流式计算框架进行处理,用Kafka收集流数据是一个不错的选择

2、SpringBoot中引入

1、引入依赖

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

2、配置文件

公共配置

## 集群地址
spring.kafka.bootstrap-servers=127.0.0.1:9092

生产者配置

# 重试次数
spring.kafka.producer.retries=1
# 应答级别
spring.kafka.producer.acks=1
# 批量大小
spring.kafka.producer.batch-size=16384
# 提交延时
spring.kafka.producer.properties.linger.ms=0
## 当生产者累计的消息达到batch-size或时间延迟到了linger.ms后就要提交消息了
# 生产端缓冲区大小
spring.kafka.producer.buffer-memory=33554432
# Kafka提供的序列化和反序列化类
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

batch-size和buffer-memory的区别?

​ Kafka生产者发送的数据不是直接就发送出去,而是会存放在本地内存中,然后把多个消息收集成一个个的Batch,再发送到Broker上。

image-20210712215910937

​ buffer-memory就是规定生产者消息存放内存的大小,默认值是32M,而batch-size就是每个Batch的大小,Batch数值越高系统的吞吐量越大,但是响应的消息延迟也会变高。

消费者配置

# 消费者组id
spring.kafka.consumer.properties.group.id=defaultConsumerGroup
# 是否自动提交偏移量
spring.kafka.consumer.enable-auto-commit=false
# 自动提交偏移量延时
spring.kafka.consumer.auto.commit.interval.ms=1000
# 当kafka中没有初始offset或offset超出范围时将自动重置offset
# 1 earliest:重置为分区中最小的offset;
# 2 latest:重置为分区中最新的offset(消费分区中新产生的数据);
# 3 none:只要有一个分区不存在已提交的offset,就抛出异常;
spring.kafka.consumer.auto-offset-reset=latest
# 消息会话超时时间,心跳超时后会发起rebalance
spring.kafka.consumer.properties.session.timeout.ms=120000
# 消费请求超时时间
spring.kafka.consumer.properties.request.timeout.ms=180000
# Kafka提供的序列化和反序列化类
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
# 消费端监听的topic不存在时,项目启动会报错(关掉)
spring.kafka.listener.missing-topics-fatal=false
# 在侦听器容器中运行的线程数。
spring.kafka.listener.concurrency:5
 #listner负责ack,每调用一次,就立即commit
spring.kafka.listener.ack-mode: manual_immediate
# 设置批量消费
spring.kafka.listener.type=batch
# 批量消费每次最多消费多少条消息
spring.kafka.consumer.max-poll-records=50

ack-mode的类型:

AckMode模式 作用
MANUAL 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
MANUAL_IMMEDIATE 手动调用Acknowledgment.acknowledge()后立即提交
RECORD 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
BATCH 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
TIME 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
COUNT 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
COUNT_TIME TIMECOUNT 有一个条件满足时提交

3、消费者

代码:

@Component
@Slf4j
public class KafkaConsumer {
    @KafkaListener(topics = {"testTopic"}, groupId = {"group1"})
    public void topic_test(ConsumerRecord<?, ?> record, Acknowledgment ack, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
        Optional message = Optional.ofNullable(record.value());
        if (message.isPresent()) {
            Object msg = message.get();
            log.info("topic_test 消费了: Topic:" + topic + ",Message:" + msg);
            ack.acknowledge();
        }
    }

@KafkaListener属性:

  • id:消费者id

  • groupId:消费组id

  • topics:监听的topic,可以多个

  • topicPartitions:可配置更加详细的监听信息,可指定topic、parition、offset监听

  • errorHandler:指定异常处理器

    ps:topicstopicPartitions不能同时使用

批量消息

当在application.properties中设置消费方式为批量时,需要用List接受ConsumerRecord.

@KafkaListener(id = "consumer2",groupId = "felix-group", topics = "topic1")
public void onMessage3(List<ConsumerRecord<?, ?>> records) {
    System.out.println(">>>批量消费一次,records.size()="+records.size());
    for (ConsumerRecord<?, ?> record : records) {
        System.out.println(record.value());
    }
}

异常处理器

通过异常处理器,我们可以处理consumer在消费时发生的异常。

新建一个 ConsumerAwareListenerErrorHandler 类型的异常处理方法,用@Bean注入,BeanName默认就是方法名,然后我们将这个异常处理器的BeanName放到@KafkaListener注解的errorHandler属性里面,当监听抛出异常的时候,则会自动调用异常处理器,

java代码


// 新建一个异常处理器,用@Bean注入
@Bean
public ConsumerAwareListenerErrorHandler consumerAwareErrorHandler() {
    return (message, exception, consumer) -> {
        System.out.println("消费异常:"+message.getPayload());
        return null;
    };
}
// 将这个异常处理器的BeanName放到@KafkaListener注解的errorHandler属性里面
@KafkaListener(topics = {"topic1"},errorHandler = "consumerAwareErrorHandler")
public void onMessage4(ConsumerRecord<?, ?> record) throws Exception {
    throw new Exception("简单消费-模拟异常");
}
// 批量消费也一样,异常处理器的message.getPayload()也可以拿到各条消息的信息
@KafkaListener(topics = "topic1",errorHandler="consumerAwareErrorHandler")
public void onMessage5(List<ConsumerRecord<?, ?>> records) throws Exception {
    System.out.println("批量消费一次...");
    throw new Exception("批量消费-模拟异常");

消息过滤

​ 消息过滤器是在消息抵达consumer之前被拦截的,在实际应用中根据自身需要的业务去处理对应的消息。

​ 配置消息过滤只需要为 监听器工厂 配置一个RecordFilterStrategy(消息过滤策略),返回true的时候消息将会被抛弃,返回false时,消息能正常抵达监听容器。


@Component
public class KafkaConsumer {
    @Autowired
    ConsumerFactory consumerFactory;
    // 消息过滤器
    @Bean
    public ConcurrentKafkaListenerContainerFactory filterContainerFactory() {
        ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
        factory.setConsumerFactory(consumerFactory);
        // 被过滤的消息将被丢弃
        factory.setAckDiscarded(true);
        // 消息过滤策略
        factory.setRecordFilterStrategy(consumerRecord -> {
            if (Integer.parseInt(consumerRecord.value().toString()) % 2 == 0) {
                return false;
            }
            //返回true消息则被过滤
            return true;
        });
        return factory;
    }
    // 消息过滤监听
    @KafkaListener(topics = {"topic1"},containerFactory = "filterContainerFactory")
    public void onMessage6(ConsumerRecord<?, ?> record) {
        System.out.println(record.value());
    }
}

消息转发

在实际开发中,我们可能有这样的需求,应用A从TopicA获取到消息,经过处理后转发到TopicB,再由应用B监听处理消息,即一个应用处理完成后将该消息转发至其他应用,完成消息的转发。

在SpringBoot集成Kafka实现消息的转发也很简单,只需要通过一个@SendTo注解

@KafkaListener(topics = {"topic1"})
@SendTo("topic2")
public String onMessage7(ConsumerRecord<?, ?> record) {
    return record.value()+"-forward message";
}

4、生产者

代码:

@Component
@Slf4j
public class KafkaProducer {
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    //自定义topic
    public static final String TOPIC_TEST = "topic.test";
	
    /**
    * 带回调的send
    **/
    public void sendCallback(Object obj) {
        String obj2String = JSONObject.toJSONString(obj);
        log.info("准备发送消息为:{}", obj2String);
        //发送消息
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(TOPIC_TEST, obj);
        future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
            @Override
            public void onFailure(Throwable throwable) {
                //发送失败的处理
                log.info(TOPIC_TEST + " - 生产者 发送消息失败:" + throwable.getMessage());
            }

            @Override
            public void onSuccess(SendResult<String, Object> stringObjectSendResult) {
                //成功的处理
                log.info(TOPIC_TEST + " - 生产者 发送消息成功:" + stringObjectSendResult.toString());
            }
        });
    }
	
    /**
    * 事务发送
    **/
    public void sendMessage7(){
        // 声明事务:后面报错消息不会发出去
        kafkaTemplate.executeInTransaction(operations -> {
            operations.send("topic1","test executeInTransaction");
            throw new RuntimeException("fail");
        });
    
        // 不声明事务:后面报错但前面消息已经发送成功了
       kafkaTemplate.send("topic1","test executeInTransaction");
       throw new RuntimeException("fail");
    }

    
}

5、自定义分区器

kafka中的topic都是划分为多个分区的,每个消息也都是按照一定的规则分到不同的区中,我们也可以自定义消息分区的策略

消息路由机制:

  • 若发送消息时指定了分区策略,则将消息append到指定分区中。
  • 若发送消息时未指定 patition,但指定了 key(kafka允许为每条消息设置一个key),则对key值进行hash计算,根据计算结果路由到指定分区,这种情况下可以保证同一个 Key 的所有消息都进入到相同的分区;
  • patitionkey 都未指定,则使用kafka默认的分区策略,轮询选出一个 patition

​ 代码:

public class CustomizePartitioner implements Partitioner {
    
    /**
    ** 返回值就是路由到第n个分区中
    **/
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 自定义分区规则(这里假设全部发到0号分区)
        // ......
        return 0;
    }
    @Override
    public void close() {

    }
    @Override
    public void configure(Map<String, ?> configs) {

    }
}

在Application.properties中添加配置:

# 自定义分区器
spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner

3、原理

1、网络模型

​ Kafka的网络通信层模型,主要采用了1(1个Acceptor线程)+N(N个Processor线程)+M(M个业务处理线程),一种典型的NIO的Reator模型

​ Acceptor线程主要负责和客户端创建链接,链接创建完成后就绑定到Processor线程上,Processor线程主要负责对Socket进行读写,而Worker线程是正常处理业务的地方。

框架图:

image-20210710232343639

  1. Acceptor监听OP_ACCEP事件并创建链接
  2. Processor都有自己的Selector,他会将Acceptor分配的SocketChannel注册相应的OP_READ事件,大小由num.networker.threads配置
  3. RequestChannel:在Kafka的网络通信层中,RequestChannel为Processor处理器线程与KafkaRequestHandler线程之间的数据交换提供了一个数据缓冲区,是通信过程中Request和Response缓存的地方。因此,其作用就是在通信中起到了一个数据缓冲队列的作用。Processor线程将读取到的请求添加至RequestChannel的全局请求队列—requestQueue中;KafkaRequestHandler线程从请求队列中获取并处理,处理完以后将Response添加至RequestChannel的响应队列—responseQueue中,并通过responseListeners唤醒对应的Processor线程,最后Processor线程从响应队列中取出后发送至客户端
  4. KafkaRequestHandlerPool (线程池大小配置通过参数num.io.threads)就是实际处理业务的线程池
  5. KafkaRequestHandler:KafkaRequestHandler也是一种线程类,在KafkaServer实例启动时候会实例化一个线程池—KafkaRequestHandlerPool对象(包含了若干个KafkaRequestHandler线程),这些线程以守护线程的方式在后台运行。在KafkaRequestHandler的run方法中会循环地从RequestChannel中阻塞式读取request,读取后再交由KafkaApis来具体处理
  6. KafkaApis:KafkaApis是用于处理对通信网络传输过来的业务消息请求的中心转发组件。该组件反映出Kafka Broker Server可以提供哪些服务

调用的流程:

  1. 客户端连接到Acceptor上,Acceptor创建一个socketChannel并将链接注册到Processor上的OP_READ事件上
  2. 当Processor上拿到响应OP_READ事件的链接时,将请求放入到RequstChannel的全局requestQueue
  3. KafkaRequestHandlerPool中的Handler会从requestQueue中拿取可以执行的请求,并进行处理。处理后通过KafkaApis将响应放入到RequestChannel中的对应Processor的responseQueue
  4. Processor将响应数据返回给客户端

2、术语和概念

1、主题Topic

​ Kafka将一组消息抽象归纳为一个主题(Topic),也就是说,一个主题就是对消息的一个分类。生产者将消息发送到特定主题,消费者订阅主题或主题的某些分区进行消费,如电商项目中的购买就可以看作一个主题,购买后生产者发送一个消息进入到主题消费中,这个时候积分模块,仓库模块就可以消费这个主题

2、消息Message

​ 消息是Kafka通信的基本单位,由一个固定长度的消息头和一个可变长度的消息体构成。在老版本中,每一条消息称为Message;在由Java重新实现的客户端中,每一条消息称为Record

3、分区Partition和副本

​ Kafka将一组相同业务的消息称为一个主题,一个主题会被被分为多个分区。每个分区是由多个有序的消息组成的,每个分区对应一个文件夹名称格式为:主题名称_分区编号。分区内容在物理保存上是由多个段Segment组成的,默认的一个段为1G大小,超过大小就会生成另一个段文件。

​ 每个分区都会有一到多个副本数据,副本分布在其他的Broker上,提高服务的可用性。Kafak只能保证一个分区内的消息顺序性,每条消息被追加到分区中是顺序写磁盘的,因此效率高,这是Kafka吞吐量高的重要保障。

4、日志段

​ 一个日志又被划分为多个日志段(LogSegment),日志段是Kafka日志对象分片的最小单位。与日志对象一样,日志段也是一个逻辑概念,一个日志段对应磁盘上一个具体日志文件和两个索引文件。日志文件是以“.log”为文件名后缀的数据文件,用于保存消息实际数据。两个索引文件分别以“.index”和“.timeindex”作为文件名后缀,分别表示消息偏移量索引文件和消息时间戳索引文件。

5、控制器

​ 控制器就是一个Broker,他除了拥有Broker的功能之外还可以进行分区首领的选举。集群中第一个启动的Broker会自动成为控制器(通过向ZK中注册临时节点的方式),一旦控制器被关闭或者在zk中断开链接,其他Broker通过watcher会了解目前集群中无控制器,然后所以Broker会进行争夺。

​ 第一个Broker争夺成功后其他机器就会向之前一样在节点上添加Watcher,新选出来的控制器会通过zk生成一个全新的递增的epoch,其他broker在知道当前epoch之后,如果收到较旧的epoch消息就会忽略。

​ 当控制器发现有broker离开集群时,控制器会去判断这个broker有哪些首领,并对这些首领进行遍历选择其同步状态的副本作为一个新首领。然后向所有broker发送消息,消息著名了谁是新首领,谁是新首领的跟随者的信息。

​ 当控制器发现有新的Broker加入集群时,他会判断这个brokerID是否包含现有分区的副本,如果包含,控制器就会把变更通知发送给新加入的Broker和其他Broker,新broker上的副本开始从首领获取复制消息。

3、复制

副本有两种类型:

  1. 首领副本,每个分区都有一个首领副本。为了保证数据一致性,所有请求都会经过这个副本
  2. 跟随副本,不处理请求,唯一的作用是复制笑嘻嘻,当首领副本崩溃时,作为备份替换成首领副本

首领节点需要知道哪个跟随者的状态和自己的一致的。跟随者副本会向首领发送获取数据的请求,这种请求和消费者为了获取消息而发送的请求是一样的,需要带上请求的偏移量。首领将响应消息返回给跟随者。如果跟随着10s内没有请求消息或者10s内请求的偏移量没有变化,那么这个跟随者就会被认为是不同步。不同步的跟随者无法成为新领导。

​ 判定跟随者是不同步节点可以通过replica.lag.time.max,ms进行设置。

1、首选首领

除了当前的首领之外,每个分区还有一个首选首领,创建主题时选定的首领就是分区的首选首领。在创建分区时,需要在broker之间均衡首领。因此,我们希望首选首领成为真正的首领时,broker之间的负载自重会得到均衡。默认情况下:auto.leader.rebalance.enable设为ture时,他会判断首选首领是不是当前首领,如果不是且该副本是同步的,那么就会触发首领选举,让首选首领成为首领。

2、分布副本和首领算法

Todo..待续

3、Rebalance过程

Todo ...待续

4、处理请求

​ 生产请求和获取请求都必须发送给分区的首领副本,如果broker收到了一个针对特定分区的请求且不是本分区,则会返回一个非分区首领的错误信息给客户端,Kafka客户端需要自行负责把生产请求和消费请求发送到正确的broker上。

​ 客户端会通过元数据请求发送给任意一个broker上,因为任何一个broker都有所有broker的信息,客户端拿到了集群的元数据后会将数据缓存到本地内存中,并通过metadata.max.age.ms参数设置的时间段进行循环调用保证数据的准确性。

image-20210711124140132

生产请求

生产请求中有个重要的参数acks

  • 0 ,不需知道消息是否发送成功,生产者只负责发送数据。吞吐量最大,但是会引起数据丢失
  • 1 , 只需要首领节点保存数据成功就代表生产者请求发送成功
  • all,首领节点和副本都保存了数据,才算发送成功

对于参数0是肯定不安全的,因为可能在发送的过程中出现了错误,对于参数1也是不安全的,设想这样一种情景:首领将数据保存到了页缓存中(Linux的IO不是实时的,会先将数据写入到PageCache中,再某个时刻将缓存中的数据全部刷新到硬盘上,增加硬盘IO的吞吐量)这个时候首领集群宕机了,而副本这个时候还处于同步状态的,所以可以将副本转换成首领,那么这个请求就已经被丢失了。

消费请求

​ 客户端发送请求向broker中特定主题中偏移量为x的消息,还可以指定一次返回多少数据(防止OOM)。broker会判断此偏移量是否是本机的数据,如果是则通过零拷贝技术向客户端发送消息数据。

​ 客户端还可以设置数据的下限,当数据没有达到下线时,broker会一直等待,直到数据达到指定的下限或者等待时间已经满了才会将数据返回。这样做会减少网络IO的次数,加大吞吐量。

image-20210711125343959

等待超时时间参数:replica.lag.time.max.ms

ps:客户端只能接受到标记为安全的数据,没有被足够多副本复制的消息被认为是不安全数据,不安全数据容易再首领发生崩溃后丢失数据。

image-20210711125604444

5、数据存储

​ 在创建主题时,Kafka 首先会决定如何在 broker 间分配分区。假设你有 6 个 broker,打算创建一个包含 10 个分区的主题,并且复制系数为 3。那么 Kafka 就会有 30 个分区副本,它们可以被分配给 6 个 broker。在进行分区分配时,我们要达到如下的目标。

  • broker 间平均地分布分区副本。对于我们的例子来说,就是要保证每个 broker 可以

分到 5 个副本。

  • 确保每个分区的每个副本分布在不同的 broker 上。假设分区 0 的首领副本在 broker 2 上,

那么可以把跟随者副本放在 broker3broker 4 上,但不能放在 broker 2 上,也不能两

个都放在 broker 3 上。

  • 如果为 broker 指定了机架信息,那么尽可能把每个分区的副本分配到不同机架的 broker上。这样做是为了保证一个机架的不可用不会导致整体的分区不可用。

​ 在一个大文件中查找数据和删除数据是很费时的,所以Kafka把分区分成若干个片段,默认情况下每个判断包含1G或者最近一周的数据。broker往段中写数据时,超过的数据量就会写入新的段中。如果生产者发送的是压缩过的消息,那么同一批次的消息会被压缩在一起,broker会把消息记录下来,将原始格式发送给消费者,消费者端自行去解压缩。

image-20210711150946877

​ 为了帮助更快的定位到指定偏移量,Kafka为每个分区维护了一个索引,索引把偏移量映射到段文件的位置中,可以通过索引文件快速定位到偏移量在段中的位置。如果索引文件出现损坏,Kafka会通过重新读取消息并生成索引。

6、保证可靠数据

Kafka 在数据传递可靠性方面具备很大的灵活性。我们知道,Kafka 可以被用在很多场景里,从跟踪用户点击动作到处理信用卡支付操作。有些场景要求很高的可靠性,而有些则更看重速度和简便性。Kafka 被设计成高度可配置的,而且它的客户端 API 可以满足不同程度的可靠性需求

1、Broker配置

broker 有 3 个配置参数会影响 Kafka 消息存储的可靠性。与其他配置参数一样,它们可以应用在 broker 级别,用于控制所有主题的行为,也可以应用在主题级别,用于控制个别主题的行为。

复制系数

参数:

主题级别配置:replication.factor

broker级别配置:default.replication.factor,默认为3

​ 如果replication.factor配置为3,则说明一个分区会被3个不同的broker复制3此,Kafaka默认的复制系数就是3。复制系数越高说明数据的备份越多,数据越安全。同样消耗的磁盘空间也是逐步增加,所以复制系数就是安全和成本的衡量,在一些金融行业经常会将复制系数设置为5,一般设置为3就比较安全了。

不完全的首领选举

参数:

broker级别配置:unclean.leader.election,默认值ture

是否允许不同步的副本选为首领节点。

​ 如果允许不同步的副本选举为首领那么就要忍受数据丢失,数据不一致的风险。如果不允许就要接受可能造成的不可用的情况,大部分银行宁愿几分钟或者几小时不处理支付事务,也不会冒险处理错误消息。对于可用性要求高的系统里需要设置为true。

最少同步副本

参数:

主题和broker配置:min.insync.replicas

某个主题的复制系数为3,那么这个主题就拥有3个副本。如果将最少同步副本数设置为2,如果broker判断到此时同步副本数小于2个时,就不会给生产者投递消息,并会报错

2、生产者配置

​ 即使我们尽可能把 broker 配置得很可靠,但如果没有对生产者进行可靠性方面的配置,整个系统仍然有可能出现突发性的数据丢失。

  • ​ 复制系数为3,禁用不完全首领选举,但是ack设置为1,最少同步副本设置为2,这种情况也是会存在丢失数据,当消息投递到broker时,broker还没来得及复制给副本就发生宕机,这个时候选举上来的首领不会有这条消息。
  • 在上面的基础上,将ack设置为all,如果发送消息时,broker不可用时,生产者投递消息时会返回一个错误给生产者,这个时候生产者如果不做重试处理或者记录也会丢失数据

发送确认

参数:

acks

  1. acks = 0,如果生产者能够通过网络把消息发送出去,那么就认为消息已成功写入Kafka。
  2. acks = 1,首领在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应。
  3. acks = all,首领在返回确认或错误响应之前,会等待所有同步副本都收到消息。如果和 最少同步副本(min.insync.replicas) 参数结合起来,就可以决定在返回确认前至少有多少个副本能够收到消息。这是最保险的做法

配置重试参数

生产者需要处理两种类型的错误。

  1. 生产者自动处理错误,如网络抖动
  2. 代码层面的错误

参数:

retries,默认情况会无限制重试。

​ 注意的是重试机制可能会导致消息重复问题,如消息已经投递成功但是返回成功的报文丢包了或者由其他网路问题导致的数据接受不到,那么重新发送数据就会导致有重复数据,所以消费者需要对接口做幂等。

额外的错误处理

​ 对于不可重试的错误,如认证错误,消息大小错误,序列化错误等需要通过程序员手动去处理。如丢弃不合法的数据,并记录下来。

3、消费者配置

​ 消费者配置主要体现在对于偏移量的提交方面,如果偏移量提交的过早可能导致丢失消息,如果偏移量提交的过晚可能会导致重复消息问题。

重置请求

​ 当客户端请求到的broker没有指定的偏移量时,消费者会做些什么

参数:

auto.offset.reset

  • earliest,消费者会从分区的开始位置读取数据,不管偏移量是否有效。
  • latest,消费者会从末尾读取数据。

自动提交偏移量

​ 消费者会自动启动一个调度任务去提交偏移量,减少消费者的代码量。自动提交的缺点在于无法确定消息是否处理完成就提交,因为提交线程和事务执行线程是异步的。

参数:

enable.auto.commit:是否启动自动任务提交

auto.commit.interval.ms:自动提交任务的定时任务的周期

显式提交偏移量

​ 既然自动提交可能会导致诸多的问题,那么就去手动提交偏移量。显式提交偏移量的关键在于什么时候去提交偏移量。

  1. 全部事件处理完成后进行提交
  2. 仅一次传递:可以将结果写入到一个支持唯一键的系统中,如ES和DB中。这样重复的数据进行只会刷新已经存在的数据而不会出现一模一样的数据。称作幂等性写入

如果在消费者中遇到了业务问题处理失败,如数据库暂时的不可用。这个时候如果偏移量直接给提交了,那么说明这个消息也是被成功消费了,但是实际情况没有被消费。该如何去做呢?

方法1:提交做后一个处理成功的偏移量,将还没处理好的消息保存到缓冲区中,调用pasue方法确保消费者和broker建立链接但是不接受数据,一直重试直到重试次数达到上限,将无法重试成功的数据记录下来并丢弃数据,调用resume重新获取接下来的数据。

方法2:新建一个独立的主题,将处理失败的消息放入这个主题中让专门处理重试问题的消费者去处理,类似于死信队列

posted @ 2021-08-27 14:57  程序木虫  阅读(693)  评论(0)    收藏  举报