顺序写磁盘

Kafka 的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka 的特性之一就是高吞吐率。
即使是普通的服务器,Kafka 也可以轻松支持每秒百万级的写入请求,超过了大部分的消息中间件,这种特性也使得 Kafka 在日志处理等海量数据场景广泛应用。
针对 Kafka 的基准测试可以参考,Apache Kafka 基准测试:每秒写入 2 百万(在三台廉价机器上)

顺序写入

磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。在顺序读写的情况下,磁盘的顺序读写速度和内存持平。

因为硬盘是机械结构,每次读写都会寻址 -> 写入,其中寻址是一个 “机械动作”,它是最耗时的。所以硬盘最讨厌随机 I/O,最喜欢顺序 I/O。为了提高读写硬盘的速度,Kafka 就是使用顺序 I/O。

磁盘的原理:读写一份连续的数据比读写一份随机的数据效率要非常的高,因为少了寻址的时间

而且 Linux 对于磁盘的读写优化也比较多,包括 read-ahead 和 write-behind,磁盘缓存等。如果在内存做这些操作的时候,一个是 JAVA 对象的内存开销很大,另一个是随着堆内存数据的增多,JAVA 的 GC 时间会变得很长,使用磁盘操作有以下几个好处:
1、顺序写入磁盘顺序读写速度超过内存随机读写
2、顺序写入 JVM 的 GC 效率低,内存占用大。使用磁盘可以避免这一问题
3、顺序写入系统冷启动后,磁盘缓存依然可用

Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s(这太极端了,实际上不会那么夸张)。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。

顺序写之后,你读也是顺序读,按顺序来肯定速度就快.

下图就展示了 Kafka 是如何写入数据的, 每一个 Partition 其实都是一个文件 ,收到消息后 Kafka 会把数据插入到文件末尾(虚框部分):

Kafka 高效读写数据,为什么那么快_零拷贝

这种方法有一个缺陷——没有办法删除数据 ,所以 Kafka 是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个 Topic 都有一个 offset 用来表示读取到了第几条数据 。

如果数据满了Kafka也会有删除策略.比如说基于时间和基于Partition文件大小的.

Kafka 高效读写数据,为什么那么快_零拷贝_02

两个消费者:

1、顺序写入 Consumer1 有两个 offset 分别对应 Partition0、Partition1(假设每一个 Topic 一个 Partition);

2、顺序写入 Consumer2 有一个 offset 对应 Partition2。

这个 offset 是由客户端 SDK 负责保存的,Kafka 的 Broker 完全无视这个东西的存在;一般情况下 SDK 会把它保存到 Zookeeper 里面,所以需要给 Consumer 提供 zookeeper 的地址。

Memory Mapped Files

即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以 Kafka 的数据并不是实时的写入硬盘 ,它充分利用了现代操作系统分页存储来利用内存提高 I/O 效率。

Memory Mapped Files(后面简称 mmap) 也被翻译成 内存映射文件 ,在 64 位操作系统中一般可以表示 20G 的数据文件,它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。

完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。

通过 mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。

使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销(调用文件的 read 会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中。)

但也有一个很明显的缺陷——不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘。

Kafka 提供了一个参数——producer.type 来控制是不是主动 flush,如果 Kafka 写入到 mmap 之后就立即 flush 然后再返回 Producer 叫 同步 (sync);写入 mmap 之后立即返回 Producer 不调用 flush 叫异步 (async)。

顺序读写和随机读写对于机械硬盘来说为什么性能差异巨大?

顺序读写=读取一个大文件
随机读写=读取多个小文件

顺序读写比随机读写快的原因
①顺序读写,主要时间花费在了传输时间,而这个时间两种读写可以认为是一样的。
随机读写,需要多次寻道和旋转延迟。而这个时间可能是传输时间的许多倍。
②顺序读写,磁盘会预读,预读即在读取的起始地址连续读取多个页面

(现在不需要的页面也读取了,这样以后用时就不用再读取,当一个页面用到时,大多数情况下,它周围的页面也会被用到)
而随机读写,因为数据没有在一起,将预读浪费掉了。

③另一个原因是文件系统的overhead。
读写一个文件之前,得一层层目录找到这个文件,以及做一堆属性、权限之类的检查。
写新文件时还要加上寻找磁盘可用空间的耗时。
对于小文件,这些时间消耗的占比就非常大了。

接着看看
​​​https://zhuanlan.zhihu.com/p/68750796​

使用PageCache

Cache Filesystem Cache PageCache缓存

PageCache是操作系统的东西了,操作系统运行是非常依赖磁盘的随机读写的,操作系统运行会产生大量磁盘随机读写. 操作系统在产生随机读写的时候并不会直接把随机读写往磁盘写,而是会在内存中开辟一片高速缓存,先给随机读写写到内存中, 每隔一段时间就给高速缓存里面的随机读写落入到磁盘中,落数据的时候就会给随机读写安排好,比如说排序,先写谁,后写谁.
就好比送快递,快递来一个送一个肯定是很慢的,你攒一批排好序,哪栋楼近就先给这栋楼的快递都送过去, 这样效率肯定是很高的.

Kafka数据持久化是直接持久化到Pagecache中,这样会产生以下几个好处:

  1. I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能
  2. I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间
  3. 充分利用所有空闲内存(非 JVM 内存,如果用堆内存的话,如果用的越多的话,那么内存回收越难的)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担
  4. 读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据
  5. 如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用
    尽管持久化到Pagecache上可能会造成宕机丢失数据的情况,但这可以被Kafka的Replication机制解决。如果为了保证这种情况下数据不丢失而强制将 Page Cache 中的数据 Flush 到磁盘,反而会降低性能。

零拷贝技术

我们在做文件读写和复制的时候,操心系统内存分为内核层和应用层, 我们所有应用程序全部运行在应用层的,内核层是运行我们操作系统的内核部分的.
为什么做分层呢,因为操作系统先要保证自己运行,才能提供正常运行的平台来给其它的应用程序提供良好的应用资源.
内核层内存一般都是负责一些内核驱动的工作,比如说读写磁盘,硬件操作,驱动显卡等等.
为了安全期间内核层和应用层是完全隔离的.相当于两块儿独立的空间

当发生文件复制的时候,文件是不能直接接触硬件的,需要驱动我们的操作系统内核,让内核的io读写器来帮你做这写事情,就是操作系统的io功能来帮你做这个事情.操作系统内核帮你读文件.
这个文件首先读到内核层的内存里面,然后读到用户层的应用缓存里面,让应用能看到这个文件的信息啥的,然后再这个io流写出去,写出去还是要驱动操作系统内核层,将应用内存复制到内核层内存.
内核层最终再给我的文件写入到硬盘上,
以上步骤是 内核层内存–>应用层内存–>内核层内存–>硬盘 . 这样是多了一个步骤的,而我Kafka复制的话,程序是不需要看一遍文件的数据的,只需要知道我这个文件被复制了一遍就行了. 所以就有了零拷贝了.

Kafka零拷贝过程是: 文件读到内核层内存, 直接让内核层内存给文件复制到新的地方就行了, 这样不经过应用层内存. , 这个复制技术就相比上面少了一个copy过程.

零拷贝也是一项操作系统提供的优化措施,Kafka也仅仅是调用了操作系统的方法而已,当然如果你的操作系统不支持零拷贝技术的话,那么这个Kafka优化就失效了,不过一般来说服务器都是Linux是支持零拷贝技术的.

零拷贝不仅仅应用文件的复制,文件的发送也可以应用到.

当然零拷贝的坏处就是无法编辑这个文件,如果你想编辑这个文件的话,就需要给这个文件读到用户内存中.

Kafka的零拷贝技术

kafka中的消费者在读取服务端的数据时,需要将服务端的磁盘文件通过网络发送到消费者进程,网络发送需要经过几种网络节点。如下图所示:

Kafka 高效读写数据,为什么那么快_kafka_03

传统的读取文件数据并发送到网络的步骤如下:

(1)操作系统将数据从磁盘文件中读取到内核空间的页面缓存;
(2)应用程序将数据从内核空间读入用户空间缓冲区;
(3)应用程序将读到数据写回内核空间并放入socket缓冲区;
(4)操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送。

通常情况下,Kafka的消息会有多个订阅者,生产者发布的消息会被不同的消费者多次消费,为了优化这个流程,Kafka使用了“零拷贝技术”,如下图所示:

Kafka 高效读写数据,为什么那么快_kafka_04

“零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。
如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。

批量压缩

在很多情况下,系统的瓶颈不是 CPU 或磁盘,而是网络 IO,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。进行数据压缩会消耗少量的 CPU 资源, 不过对于 kafka 而言, 网络 IO 更应该需要考虑。

1、如果每个消息都压缩,但是压缩率相对很低,所以 Kafka 使用了批量压缩,即将多个消息一起压缩而不是单个消息压缩

2、Kafka 允许使用递归的消息集合,批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩

3、Kafka 支持多种压缩协议,包括 Gzip 和 Snappy 压缩协议

总结

Kafka 速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络 IO 损耗,通过 mmap 提高 I/O 速度,写入数据的时候由于单个 Partion 是末尾添加所以速度最优;读取数据的时候配合 sendfile 直接暴力输出。

参考

​https://zhuanlan.zhihu.com/p/68750796​

​https://mp.weixin.qq.com/s?__biz=MzAxMjEwMzQ5MA==&mid=2448889297&idx=2&sn=8a26dc14cc44d13fdcceb10d5fbf6f55&chksm=8fb54ffcb8c2c6ea49eaa83d439b0306c8de1fdbbb019107ba41daab7dea8c8ad1dc3b347207&scene=21