一文详解Kafka

一文详解Kafka

一、介绍

1.1 简介

Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.

Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于Zookeeper协调的分布式日志系统(也可以当做MQ系统[消息系统]),常见可以用于Web/Nginx日志、访问日志,消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。具有横向扩展、容错、wicked fast(变态快)等优点,并已在成千上万家公司运行。主要应用场景是:日志收集系统和消息系统。

1.2 特性

Kafka是一种高吞吐量的分布式发布订阅消息系统,有如下特性:

  • 通过O(1)的磁盘数据结构提供消息的持久化,即使是数以TB的消息存储也能保持长时间的稳定性能。
  • 【O(1)是最低的时间复杂度,表示无论数据量多大,都能够通过一次计算定位到目标数据】
  • 高吞吐量:即使是很一般的硬件环境,Kafka也能支持每秒数百万的消息
  • 支持Kafka Server之间的消息分区,以及分布式消费。同时保证每个分区内的消息顺序传递
  • 同时支持离线数据处理和即时数据处理
  • 支持在线水平拓展(scale out)

1.3 消息系统介绍(Kafka的主要应用情形)

一个消息系统负责将数据从一个应用传递到另一个应用,应用只需要关注数据本身,无需关注数据是如何在两个或多个应用之间传递的。分布式消息传递依赖于可靠的消息队列(MQ),在客户端应用和消息系统之间异步传递。有两种主要的传递模式:点对点消息传递模式、发布-订阅消息传递模式。Kafka就是发布-订阅模式。

1.3.1 点对点消息传递模式

在点对点模式的消息系统中,一个消息持久化到一个消息队列中,这时有一个或多个消费者去消费 队列中的数据。但一个数据只能被消费一次,当一个数据被消费后,就会从消息队列中删除。这使得该模式下,即便有多个消费者同时消费数据,也能保证数据处理的顺序。

*一条数据发送到一个队列中,之后只能被一个消费者所消费。

1.3.2发布-订阅消息传递模式

在发布-订阅模式中,一条消息持久化到一个topic中。与点对点模式不同的是,一个消费者可以订阅一个或多个topic,消费者可以消费某topic中的所有消息,一条消息可以被多个消费者所消费,消息被消费后不会立马删除。在发布-订阅模式中,消息的生产者被称为发布者,消费者被称为订阅者。

*发布者发布一条消息到一个topic中,所有订阅了该topic的订阅者都会收到该消息。

二、Kafka的优点(解耦、削峰、异步、延迟、分布式)

2.1 解耦

在项目启动之初就预测项目将会遇到的需求,是极其困难的。Kafka消息系统在处理过程中插入了一个隐含的、基于数据的接口层。两边的处理过程都要实现这个接口,这使你能够独立的拓展或修改两边的处理过程,只要两边都遵循同样的接口约束。

2.2 数据持久化

有时,数据处理的过程会失败,此时除非数据已被持久化处理,否则将造成丢失。使用消息队列将数据进行持久化直到他们已经被完全处理,通过这一方式规避了数据丢失的风险。消息队列遵循“插入-获取-删除”范式,但是在删除之前,要求你的处理程序明确指出该消息已经被完全处理完毕,从而确保了你的一直被安全保存直到你使用完毕。

2.3 拓展性

消息队列实现了对处理过程的解耦,所以要对处理过程进行拓展是相当容易的。

2.4 缓冲/流量削峰

消息队列能够使关键组件顶住突发的大量访问,而不会就此彻底崩溃。

2.5 可恢复性

系统中的一部分组件失效时,不会影响到整个系统。消息队列降低了进程之间的耦合度,即使一个进程挂掉,已经加到消息队列中的数据也可以在系统恢复后继续进行处理。

2.6 顺序保证

在大多数情形下,数据处理的顺序是很重要的。大部分消息队列本身就是排序的,并且能保证数据被按照一定的顺序被处理。Kafka保证一个Partition内的数据的有序性。

2.8 异步/延时

很多时候,用户不想且不需要去即时处理消息。消息队列提供了异步处理机制,允许将消息放入队列中,但不去立即处理它,当想处理的时候再进行处理。

三、术语

3.1概述

下图展示了Kafka相关术语之间的关系:

在这里插入图片描述

3.2 术语解释

  • producer:生产者、发布者。产生信息的主体。
  • consumer:消费者、订阅者。消费信息的主体。通过订阅topic来获取消息。
    • 多个consumer会属于多个不同的group。如果不手动指定group,那就会属于默认分组(也是一个组)__cousumer_default_group protected _ private __
    • 订阅和消费是以组的名义来进行的。组去申请订阅和消费。
    • 一个组内的consumer不能同时消费同一个partition分区下的消息,但是不同组的consumer可以同时消费同一个分区消息。即对一个组来说,他订阅的一个partition只能指派给一个consumer。
    • 消费者组是动态维护的,如果一个组内的一个消费者发生了故障,那么他订阅的分区将分配给组内的另一个消费者
  • broker:Kafka集群的一个服务器节点。多个broker组成Kafka服务器集群。已发布的消息以topic分类保存在Kafka集群中。集群中的每一个服务器都是一个代理(Broker)。 消费者可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。其下为topic。
    • 如果broker 有N个,topic有N个,那么每个broker存放一个topic。
    • 如果broker有N+M个,topic有N个,那么有N个broker内各存放一个topic,有M个broker空着。
    • 如果broker有N个,topic有N+M个。那么每个broker存放一个或多个topic。应当尽量避免这种情况,会导致集群数据不均衡.
  • topic:主题。Kafka将消息分门别类,每一类的消息称之为一个主题(Topic)。其下为partition。
  • partition:分区。topic的物理分组,每一个partition都是一个有序的、不可改变的消息队列。每个topic有几个partition是在创建topic时指定的。其下为segment。
    • partition是以文件的形式存储在文件系统中
  • segment:段。多个大小相同的段组成一个partition。其下为record。
  • record:消息。Kafka中最基本的传递对象,有固定的格式:由key、value和时间戳构成。
  • offset:偏移量。一个连续的用于定位被追加到分区的record消息的序列号,这个偏移量是该分区中一条消息的唯一标示符。也记录了消费者的消费位置信息(消费到哪个消息)
    • 【例如,一个位置是5的消费者(说明已经消费了0到4的消息),下一个接收消息的偏移量为5的消息。】。最大值为64位的long大小,19位数字字符长度。。Kafka默认是定期帮你自动提交位移的(enable.auto.commit = true),你当然可以选择手动提交位移实现自己控制。每个consumer group保存自己的位移信息,那么只需要简单的一个整数表示位置就够了。另外kafka会定期把group消费情况保存起来,做成一个offset map。

Java -> read(读) write(写) seek(查寻位置, begin、end、index) 系统原语(内核提供的接口) -> CPU指令集(Intel、AMD、…)

3.3 其他

  • segment由index和data文件组成,两个文件成对出现,分别存储索引和数据。
  • segment文件命名规则:对于所有的partition来说,segment名称从0开始,之后的每一个segment名称为上一个segment文件最后一条消息的offset值。

四、Kafka的安装

4.1 Linux下的安装

步骤1: 安装Java

$ tar -zxvf jdk-8u261-linux-x64.tar.gz
$ mv jdk1.8.0_261 /usr/local/
$ vim /etc/profile
#java
export JAVA_HOME=/usr/local/jdk1.8.0_261
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib

步骤2: 安装ZooKeeper

ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services. All of these kinds of services are used in some form or another by distributed applications. Each time they are implemented there is a lot of work that goes into fixing the bugs and race conditions that are inevitable. Because of the difficulty of implementing these kinds of services, applications initially usually skimp on them, which make them brittle in the presence of change and difficult to manage. Even when done correctly, different implementations of these services lead to management complexity when the applications are deployed.

$ tar -zxf zookeeper-3.4.6.tar.gz
$ mv zookeeper-3.4.6 /usr/local/
$ cd /usr/local/zookeeper-3.4.6
$ mv conf/zoo_sample.cfg zoo.cfg
$ vim zoo.cfg #修改配置,zookeeper配置请自行查看
$ bin/zkServer.sh start

步骤3: 安装Kafka

$ tar -zxf kafka_2.11.0.9.0.0 tar.gz
$ mv kafka_2.11.0.9.0.0 /usr/local/
$ bin/kafka-server-start.sh -daemon config/server.properties
$ jps #jdk提供的一个查看当前java进程的小工具
$ netstat -ntlp #查看2181、9092端口是否已经存在 (所有TCP连接/UDP)

步骤4: 查看Kafka是否已经安装成功

#创建一个名为 test 的主题,其中包含一个分区和一个副本因子
$ bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
#获取Kafka服务器中的主题列表
$ bin/kafka-topics.sh --list --zookeeper localhost:2181
#启动生产者以发送消息
$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
#启动消费者以接收消息
$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning

4.2 Zookeeper配置文件详解

  • tickTime

    基本事件单元,以毫秒为单位。这个时间是作为zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每隔tickTime时间会发送一个心跳。

  • dataDir

    存储内存中数据库快照的位置,即zookeeper保存数据的目录,默认情况下,zookeeper将写数据的日志文件也保存在这个目录里。

  • clientPort

    这个端口就是客户端连接Zookeeper服务器的端口,Zookeeper会监听这个端口,接受客户端的访问请求。

  • initLimit

    这个配置项是用来配置Zookeeper接受客户端初始化连接时最长能忍受多少个心跳时间间隔数。如果initLimit=5,那么当超过5个心跳时间(即tickTime)长度后,Zookeeper服务器还没有收到客户端的返回信息,那么表面这个客户端连接失败。

  • syncLimit

    这个配置标识Leader与Follwer之间发送消息,请求和应答时间长度,最长不能超过多少个tickTime的时间长度。

  • server.1=hadoop05:2888:3888

    1表示这个是第几号服务器,hadoop05表示的是这个服务器的IP,2888表示的是这个服务器与集群中的Leader服务器交换信息的端口,3888表示万一Leader服务器挂了,需要一个端口来重新选举.

4.2 Kafka配置文件详解

#broker的全局唯一编号,不能重复
broker.id=0
#用来监听链接的端口,producer或consumer将在此端口建立连接
port=9092
#处理网络请求的线程数量,也就是接收消息的线程数。接收线程会将接收到的消息放到内存中,然后再从内存中写入磁盘。
num.network.threads=3
#消息从内存中写入磁盘是时候使用的线程数量。用来处理磁盘IO的线程数量
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接受套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka运行日志存放的路径
log.dirs=/tmp/kafka-logs
#topic在当前broker上的分片个数
num.partitions=2
#我们知道segment文件默认会被保留7天的时间,超时的话就会被清理,那么清理这件事情就需要有一些线程来做。这里就是用来设置恢复和清理data下数据的线程数量
num.recovery.threads.per.data.dir=1
#segment文件保留的最长时间,默认保留7天(168小时),超时将被删除,也就是说7天之前的数据将被清理掉。
log.retention.hours=168
#滚动生成新的segment文件的最大时间
log.roll.hours=168
#日志文件中每个segment的大小,默认为1G
log.segment.bytes=1073741824
#上面的参数设置了每一个segment文件的大小是1G,那么就需要有一个东西去定期检查segment文件有没有达到1G,多长时间去检查一次,就需要设置一个周期性检查文件大小的时间(单位是毫秒)。
log.retention.check.interval.ms=300000
#日志清理是否打开
log.cleaner.enable=true
#broker需要使用zookeeper保存meta数据
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181
#zookeeper链接超时时间
zookeeper.connection.timeout.ms=6000
#上面我们说过接收线程会将接收到的消息放到内存中,然后再从内存
#写到磁盘上,那么什么时候将消息从内存中写入磁盘,就有一个
#时间限制(时间阈值)和一个数量限制(数量阈值),这里设置的是
#数量阈值,下一个参数设置的则是时间阈值。
#partion buffer中,消息的条数达到阈值,将触发flush到磁盘。
log.flush.interval.messages=10000
#消息buffer的时间,达到阈值,将触发将消息从内存flush到磁盘,单位是毫秒。
log.flush.interval.ms=3000
#删除topic需要server.properties中设置delete.topic.enable=true否则只是标记删除
delete.topic.enable=true
#此处的host.name为本机IP(重要),如果不改,则客户端会抛出:Producer connection to localhost:9092 unsuccessful 错误!
host.name=kafka01

#以下介绍几个非常重要的配置项
#以下四个配置已废弃
advertised.host.name=
advertised.host.port=
host.name=
host.port=
#学名叫监听器,其实就是告诉外部连接者要通过什么协议访问指定主机名和端口开放的 Kafka 服务。可以绑定所有网卡。
listeners=PLAINTEXT://0.0.0.0:9092  (哪张网卡在监听)
#注册到Zookeeper上面的地址,参数值必须为listeners中定义的某个listener
advertised.listeners=PLAINTEXT://122222.33434:9092

五、Kafka存储解析

5.1 概念复习

Kafka中的Message是以topic为基本单位组织的,不同的topic之间是相互独立的。每个topic又可以分成几个不同的partition(每个topic有几个partition是在创建topic时指定的),每个partition存储一部分Message。partition是以文件的形式存储在文件系统中。

5.2 Partition的数据文件

Partition中的每条Message由offset来表示它在这个partition中的偏移量,这个offset不是该Message在partition数据文件中的实际存储位置,而是逻辑上一个值,它唯一确定了partition中的一条Message。因此,可以认为offset是partition中Message的id。partition中的每条Message包含了以下三个属性:offset、MessageSize、data。

5.3 引出分段及索引

我们来思考一下,如果一个partition只有一个数据文件会怎么样?

  1. 新数据是添加在文件末尾(调用FileMessageSet的append方法),不论文件数据文件有多大,这个操作永远都是O(1)的。
  2. 查找某个offset的Message(调用FileMessageSet的searchFor方法)是顺序查找的。因此,如果数据文件很大的话,查找的效率就低。

那Kafka是如何解决查找效率的的问题呢?有两大法宝:1) 分段 2) 索引。

5.4 分段

Kafka解决查询效率的手段之一是将数据文件分段,比如有100条Message,它们的offset是从0到99。假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中。

5.5 索引

数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分(均为4个字节的数字),分别为相对offset和position。

(稀疏索引: 文件全部)

(操作系统: 二级索引)

search

5.6 总结

Kafka的Message存储采用了分区(partition),分段(LogSegment)和稀疏索引这几个手段来达到了高效性。

六、Kafka操作

6.1 主题操作

#修改主题
$ bin/kafka-topics.sh —zookeeper localhost:2181 --alter --topic test --parti-tions count
#删除主题
$ bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic test
#清楚主题数据
$ bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic test

七、Java对接Kafka

7.1 添加添加依赖

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

7.2 配置项

server.port=8080
spring.kafka.bootstrap-servers=120.79.225.209:9092

#=============== producer  =======================
# 发生错误后,消息重发的次数
spring.kafka.producer.retries=0
# 每次批量发送消息的数量
spring.kafka.producer.batch-size=16384
spring.kafka.producer.buffer-memory=33554432
#只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
spring.kafka.producer.acks=1
# 指定消息key和消息体的编解码方式
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

#=============== consumer  =======================
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.enable-auto-commit=false
# 指定消息key和消息体的编解码方式
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

7.3 生产者

package com.example.demo.kafka;

@Component
@Slf4j
public class KafkaProducer {
  	@Autowired
    private KafkaTemplate kafkaTemplate;

    public void send(String topic, String msg) {
        //发送消息
        ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, msg);
        future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
            @Override
            public void onFailure(Throwable throwable) {
                log.info("test" + " - 生产者 发送消息失败:" + throwable.getMessage());
            }
            @Override
            public void onSuccess(SendResult<String, String> stringObjectSendResult) {
                log.info("test" + " - 生产者 发送消息成功:" + stringObjectSendResult.toString());
            }
        });
    }
}

7.4 消费者

package com.example.demo.kafka;

@Component
@Slf4j
public class KafkaConsumer {
    @KafkaListener(topics = "test", 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(topics = "test", groupId = "group1")
    public void topic_test1(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_test1 消费了: Topic:" + topic + ",Message:" + msg);
            ack.acknowledge();
        }
    }
}

八、消费者组与重平衡机制

8.1 前言

消费组组(Consumer group)可以说是kafka很有亮点的一个设计。传统的消息引擎处理模型主要有两种,队列模型,和发布-订阅模型

队列模型(点对点):早期消息处理引擎就是按照队列模型设计的,所谓队列模型,跟队列数据结构类似,生产者产生消息,就是入队,消费者接收消息就是出队,并删除队列中数据,消息只能被消费一次。但这种模型有一个问题,那就是只能由一个消费者消费,无法直接让多个消费者消费数据。基于这个缺陷,后面又演化出发布-订阅模型。

发布-订阅模型:发布订阅模型中,多了一个主题。消费者(按组)会预先订阅主题,生产者写入消息到主题中,只有订阅了该主题的消费者才能获取到消息。这样一来就可以让多个消费者消费数据。

8.2 kafka消费者组

消费者组由消费者组成的,组内可以有一个或多个消费者实例,而这些消费者实例共享一个id,称为group id。默认创建消费者的group id是在KAFKA_HOME/conf/consumer.properties文件中定义的,打开就能看到。默认的group id值是test-consumer-group。

消费者组内的所有成员一起订阅某个主题的所有分区,注意一个消费者组中,每一个分区只能由组内的一消费者订阅。

img

当消费者组中只有一个消费者的时候,就是消息队列模型,不然就是发布-订阅模型,并且易于伸缩。

8.3 消费者组内消费者数量

  • 消费者数 > 分区数:有消费者空闲;
  • 消费者数 = 分区数:刚刚好一对一;
  • 消费者数 < 分区数:有消费者对应了对个分区;

8.4 查看集群中的消费者组

#列出当前集群中的kafka组信息
$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
#具体到某个组的消费者情况
$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group

8.5 重平衡(Rebalance)

8.5.1 什么是Rebalance

将分区分配给消费者组中消费者的过程,叫做重平衡。

8.5.2 Rebalance触发条件

重平衡的触发条件主要有三个:

  • 消费者组内成员发生变更,这个变更包括了增加和减少消费者。注意这里的减少有很大的可能是被动的,就是某个消费者崩溃退出了
  • 主题的分区数发生变更,kafka目前只支持增加分区,当增加的时候就会触发重平衡
  • 订阅的主题发生变化,当消费者组使用正则表达式订阅主题,而恰好又新建了对应的主题,就会触发重平衡
8.5.3 优点的同时也是缺点

为什么说重平衡为人诟病呢?**因为重平衡过程中,消费者无法从kafka消费消息,这对kafka的TPS影响极大,而如果kafka集内节点较多,比如数百个,那重平衡可能会耗时极多。数分钟到数小时都有可能,而这段时间kafka基本处于不可用状态。**所以在实际环境中,应该尽量避免重平衡发生。

8.5.4 三种重平衡策略
  • Range

这种分配是基于每个主题的分区分配,如果主题的分区不能平均分配给组内每个消费者,那么对该主题,某些消费者会被分配到额外的分区。我们来看看具体的例子。

举例:目前有两个消费者C0和C1,两个主题t0和t1,每个主题三个分区,分别是t0p0,t0p1,t0p2,和t1p0,t1p1,t1p2。

那么分配情况会是:

  • C0:t0p0, t0p2, t1p0, t1p2
  • C1:t0p1, t1p1
  • RoundRobin

RoundRobin是基于全部主题的分区来进行分配的,同时这种分配也是kafka默认的rebalance分区策略。还是用刚刚的例子来看,

**举例:**两个消费者C0和C1,两个主题t0和t1,每个主题三个分区,分别是t0p0,t0p1,t0p2,和t1p0,t1p1,t1p2。

由于是基于全部主题的分区,那么分配情况会是:

  • C0:t0p0, t0p1, t1p1
  • C1:t1p0, t0p2, t1p2
  • Sticky

Sticky分配策略是最新的也是最复杂的策略。这种分配策略是在0.11.0才被提出来的,主要是为了一定程度解决上面提到的重平衡非要重新分配全部分区的问题。称为粘性分配策略。

本策略复杂且用的不多,这里不做介绍。

8.5.5 避免重平衡
  • session.timout.ms:心跳超时时间
  • heartbeat.interval.ms:心跳发送频率
  • max.poll.interval.ms:控制poll的间隔

九、延迟队列、重试队列、死信队列

9.1 延迟队列

在发送延时消息的时候并不是先投递到要发送的真实主题(real_topic)中,而是先投递到一些 Kafka 内部的主题(delay_topic)中,这些内部主题对用户不可见,然后通过一个自定义的服务拉取这些内部主题中的消息,并将满足条件的消息再投递到要发送的真实的主题中,消费者所订阅的还是真实的主题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-otuizYF7-1620730343236)(https://www.pianshen.com/images/643/42ecb3200ab26fe24a4937db3f6b288b.png)]

9.2 死信队列和重试队列

死信可以看作消费者不能处理收到的消息,也可以看作消费者不想处理收到的消息,还可以看作不符合处理要求的消息。比如消息内包含的消息内容无法被消费者解析,为了确保消息的可靠性而不被随意丢弃,故将其投递到死信队列中,这里的死信就可以看作消费者不能处理的消息。再比如超过既定的重试次数之后将消息投入死信队列,这里就可以将死信看作不符合处理要求的消息。

重试队列其实可以看作一种回退队列,具体指消费端消费消息失败时,为了防止消息无故丢失而重新将消息回滚到 broker 中。与回退队列不同的是,重试队列一般分成多个重试等级,每个重试等级一般也会设置重新投递延时,重试次数越多投递延时就越大。

理解了他们的概念之后我们就可以为每个主题设置重试队列,消息第一次消费失败入重试队列 Q1,Q1 的重新投递延时为5s,5s过后重新投递该消息;如果消息再次消费失败则入重试队列 Q2,Q2 的重新投递延时为10s,10s过后再次投递该消息。

然后再设置一个主题作为死信队列,重试越多次重新投递的时间就越久,并且需要设置一个上限,超过投递次数就进入死信队列。重试队列与延时队列有相同的地方,都需要设置延时级别。

十. 消息的精确一次传递

10.1 概念

最多一次(at most once):可能丢失 不会重复

至少一次(at least once): 可能重复 不会丢失

精确传递一次(exactly once): 不丢失 不重复 就一次

10.2 问题

1)spring.kafka.producer.acks (req ack)

  • 0: producer完全不管broker的处理结果 回调也就没有用了 并不能保证消息成功发送 但是这种吞吐量最高
  • all或者-1: leader broker会等消息写入 并且ISR都写入后才会响应,这种只要ISR有副本存活就肯定不会丢失,但吞吐量最低。
  • 1: 默认的值 leader broker自己写入后就响应,不会等待ISR其他的副本写入,只要leader broker存活就不会丢失,即保证了不丢失,也保证了吞吐量。

kafka producer 的参数acks 的默认值为1,所以默认的producer级别是at least once。并不能exactly once。

2)enable.auto.commit

  • true consumer在消费之前提交位移 就实现了at most once。
  • false 若是消费后提交 就实现了 at least once 默认的配置就是这个。

kafka consumer的参数enable.auto.commit的默认值为false ,所以默认的consumer级别是at least once。也并不能exactly once。

10.3 解决

1)producer的exactly once

kafka 0.11.0.0版本引入了idempotent producer机制,在这个机制中同一消息可能被producer发送多次,但是在broker端只会写入一次,他为每一条消息编号去重,而且对kafka开销影响不大。如何设置开启呢? 需要设置producer端的新参数 enable.idempotent 为true。

而多分区的情况,我们需要保证原子性的写入多个分区,即写入到多个分区的消息要么全部成功,要么全部回滚。这时候就需要使用事务,在producer端设置 transcational.id为一个指定字符串。

这样幂等producer只能保证单分区上无重复消息;事务可以保证多分区写入消息的完整性,实现了exactly once。

2)consumer的exactly once

consumer端由于可能无法消费事务中所有消息,并且消息可能被删除,所以事务并不能解决consumer端exactly once的问题,我们可能还是需要自己处理这方面的逻辑。比如

  • 自己管理offset的提交,不要自动提交,也是可以实现exactly once的;
  • 消息处理方法的幂等性,就算发生重复处理也不没有影响;
  • 使用kafka自己的流处理引擎Kafka Streams,设置processing.guarantee=exactly_once,就可以轻松实现exactly once了。

九、Zookeeper中的脑裂(Split-Brain)及假死

9.1 脑裂

官方定义:当一个集群的不同部分在同一时间都认为自己是活动的时候,我们就可以将这个现象称为脑裂症状。通俗的说,就是比如当你的 cluster 里面有两个结点,它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master。于是 cluster 里面就会有两个 master。举例:

img

对于Zookeeper来说有一个很重要的问题,就是到底是根据一个什么样的情况来判断一个节点死亡down掉了。 在分布式系统中这些都是有监控者来判断的,但是监控者也很难判定其他的节点的状态,唯一一个可靠的途径就是心跳,Zookeeper也是使用心跳来判断客户端是否仍然活着,但是使用心跳机制来判断节点的存活状态也带来了假死问题。

9.2 假死

由于心跳超时(网络原因导致的)认为master死了,但其实master还存活着。

9.3 解决方案

  • Quorums(ˈ法定人数) :比如3个节点的集群,Quorums = 2, 也就是说集群可以容忍1个节点失效,这时候还能选举出1个leader,集群还可用。比如4个节点的集群,它的Quorums = 3,Quorums要超过3,相当于集群的容忍度还是1,如果2个节点失效,那么整个集群还是无效的
  • Redundant communications:冗余通信的方式,集群中采用多种通信方式,防止一种通信方式失效导致集群中的节点无法通信。
  • Fencing, 共享资源的方式:比如能看到共享资源就表示在集群中,能够获得共享资源的锁的就是Leader,看不到共享资源的,就不在集群中。

ZooKeeper默认采用了Quorums这种方式,即只有集群中超过半数节点投票才能选举出Leader。这样的方式可以确保leader的唯一性,要么选出唯一的一个leader,要么选举失败。在ZooKeeper中

Quorums有2个作用:

  • 集群中最少的节点数用来选举Leader保证集群可用:通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。一旦这些节点保存了该数据,客户端将被通知已经安全保存了,可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据。
  • 假设某个leader假死,其余的followers选举出了一个新的leader。这时,旧的leader复活并且仍然认为自己是leader,这个时候它向其他followers发出写请求也是会被拒绝的。因为每当新leader产生时,会生成一个epoch,这个epoch是递增的,followers如果确认了新的leader存在,知道其epoch,就会拒绝epoch小于现任leader epoch的所有请求。那有没有follower不知道新的leader存在呢,有可能,但肯定不是大多数,否则新leader无法产生。Zookeeper的写也遵循quorum机制,因此,得不到大多数支持的写是无效的,旧leader即使各种认为自己是leader,依然没有什么作用。

总结一下就是,通过Quorums机制来防止脑裂和假死,当leader挂掉之后,可以重新选举出新的leader节点使整个集群达成一致;当出现假死现象时,通过epoch大小来拒绝旧的leader发起的请求,在前面也已经讲到过,这个时候,重新恢复通信的老的leader节点会进入恢复模式,与新的leader节点做数据同步,perfect。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值