Kafka架构(下方该是Consumer) Kafka 是一个分布式的基于发布/订阅模式的消息队列,依靠其强悍的吞吐量,Kafka 主要应用于大数据实时处理领域。在数据采集、传输、存储的过程中发挥着举足轻重的作用。
ZooKeeper 在 Kafka 中有举足轻重的地位,一般提供如下功能:
Broker 是分布式部署并且相互之间相互独立,但是需要有一个注册系统能够将整个集群中的Broker管理起来,比如用ZooKeeper。
在 Kafka 中同一个 Topic 的消息会被分成多个 Partition 并将其分布在多个 Broker 上,这些 Partition 信息及与 Broker 的对应关系也都是由 Zookeeper 在维护,由专门的节点来记录。
同一个Topic消息会被分区并将其分布在多个Broker上,因此,生产者需要将消息合理地发送到这些分布式的Broker上。
Kafka 中的消费者同样需要进行负载均衡来实现多个消费者合理地从对应的 Broker 服务器上接收消息,每个消费者分组包含若干消费者,每条消息都只会发送给分组中的一个消费者
,不同的消费者分组消费自己特定的Topic下面的消息,互不干扰。
Kafka 会为每个 Consumer Group 分配个全局唯一 Group ID,Group 内的 Consumer 共享该 ID,Kafka规定 每个partition信息只能被同组的一个Consumer 消费,在Zk中记录partition 跟 Consumer关系,每个消费者一旦确定了对一个消息分区的消费权力,需要将其Consumer ID 写入到 Zookeeper 对应消息分区的临时节点上。
Consumer 对指定消息分区进行消费的过程中,需要定时地将分区消息的消费进度 Offset 记录到 Zookeeper 上,以便在该 Consumer 进行重启或者其他 Consumer 重新接管该消息分区的消息消费后,能够从之前的进度开始继续进行消息消费。
为让同一个 Topic 下不同分区的消息尽量均衡地被多个 Consumer 消费而进行 Consumer 与消息分区分配的过程。
producer采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于 顺序写磁盘 ,顺序写比随机写要起码提速3个数量级!
消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些 分区日志 Partition Logs 组成,其组织结构如下图所示:
Partition发生 可以看到每个 Partition 中的消息都是有序的,生产的消息被不断追加到 Partition log 上,其中的每一个消息都被赋予了一个唯一的 offset 值。
消费者 通过分区可以 方便在集群中扩展,可以提高并发。
形象理解:
Kafka 的设计源自生活,好比为公路运输,不同的起始点和目的地需要修不同高速公路(主题),高速公路上可以提供多条车道(分区),流量大的公路(主题)多修几条车道(分区)保证畅通,流量小的公路少修几条车道避免浪费。收费站好比消费者,车多的时候多开几个一起收费避免堵在路上,车少的时候开几个让汽车并道就好了。
我们需要将producer发送的数据封装成一个ProducerRecord对象。
数据封装
Kafka存储结构
.index`存放数据索引,`.log
存储数据。index文件中的元数据指向对应log文件中Message的物理偏移地址(参考 kaldi、Neo4j)。分片
和索引
机制,将每个partition分为多个segment。每个 segment 对应.index`跟`.log
。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称 + 分区序号。例如 first 这个 topic 有三个分区,则其对应的文件夹为first-0、first-1、first-2。00000000000000000000.index
00000000000000000000.log
00000000000000170410.index
00000000000000170410.log
00000000000000239430.index
00000000000000239430.log
注意:index 和 log 文件以当前segment的第一条消息的 offset 命名。
数据查找过程
多线程消费
多个消费者
单线程消费
内存队列
消息传递语义 message delivery semantic ,Kafka 为确保消息在 producer 和 consumer 之间传输。有以下三种传输保障(delivery guarantee):
理想情况下肯定希望系统的消息传递是严格 exactly once,但很难做到。接下来会按照 消息的传播流程大致说下。
大致步骤如下:
对于Leader回复 ack,Kafka 为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡。
不等待
broker 的ack,提供了一个最低的延迟,broker接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据,对应 At Most Once 模式。leader落盘成功后返回ack
,如果在follower同步成功之前leader故障,那么将会丢失数据;认为leader返回 信息就成功了。全部落盘
成功后才返回 ack。重新
给follower 发送个信息。如果业务需要数据 Exactly Once,在早期的 Kafka 版本中 只能在下游去重,现在引入了个幂等性,意思就是无论生产者发送多少个重复消息,Server端只会持久化一条数据,
At Least Once + 幂等性 = Exactly Once
启动幂等性,在生产者参数中 enable.idompotence= true,开启幂等性的生产者在初始化时候会被分配一个PID,发送同一个Partition的消息会附带Sequence Number,Broker会对做缓存,以此来判断唯一性。但是如果PID重启就会发生变化,同时不同partition也具有不同的主键,幂等性无法保证跨分区会话的 Exactly Once。
数据落盘过程
Kafka Broker 收到信息后,如何落盘是通过 producer.type 来设定的,一般两个值。
消费数据 Consumer 是以 Consumer Group 消费者组的方式工作,由一个或者多个消费者组成一个组,共同消费一个topic。每个分区在同一时间只能由group中的一个消费者读取,但是多个group可以同时消费这个partition。如果一个消费者失败了,那么其他的 group 成员会自动负载均衡读取之前失败的消费者读取的分区。Consumer Group 从 Broker 拉取消息来消费主要分为两个阶段:
如果你先提交 offset 再处理数据可能在处理数据时出现异常导致数据丢失。而如果你先处理数据再提交 offset, 如果提交 offset 失败可能导致信息重复消费。
PS:
pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为 timeout。
同一个 group.id 中的消费者,对于一个 topic 中的多个 partition 中的消息消费,存在着一定的分区分配策略。
在 Kafka 中存在着两种分区分配策略,通过 partition.assignment.strategy 来设置。
Range 范围分区策略是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。假如现在有 10 个分区,3 个消费者,排序后的分区将会是p0~p9。消费者排序完之后将会是C1-0、C2-0、C3-0。通过 Partitions数 / Consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
消费者 | 消费的分区 |
---|---|
C1-0 | 消费 p0、1、2、3分区 |
C2-0 | 消费 4、5、6分区 |
C3-0 | 消费 7、8、9分区 |
Range 范围分区的弊端:
如上只是针对 1 个 topic 而言,C1-0 消费者多消费1个分区影响不是很大。如果有 N 多个 topic,那么针对每个 topic,消费者 C1-0 都将多消费 1 个分区,topic越多,C1-0 消费的分区会比其他消费者明显多消费 N 个分区。这就是 Range 范围分区的一个很明显的弊端了.
RoundRobin 轮询分区策略是把所有的 partition 和所有的 consumer 都列出来,然后按照 hascode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。轮询分区分为如下两种情况:
如果同一消费组内,所有的消费者订阅的消息都是相同的,那么 RoundRobin 策略的分区分配会是均匀的。
例如同一消费者组中,有 3 个消费者C0、C1和C2,都订阅了 2 个主题 t0 和 t1,并且每个主题都有 3 个分区(p0、p1、p2),那么所订阅的所以分区可以标识为t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终分区分配结果如下:
消费者 | 消费的分区 |
---|---|
C0 | 消费 t0p0、t1p0 分区 |
C1 | 消费 t0p1、t1p1 分区 |
C2 | 消费 t0p2、t1p2 分区 |
同一消费者组内,所订阅的消息是不相同的,那么分区分配就不是完全的轮询分配,有可能会导致分区分配的不均匀。如果某个消费者没有订阅消费组内的某个 topic,那么在分配分区的时候,此消费者将不会分配到这个 topic 的任何分区。
例如同一消费者组中有3个消费者C0、C1、C2,他们共订阅了 3 个主题t0、t1、t2,这 3 个主题分别有 1、2、3 个分区(即t0有1个分区(p0),t1有2个分区(p0、p1),t2有3个分区(p0、p1、p2)),即整个消费者所订阅的所有分区可以标识为 t0p0、t1p0、t1p1、t2p0、t2p1、t2p2。然后消费者 C0 订阅的是主题t0,消费者C1订阅的是主题t0和t1,消费者C2订阅的是主题t0、t1和t2,最终分区分配结果如下:
消费者 | 消费的分区 |
---|---|
C0 | 消费 t0p0 分区 |
C1 | 消费 t1p0 分区 |
C2 | 消费 t1p1、 t2p0、 t2p1、 t2p2 分区 |
Kafka 可支持百万 TPS 跟如下几个特性有关。
信息存储在硬盘中,硬盘由很多盘片组成,显微镜观察盘片会看见盘片表面凹凸不平,凸起的地方被磁化代表数字1,凹的地方是没有被磁化代表数字0,因此硬盘可以以二进制来存储表示文字、图片等信息。
磁盘平面图 上图是硬盘的实际图,可能无法理解内部构造,我们来看个形象的图:
磁盘内部图
磁头
从盘面
读取数据,磁头在盘面上的飞行高度只是人类头发直径的千分之一。盘片
跟CD光盘的长相类似,一个盘片
有上下两个盘面
,每个盘面
都可以存储数据。盘面
会被划分出超级多的同心圆磁道
,同心圆的半径是不同的。柱面
,相同磁道
的同一个扇区
被称为簇
。数据的读写按照柱面
从上到下进行,一个柱面写满后,才移到下一个扇区开始写数据。圆弧
(扇区
),每个扇区
用来存储 512个字节跟其他信息。由于同心圆的扇区弧度相同而半径不同所以外圈线速度比内圈线速度大。block
来进行读取数据的,一个block
(块)一般有多个扇区组成。在每块的大小是 4~64KB。page
,默认4KB,操作系统经常与内存和硬盘这两种存储设备进行通信,类似于块的概念,都需要一种虚拟的基本单位。所以与内存操作,是虚拟一个页的概念来作为最小单位。与硬盘打交道,就是以块为最小单位。一次访盘的读/写请求完成过程由三个动作组成:
可以发现读取主要耗时是在前两个,如果我顺序读取则寻道
跟旋转延迟
只用一次即可。而如果随机读取呢则可能经历多次寻道
跟旋转延迟
,两者相差几乎 3个数量级。
随机跟顺序读写在磁盘跟内存中
内存映射
CPU 发出指令操作 IO 来进行读写操作,大部分情况下其实只是把数据读取到内存,然后从内存传到IO即可,所以数据其实可以不经过CPU的。
Direct Memory Access
的出现就是为批量数据的输入/输出而提速的。DMA 是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。这样数据的传送速度就取决于存储器和外设的工作速度。
如果数据传输的时候只用到了 DMA 传输而没经过 CPU 复制数据,则我们称之为零拷贝 Zero Copy。用了 Zero Copy 技术耗时性能起码减半。
零拷贝
如上黑色流程是没用 Zero Copy
技术流程:
红色流程是用 Zero Copy
技术流程:
消费者拉取数据的时候,Kafka 不是一个一个的来送数据的,而是批量发送来处理的,这样可以节省网络传输,增大系统的TPS,不过也有个缺点就是,我们的数据不是真正的实时处理的,而真正的实时还是要看 Flink
。