消息在Kafka中的文件目录布局
- 消息存储在日志中,每个broker中一个分区副本对应一个日志
- Kafka分区中的消息以唯一的序列号,即偏移量作为分区中的唯一标识
- 为了防止分区的日志过大,Kafka会对分区日志进行分段,一个日志分段对应磁盘上的一个日志文件和两个索引文件,每个分区副本只有最后一个分段才可以写入
- 每个日志分段具有两个索引文件,偏移量索引文件和时间戳索引文件。日志分段文件的命名是其中存储的第一条消息的偏移量
偏移量索引
- 偏移量索引中会记录每条消息相对于这个分段第一条消息的偏移量和消息在这个分段日志文件中的物理地址。一个索引项由一个相对便宜量(4字节)+一个物理地址(4字节)
- 消息的相对偏移量长度为4字节,所以日志分段文件最长长度不超过INT_MAX
- 查找消息的过程:先通过跳表查找到消息位于哪个消息分段中,再通过二分法查找偏移量索引中相对偏移量不大于目标的索引项,找到对应的物理位置后,从该位置开始检索目标消息
时间戳索引
- 时间戳索引用于查找某个时间的消息
- 时间戳索引的索引项由一个时间戳(8字节)+相对偏移量(4字节)构成,时间戳代表日志被追加到文件的时间
- 查找消息的过程:
- 将目标时间戳和每个日志分段中的最大时间戳逐一对比,找到对应的日志分段
- 对索引项的时间戳进行**二分查找 **,找到相对偏移量
- 使用偏移量索引查找消息
页缓存
- 页缓存是操作系统的一种磁盘缓存,将磁盘中的数据缓存到内存中,减少磁盘I/O次数
- 一个进程读取磁盘内容时,会先检查页缓存,如果不存在的话再去读取磁盘内容,并将结果缓存至内存中
- 同理,一个进程想向磁盘写入内容时,也会先检查页缓存中是否存在,如果不存在的话会将磁盘中的对应页读取到页缓存,修改内存中的页,使之变为脏页。操作系统会在合适的时机将脏页写入磁盘
- 对一个进程而言,它会在进程内部缓存其要处理的数据,这些数据有可能也存在于页缓存中。且在进程内部,数据按照编程语言的规则被存储为对象,其内存占用可能远大于其在页缓存中以字节码的方式排列。所以,使用页缓存存储数据而非作为编程语言的对象保存在进程内部,有以下几点优势:
- 更加节省内存
- 对于某些语言可以节省GC的时间与性能开销
- 进程重启后,进程内的数据会丢失,但是页缓存中的数据不会
- Linux系统会将磁盘的一部分作为swap分区,将非活跃进程调入其中。Linux可以通过参数控制swap分区交换的活跃程度。因为Kafka大量使用页缓存,应该将这个参数设置的小一点,减少内存交换次数
磁盘I/O流程
- 用户调用标准C库的数据流:
- 应用程序 Buffer
- C标准库I/O Buffer
- 文件系统页缓存
- 通过具体文件系统到磁盘
- 用户调用文件I/O的数据流:
- 应用程序 Buffer
- C标准库I/O Buffer
- 文件系统页缓存
- 通过具体文件系统到磁盘
- 用户打开文件使用O_DIRECT:绕过页缓存直接写入磁盘
- 操作系统的调度器会将一个或多个进程的读操作合并在一起读,将一个或多个进程的写操作合并在一起写。尽量优先满足读需求,对于I/O请求的调度,有多种算法,此处暂不赘述,可以参见《深入理解Kafka核心设计与实践原理》P198
零拷贝
- 零拷贝:将磁盘上的数据直接拷贝至网卡设备中,依赖Linux底层的sendfile()方法
- 进程直接发送数据的过程:
- 文件中的内容被复制到内核态
- CPU将内核态数据复制到用户态
- 为了复制到网卡,再将数据复制到内核态
- 将数据复制到网卡
- 零拷贝流程:
- 文件中的内容被复制到内核态
- DMA引擎直接将数据送至网卡
总结
- Kafka提升性能的几种方式
- 消息追加:通过磁盘顺序追加的方式加快写速度
- 页缓存
- 零拷贝