介绍
Kafka 属于分布式的消息引擎系统,它的主要功能是提供一套完备的消息发布与订阅解决方案
什么是消息引擎系统
消息引擎系统又叫消息中间件、消息队列。常见的消息引擎系统使用两种方法将消息传递出去:
- 点对点模型:系统A发送的消息只能被系统B接收,其他任何系统都不能读取A发送的消息。日常生活的例子比如电话客服就属于这种模型:同一个客户呼入电话只能被一位客服人员处理,第二个客服人员不能为该客户服务。
- 发布/订阅模型:与上面不同的是,它有一个主题(Topic)的概念,该模型也有发送方和接收方,只不过提法不同。发送方也称为发布者(Publisher),接收方称为订阅者(Subscriber)。和点对点模型不同的是,这个模型可能存在多个发布者向相同的主题发送消息,而订阅者也可能存在多个,它们都能接收到相同主题的消息。生活中的报纸订阅就是一种典型的发布/订阅模型。
为什么要用消息引擎系统
- 削峰
缓冲上下游瞬时突发流量,使其更平滑。特别是对于那种发送能力很强的上游系统,如果没有消息引擎的保护,"脆弱"的下游系统可能会直接被压垮导致全链路服务"雪崩"
- 异步
使用了消息队列,生产者一方,把消息往队列里一扔,就可以立马返回,响应用户了。无需等待处理结果。处理结果可以让用户稍后自己来取
- 解耦
上游系统不需要直接与下游系统通信。常有系统产生消息之后,向Kafka Broker发送一条订单消息即可。下游的各个子服务订阅Kafka中的对应主题,并从中获取到消息进行处理,从而实现了上游服务与下游处理服务的解耦。
常见的消息引擎系统对比
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机吞吐量 | 万级,比RocketMQ 和Kafka 低一个数量级 | 10万级,支持高吞吐 | 100万级,高吞吐,一般配合大数据类的系统进行实时计算、日志采集等场景 | |
Topic数量对吞吐量的影响 | - | - | topic 达到几百、几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器数量下,可以支持大量的 topic | topic 从几十到几百个的时候,吞吐量会大幅度下降,在同等的机器数量下,Kalka 尽量保证 topic数量不要过多,如果需要支撑大规模的 topic,需要增加机器资源 |
时效性 | 毫秒级 | 微秒级,这是RabbitMQ 的一大特性,低延迟 | 毫秒 | 延迟在毫秒级以内 |
可用性 | 基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | - | 经参数优化配置,可以做到0丢失 | 同RocketMQ |
功能支持 | 支持完善的MQ功能 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,支持分布式扩招 | 支持完善的MQ功能,主要用于大数据领域,如实时计算和日志采集场景 |
kafka具有超高的吞吐量,极低的消息丢失率,支持分布式、横向扩展,社区文档也非常完善,所以我们选择使用Kafka
常用概念简介
消息:Record。Kafka 是消息引擎,这里的消息就是指 Kafka 处理的主要对象。
-
服务器Broker:Kafka进程在服务器端被称为Broker , 每个Kafka集群由多个Broker组成 .Broker负责接收并处理客户端发送的消息,并负责对消息进行持久化。
-
主题:Topic。主题是承载消息的逻辑容器,在实际使用中多用来区分具体的业务。
-
分区:Partition。一个有序不变的消息序列。每个主题下可以有多个分区。分区从下标0开始,每条消息只会存在与一个分区中
-
消息位移:Offset。表示分区中每条消息的位置信息,是一个单调递增且不变的值。
-
副本:Replica。Kafka 中同一条消息能够被拷贝到多个地方以提供数据冗余,这些地方就是所谓的副本。副本还分为领导者副本(Leader)和追随者(Follower)副本,各自有不同的角色划分。副本是在分区层级下的,即每个分区可配置多个副本实现高可用。
最早的主从关系用Master-Slave来替代,后来slave有奴隶的意思,大部分系统就换为Leader-Follower了
最早只有Leader副本可被消费者读,从kafka2.4开始,Follower副本也可被消费者读
- 消费者组:Consumer Group。多个消费者实例共同组成的一个组,同时消费多个分区以实现高吞吐。
- 协调者:协调者本身是kafka集群的一个broker,负责一个或多个消费者组的消费位移offset提交以及Rebalance工作
- 重平衡:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的协议与过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。
发送端
发送一条消息的过程
拦截器
Kafka拦截器分为生产者拦截器和消费者拦截器
生产者拦截器允许你在发送消息前以及消息提交成功后植入你的拦截器逻辑
消费者拦截器支持在消费消息前以及提交位移后编写特定逻辑
比如我们想在发送消息前执行两个"前置动作":第一个是为消息增加一个头信息,封装发送该消息的时间,第二个是更新发送消息数字段,那么我们就可以使用生产者拦截器
序列化器
生产者发送到broker时,对ProducerRecord的key和value的序列化。
分区器
分区器用于决定消息会进去哪个分区。
- 如果消息指定了分区,则发往指定的分区
- 如果未指定分区,但指定了key,则采用哈希算法来指定分区
- 如果未指定分区,且未指定key,则用分区策略选择分区
kafka使用分区来提供负载均衡的能力
通过将多个分区部署在多实例上,达到增加吞吐量的目的,同时能够实现系统的高伸缩性
不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。并且,我们还可以通过添加新的节点机器来增加整体系统的吞吐量
常见分区策略
分区策略是决定生产者将消息发送到哪个分区的算法
1. 轮训
它总是能保证消息最大限度地被平均分配到所有分区上,是我们最常用的分区策略之一
2. 随机
本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,还是使用轮询策略比较好
3. 按范围分区 Range
该策略会将连续的分区分给消费者 ,比如2个topic中各有3个分区,两个消费者C1 C2 ,range策略下,C1分到T1(P1,P2) C2分到T1(P3),且当进入第二个topic时,C1分到T2(P1,P2) C2分到T2(P3)
该策略可能会导致分配不均匀
消息累加器
当消息确定好要发到那个Topic与Partition之后,就会进入消息累加器,将消息内容放到一个ProducerBatch当中
- 每个Batch中的消息都是发送到相同的Topic和Partition。
- 为了提高吞吐量,降低网络请求次数,ProducerBatch中可能包含一个或多个消息, 多个再一次性发送到Broker上去的,以此提高服务吞吐量
消息累加器的配置
-
在client端,消息累加器的大小通过参数buffer.memory配置,默认32MB。每个Batch通过batch.size控制大小,默认16KB
-
在client端,若batch 一定时间内还没满,则直接发送. 通过参数linger.ms调整Batch不满时的等待时间。
-
在client端,若消息大于batch.size且不超过max.request.size,则消息不经过batch直接发送
-
每次发送消息可以发送多个batch,但是消息体总和大小不能超过max.request.size
-
在broker端,若消息体大小大于broker配置的message.max.bytes ,则拒绝接受本条消息并报错
发送消息
Sender线程本身是一个后台线程,他会在Client启动的时候被创建。
他的主要完成的工作就是将消息添加器中添加的消息块分节点组装到网络发送客服端中与 更新位移信息
Broker
请求分发
在Kafka的架构中,会有很多客户端向Broker端发送请求,Broker 端有个 SocketServer 组件,用来和客户端建立连接,然后通过Acceptor线程来进行请求的分发,由于Acceptor不涉及具体的逻辑处理,非常得轻量级,因此有很高的吞吐量。
接着Acceptor 线程将请求放入一个网络线程池当中
网络线程池
当网络线程拿到请求后,会将请求放入到一个共享请求队列中。
IO线程池
同时,Broker 端还有个 IO 线程池,负责从该队列中取出请求,执行真正的处理。如果是 Produce 生产请求,则将消息顺序写入到底层的磁盘日志中;如果是 Fetch 请求,则从磁盘或页缓存中读取消息。
零拷贝
当请求为Fetch请求时,消息需要从Broker发送到消费者上。
考虑一个文件中读出数据并将数据传输到网络上另一程序的场景
lua
File.read(fileDesc, buf, len);Socket.send(socket, buf, len);
大概可以分成四个过程:
- 第一次:将磁盘文件,读取到操作系统内核缓冲区;
- 第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
- 第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
- 第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。
其中会涉及4次用户态-内核态切换,2次CPU拷贝和2次DMA拷贝
Memory Mapped Files
简称mmap,其作用就是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
- 原理
mmap的能够利用操作系统的Page来实现文件到物理内存的映射。通过mmap,我们能够像读写内存(虚拟内存)一样来读写硬盘内存。
- 好处
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销。
- 不足
mmap也有一个很明显的缺陷------不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。
Kafka提供了一个参数------producer.type来控制是不是主动flush;如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。
SendFile
Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),
除了减少数据拷贝外,因为整个读文件 -- 网络发送由一个 sendfile 调用完成,整个过程只有2次用户态-内核态切换与2次DMA拷贝,减少了2次CPU切换和2次用户态-内核态切换,大大提高了性能。sendFile零拷贝过程如下图所示。
消费端
消费者与消费者组
Kafka支持消息引擎系统常见的点对点与发布/订阅模式。
- 如果所有的消费者都在同一个消费者组,那么每个消息只会被一个消费者处理,这就相当于点对点模式的应用
- 如果所有的消费者不再同一个消费者组,那么所有的消息都会被广播给所有的消费者,这就相当于发布/订阅模式
每个消费者组由多个消费者实例组成,他们共享相同的GroupId,组内的所有消费者协调在一起来消费订阅主题
重平衡
Rebalance本质上是一种协议,规定了一个Consumer Group下的所有Consumer如何达成一致,来分配订阅Topic的每个分区。
触发条件
- 组成员数发生变更。比如有新的Consumer实例加入组或者离开组
- 订阅主题数发生变更。Consumer Group可以使用正则表达式的方式订阅主题
- 订阅主题的分区数发生变更。
- 消费者处理消息耗时超过session timeout(默认30s),会触发重平衡
大致流程
阶段一:JoinGroup
- 协调者 感知到 消费者组的变化,
- 心跳的过程中发送重平衡信号通知各个消费者离组,
- 消费者重新以 JoinGroup 方式加入 协调者,并选出Consumer Leader,通常会选择第一上报的消费者作为leader。
- 当选举出leader后,协调者会在JoinGroup的响应信息中将leader实例信息通知给所有消费者实例,
阶段二: SyncGroup
- 每个消费者实例向协调者发送SyncGroup实例,其中leader实例回将它生成的Rebalance方法附带在请求中
- 协调者会在SyncGroup的响应附带将每一个消费者需要重新消费的topic及partition信息。之后消费者组会正常消费数据了
弊端
- 如果消费者组中的消费者很多,那重平衡会很慢,甚至几个小时之久。
- 重平衡过程中 kafka 基本处于不可用状态,这个时段消费者是无法消费消息的
- 若设置offset为自动提交。重平衡前的offset没有提交,有可能导致重平衡后消息被重复消费
参考文章
后续
本文编写的还是比较"泛" ,很多细节都简单掠过,比如 broker 日志分段、高水位与低水位、副本系统等,后续也会在专门的文章中写一些