Kafka是一种高吞吐量、持久性、分布式的发布订阅的消息队列系统。它最初由LinkedIn(领英)公司发布,使用Scala语言编写,与2010年12月份开源,成为Apache的顶级子项目
1、Kafka的组成结构
- Producer:消息的生产者。
- Broker : Broker 是 kafka 的实例,每个服务器有一个或者多个 Kafka实例。Kafka 集群内的 broker 有不重复的编号。
- Topic: 消息主题,可以理解为消息的分类,Kafka 的数据保存在 topic 中,有点类似队列,每个broker 可以创建多个 topic 。
- Partition: Topic 的分区,每个topic 可以有多个分区。分区的作用是负载,提高 kafka 的吞吐量。同一个 Topic 在不同分区上的的数据是不重复的,partion 的表现形式是一个个文件夹。
- Replication:每个分区有多个副本,当主分区(Leader)出现故障是,会选择一个副本(Follower)上位。成为新Leader 。Kafka 默认副本数为10 个,副本数量不能大于 Broker 的数量,follower 和leader 在不同机器。
- Message:消息主体
- Consumer:消息消费者
- Consumer Group :可以将多个消费者组成一个消费者组,同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个 topic 的不同分区的数据。提高 Kafka 的吞吐量。
- zookeeper:kafka 集群依赖 zookeeper 保存集群信息,保证系统的可用性。
2、生产者发送数据
producer 是生产者,也是数据的入口,Producer 在写入数据时(需要指定broker地址),写入leader , 不会将数据写入 follower。leader会维持一个与其保持同步的follower(副本)集合,该集合就是ISR,每一个partition都有一个ISR,它由leader动态维护
producer 是采用 push 模式将数据发布到broker,每条消息顺序追加到分区中(如果分区key为空,kafka使用默认的RoundRobin算法将消息均衡地分布在各个partition上,如果不为空则采用Hash散列,也可以自定义),顺序写入磁盘,所以保证同一分区内数据是有序的。
一个topic可以有多个partion ,可以提高系统的扩展性。
Partion 文件夹下有很多组 segment 文件,每组 segement 文件包含 .index 、.log 文件、.timeindex 文件,这个三个文件。
- log 文件是实际存储 message 的地方
- index 和 timeindex 文件为索引文件,用于检索消息。
- 消息顺序写入segement中,只有最后一个segement才能执行写入操作,segement文件大小默认为1G。
每个partion 有多个 segement ,每个 segment 以 最小offset 来命名,如000.index存储offset为0~368795的消息,kafka就是利用分段+索引的方式来解决查找效率的问题。
发送过程如何保证消息不丢失?
producer 向 kafka 发送消息时,要集群保证消息不丢失,其实是通过 ACK 机制, 当生产者写入数据,可以通过设置参数来确定 Kafka 是否接收到数据。
- 0 代表 producer 往集群发送数据,不需要等待集群返回,不确保消息发送成功。安全性低,效率高。
- 1 代表 producer 往集群发送数据,只需要leader 应答即可,只确保了leader 接收到了消息数据,安全性和效率折中。
- all 代表 producer 往集群发送数据,需要所有的 follower 与leader 完成数据同步,生产者 producer 才会发送下一条消息。安全性最高,效率最低。
Message 结构
Message 是存储在log 里面的,Message 结构主要分成几个部分,消息体,消息大小,offset、压缩类型等。
- offset: offset 是一个有序 id ,可以为其确定每条消息在 partion 内的位置,占 8Byte
- 消息大小:描述消息的大小,占4byte
- 消息体:消息存放的实际消息数据
保留策略
无论消息是否被消费,Kafka 都会保存所有的消息, kafka 是删除旧消息策略
- 基于时间策略,默认配置 168小时(7天)
- 基于大小策略,当topic 所占日志大小大于一个阀值时,则可以开始删除最旧的消息了
3、消费者消费消息
消费者是从 Leader 中去拉取消息的,多个消费者可以组成一个消费组,每个消费组有一个组 id, 同一个消费组者的消费者可以消费同一个 topic 下不同分区的数据,但是不会组内多个消费者消费同一个分区的数据。 一个分区只能被一个消费者消费。一个消费者可以消费多个分区。因此线程数应当小于等于分区数
怎么根据消费者的offset查找到对应消息呢?
假设消费者的offset为368801
步骤一、先找到offset为368801所在的segment文件(利用二分法查找),这里找到的就是在第二个segment文件,如下图所示。
步骤二、打开对应segment中的.index文件(也就是368796.index文件,该文件起始偏移量为368796+1,我们要查找的offset为368801的message在该index内的偏移量为368796+5=368801,所以这里要查找的相对offset为5)。由于index文件采用的是稀疏索引的方式存储着相对offset及对应message物理偏移量的关系,所以直接找相对offset为5的索引可能找不到,这里同样利用二分法查找相对offset小于或者等于指定的相对offset的索引条目中最大的那个相对offset,所以找到的是相对offset为4的这个索引。
步骤三、根据找到的相对offset为4的索引确定message存储的物理偏移位置为256。打开数据文件,从位置为256的那个地方开始顺序扫描直到找到offset为368801的那条Message。
位移提交
(1)自动位移提交(默认)
自动位移就是定期提交,默认5秒一次,有可能会导致重复消费和消息丢失。
重复消费: 假设刚刚提交完一次消费位移,然后拉取一批消息进行消费,在下一次自动提交消费位移之前,消费者崩溃了,那么又得从上一次位移提交的地方重新开始消费,这样便发生了重复消费的现
消息丢失: 消费方拉取的消息在逻辑处理线程还未处理结束,此时到达了自动提交窗口期,自动提交线程将拉取到的每个分区的最大消息位移进行提交,就会发生消息丢失。
(2)手动位移提交
手动位移提交可以分为同步提交和异步提交。因为同步提交一定会成功、异步可能会失败,所以一般的场景是同步和异步一起来做。其实如果是位移提交异常的话,倒是问题不大,因为最多是重复消费。如果是业务异常,可能消息未正常处理,如果武断的提交位移的话,就会导致消息丢失。因此在捕捉到异常后,要分析是什么异常,根据异常类型决定是否要执行最终的位移提交。如下方代码所示
try { while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1)); process(records); commitAysnc(); } } catch (Exception e) { if(e==不需要重复消费){ try { consumer.commitSync(); } finally { consumer.close(); } } }
4、集群管理
Broker注册
Broker是分布式部署并且相互之间相互独立,但是需要有一个注册系统能够将整个集群中的Broker管理起来,此时就使用到了Zookeeper。在Zookeeper上会有一个专门用来进行Broker服务器列表记录的节点:
/brokers/ids
每个Broker在启动时,都会到Zookeeper上进行注册,即到/brokers/ids下创建属于自己的节点,如/brokers/ids/[0...N]。
Kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker ID进行注册,创建完节点后,每个Broker就会将自己的IP地址和端口信息记录到该节点中去。其中,Broker创建的节点类型是临时节点,一旦Broker宕机,则对应的临时节点也会被自动删除。
控制器选举
所有的Broker节点会一起去Zookeeper上注册一个表明自己是控制器的临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Broker会成为Controller,其他的broker叫follower(如果controller宕机,则临时节点会删除,其它follower会竞争新一轮控制器选举)。
Topic注册
在Kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:
/borkers/topics
Kafka中每个Topic都会以/brokers/topics/[topic]的形式被记录,如/brokers/topics/login和/brokers/topics/search等。Broker服务器启动后,会到对应Topic节点(/brokers/topics)上注册自己的Broker ID并写入针对该Topic的分区总数,如/brokers/topics/login/3->2,这个节点表示Broker ID为3的一个Broker服务器,对于"login"这个Topic的消息,提供了2个分区进行消息存储,同样,这个分区节点也是临时节点。
消费者注册
当有新的消费者注册到zk中,zk会创建专用的节点来保存相关信息,路径:
/consumers/{group_id}/[ids,owners,offset]
Ids:记录该消费分组有几个正在消费的消费者,
Owners:记录该消费分组消费的topic信息,
Offset:记录topic每个分区中的每个offset
消费者负载均衡
监听/consumers/{group_id}/ids的子节点的变化,一旦发现消费者新增或者减少及时调整消费者的负载均衡
5、消息丢失和重复消费
消息丢失
一、Producer写入消息失败
1)使用同步模式的时候,当request.required.acks配置为1(只保证写入leader成功)的话,如果刚好leader partition挂了,数据就会丢失。
2)使用异步模式的时候,当缓冲区满了,如果阻塞等待的时间配置为0(还没有收到确认的情况下,缓冲池一满,就清除缓冲池里的消息),数据就会被立即丢弃掉。
解决方案:
1)同步模式下,确认机制设置为-1,也就是让消息写入leader和所有的ISR副本,代价是性能较低。
2)在异步模式下,如果消息发出去了,但还没有收到确认的时候,缓冲池满了,在配置文件中设置成不限制阻塞超时的时间,也就说让生产端一直阻塞,这样也能保证数据不会丢失。
二、消费者未来得及消费消息
1)使用自动位移提交时,消费方拉取的消息在逻辑处理线程还未处理结束,此时到达了自动提交窗口期,自动提交线程将拉取到的每个分区的最大消息位移进行提交,就会发生消息丢失。
2)使用手动位移提交时,如果产生了业务异常,可能消息未正常处理,此时提交位移的话,就会导致消息丢失。
解决方案:
1)关闭自动位移提交,同步提交和异步提交组合使用,先异步,若有异常,区分提交异常和业务异常,如果是提交异常则使用同步提交,如果是业务异常可以不处理,让kafka重复消费
消息重复消费
一、Producer重复写入消息
producer发出一条消息,broke落盘之后由于网络等种种缘由发送端获得一个发送失败的响应或者网络中断,而后producer收到一个可恢复的Exception重试消息致使消息重复。
解决方案:
1)同步模式下,确认机制设置为-1,也就是让消息写入leader和所有的ISR副本,代价是性能较低。
2)在异步模式下,如果消息发出去了,但还没有收到确认的时候,缓冲池满了,在配置文件中设置成不限制阻塞超时的时间,也就说让生产端一直阻塞,这样也能保证数据不会丢失。
二、消费者未来得及消费消息
1)使用自动位移提交时,消费方拉取的消息在逻辑处理线程还未处理结束,此时到达了自动提交窗口期,自动提交线程将拉取到的每个分区的最大消息位移进行提交,就会发生消息丢失。
2)使用手动位移提交时,如果产生了业务异常,可能消息未正常处理,此时提交位移的话,就会导致消息丢失。
解决方案:
1)关闭自动位移提交,同步提交和异步提交组合使用,先异步,若有异常,先捕捉异常,然后区分提交异常和业务异常,如果是提交异常则使用同步提交,如果是业务异常可以不处理,让kafka重复消费
6、Kafka高性能
(1)分区
通过负载均衡机制传递到不同的分区以减轻单个服务器实例的压力
(2)网络开销少
批量发送:在发送消息的时候,kafka会先将消息缓存在内存中,当超过一个的大小或者超过一定的时间,才会将这些消息进行批量发送。
端到端压缩:kafaka会将批量的数据进行压缩,发送broker服务器后,最终还是以压缩的方式传递到消费者的手上。
(3)优秀的存储结构
顺序写入:磁盘线性写入性能非常高
索引结构:通过分段存储和稀疏索引,可以使用二分算法高效查找
(4)零拷贝(sendfile和mmap)
Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。传统拷贝(左图)和零拷贝(右图)过程如下图所示。
与此同时,kafka的持久化落盘用到了内存映射文件,简称mmap。简单描述其作用就是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销。
7、Kafka、RabbitMQ、RocketMQ对比
事务消息实现--其实就是增加了一个回调函数(比如阿里的Notify)
重试队列和延时队列实现--新建一组队列(不同的重试间隔或延时间隔有不同的队列),或者基于JUC中的延时队列或时间轮自己实现
优先级实现--基于Redis的Sorted Set实现
功能 | Apache RocketMQ | Apache Kafka | RabbitMQ |
安全防护 | 不支持 | 不支持 | 支持 |
主子账号支持 | 不支持 | 不支持 | 不支持 |
可靠性 | - 同步刷盘 - 异步刷盘 | 异步刷盘,丢数据概率高 | 同步刷盘 |
可用性 | 好 | 好 | 好 |
横向扩展能力 | 支持 | 支持 | - 集群扩容依赖前端 - LVS 负载均衡调度 |
Low Latency | 不支持 | 不支持 | 不支持 |
消费模型 | Push / Pull | Pull | Push / Pull |
定时消息 | 支持(只支持18个固定 Level) | 不支持 | 支持 |
事务消息 | 不支持 | 不支持 | 不支持 |
顺序消息 | 支持 | 支持 | 不支持 |
全链路消息轨迹 | 不支持 | 不支持 | 不支持 |
消息堆积能力 | 百亿级别 影响性能 | 影响性能 | 影响性能 |
消息堆积查询 | 支持 | 不支持 | 不支持 |
消息回溯 | 支持 | 不支持 | 不支持 |
消息重试 | 支持 | 不支持 | 支持 |
死信队列 | 支持 | 不支持 | 支持 |
性能(常规) | 非常好 十万级 QPS | 非常好 百万级 QPS | 一般 万级 QPS |
性能(万级 Topic 场景) | 非常好 十万级 QPS | 低 | 低 |
性能(海量消息堆积场景) | 非常好 十万级 QPS | 低 | 低 |
发送性能对比
Kafka的吞吐量高达17.3w/s,不愧是高吞吐量消息中间件的行业老大。这主要取决于它的队列模式保证了写磁盘的过程是线性IO。此时broker磁盘IO已达瓶颈。
RocketMQ也表现不俗,吞吐量在11.6w/s,磁盘IO %util已接近100%。RocketMQ的消息写入内存后即返回ack,由单独的线程专门做刷盘的操作,所有的消息均是顺序写文件。
RabbitMQ的吞吐量5.95w/s,CPU资源消耗较高。它支持AMQP协议(增加了确认机制),实现非常重量级,为了保证消息的可靠性在吞吐量上做了取舍。我们还做了RabbitMQ在消息持久化场景下的性能测试,吞吐量在2.6w/s左右。
在服务端处理同步发送的性能上,Kafka>RocketMQ>RabbitMQ。
RocketMQ(MetaQ)存储结构(和kafka对比)
上图是kafka的存储方式,下图是RocketMQ的存储方式。
kafka的一个分区对应一个消费线程去消费,所以为了消费速度跟上生产速度,面向多消费者服务,往往会创建多个分区在Kafka中,每个topic_partition一个文件。虽然每个文件是顺序IO,但topic或者partition过多,每个文件的顺序IO,表现到磁盘上,还是随机IO。消费分散的落盘策略会导致磁盘io竞争激烈,进而成为性能瓶颈。
而在RocketMQ中,做了一个重要改变,就是把所有消息存到一个文件commitLog里面,单文件的顺序写,所以高并发写性能突出。当消费者要消费一条消息时,它怎么知道从CommitLog中具体获取哪个消息呢? 这时就用到另一个磁盘文件ConsumeQueue,这个ConsumeQueue文件储存的就是一条消息在CommitLog中的偏移量。