Kafka 是基于发布与订阅的消息系统。它最初由 LinkedIn 公司开发,之后成为 Apache 项目的一部分。Kafka 是一个分布式的,可分区的,冗余备份的持久性的日志服务。它主要用于处理活跃的流式数据
🦅 Kafka 的主要特点?
- 1、同时为发布和订阅提供高吞吐量。据了解,Kafka 每秒可以生产约 25 万消息(50MB),每秒处理 55 万消息(110MB)。
- 2、可进行持久化操作。将消息持久化到磁盘,因此可用于批量消费,例如 ETL ,以及实时应用程序。通过将数据持久化到硬盘,以及replication ,可以防止数据丢失。
- 3、分布式系统,易于向外扩展。所有的 Producer、Broker 和Consumer 都会有多个,均为分布式的。并且,无需停机即可扩展机器。
-
4、消息被处理的状态是在 Consumer 端维护,而不是由 Broker 端维护。当失败时,能自动平衡。
使用zookeeper做分布式管理
Kafka 可以起到两个作用:
- 降低系统组网复杂度。
- 降低编程复杂度,各个子系统不在是相互协商接口,各个子系统类似插口插在插座上,Kafka 承担高速数据总线的作用。
🦅 聊聊 Kafka 的设计要点?
1)吞吐量
高吞吐是 Kafka 需要实现的核心目标之一,为此 kafka 做了以下一些设计:
-
1、数据磁盘持久化:消息不在内存中 Cache ,直接写入到磁盘,充分利用磁盘的顺序读写性能。
直接使用 Linux 文件系统的 Cache ,来高效缓存数据。
采用 Linux Zero-Copy 提高发送性能。
- 传统的数据发送需要发送 4 次上下文切换。
- 采用 sendfile 系统调用之后,数据直接在内核态交换,系统上下文切换减少为 2 次。可以提高60%的数据发送性能
kafka 线性写入数据到磁盘,性能远远大于任意位置写的性能
详细解读zero-copy
1.数据直接在内核态交换,不需要切换到用户态
发起数据读取请求,切换到内核态,才能发送IO请求,把磁盘控制器缓冲区的数据拷贝到内存中
整个流程如下:
- 1.用户进程调用read等系统调用向操作系统发出IO请求,请求读取数据到自己的内存缓冲区中。自己进入阻塞状态。
- 2.操作系统收到请求后,进一步将IO请求发送磁盘。
- 3.磁盘驱动器收到内核的IO请求,把数据从磁盘读取到驱动器的缓冲中。此时不占用CPU。当驱动器的缓冲区被读满后,向内核发起中断信号告知自己缓冲区已满。
- 4.内核收到中断,使用CPU时间将磁盘驱动器的缓存中的数据拷贝到内核缓冲区中。
- 5.如果内核缓冲区的数据少于用户申请的读的数据,重复步骤3跟步骤4,直到内核缓冲区的数据足够多为止。
- 6.将数据从内核缓冲区拷贝到用户缓冲区,同时从系统调用中返回。完成任务。
缺点:用户的每次IO请求,都需要CPU多次参与
磁盘 --> 磁盘驱动器缓冲区 --> 内核缓冲区 --> 用户缓冲区
- 1.用户进程调用read等系统调用向操作系统发出IO请求,请求读取数据到自己的内存缓冲区中。自己进入阻塞状态。
- 2.操作系统收到请求后,进一步将IO请求发送DMA。然后让CPU干别的活去。
- 3.DMA进一步将IO请求发送给磁盘。
- 4.磁盘驱动器收到DMA的IO请求,把数据从磁盘读取到驱动器的缓冲中。当驱动器的缓冲区被读满后,向DMA发起中断信号告知自己缓冲区已满。
- 4.DMA收到磁盘驱动器的信号,将磁盘驱动器的缓存中的数据拷贝到内核缓冲区中。此时不占用CPU。这个时候只要内核缓冲区的数据少于用户申请的读的数据,内核就会一直重复步骤3跟步骤4,直到内核缓冲区的数据足够多为止。
- 5.当DMA读取了足够多的数据,就会发送中断信号给CPU。
- 6.CPU收到DMA的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回。
跟IO中断模式相比,DMA模式下,DMA就是CPU的一个代理,它负责了一部分的拷贝工作,从而减轻了CPU的负担。
DMA的优点就是:中断少,CPU负担低。 但此时仍然没有减少文件数据copy次数,还是需要从内核缓冲区,复制到用户内存缓冲区
由图可知,整个过程总共发生了四次拷贝和四次的用户态和内核态的切换。
3.1 sendfile跟splice的局限性
上面提到的用来实现零拷贝的sendfile和splice接口,仅限于文件跟文件,文件跟sock之间传输数据,但是没法直接在两个socket之间传输数据的。这就是sendfile和splice接口的局限性。
如果要实现socket跟socket之间的数据直接拷贝,需要开辟一个pipe,然后调用两次splice。这样还是带来跟传统IO读写一样的问题。系能其实并没有什么大的提升。
fork子进程时,直接共享内存页;只有该子进程需要修改某一块数据,才会将这块数据copy 到自己的app buffer中进行修改。
传统的数据传输方法
可见,不仅拷贝的次数变成了3次,上下文切换的次数也减少到了2次,效率比传统方式高了很多。但是它还并非完美状态,下面看一看让它变得更优化的方法
Scatter/Gather DMA, 维护一个块描述符链表,包含数据起始地址和长度。所以就直接从内核缓冲区获取数据,无需copy
对内存映射(mmap)的支持 RocketMQ方案
上面讲的机制看起来一切都很好,但它还是有个缺点:如果我想在传输时修改数据本身,就无能为力了。不过,很多操作系统也提供了内存映射机制,对应的系统调用为mmap()/munmap()。通过它可以将文件数据映射到内核地址空间,直接进行操作,操作完之后再刷回去。其对应的简要时序图如下。
flush到磁盘后,消息订阅者才能订阅到
2)负载均衡
- 1、Producer 根据用户指定的算法,将消息发送到指定的 Partition 中。
- 2、Topic 存在多个 Partition ,每个 Partition 有自己的replica ,每个 replica 分布在不同的 Broker 节点上。多个Partition 需要选取出 Leader partition ,Leader Partition 负责读写,并由 Zookeeper 负责 fail over 。
- 3、相同 Topic 的多个 Partition 会分配给不同的 Consumer 进行拉取消息,进行消费。
3)拉取系统
由于 Kafka Broker 会持久化数据,Broker 没有内存压力,因此, Consumer 非常适合采取 pull 的方式消费数据,具有以下几点好处:
- 1、简化 Kafka 设计。
- 2、Consumer 根据消费能力自主控制消息拉取速度。
- 3、Consumer 根据自身情况自主选择消费模式,例如批量,重复消费,从尾端开始消费等。
Kafka 的整体架构非常简单,是分布式架构,Producer、Broker 和Consumer 都可以有多个。
- Producer,Consumer 实现 Kafka 注册的接口。
- 数据从 Producer 发送到 Broker 中,Broker 承担一个中间缓存和分发的作用。
- Broker 分发注册到系统中的 Consumer。Broker 的作用类似于缓存,即活跃的数据和离线处理系统之间的缓存。
- 客户端和服务器端的通信,是基于简单,高性能,且与编程语言无关的 TCP 协议。
一个消费组的consumer,只能消费一条消息。
多个不同的消费组,可以都消费这条消息
Topic中的所有消息,采用一定的算法分摊到不同的broker机器
Producer 发布消息,采用一定算法选定Partition;Consumer 消费消息,采用一定算法选择partition,从partition拉取消息
Kafka 的应用场景有哪些?
1)消息队列
比起大多数的消息系统来说,Kafka 有更好的吞吐量,内置的分区,冗余及容错性,这让 Kafka 成为了一个很好的大规模消息处理应用的解决方案。消息系统一般吞吐量相对较低,但是需要更小的端到端延时,并常常依赖于 Kafka 提供的强大的持久性保障。在这个领域,Kafka 足以媲美传统消息系统,如 ActiveMQ 或 RabbitMQ 。
2)行为跟踪
Kafka 的另一个应用场景,是跟踪用户浏览页面、搜索及其他行为,以发布订阅的模式实时记录到对应的 Topic 里。那么这些结果被订阅者拿到后,就可以做进一步的实时处理,或实时监控,或放到 Hadoop / 离线数据仓库里处理。
3)元信息监控
作为操作记录的监控模块来使用,即汇集记录一些操作信息,可以理解为运维性质的数据监控吧。
4)日志收集
日志收集方面,其实开源产品有很多,包括 Scribe、Apache Flume 。很多人使用 Kafka 代替日志聚合(log aggregation)。日志聚合一般来说是从服务器上收集日志文件,然后放到一个集中的位置(文件服务器或 HDFS)进行处理。
然而, Kafka 忽略掉文件的细节,将其更清晰地抽象成一个个日志或事件的消息流。这就让 Kafka 处理过程延迟更低,更容易支持多数据源和分布式数据处理。比起以日志为中心的系统比如 Scribe 或者 Flume 来说,Kafka 提供同样高效的性能和因为复制导致的更高的耐用性保证,以及更低的端到端延迟。
5)流处理
这个场景可能比较多,也很好理解。保存收集流数据,以提供之后对接的 Storm 或其他流式计算框架进行处理。很多用户会将那些从原始 Topic 来的数据进行阶段性处理,汇总,扩充或者以其他的方式转换到新的 Topic 下再继续后面的处理。
例如一个文章推荐的处理流程,可能是先从 RSS 数据源中抓取文章的内容,然后将其丢入一个叫做“文章”的 Topic 中。后续操作可能是需要对这个内容进行清理,比如回复正常数据或者删除重复数据,最后再将内容匹配的结果返还给用户。这就在一个独立的 Topic 之外,产生了一系列的实时数据处理的流程。Strom 和 Samza 是非常著名的实现这种类型数据转换的框架。
6)事件源
事件源,是一种应用程序设计的方式。该方式的状态转移被记录为按时间顺序排序的记录序列。Kafka 可以存储大量的日志数据,这使得它成为一个对这种方式的应用来说绝佳的后台。比如动态汇总(News feed)。
7)持久性日志(Commit Log)
Kafka 可以为一种外部的持久性日志的分布式系统提供服务。这种日志可以在节点间备份数据,并为故障节点数据回复提供一种重新同步的机制。Kafka 中日志压缩功能为这种用法提供了条件。在这种用法中,Kafka 类似于 Apache BookKeeper 项目。
副本都写入成功后,给leader partition发送ack,此时broker才给producer发ack确认
🦅 2)Broker 存储消息
物理上把 Topic 分成一个或多个 Patition,每个 Patition 物理上对应一个文件夹(该文件夹存储该 Patition 的所有消息和索引文件)。
🦅 3)Consumer 消费消息
high-level Consumer API 提供了 consumer group 的语义,一个消息只能被 group 内的一个 Consumer 所消费,且 Consumer 消费消息时不关注 offset ,最后一个 offset 由 ZooKeeper 保存(下次消费时,该 group 中的 Consumer 将从 offset 记录的位置开始消费)。
注意:
- 1、如果消费线程大于 Patition 数量,则有些线程将收不到消息。
- 2、如果 Patition 数量大于消费线程数,则有些线程多收到多个 Patition 的消息。
- 3、如果一个线程消费多个 Patition,则无法保证你收到的消息的顺序,而一个 Patition 内的消息是有序的。
🦅 Kafka Producer 有哪些发送模式?
Kafka 的发送模式由 Producer 端的配置参数 producer.type
来设置。
- 这个参数指定了在后台线程中消息的发送方式是同步的还是异步的,默认是同步的方式,即
producer.type=sync
。 - 如果设置成异步的模式,即
producer.type=async
,可以是 Producer 以 batch 的形式 push 数据,这样会极大的提高 Broker的性能,但是这样会增加丢失数据的风险。 - 如果需要确保消息的可靠性,必须要将
producer.type
设置为 sync 。
- 以 batch 的方式推送数据可以极大的提高处理效率,Kafka Producer 可以将消息在内存中累计到一定数量后作为一个 batch 发送请求。batch 的数量大小可以通过 Producer 的参数(
batch.num.messages
)控制。通过增加 batch 的大小,可以减少网络请求和磁盘 IO 的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。 - 在比较新的版本中还有
batch.size
这个参数。Producer 会尝试批量发送属于同一个 Partition 的消息以减少请求的数量. 这样可以提升客户端和服务端的性能。默认大小是 16348 byte (16k).- 发送到 Broker 的请求可以包含多个 batch ,每个 batch 的数据属于同一个 Partition 。
- 太小的 batch 会降低吞吐. 太大会浪费内存。
🦅 Kafka Consumer 是否可以消费指定的分区消息?
Consumer 消费消息时,向 Broker 发出“fetch”请求去消费特定分区的消息,Consumer 指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,Consumer 拥有了 offset 的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的。
Kafka 的网络模型是怎么样的?
Kafka 基于高吞吐率和效率考虑,并没有使用第三方网络框架,而且自己基于 Java NIO 封装的。
🦅 1)KafkaClient ,单线程 Selector 模型。
上面讲的是如果要找某个 offset 的流程,但是我们大多数时候并不需要查找某个 offset ,只需要按照顺序读即可。而在顺序读中,操作系统会对内存和磁盘之间添加 page cahe ,也就是我们平常见到的预读操作,所以我们的顺序读操作时速度很快。但是 Kafka 有个问题,如果分区过多,那么日志分段也会很多,写的时候由于是批量写,其实就会变成随机写了,随机 I/O 这个时候对性能影响很大。所以一般来说 Kafka 不能有太多的Partition 。针对这一点,RocketMQ 把所有的日志都写在一个文件里面,就能变成顺序写,通过一定优化,读也能接近于顺序读
在基于 Kafka 的分布式消息队列中,ZooKeeper 的作用有:
其实,总结起来,就是两类功能:
总的来说,Kafka 和 RocketMQ 的高可用方式是比较类似的,主要的差异在 Kafka Broker 的副本机制,和 RocketMQ Broker 的主从复制,两者的差异,以及差异带来的生产和消费不同。当然,实际上,都是和“主” Broker 做消息的发送和读取不是?!
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
一个topic 可以配置几个partition,produce发送的消息分发到不同的partition中,consumer接受数据的时候是按照group来接受,kafka确保每个partition只能同一个group中的同一个consumer消费,如果想要重复消费,那么需要其他的组来消费。Zookeerper中保存这每个topic下的每个partition在每个group中消费的offset 。
所以,如果要一个group用几个consumer来同时读取的话,需要多线程来读取,一个线程相当于一个consumer实例。当consumer的数量大于分区的数量的时候,有的consumer线程会读取不到数据。
Topic在逻辑上可以被认为是一个queue,每条消费都必须指定它的Topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件
Kafka会为每一个Consumer Group保留一些metadata信息——当前消费的消息的position,也即offset。这个offset由Consumer控制。正常情况下Consumer会在消费完一条消息后递增该offset。当然,Consumer也可将offset设成一个较小的值,重新消费一些消息。
如果一个Topic对应一个文件,那这个文件所在的机器I/O将会成为这个Topic的性能瓶颈,而有了Partition后,不同的消息可以并行写入不同broker的不同Partition里,极大的提高了吞吐率。
在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Parition。Paritition机制可以通过指定Producer的paritition. class这一参数来指定
我们大家都知道,kafka消费者在会保存其消费的进度,也就是offset,存储的位置根据选用的kafka api不同而不同。
首先来说说消费者如果是根据javaapi来消费,也就是【kafka.javaapi.consumer.ConsumerConnector】,我们会配置参数【zookeeper.connect】来消费。这种情况下,消费者的offset会更新到zookeeper的【consumers/{group}/offsets/{topic}/{partition}】目录下,例如:
[zk: localhost(CONNECTED) 0] get /kafka/consumers/zoo-consumer-group/offsets/my-topic/0
5662
cZxid = 0x20006d28a
ctime = Wed Apr 12 18:20:51 CST 2017
mZxid = 0x30132b0ed
mtime = Tue Aug 22 18:53:22 CST 2017
pZxid = 0x20006d28a
cversion = 0
dataVersion = 5758
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
如果是根据kafka默认的api来消费,即【org.apache.kafka.clients.consumer.KafkaConsumer】,我们会配置参数【bootstrap.servers】来消费。而其消费者的offset会更新到一个kafka自带的topic【__consumer_offsets】下面,查看当前group的消费进度,则要依靠kafka自带的工具【kafka-consumer-offset-checker】,例如:
上面结果的说明:
- Group : 消费者组
- Topic : topic的名字
- Pid : partition的ID
- Offset : kafka消费者在对应分区上已经消费的消息数【位置】
- logSize : 已经写到该分区的消息数【位置】
- Lag : 还有多少消息未读取(Lag = logSize - Offset)
- Owner : 分区创建在哪个broker
offset更新的方式,不区分是用的哪种api,大致分为两类:
- 自动提交,设置enable.auto.commit=true,更新的频率根据参数【auto.commit.interval.ms】来定。这种方式也被称为【at most once】,fetch到消息后就可以更新offset,无论是否消费成功。
- 手动提交,设置enable.auto.commit=false,这种方式称为【at least once】。fetch到消息后,等消费完成再调用方法【consumer.commitSync()】,手动更新offset;如果消费失败,则offset也不会更新,此条消息会被重复消费一次。
kafka快之谜
一般来说制约服务器的性能的主要因素是磁盘IO、网络IO、CPU等这三大因素。MQ主要是进行消息暂存,不需要耗费大量的CPU,所以主要的瓶颈是磁盘IO和网络IO。
磁盘一般分为机械硬盘(HDD)、固态硬盘(SSD)以及混合硬盘(SSHD)。 影响HDD磁盘的关键因素是磁盘服务时间,即磁盘完成一个I/O请求所花费的时间,它由寻道时间、旋转延迟和数据传输时间三部分构成。随机读取的场景下SSD的速度是HDD的3到4倍,但是在顺序读取的场景下其速度相当。对于HDD来说,磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。
本文主要从两个方面服务端和客户端去揭秘kafka的快。
服务端
1、多Partition并行写入
我们都知道kafka是按照topic进行订阅消息,每个 Topic 都包含一个或多个 Partition,不同 Partition 可位于不同节点,每个partition放在不同的一方面,由于不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。另一方面,由于 Partition 在物理上对应一个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。 能并行处理,速度肯定会有提升,多个工人肯定比一个工人干的快。
3、充分利用Page Cache
使用 Page Cache 的好处:
- I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能;
- I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间;
- 充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担;
- 读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据 ;
- 如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用 ;
Broker 收到数据后,写磁盘时只是将数据写入 Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache 内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由 Kafka 层面的 Replication 机制去解决。如果为了保证这种情况下数据不丢失而强制将 Page Cache 中的数据 Flush 到磁盘,反而会降低性能。也正因如此,Kafka 虽然提供了 flush.messages
和 flush.ms
两个参数将 Page Cache 中的数据强制 Flush 到磁盘,但是 Kafka 并不建议使用。
4、零拷贝技术
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。
Memory Mapped Files:简称 mmap,也有叫 MMFile 的,使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程。它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上。使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销。
1、mmap将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上,减少CPU拷贝过程;
2、sendfile 是将读到内核空间的数据,转到socket buffer,进行网络发送;
5、IO多路复用
Kafka 客户端底层使用了Java的 selector,selector 在Linux上的实现机制是epoll。
IO模型的区别
1、BIO多线程的缺点:大量线程上下文切换,线程利用率不高;
2、NIO的缺点:加入有1w连接,每次循环中的read都要进行一次系统调用,需要进行用户态内核态切换;
3、多路复用:一次系统调用可监控多个文件描述符的是否都相应事件发生;