Kafka 服务端设计

前言

最近对Kafka做基准测试,看了一些关于Kafka底层原理相关内容,以便及时发现一些未来可能遇到的坑,在此总结一下。

我们都知道Kafka是分布式的消息系统,需要处理海量的消息,设计初衷是把所有消息都写入速度低容量大的硬盘,以此来换取更强的存储能力,但是实际上,使用硬盘并没有带来过性能的损失。

Kafka如何实现高吞吐

Kafka会把收到的消息都写入到硬盘中,不会丢失数据。为了优化写入速度Kafka采用了两个技术, 顺序写入 和 MMFile 。

顺序读写

如图所示,Kafka的消息是不断追加Partition中的,其中每个Partition实际对应一个文件,收到消息时候会把数据插入到文件末尾,这个特性使它可以充分利用磁盘的顺序读写能力,但也有个缺陷,就是无法删除数据,所以Kafka会把所有数据都保存下来(如果不删除硬盘肯定会被撑满,Kakfa提供了两种策略来删除数据。一是基于时间,二是基于Partition文件大小,具体参见配置文档),每个消费者(Consumer)对每个Topic都有一个Offset用来表示 读取到了第几条数据 。

Page Cache & MMFile

在现代操作系统中,为了弥补硬盘写入的速度的不足,系统越来越激进的使用内存作为文件系统的缓存,甚至会使用所有空闲的内存作为磁盘缓存(即Page Cache)。Page Cache提供了预读和回写功能。简单来说,预读就是当顺序读取文件内容时,Page Cache会提前将当前读取页面之后的几个页面也加载到Page Cache当中,这样程序相当于直接读取Cache中的内容,而不必直接与磁盘交互。回写就是当磁盘进行写入时,会写入到Page Cache当中,由操作系统在恰当的时候再写入磁盘。很多人不知道的是,所有我们的常规IO操作全部都要经过Page Cache,这个特性是在操作系统层面决定的,很难取消掉。这里面仍然存在一些问题。首先,当我们使用常规方式读取文件内容时,系统内核必须将Page Cache中的文件内容复制到User Buffer中。这不仅浪费了CPU时间,而且还将导致系统的物理内存中出现两份数据,浪费了物理内存空间。另外,由于Kafka是构建在JVM上的,对于JVM比较了解会知道如下两条规律:

  1. JVM中对象消耗的内存非常大,经常会达到实际数据的2倍以上,甚至跟多
  2. 随着数据量的增长,JVM的垃圾回收将会越来越慢,甚至不可忍受(这部分是有调优空间的,可以使用G1代替CMS垃圾回收器)。

差一句题外话,简单的JVM性能优化,推荐如下配置(可根据实际情况进行修改):

-Xms30g -Xmx30g -XX:PermSize=48m -XX:MaxPermSize=48m -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35

基于以上考虑,Kafka并没有使用常规的磁盘操作,而是使用了Memory-mapped files(简称 MMFile)。当使用Memory-mapped files时,系统内核会将程序的Virtual Memory直接映射到Page Cache,使我们可以把文件数据当做内存数据一样操作。这样不仅避免了数据在内核空间和用户空间之间复制,也避免了使用Java对象带来的一些问题,从而极大提高了Kafka读写效率。在Java的NIO中提供了使用Memory-mapped files的API,即MappedByteBuffer(继承自ByteBuffer)。

Zero-Copy

频繁的小数据量网络IO操作和过多的字节拷贝也会影响性能,为了避免频繁的网络往返带来的性能开销,Kafka将消息组合在一起形成一个“消息集”。使用这种方式可以将消息分批发送,而不是单条发送,从而分摊了网络往返的开销。当数据量巨大的时候,这种方式可以极大的提升网络IO的性能。Kafka的生产者和消费者都是采用这种方式向Kafka发送数据和从Kafka拉取数据的。

从图中可以看到,使用Sendfile可以直接从Page Cache复制数据到网卡缓冲,避免了不必要的系统调用和数据复制,非常高效。

由于Kafka的一个Topic往往有多个消费者组在消费,所以采用Zero-Copy的方式,让数据只从磁盘读取到Page Cache一次,就可以服务所有的消费了。通过使用Page Cache和Sendfile,在消费者消费Kafka中数据的时候,磁盘几乎没有任何读取活动,全部的数据都来自于Page Cache中。

在java中,java.nio.channels.FileChannel类提供了transferTo()方法来实现Zero-Copy(当然还取决与操作系统,在Unix和多数Linux上transferTo()方法会进行Sendfile系统调用)。

端到端批量压缩

很多时候,数据传输的性能瓶颈不在于CPU或硬盘,而在于网络带宽。这种情况在远距离的公网传输中最为常见。为了解决这个问题,Kafka提供了端到端的批量压缩功能。虽然用户也可以对每条消息自行压缩,但是一些数据格式可能导致单条压缩的压缩比较低。举例来说,在一批JSON数据中,字段名称其实是重复的,单条压缩会造成很多冗余。

而Kafka把一批消息抽象为“消息集”,producer对数据集进行压缩,这些数据将会以被压缩的格式传输到服务器并写入到数据日志中,只有当消费者读取这些数据后它们才会被解压缩。Kafka目前支持GZIP,Snappy和LZ4压缩方式。

总结

Kafka很多设计都非常精妙,值得我们学习和借鉴,关于学习任何开源项目最好的途径就是官方文档与源代码,还有一点就是,操作系统的知识是必不可少的一定要好好学Linux,好好学!

----本文结束 感谢您的阅读----