本篇主要是描述了kafka需要达到的能力以满足广泛的用例,也介绍了kafka的文件系统和持久化原理,最后提到了kafka如如何提高效率的。想要第一时间了解作者编写的文章就快来我的博客吧
PiPi小窝
动机
由于Kafka被设计的能够充当统一平台来处理大公司可能拥有的所有实时数据源,所以要考虑相当广泛的用例,具有以下特定
- 必须具有高吞吐量才能支持大容量事件流,例如实时日志聚合
- 需要妥善处理大量积压数据,以便能够支持离线系统的定期数据加载
- 必须处理低延迟交付,以处理传统的消息传递用例
- 需要支持分布式,实时处理,分区,所有有了分区和消费者模型
- 需要在和其他系统进行交互时,能保证机器故障时的容错
持久化
为什么选择文件系统
Kafka是在JVM之上构建的,但是为啥数据不存在JVM中,而是单独使用一个文件系统放在磁盘中呢?
操作系统层面原因
Kafka是依赖文件系统来存储和缓存消息,但是人们普遍认为“磁盘的速度很慢”,这样认为也没错,因为磁盘比人们预期的要慢很多,也快得多——在你使用方式正确的情况下,设计得当的磁盘结构往往可以和网络一样快
- 磁盘吞吐量和磁盘寻道的延迟存在差异,如果是顺序写入那么速度会非常快,六个 7200rpm SATA RAID-5 阵列的JBOD 配置上
- 线性写入性能约为 600MB/秒
- 随机写入性能约为100k/秒
- 现代操作系统提供预读和后写技术,以大块倍数预取数据,并将较小的逻辑写入分组为较大的物理写入
- 预读:类似读取数组的某个元素到缓存中时,顺便读这个元素的周围的元素,可以提高缓存命中率
- 后写:合并多个小的写操作为块写入
- 所以某些情况下,顺序磁盘访问可能比随机内存访问更快
- 操作系统一般会将主内存给磁盘的数据做缓存
- 并且规定磁盘读写都要经过这个缓存,可以保证缓存的一致性和
- 但是如果应用进程内存也自己设计了缓存,那么有可能会出现内容缓存重复。
JVM的原因
了解Java内存的都知道:
- 对象的内存开销非常高,会使数据大小增加大于一倍,就是因为对象存储的不只是数据还有结构以及其他信息
- 堆内存的数据越来越多,Java垃圾回收会越来越慢,不管是分代还是分区模型,数据量大起来后整理和清洁都比较繁琐和缓慢
相比于JVM的堆内存和对象,直接使用文件系统的优势
- 可以最大程度地利用空闲内存作为缓存,避免内存开销过高;
- 通过存储紧凑的字节结构而不是单个对象,可以进一步提高可用缓存量;
- 即使服务重新启动,文件系统和页面缓存中的数据仍然保持热状态,而内部缓存可能需要花费较长时间重新构建;
- 简化代码逻辑,将维护缓存和文件系统之间的一致性交给操作系统处理,提高效率和正确性;
- 如果磁盘读取是线性的,预读操作将有效地利用这个缓存。
所以Kafka的持久化设计非常简单:所有数据都会立即写入文件系统的吃酒日志,不必刷新到磁盘
恒定的时间
消息传递系统中使用的持久数据结构通常是每个消费者的队列,具有关联的 B树 或其他通用随机访问数据结构,以维护有关消息的元数据
- 虽然磁盘访问中B树是一种最通用的数据结构,且支持消息传递系统中的各种事务性和非事务性语义,但是成本却很高
- 虽然操作复杂的为O(logN)被认为是常数时间,但对于磁盘操作来说却不是,因为B树找数据要先找索引以及数据,磁盘就要多次寻道
- 如果使用简单的读取来进行访问,如同日志记录的解决方案一样,如同数组根据索引或者顺序遍历一样,所有操作都是O(1)的,同样和数组有相同的痛点:占据大量的连续空间和删除元素消耗性能
- 通过批量处理删除操作,减少单个消息删除时的性能开销
- 使用日志段(log segment)来管理消息存储,当日志段满了之后,会进行合并和清理操作,以减少空间占用和提高性能。
- 所以即使使用寻道性能较差的磁盘,虽然随机读写满,但是顺序读写快、便宜大碗,容量吊打同价格的磁盘
- 所以即使访问容量无限的磁盘,也不会影响性能,那么我们可以在磁盘容量允许的情况下,将消息保留较长的一段时间后再删除。
效率
由于Kafka最为消息系统,需要应付的就是消费者和生产者,其中生产者的消息生产和消费者的消息消费都是通过Web活动传递
通过Web活动传递,那么就会出现CPU不断地去网卡拉取数据,和推送数据,即使上面提到的磁盘访问模式,解决了一部分读写问题,但是都会涉及到字节复制以及小型I/O操作
优化小型I/O
- 小型I/O不仅出现在客户端和服务器之间,也发生在服务器自身的持久化操作中。
- 优化这种情况Kafka的解决方式是围绕“消息 集”抽象构建,也就是将消息分组在一起,统一在一次网络传输中传递
- 生产者一次发送多个消息
- 消费者一次拉取多个消息
- 这种优化方式被称为“批处理”,导致更大的网络数据包,更大的顺序磁盘操作、连续的内存块等,将随机消息写入的突发流,转换为流向消费者的线性写入
优化字节复制
- Linux中文件到套接字传输数据是通过sendfile系统完成的,方式如下:
- 操作系统将数据从磁盘读取到内核空间的pagecache中
- 应用程序将数据从内核空间读取到用户空间缓冲区
- 应用程序将数据写回到内核空间的套接字缓冲区中
- 操作系统将数据从套接字缓冲区复制到 NIC 缓冲区,并在其中通过网络发送
- 上面的方式,我可以想到,为啥不从第一步就到第四步呢,也就是直接将磁盘读出的数据复制到NIC中
- Kafka的开发者们也是这样想的,所以使用了“零拷贝”技术
端到端的批量压缩
可以发现上述的两个优化方式,都是在优化数据在网络中传输的速度,且小型I/O会增加一次网络传输的数据包大小,且在网络传输中,数据越小肯定接受完成的时间更短
- 某些情况下,瓶颈实际上不是 CPU 或磁盘,而是网络带宽
- 可以一次压缩一个消息,而不需要 Kafka 的任何支持,但这可能会导致压缩率非常差
- 因为大部分冗余是由于相同类型的消息之间的重复造成的(例如, JSON 或网络日志中的用户代理或常见字符串值)。
- 有效的压缩需要将多个消息压缩在一起,而不是单独压缩每个消息。
- Kafka 通过高效的批处理格式来支持这一点。一批消息可以聚集在一起压缩并以这种形式发送到服务器。
- 这批消息将以压缩形式写入,并在日志中保持压缩状态,仅由消费者解压缩。
- 支持 GZIP、Snappy、LZ4 和 ZStandard 压缩协议