文章目录
- 页缓存
- 顺序写
- 零拷贝
Kafka依赖于文件系统(更底层地来说就是磁盘)来存储和缓存消息 。 那么kafka是如何让自身在使用磁盘存储的情况下达到高性能的?接下来主要从3各方面详细解说。
页缓存
页缓存是操作系统实现的一种主要的磁盘缓存, 以此用来减少对磁盘I/0 的操作。 具体来说, 就是把磁盘中的数据缓存到内存中, 把对磁盘的访间变为对内存的访问。 为了弥补性能上的差异, 现代操作系统越来越 “ 激进地 ” 将内存作为磁盘缓存, 甚至会非常乐意将所有可用的内存用作磁盘缓存, 这样当内存回收时也几乎没有性能损失, 所有对于磁盘的读写也将经由统一的缓存。
当一个进程准备读取磁盘上的文件内容时, 操作系统会先查看待读取的数据所在的页
(page)是否在页缓存(pagecache)中, 如果存在(命中)则直接返回数据, 从而避免了对物理磁盘的I/O操作;如果没有命中, 则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存, 之后再将数据返回给进程。 同样, 如果 一个进程需要将数据写入磁盘, 那么操作系统也会检测数据对应的页是否在页缓存中, 如果不存在, 则会先在页缓存中添加相应的页, 最后将数据写入对应的页。 被修改过后的页也就变成了脏页, 操作系统会在合适的时间把脏页中的数据写入磁盘, 以保持数据的一致性。
Linux操作系统中的vm.dirty_background_ra巨o参数用来指定当脏页数量达到系统
内存的百分之多少之后就会触发pdflush/flush/kdmflush等后台回写进程的运行来处理脏页, 一般设置为小千10 的值即可,但不建议设置为0。与这个参数对应的还有一个vm.dirty_ratio参数, 它用来指定当脏页数量达到系统内存的百分之多少之后就不得不开始对脏页进行处理,在此过程中, 新的VO请求会被阻挡直至所有脏页被冲刷到磁盘中。
对一个进程而言, 它会在进程内部缓存处理所需的数据, 然而这些数据有可能还缓存在操作系统的页缓存中, 因此同 一份数据有可能被缓存了两次。 并且, 除非使用DirectI/0的方式,否则页缓存很难被禁止。 此外, 用过Java的人一般都知道两点事实: 对象的内存开销非常大,通常会是真实数据大小的几倍甚至更多, 空间使用率低下; Java的垃圾回收会随着堆内数据的增多而变得越来越慢。 基千这些因素, 使用文件系统并依赖于页缓存的做法明显要优于维护 一个进程内缓存或其他结构, 至少我们可以省去了一份进程内部的缓存消耗, 同时还可以通过结构紧凑的字节码来替代使用对象的方式以节省更多的空间。如此, 我们可以在32GB的机器上使用28GB至30GB的内存而不用担心GC所带来的性能间题。 此外, 即使Kafka服务重启,页缓存还是会保持有效, 然而进程内的缓存却需要重建。 这样也极大地简化了代码逻辑, 因为维护页缓存和文件之间的一致性交由操作系统来负责, 这样会比进程内维护更加安全有效。
Kafka 中大量使用了页缓存, 这是Kafka 实现高吞吐的重要因素之一。 虽然消息都是先被写入页缓存, 然后由操作系统负责具体的刷盘任务的, 但在Kafka中同样提供了同步刷盘及间断性强制刷盘( fsync )的功能,这些功能可以通过 log.flush.interval . messages 、log.flush .int erval .m s 等参数来控制。同步刷盘可以提高消息的可靠性,防止由于机器掉电等异常造成处于页缓存而没有及时写入磁盘的消息丢失。不过笔者并不建议这么做,刷盘任务就应交由操作系统去调配,消息的可靠性应该由多副本机制来保障,而不是由同步刷盘这种严重影响性能的行为来保障 。
Linux 系统会使用磁盘的 一部分作为 swap 分区,这样可以进行进程的调度:把当前非活跃的进程调入 swap 分区,以此把内存空出来让给活跃的进程。对大量使用系统页缓存的 Kafka而言,应当尽量避免这种内存的交换,否则会对它各方面的性能产生很大的负面影响 。我们可以通过修改 vm.swappiness 参数 ( Linux 系统参数〉来进行调节 。 vm. swappiηess 参数的上限为 100,它表示积极地使用 swap 分区,并把内存上的数据及时地搬运到 swap 分区中;vm.swappiness 参数的下限为 0 ,表示在任何情况下都不要发生交换( vm . swappiness=0的含义在不同版本的 Linux 内核中不太相同,这里采用的是变更后的最新解释) ,这样一来 ,当内存耗尽时会根据一定的规则突然中止某些进程。可以将这个参数的值设置为 1 ,这样保留了 swap 的机制而又最大限度地限制了它对 Kafka 性能的影响 。
顺序写
Kafka顺序写磁盘是其实现高性能数据存储的关键技术之一。
-
日志分段:Kafka的每个分区在磁盘上以日志文件的形式存储,这些日志文件会被切分成多个日志段。新消息会不断追加到当前活跃的日志段末尾,当达到一定条件(如文件大小达到阈值或时间间隔),就会创建新的日志段。
-
顺序追加:生产者发送到Kafka的消息会按照到达的顺序依次追加到分区日志文件中,不会随机插入或修改中间的内容,保证了磁盘写入是顺序的。
优势
-
提升写入性能:与随机写相比,顺序写磁盘时磁头移动距离小,减少了寻道时间和旋转延迟,能充分利用磁盘的顺序读写带宽,大幅提升写入速度,使Kafka能处理大量并发写入请求。
-
提高数据可靠性:顺序写的方式使数据在磁盘上连续存储,降低了数据碎片化和存储错误的风险,同时也便于Kafka进行日志的分段、清理和压缩等管理操作,有助于保证数据的完整性和一致性。
实现条件
-
底层文件系统支持:Kafka依赖底层文件系统提供的顺序写支持,如常见的ext4、XFS等文件系统都能很好地配合Kafka实现顺序写。
-
合理配置参数:通过合理设置 log.segment.bytes (日志段大小)、 log.roll.hours (日志滚动时间间隔)等参数,能确保Kafka按预期进行日志分段和顺序写入,避免因参数设置不当导致的写入性能下降。
零拷贝
Kafka 还使用零拷 贝 ( Zero-Copy )技术来进一步提升性能 。 所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手 。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换 。 对 Linux操作系统而言,零拷贝技术依赖于底层的 sendfile() 方法实现 。 对应于 Java 语言,
FileChannal.transferTo()方法的底层实现就是 sendfile()方法 。
传统的文件读写中 文件经历了 4 次复制的过程:
- (1)调用read()时, 文件A中的内容被复制到了内核态下的Read Buffer中。
- (2)CPU控制将内核模式数据复制到用户模式下。
- (3)调用write()时, 将用户模式下的内容复制到内核模式下的SocketBuffer中。
- (4)将内核模式下的SocketBuffer的数据复制到网卡设备中传送。
从上面的过程可以看出, 数据平白无故地从内核模式到用户模式 “ 走了一 圈 ” , 浪费了2次复制过程: 第一次是从内核模式复制到用户模式;第二次是从用户模式再复制回内核模式,即上面4次过程中的第2步和第3步。 而且在上面的过程中, 内核和用户模式的上下文的切换也是4次。
如果采用了零拷贝技术, 那么应用程序可以直接请求内核把磁盘中的数据传输给Socket,如图所示。
零拷贝技术通过 DMA (Direct Memory Access) 技术将文件内容复制到内核模式下的 Read Buffer 中。 不过没有数据被复制到 Socket Buffer, 相反只有包含数据的位置和长度的信息的文件描述符被加到 Socket Buffer 中。 DMA 引擎直接将数据从内核模式中传递到网卡设备(协议引擎)。 这里数据只经历了2次复制就从磁盘中传送出去了, 并且上下文切换也变成了2次。零拷贝是针对内核模式而言的, 数据在内核模式下实现了零拷贝。