批量发送接收
- kafka,一次发送多条消息,微批处理。
- 生产者发送消息,需要2次rpc
- 发送消息
- broker返回ACK信号,表示已经接收消息
- 消费者消费消息,3次rpc
- 消费者请求接收消息
- broker返回消息
- 消费者返回ACK信号,表示已经消费
客户端优化
- 新版客户端摒弃单线程,采用双线程模式 主线程+Sender线程
- 主线程负责将消息置入客户端缓存(缓存会将多个消息聚合为1个批次)
- Sender线程将缓存中聚合好的批次消息发送到Broker
优良的日志消息格式
- 0.11.0版本开始日志消息格式
- 引用了变长字段Varints和ZigZag编码,有效降低了附加字段占用的空间,降低了网络传输、日志存盘占用开销。
数据压缩机制
支持多种消息压缩方式(gzip、snappy、lz4)。对消息进行压缩可以极大地减少网络传输 量、降低网络I/O,从而提高整体的性能。消息压缩是一种使用时间换空间的优化方式,如果对时间延有一定的要求,则不推荐对消息进行压缩。
Partition机制
对消息进行分区,提高了数据生产与消费的并行度,有效的提升了数据的吞吐量
索引 快速检索
为每个日志分段文件提供了2个索引文件(偏移量索引文件.index、时间戳索引文件.timeindex),提高了消息的查询效率
顺序写盘
- 操作系统可以针对线性读写做深层次的优化,比如预读(read-ahead,提前将一个比较大的磁盘块 读入内存) 和后写(write-behind,将很多小的逻辑写操作合并起来组成一个大的物理写操作)技术
- 文件追加的方式来写入消息,只能在日志文件的尾部追加新的消息,并且也不允许修改已写入的消息
- 寻址 磁盘疯狂转动
页缓存
- Memory Mapped Files
- 简称:mmap,将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
- 它的工作原理是直接利用操作系统的Page来实现磁盘文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
- 通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存)。使用这种方式可以获取很大的 I/O提升,省去了用户空间到内核空间复制的开销
问题
- 不可靠,写到mmap中的数据并没有被真正的写到硬盘
- 操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘
读流程
- 操作系统会先查看待读取的数据所在的页 (page)是否在页缓存(pagecache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘的 I/O 操作
- 如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存,之后再将数据返回给进程。
写流程
- 操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页最后将数据写入对应的页。
- 被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。
kafka应用
- Kafka提供了一个参数 producer.type 来控制是不是主动flush
- 如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync)
- 写入mmap之后立即返回Producer不调用flush叫异步(async)
- 大量使用了页缓存,这是 Kafka 实现高吞吐的重要因素之一。
- 减少对磁盘 I/O 的操作(具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内 存的访问)
- 维护页缓存和文件之间的一致性交由操作系统来负责,比进程内维护更加安全有效
零拷贝
传统
先读取、再发送,实际经过1~4四次copy
实际IO读写,需要进行IO中断,需要CPU响应中断(内核态到用户态转换),尽管引入DMA(DirectMemory Access,直接存储器访问)来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的
实际上并不需要第二个和第三个数据副本。数据可以直接从读缓冲区传输到套接字缓冲区。
零拷贝
两个过程
数据落盘通常都是非实时的,Kafka的数据并不是实时的写入硬盘,它充分利用了操作系统分页存储来利用内存提高I/O效率
- 网络数据持久化到磁盘 (Producer 到 Broker)
- 磁盘文件通过网络发送(Broker 到 Consumer)
- 磁盘数据通过DMA(Direct Memory Access,直接存储器访问)拷贝到内核态 Buffer
- 直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝
- 除了减少数据拷贝外,整个读文件 ==> 网络发送由一个 sendfile 调用完成,整个过程只有两次上 下文切换,因此大大提高了性能
4. Java NIO
对sendfile的支持就是FileChannel.transferTo()/transferFrom()