参考看完这篇Kafka,你也许就会了Kafka
参考Kafka消费者客户端详解
docker下载阿里云镜像下载
docker安装官网下载
docker介绍+命令Docker&Docker命令学习
这个写的挺好的。。aliyun容器镜像加速使用Mac系统docker安装配置及基本使用
Kafka是一种消息队列,主要用来处理大量数据状态下的消息队列,一般用来做日志的处理。既然是消息队列,那么Kafka也就拥有消息队列的相应的特性了。
消息队列的好处
异步处理
不需要让流程走完就返回结果,可以将消息发送到消息队列中,然后返回结果,剩下的处理让其他业务接口从消息队列中拉取消费处理即可。
流量削峰
高流量的时候,使用消息队列作为中间件可以将流量的高峰保存在消息队列中,从而防止了系统的高请求,减轻服务器的请求处理压力。
kafka应用场景
主要有两种,分别为:
一对一
被消费后就在队列中删除,队列支持多个消费者,但是对于一条消息而言,只能被一个消费者消费。 一条消息只能被一个消费者消费
也称为发布订阅模式,topic存储消息,生产者将消息发布到topic中。同时有多个消费者订阅topic,消费者可以从中消费。因此此时,topic中的消息会被多个消费者消费,数据不会被清除,kafka会默认保留一段时间,然后再删除。
Kafka像其他Mq一样,也有自己的基础架构,kafka架构中包含四个组件:生产者Producer
、集群Broker
、消费者Consumer
、Zookeeper集群
Producer
:消息生产者,向Kafka中发布消息的角色。
Consumer
:消息消费者,即从Kafka中拉取消息消费的客户端。
Consumer Group
:实现一个Topic消息的广播(发给所有的Consumer)和单播(发给某一个Consumer)的手段。一个Topic可以对应多个Consumer Group,因此如果需要实现广播,只要每个Consumer有一个独立的Group就可以了,每个都发。要实现单播只要所有的Consumer在同一个Group里,只发一个。用Consumer Group还可以将Consumer进行自由的分组而不需要多次发送消息到不同的Topic。
Broker
:经纪人,一台Kafka服务器就是一个Broker,一个集群由多个Broker组成,一个Broker可以容纳多个Topic。
Topic
:每条发布到kafka集群的消息都有一个类别,这个类别称为topic,其实就是将消息按照topic来分类,topic就是逻辑上的分类,同一个topic的数据既可以在同一个broker上也可以在不同的broker结点上。
Partition
:分区,一个Topic可以分为多个Partition,每个分区在物理上对应一个文件夹,该文件夹里面存储了这个分区的所有消息和索引文件,在创建topic时可指定parition数量,生产者将消息发送到topic时,消息会根据分配策略追加到分区文件的末尾(追加写),属于顺序写磁盘,因此效率非常高。还有同一partition中的数据是有序的,但topic下的多个partition之间在消费数据时不能保证有序性
分配策略
:所谓分区策略就是决定生产者将消息发送到哪个分区的算法。
offset
:partition中的每条消息都被标记了一个序号,这个序号表示消息在partition中的偏移量,称为offset,每一条消息在partition都有唯一的offset,消息者通过指定offset来指定要消费的消息。而这个offset并不是由broker控制的,是由consumer,因为消息被分区存储,但是决定消费的是消费者,consumer想消费哪一条消息就消费哪一条消息,并不是broker。
Replica
:副本Replication,为保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常的工作,Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader和多个Follower
Leader
:每个partition有多个副本,其中有且仅有一个作为leader,leader会负责所有的客户端读写操作。消费者消费数据的对象都是Leader。
Follower
:每follower不对外提供服务,只与leader保持数据同步,如果leader失效,则选举一个follower来充当新的leader。
这部分内容参考:kafka架构原理
关于整体
一个典型的kafka集群中包含若干producer,若干broker,若干consumer group,以及一个zookeeper集群。kafka通过zookeeper协调管理kafka集群,选举分区leader,以及在consumer group发生变化时进行rebalance。
关于topic
kafka的topic被划分为一个或多个分区,多个分区可以分布在一个或多个broker节点上,同时为了故障容错,每个分区都会复制多个副本(leader+follower都称为分区副本),分别位于不同的broker节点。其中
leader
负责所有的客户端读写操作,follower
不对外提供服务,仅仅从leader上同步数据,当leader出现故障时,其中的一个follower会顶替成为leader,继续对外提供服务。
传统mq区别
对于传统的MQ而言,已经被消费的消息会从队列中删除,但在Kafka中被消费的消息也不会立马删除,在kafka的server.propertise配置文件中定义了数据的保存时间,当文件到设定的保存时间时才会删除。因此kafka是无状态的,消息是否被消费,或者是否被重复消费(offset)也是由消费者决定。
kafka吞吐量为什么那么高?
顺序写:Kafka是将消息持久化到本地磁盘中的,一般人会认为磁盘读写性能差,可能会对Kafka性能提出质疑。实际上不管是内存还是磁盘,快或慢的关键在于
寻址方式
.
磁盘分为顺序读写
与随机读写
,内存一样也分为顺序读写与随机读写.基于磁盘的随机读写确实很慢,但基于磁盘的顺序读写性能却很高,一般而言要高出磁盘的随机读写三个数量级,一些情况下磁盘顺序读写性能甚至要高于内存随机读写
page cache:基于系统内存而不是JVM空间内存。
1.JVM中一切皆对象,对象的存储会带来额外的内存消耗;
2.使用JVM会受到GC的影响,随着数据的增多,垃圾回收也会变得复杂与缓慢,降低吞吐量;
零拷贝:利用 linux 操作系统的 “zero-copy” 。
传统消费数据时流程:
1.操作系统从磁盘读取数据到内核空间(kernel space)的page cache;
2.应用程序读取page cache的数据到用户空间(user space)的缓冲区;
3.应用程序将用户空间缓冲区的数据写回内核空间的socket缓冲区(socket buffer);
4.操作系统将数据从socket缓冲区复制到硬件(如网卡)缓冲区;
整个过程如上图所示,这个过程包含4次copy操作
和2次系统上下文切换
,而上下文切换是CPU密集型的工作,数据拷贝是I/O密集型的工作,性能其实非常低效
零拷贝:将数据从page cache直接发送到Socket缓冲区,避免了系统上下文的切换,消除了从内核空间到用户空间的来回复制。
分区后分段(segment)
消息按照topic分类存储,topic又按照分区存储到不同的broker节点。partition又按照segment分段存储。通过这种分区分段的设计,Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。
进一步对查询进行优化,Kafka又默认为分段后的数据文件建立了索引文件,也就是.index
文件,这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。
docker指令
执行命令后,可能会返回一个长字符串,这个字符串叫做容器id,对每个容器都是唯一的,可以通过容器id来看对应的容器发生了什么
确定是否有运行的镜像在容器
docker ps
启动容器
- i: 交互式操作。
- t: 终端。
- ubuntu: ubuntu 镜像。
- /bin/bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 /bin/bash。
docker run -it ubuntu /bin/bash
停止当前镜像
docker stop containId
查看停止的镜像,重启当前镜像
docker ps -a
docker start containId
删除容器
docker rm -f containId
仓库管理:仓库(Repository)是集中存放镜像的地方。
登录需要输入用户名和密码,登录成功后,我们就可以从 docker hub 上拉取自己账号下的全部镜像。
docker login
docker logout
拉取镜像
docker search ubuntu
docker pull ubuntu
推送镜像到docker hub,username改为docker账户用户名
docker tag ubuntu:18.04 username/ubuntu:18.04
docker image ls
compose是用于定义和运行多容器docker应用程序的工具,通过compose,可以使用 YML 文件来配置应用程序需要的所有服务。
三个步骤
1. 使用 Dockerfile 定义应用程序的环境。
2. 使用 docker-compose.yml 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。
3. 最后,执行 docker-compose up 命令来启动并运行整个应用程序。
示例:
1. 准备一个测试目录
mkdir composetest
cd composetest
2. 在composetest下创建 Dockerfile文件,这个dockerfile就保存了镜像信息
3. 在composetest下创建 docker-compose.yml
4. 使用Compose命令构建和运行应用
docker-compose up
# docker直接拉取kafka和zookeeper的镜像
docker pull wurstmeister/kafka
docker pull wurstmeister/zookeeper
# 首先需要启动zookeeper,如果不先启动,启动kafka没有地方注册消息
# -i:表示以“交互模式”运行容器 -t:表示容器启动后会进入其命令行 -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>
docker run -it --name zookeeper -p 12181:2181 -d wurstmeister/zookeeper:latest
# 启动kafka容器,注意需要启动三台,注意端口的映射,都是映射到9092
# 第一台
docker run -it --name kafka01 -p 19092:9092 -d -e KAFKA_BROKER_ID=0 -e KAFKA_ZOOKEEPER_CONNECT=192.168.233.129:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.233.129:19092 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第二台
docker run -it --name kafka02 -p 19093:9092 -d -e KAFKA_BROKER_ID=1 -e KAFKA_ZOOKEEPER_CONNECT=192.168.233.129:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.233.129:19093 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第三台
docker run -it --name kafka03 -p 19094:9092 -d -e KAFKA_BROKER_ID=2 -e KAFKA_ZOOKEEPER_CONNECT=192.168.233.129:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.233.129:19094 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
命令
# 创建topic名称为first,3个分区,1个副本
./kafka-topics.sh --zookeeper 192.168.233.129:12181 --create --topic first --replication-factor 1 --partitions 3
# 查看first此topic信息
./kafka-topics.sh --zookeeper 192.168.233.129:12181 --describe --topic first
Topic: first PartitionCount: 3 ReplicationFactor: 1 Configs:
Topic: first Partition: 0 Leader: 2 Replicas: 2 Isr: 2
Topic: first Partition: 1 Leader: 0 Replicas: 0 Isr: 0
Topic: first Partition: 2 Leader: 1 Replicas: 1 Isr: 1
# 调用生产者生产消息
./kafka-console-producer.sh --broker-list 192.168.233.129:19092,192.168.233.129:19093,192.168.233.129:19094 --topic first
# 调用消费者消费消息,from-beginning表示读取全部的消息
./kafka-console-consumer.sh --bootstrap-server 192.168.233.129:19092,192.168.233.129:19093,192.168.233.129:19094 --topic first --from-beginning
# 删除topic
Topic是逻辑上的改变,Partition是物理上的概念,每个Partition对应着一个log文件,该log文件中存储的就是producer生产的数据。
每条数据都会有自己的offset,每个消费者都会记录自己消费到了哪一个offset,以便出错,能够恢复到上次位置继续消费。
生产者不断向log文件追加消息,为了防止log文件过大导致查询效率低。kafka的log文件以1G为一个分界点,当.log文件大小超过1G,此时会创建一个新的.log文件。
同时为了快速定位大文件中的消息位置,分区信息中使用了分片和索引来加速定位,主要包括.index
和.log
文件组成
.log
中存放的是真实的数据
.index
文件存储的消息的offset+真实的起始偏移量
为什么要分区?
注意:kafka涉及到的选举有多处,最常提及的也有:
① controller选举
② 分区leader选举
③ consumer group leader选举
kafka集群中有多个broker,有一个会被选举为controller,因此这个选举的是
broker的leader
,也称为controller.
controller的设置
controller的选举是通过broker在zookeeper的"/controller"节点下创建临时节点
来实现的,并在该节点中写入当前broker的信息 {“version”:1,”brokerid”:1,”timestamp”:”1512018424988”} ,一个节点只能被一个客户端创建成功,创建成功的broker即为controller,即"先到先得"
controller的选举
当controller宕机或者和zookeeper失去连接时,zookeeper检测不到心跳,zookeeper上的临时节点
会被删除,而其它broker会监听临时节点的变化,当节点被删除时,其它broker会收到通知,重新发起controller选举
分区leader
的选举由 controller
负责管理和实施,当leader发生故障时,controller会将leader的改变直接通过RPC的方式通知需要为此作出响应的broker,哪些需要做出响应呢?那肯定是ISR
中follower
所在broker
,因此kafka在zookeeper中动态维护了一个ISR,只有ISR里的follower才有被选为Leader的可能。
具体流程:
按照AR集合中副本的顺序查找到第一个存活的、并且属于ISR集合的 副本作为新的leader。一个分区的AR集合在创建分区副本就确定的,只要不发生重分配,那么就保持不变。
这时候有个问题既然要存活又要在ISR中选择,那么为啥不在ISR之中直接找呢?
分区的ISR集合上面说过因为同步滞后等原因可能会改变,所以注意这里是根据AR的顺序而不是ISR的顺序找
一个极端情况:如果partition的所有副本都不可用时,怎么办?
方案1:
选择ISR中 第一个活过来的副本作为Leader
方案2:
选择第一个活过来的副本(不一定是ISR中的)作为Leader
如果一定要等待ISR中的副本活过来,那不可用的时间可能会相对较长。
如果选择第一个活过来的副本作为Leader,如果这个副本不在ISR中,那数据的一致性则难以保证(可能之前是OSR)
组协调器会为消费组内
的所有消费者选举出一个leader,这个选举的算法也很简单,第一个
加入consumer group的consumer即为leader,如果某一时刻leader消费者退出了消费组,那么会重新随机
选举一个新的leader。
为保证producer发送的数据能够可靠的发送到指定的topic中,topic的每个partition收到producer发送的数据后,都需要向producer发送ack
acknowledgement,如果producer收到ack就会进行下一轮的发送,否则重新发送数据。(因为存在leader和follower的分区副本,要确保这些信息是相同的)
发送ack的时机
半数follower同步完成就发送ack
优点是延迟低,缺点是选举新的leader的时候,容忍n台节点的故障,需要2n+1个副本。
全部follower同步完成发送ack
优点是容错率高,容忍n台节点的故障只需要n+1个副本,因为只需要剩下的一个人同意即可以发送ack,缺点就是延时高,因为需要同步全部副本
kafka选择的是第二种,不仅容错上有优势,同时在每个分区都有大量的数据,第一种方案会导致大量数据的荣誉,虽然第二种网络延迟较高,但是网络延迟对于Kafka的影响较小。
同步副本集:解决follower因为某种故障,并不能与leader进行同步数据,但是leader会持续等待的问题。如果follower长时间没有向leader同步数据,则该follower从ISR踢出,而当leader发生故障,从ISR中选举出新的leader
在0.11版本
的Kafka之前,只能保证数据不丢失,在下游对数据的重复进行去重操作,因此若存在多个下游应用上去重。性能影响很大
在0.11版本
的Kafka之后,引入了一项重大特性:幂等性,幂等性指代Producer不论向Server发送了多少次重复数据,Server端都只会持久化一条数据
启用幂等性,在Producer的参数中设置enable.idempotence=true
就行
实现实际就是从下游去重转变为上游去重。
实现方法:Producer在初始化的时候会被分配一个PID,发往同一个Partition的消息会附带Sequence Number,而Broker端会对
做缓存,当具有相同主键的消息的时候(识别为重复消息),Broker只会持久化一条。
参考图解分析:Kafka 生产者客户端工作原理
消息在通过 send() 方法发往 broker 的过程中,有可能需要经过拦截、序列化器 和 分区器 的一系列作用之后才能被真正地发往 broker。
这里主要说,生产者拦截器
那么生产者拦截器能干什么?
生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息、修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。
如何实现?
主要是自定义实现 org.apache.kafka.clients.producer.ProducerInterceptor
接口。
该接口中包含3个方法:
onSend()
: 生产者在消息序列化和计算分区前会调用拦截器的onSend() 方法来对消息进行相应的定制化操作。
onAcknowledgement()
: 会在消息被应答ack之前或消息发送失败时调用生产者拦截器
close()
:主要用于在关闭拦截器时执行一些资源的清理工作
生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka。反之,消费者需要用反序列化器(Deserializer)把从 Kafka 中收到的字节数组转换成相应的对象。
org.apache.kafka.common.serialization.StringSerializer之中,了用于 String 类型的序列化器,还有 ByteArray、ByteBuffer、Bytes、Double、Integer、Long 这几种类型,都实现了kafka.Serializer
接口
如何实现?
实现org.apache.kafka.common.serialization.Serializer
接口
configure()
:用来配置当前类,主要用来确定编码类型
serialize()
:执行序列化操作,可自定义
close()
:关闭当前的序列化器
生产者使用的序列化器和消费者使用的反序列化器是需要一一对应的,如果生产者使用了某种序列化器,比如 StringSerializer,而消费者使用了另一种序列化器,比如 IntegerSerializer,那么是无法解析出想要的数据的。
kafka本身有自己的分区策略,如果未指定,则采用默认的分区策略。
实现org.apache.kafka.clients.producer.Partitionser
指定分区器
//指定分区器
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, DefaultPartitioner.class.getName());
消息在真正发往 Kafka 之前,有可能需要经历拦截器、序列化器和分区器等一系列的作用,前面已经做了一系列分析。
可以看到整个生产者客户端由两个线程协调运行
RecordAccumulator
)之中。为啥要缓存消息?这样就可以批量发送消息,进而减少网络传输的资源消耗以提升性能,缓存大小可以配置。默认32MB。
思考一个问题,如果生产者发送消息的速度超过发送到服务器的速度,会怎样?那么生产者空间不足,这时候send()
要么被阻塞,要么抛异常。
注意 ProducerBatch 不是 ProducerRecord,ProducerBatch 中可以包含一至多个 ProducerRecord,这样设计可以使字节的使用更加紧凑,可以使字节的使用更加紧凑,增加吞吐。
当一条消息流入消息收集器,会发生什么?
① 会先寻找与消息分区所对应的双端队列(如果没有则新建)
② 再从这个双端队列的尾部获取一个 ProducerBatch(如果没有则新建),查看 ProducerBatch 中是否还可以写入这个 ProducerRecord,如果可以则写入,如果不可以则需要创建一个新的 ProducerBatch。
③ 主线程中发送过来的消息都会被追加到消息收集器的某个双端队列(Deque)中,在其的内部为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,即 Deque。
④消息写入缓存时,追加到双端队列的尾部;Sender 读取消息时,从双端队列的头部读取。
⑤ Sender 从 RecordAccumulator 中获取缓存的消息之后,会进一步将原本<分区, Deque< ProducerBatch>>
的保存形式转变成
的形式,其中 Node 表示 Kafka 集群的 broker 节点。
这里为什么要转变呢?
对于网络连接来说,生产者客户端是与具体的 broker 节点建立的连接,也就是向具体的 broker 节点发送消息,而并不关心消息属于哪一个分区,而对于 KafkaProducer 的应用逻辑而言,我们只关注向哪个分区中发送哪些消息,所以在这里需要做一个应用逻辑层面到网络I/O层面的转换。
⑥ 在转换成
的形式之后(一个broker,一个消息集合),Sender 还会进一步封装成
的形式,这样就可以以Request 请求(包含消息)发往各个 Node 了
⑦ 发送request之前,Sender 还会保存 InFlightRequests
,保存对象的具体形式为 MapInFlightRequests
还提供了管理类的方法,比如配置生产者和节点broker的连接请求数。这个配置参数为 max.in.flight.requests.per.connection,默认值为5
这个参数用来指定分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的(即ack的)
acks 参数有3种类型的值(都是字符串类型)。
acks = 1,默认值即为1。生产者发送消息之后,只要分区的leader
副本成功写入消息,那么它就会收到来自服务端的成功响应。如果没收到,有可能leader崩溃,或者重新选举leader,那么生产者就会收到一个错误响应,为了避免消息丢失,生产者可以选择重发消息。分析leader崩溃的情况,此时follower也还没有拉取消息,那么消息还是会丢失,因为新选举的leader也没有这条消息(从ISR中选举)。acks 设置为1,是消息可靠性和吞吐量之间的折中方案。
acks = 0,生产者发送消息之后不需要等待任何服务端的响应,不管服务端收没收到,生产者只管发,在其他配置环境相同的情况下,acks 设置为0可以达到最大的吞吐量。
acks = -1 或 acks = all,生产者在消息发送之后,需要等待 ISR 中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。在其他配置环境相同的情况下,acks 设置为 -1(all) 可以达到最强的可靠性。
值得注意的是acks 参数配置的值是一个字符串类型
consumer
采用pull
拉的方式来从broker
中读取数据。
push
推的模式很难适应消费速率不同的消费者,因为消息发送率是由broker
决定的,然而broker的目的就是为了以最快的速度传递消息,但是消费者的消费能力不同,就会发生consumer
来不及处理消息,典型的表现就是拒绝服务以及网络拥塞
优点:pull
方式则可以让consumer
根据自己的消费处理能力以适当的速度消费消息。
缺点:如果Kafka中没有数据,消费者一直监听等待数据消费,然而一直返回空数据,因此kafka的消费者在消费数据时会传入一个timout参数,保证如果没有数据消费时,consumer
会等待一段时间之后再返回,时长为timeout,这样就避免了一直监听【有点类似于计网通道发送数据的那个】
这里就不一一介绍消费组,分区,主题的概念了。
一个consumer group中有多个consumer,一个topic有多个partition,所以必然会涉及到partition的分配问题,即确定哪个partition由哪个consumer消费的问题。
同一个消费群组中,每个分区只会有一个消费者,而每个消费者可以消费多个分区。
所以,5个分区最多有5个消费者在消费,多余的消费者将空闲。
因此分区数决定了同组消费者个数的上限
所以,如果你的分区数是N,那么最好线程数也保持为N(非相等),这样通常能够达到最大的吞吐量。超过N的配置只是浪费系统资源,因为多出的线程不会被分配到任何分区。
看看以下的情况:
消费组中只有一个消费者
如果一个消费组(Group)里只有一个消费者(Consumer),那么这个消费者(Consumer)可以消费发送到所有分区里的消息;
消费组中消费者数量与分区数量相同
如果Group中的Consumer数量与分区数相同,则每个Consumer分配一个分区,当消息发送到一个分区时,分配对应分区的Consumer可以消费消息;
消费组中消费者数量少于分区数
如果Group中Consumer数量少于分区数,则按照分区分配策略将分区尽可能均匀的分配给各个Consumer,每个Consumer可以消费发送给对应分区的消息
消费组中消费者数量大于分区数
如果Group中Consumer数量大于分区数,那么会有一部分Consumer分配不到分区,其他Consumer一对一分配分区
多个消费组订阅相同的主题
多个消费组订阅相同的主题时,消费组之间互不影响,发往主题的消息会同时被两个消费组接收到,具体是消费组内哪个Consumer接收到消息由分区分配策略决定;
Kafka提供了消费者客户端参数partition.assignment.strategy来设置消费者与订阅主题之间的分区分配策略。
三种分区分配策略
RangeAssignor
分区分配策略的原理是:按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费组;
当然,也不会存在整除的情况,首先会将消费组内所有订阅这个主题的消费者按名称的字典顺序排序,按照范围划分,如果不够平均分配,那么字典靠前的消费者会被多分配一个分区;
假设主题T0有4个分区:P0、P1、P2、P3,消费组内有两个消费者:C0、C1,则RangeAssinger策略分区分配方案是:
T0: P0 P1 P2 P3
| | | |
C0 C0 C1 C1
假设主题T1有3个分区:P0、P1、P2 ,消费者内有两个消费者:C0、C1,则RangeAssigner策略的分区分配方案是:
T1: P0 P1 P2
| | |
C0 C0 C1
总结:range分区是按范围分配给每个消费者的
RoundRobinAssignor分区分配策略的原理是:将消费组内所有消费者及消费组订阅的所有主题的分区按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者;
RoundRobinAssignor会把消费组订阅的所有主题的所有分区排序;(range是对消费者进行排序)
消费组内每个消费者都订阅了两个主题
TO: P0 P1 P2
T1: P1 P1 P2
消费者排序: C0 C1
分区排序: T0P0 T0P1 T0P2 T1P0 T1P1 T1P2
轮询分配:T0P0 T0P1 T0P2 T1P0 T1P1 T1P2
| | | | | |
C0 C1 C0 C1 C0 C1
最终分配结果:
消费者C0: T0P0、T0P2、T1P1
消费者C1: T0P1、T1P0、T1P2
消费组内消费者订阅的主题不同
消费组排序: C0、C1、C2
分区排序:T0P0 T1P0 T1P1 T2P0 T2P1 T2P2
主题T0: P0 <-订阅--C0 <-订阅--C1 <-订阅--C2
主题T1: P0 P1 <-订阅--C1 <-订阅--C2
主题T2: P0 P1 P2 <-订阅--C2
轮询分配: T0P0 T1P0 T1P1 T2P0 T2P1 T2P2
| | | | | |
C0 C1 C2 C2 C2 C2
最终分配结果:
消费者C0: T0P0
消费者C1: T1P0
消费者C2: T1P1、T2P0、T2P1、T2P2
总结:分区是轮询着分配给各个消费者的,但在这种情况下RoundRobinAssignor策略分配的并不均匀,这样分配其实并不是最优解,C2的压力会很大。
StickAssignor分配策略又叫粘性分配策略,它有两个目标:
(1) 分区的分配要尽可能均匀;
(2) 分区的分配尽可能与上次分配的保持相同;
每个topic有多个partition,每个partition有多个副本,这些partition副本分布在不同的broker上,以保障高可用,那么这些partition副本是怎么均匀的分布到集群中的每个broker上的呢?
※ kafka分配partition副本的算法如下,
① 将所有的broker(假设总共n个broker
)和 待分配的partition排序;
② 将第i个partition分配到第(i mod n)
个broker上;
③ 第i个partition的第j个副本分配到第((i+j) mod n)
个broker上;
消费者消费完消息后会进行消费位移提交,Kafka将消费位移持久化,有了消费位移的持久化,才能使消费者在关闭、崩溃、再均衡时,能够让接替的消费者根据存储的消费位移继续进行消费
但是当消费者找不到所记录的消费位移时(比如,新的消费组建立,或者一个新的消费者订阅了新的主题后),就会根据消费者客户端参数auto.offset.reset
的配置来决定从何处开始进行消费
auto.offset.reset
参数取值:
latest
,默认参数,会从分区末尾开始消费消息;
earlist
,会从起始处开始消费
none
,表示出现查不到消费位移的时候,既不从最新的消息位置处开始消费,也不从最早的消息位置处开始消费,而是会报出NoOffsetForPartitionException异常
KafkaConsumer的seek
方法提供可以从特定的位移处开始拉取消息:
/**
* KafkaConsumer的seek方法
*/
public void seek(TopicPartition partition, long offset);
public void seekToBeginning(Collection<TopicPartition> partitions);
public void seekToEnd(Collection<TopicPartition> partitions);
在使用seek之前会使用poll方法,KafkaConsumer对外提供的poll方法有两个:poll(final long timeout)和poll(final Duration timeout),参数传递一个超时时间,用来控制poll方法阻塞的时间,在消费者的缓冲区里没有可用数据时,会发生阻塞。
public ConsumerRecords<K, V> poll(final Duration timeout);
// 内部调用的方法
private ConsumerRecords<K, V> poll(final long timeoutMs, final boolean includeMetadataInTimeout);
poll方法返回的是ConsumerRecords
,它用来表示一次拉取操作所获得的消息集,内部包含了若干ConsumerRecord(不带s)
;
接下来看这个ConsumerRecords是什么?
/**
* 消息集 ConsumerRecords 内部的方法
*/
public class ConsumerRecords<K, V> implements Iterable<ConsumerRecord<K, V>> {
// 提取消息集中指定分区的消息
public List<ConsumerRecord<K, V>> records(TopicPartition partition);
// 提取消息集中指定主题的消息
public Iterable<ConsumerRecord<K, V>> records(String topic);
// 查看拉取的消息集中的分区列表
public Set<TopicPartition> partitions();
// 循环遍历消息集中的消息
public Iterator<ConsumerRecord<K, V>> iterator();
// 返回消息集中消息的个数
public int count();
// 判断消息集是否为空
public boolean isEmpty();
// ...
}
消息ConsumerRecord
内容
/**
* 消费者客户端拉取的消息 ConsumerRecord
*/
public class ConsumerRecord<K, V> {
public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;
public static final int NULL_SIZE = -1;
public static final int NULL_CHECKSUM = -1;
private final String topic;
private final int partition;
private final long offset;
private final long timestamp;
private final TimestampType timestampType;
private final int serializedKeySize;
private final int serializedValueSize;
private final Headers headers;
private final K key;
private final V value;
private volatile Long checksum;
// ... 省略内部方法
}
对于Kafka的分区而言,分区中的每条消息都有唯一的offset,用来标识消息在分区中对应的位置;
对于消费者而言,也有一个offset,表示当前消费到分区中的某个消息所在的位置
KafkaConsumer每次调用poll()
方法时,返回的是还没有消费过的消息集,要做到这一点就要记录上一次消费时的消费位移;并且这个消费位移必须做持久化保存,而不是单单保存在内存中;这样在消费者重启、新的消费者加入、再均衡发生时,都能够知晓之前的消费位移,然后继续消费后续的消息;
就比如Consumer在消费过程中可能会出现断电宕机等故障,Consumer恢复以后,需要从故障前的位置继续消费,所以Consumer需要实时记录自己消费到了那个offset,需要持久化,以便故障恢复后继续消费。
因此每个消费者自身维护了一个offset。
Kafka0.9版本之前,consumer默认将offset保存在zookeeper中,从0.9版本之后,consumer默认将offset保存在kafka一个内置的topic中,该topic为__consumer_offsets
位移提交
位移提交时机的把握也很讲究,不同的提交时机可能造成重复消费或消息丢失的现象。
如果拉取完消息还未做消息处理前,就立即提交消费位移,有可能造成消息丢失现象。
例如:当前poll()操作拉取的消息集为[x+2,x+7]
其中x+2
代表上一次提交的消费位移,如果拉取到消息之后就进行了位移提交,那么提交[x+8]
(这里表示的是下一次需要消费的消息位移),假如当前消费到了x+5
时,消费者遇到了异常,在故障恢复后,消费者重新拉取消息,但是此时已经提交了消费位移x+8
,所以重新拉取的消息是从x+8
开始的,这样会导致x+5
到x+7
之间的消息未被处理,如此便发生了消息丢失现象;
如果位移提交动作是在消费完所有拉取的消息后才执行,有可能造成重复消费现象:
例如:当前poll()操作拉取的消息集[x+2,x+7]
其中x+2
代表上一次提交的消费位移,当消费到x+5
时遇到了异常,在故障恢复后,重新拉取消息,因为本次消费位移还未提交,重新拉取的消息是从x+2
开始的,也就是说x+2
到x+4
的消息又重新消费了一遍,故而发生了重新消费的现象;
Kafka中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数enable.auto.commit
配置,默认值为true
;
自动位移提交仍会带来重复消费和消息丢失现象;
消费者每隔5秒(由参数auto.commit.interval.ms
控制)会将拉取的每个分区中最大的消息位移进行提交.自动位移提交的动作是在poll()方法的逻辑里完成的.
commitSync()
提交消费位移时,会阻塞消费者线程直至位移提交完成;commitAsync()
执行时,消费者线程不会被阻塞,可能在提交消费位移的结果还未返回之前就开始了新一次的拉取操作;顺序写磁盘
Kafka的producer生产数据,需要写入到log文件中,写的过程是追加到文件末端,顺序写的方式,官网有数据表明,同样的磁盘,顺序写能够到600M/s,而随机写只有200K/s,这与磁盘的机械结构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
零复制技术
kafka中的消费者在读取服务端的数据时,需要将服务端的磁盘文件通过网络发送到消费者进程,网络发送需要经过几种网络节点。
传统传统的读取文件数据并发送到网络的步骤如下:
(1)操作系统将数据从磁盘文件中读取到内核空间的页面缓存;
(2)应用程序将数据从内核空间读入用户空间缓冲区;
(3)应用程序将读到数据写回内核空间并放入socket缓冲区;
(4)操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送。
通常kafka消息有多个订阅者,一个生产者发布的消息会被多个消费者多次消费,为了优化这个流程,kafka采用零拷贝技术
,如下图
零拷贝技术,只用将磁盘文件的数据复制到内核空间的页面缓存中(从而发给不同的订阅者时,都使用的是同一个页面缓存),避免重复操作。
如果有10个消费者,传统方式下,数据复制次数为4*10=40次(4个步骤),而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。
这部分参考了秒懂 kafka HA(高可用)
我们知道,kafka是基于zookeeper协调管理的,那么zookeeper中究竟存储了哪些信息?另外在后面分析broker宕机和controller宕机时,我们也需要先了解zookeeper的目录结构
ls /brokers
[ids, topics, seqid]
ls /brokers/ids
[0]
ls /brokers/topics
[__consumer_offsets, first]
ls /brokers/topics/first
[partitions]
ls /brokers/topics/first/partitions
[0, 1]
ls /brokers/topics/first/partitions/0
[state]
get /brokers/topics/first/partitions/0/state
{"controller_epoch":21,"leader":0,"version":1,"leader_epoch":8,"isr":[0]}
...
① controller
controller节点下存放的是kafka集群中controller的信息(controller即kafka集群中所有broker的leader)。
② controller_epoch
controller_epoch用于记录controller发生变更的次数(controller宕机后会重新选举controller,这时候controller_epoch的值会+1),即记录当前的控制器是第几代控制器,用于防止broker脑裂。
③ brokes
brokers下的ids存储了存活的broker信息,topics存储了kafka集群中topic的信息,其中有一个特殊的topic:_consumer_offsets
,新版本的kafka将消费者的offset就存储在_consumer_offsets
下。
brokers [ids, topics, seqid]
④ ids
每个broker的配置文件中都需要指定一个数字类型的id(全局不可重复),此节点为临时znode,比如此时broker里只有一个broker [0],如果要查看当前节点的信息那么,
get /brokers/ids/0
{"listener_security_protocol_map":{"PLAINTEXT":"PLAINTEXT"},
"endpoints":["PLAINTEXT://192.168.1.103:9092"],
"jmx_port":-1,"host":"192.168.1.103",
"timestamp":"1559232289265","port":9092,"version":4}
⑤ seqid
broker启动时检查并确保存在, 永久节点
⑥ topics
Schema:
{
"version": "版本编号目前固定为数字1",
"partitions": {
"partitionId编号": [
同步副本组brokerId列表
],
"partitionId编号": [
同步副本组brokerId列表
],
.......
}
}
分析了kafk集群结构后,思考当kafka集群中的一个broker节点宕机时(非controller节点),会发生什么?
※ 当非controller的broker宕机时,会执行如下操作
1、controller会在zookeeper的 " /brokers/ids/" 节点注册一个watcher(监视器),当有broker宕机时,zookeeper会触发监视器(fire watch)通知controller。
2、controller 从 “/brokers/ids” 节点读取到所有可用的broker。
3、controller会声明一个set_p集合,该集合包含了宕机broker上所有的partition
4、针对set_p中的每一个partition,
① 从 "/state"节点 读取该partition当前的ISR;
② 决定该partition的新leader,如果该分区的 `ISR中有存活的副本` ,则选择其中一个作为新leader,如果该partition的ISR副本全部挂了, 则选择该partition的 `AR集合` 中任一幸存的副本作为leader(此时可能出现数据不同步问题),如果该partition的所有副本都挂,则将分区的leader `设为-1`
③ 将新 leader、ISR、controller_epoch 和 leader_epoch 等信息写入 state 节点;
5、通过RPC向set_p相关的broker发送LeaderAndISR Request命令。
那么分析了broker故障时,这些决策都是由controller来发起的,那么如果controller故障的话,会发生什么?
1、当controller
宕机时会触发 controller failover,每个 broker 都会在 zookeeper 的 “/controller” 节点注册 watcher(监听器)。 ps:和上述一致,这个监视器是broker所拥有的。
2、当controller
宕机时zookeeper 中的临时节点(ids)消失,所有存活的 broker 收到 fire 的通知,每个 broker 都尝试创建新的临时节点(ids),但只有一个会创建成功并当选为 controller。
3、当新的controller
当选时,会回调KafkaController的onControllerFailover()方法
,这个方法中完成controller的初始化(初始化里有很多方法)
1.Kafka 中的 ISR(InSyncRepli)、 OSR(OutSyncRepli)、 AR(AllRepli)代表什么?
ISR:速率和leader相差低于10s的
follower
的集合
OSR:速率和leader相差大于10s的follwer
AR:所有分区的follower,其实就是AR = ISR + OSR
同步期间,follower的数据相对leader而言会有一定程度的滞后,前面所说的"一定程度同步"就是指可忍受的滞后范围,这个范围可以通过server.properties中的参数进行配置。
理想情况下,所有的follower副本都应该与leader 保持一定程度的同步,即AR=ISR,OSR集合为空
2.Kafka 中的 HW、 LEO 等分别代表什么?
HW(High Water):俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能消费HW之前的消息。
LEO(Log End Offset):标识当前日志文件中下一条待写入的消息的offset,下图offset为9的就是当前日志的LEO
下图表示一个日志文件,这个日志文件中有9条消息,第一条消息的offset为0,最后一条消息的offset为8,虚线表示的offset为9的消息,代表下一条待写入的消息。日志文件的 HW 为6,表示消费者只能拉取offset在 0 到 5 之间的消息,offset为6的消息对消费者而言是不可见的。
分区 ISR 集合中的每个副本都会维护自身的 LEO ,而 ISR 集合中最小的 LEO 即为分区的 HW
,对消费者而言只能消费 HW 之前的消息。
2.1.ISR 集合和 HW、LEO的关系
producer在发布消息到partition时,只会与该partition的leader发生交互将消息发送给leader,leader会将该消息写入其本地log,每个follower都从leader上pull数据做同步备份,follower在pull到该消息并写入其log后,会向leader发送ack,一旦leader收到了ISR中的所有follower的ack(只关注ISR中的所有follower,不考虑OSR,一定程度上提升了吞吐),该消息就被认为已经commit了。leader就增加HW(因为这些消息被消费了,水位也上移了),然后向producer发送ack。
也就是说,在ISR中所有的follower还没有备份完数据备份之前,leader并不会增加HW,也就是这条消息暂时还不能被消费者消费,只有所有ISR都更新完成,才能后移。
ISR集合中LEO最小的副本,说明offset小,说明同步数据是最慢的,这个最慢的LEO是leader的HW,消费者消费leader的HW之前的消息。
3.Kafka 中是怎么体现消息顺序性的?
在每个分区内,每条消息都有offset,所以消息在同一分区内有序,无法做到全局有序性。
为什么做不到全局有序:因为消息会发送到不同的分区,发给分区时的顺序是无法保证的。
那为什么能做到分区内消息有序:因为leader负责数据的顺序写入,而follower去同步leader的数据。
4.Kafka 中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
分区器Partitioner用来对分区进行处理的,即消息发送到哪一个分区的问题。序列化器,这个是对数据进行序列化和反序列化的工具。拦截器,即对于消息发送进行一个提前处理和收尾处理的类Interceptor,处理顺利首先通过拦截器=>序列化器=>分区器
5.Kafka 生产者客户端的整体结构是什么样子的?使用了几个线程来处理?分别是什么?
使用两个线程:main和sender 线程,main线程会一次经过拦截器、序列化器、分区器将数据发送到RecoreAccumulator线程共享变量,再由sender线程从收集器中拉取batch信息,封装成request发送给node,发送之前还需要request放入onFlightRequest,缓存那些已经发出但还没收到确认的请求。
6.消费组中的消费者个数如果超过 topic 的分区,那么就会有消费者消费不到数据”这句 话是否正确?
这句话是对的,超过分区个数的消费者不会在接收数据,主要原因是一个分区的消息只能够至多被一个消费者组中的一个消费者消费。因此分区有限,消费者很多,消费者也收不到。
7.消费者提交消费位移时提交的是当前消费到的最新消息的 offset 还是 offset+1?
生产者发送数据的offset是从0开始的,消费者消费的数据的offset是从1开始,故最新消息是offset+1,因为提交的是下一次要获取的消息offset
8.有哪些情形会造成重复消费?
全部消费完,后提交offset,如果消费途中宕机了,又回重新开始消费,从而会造成重复消费。
9.那些情景会造成消息漏消费?
先提交offset,还没消费就宕机了,则会造成漏消费
10.当你使用 kafka-topics.sh 创建(删除)了一个 topic 之后, Kafka 背后会执行什么逻辑?
会在 zookeeper 中的/brokers/topics 节点下创建一个新的 topic 节点,如:/brokers/topics/first
触发 Controller 的监听程序
kafka Controller 负责 topic 的创建工作,并更新 metadata cache
11.topic 的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
可以增加,修改分区个数–alter可以修改分区个数
12.topic 的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
不可以减少,减少了分区之后,之前的分区中的数据不好处理
13.Kafka 有内部的 topic 吗?如果有是什么?有什么所用?
有,
__consumer_offsets
主要用来在0.9版本以后保存消费者消费的offset
14.Kafka 分区分配的概念?
Kafka分区对于Kafka集群来说,分区可以做到负载均衡,对于消费者来说分区可以提高并发度,提高读取效率
15.简述 Kafka 的日志目录结构?
每一个分区对应着一个文件夹,命名为topic-0/topic-1…,每个文件夹内有.index和.log文件。
16.如果我指定了一个 offset, Kafka Controller 怎么查找到对应的消息?
offset
表示当前消息的编号,首先可以通过二分法定位当前消息属于哪个.index
文件中,随后采用seek
定位的方法查找到当前offset
在.index
中的位置,此时可以拿到初始的偏移量。通过初始的偏移量再通过seek定位到.log中的消息即可找到。
17.Kafka Controller 的作用?
Kafka集群中有一个
broker
会被选举为Controller
,负责管理集群broker的上下线、所有topic
的分区副本分配和leader
的选举等工作。Controller的工作管理是依赖于zookeeper的。
18.Kafka 中有那些地方需要选举?这些地方的选举策略又有哪些?
broker的controller、分区的leader副本、消费组leader,看目录中的具体介绍
19.失效副本是指什么?有那些应对措施?
失效副本为速率比leader相差大于10s的follower,
ISR
会将这些失效的follower踢出,等速率接近leader的10s内,会重新加入ISR
20.Kafka 的哪些设计让它有如此高的性能?
1.Kafka天生的分布式架构
2.对log文件进行了分segment,并对segment建立了索引
3.对于单节点使用了顺序读写,顺序读写是指的文件的顺序追加,减少了磁盘寻址的开销,相比随机写速度提升很多
4.使用了零拷贝技术,不需要切换到用户态,在内核态即可完成读写操作,且数据的拷贝次数也更少。