1 零拷贝
kafka高性能,是多方面协同的结果,包括宏观架构、分布式partition存储、ISR数据同步、以及“无所不用其极”的高效利用磁盘/操作系统特性。
零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。通常是说在IO读写过程中。
传统
IO
比如:读取文件,
socket
发送
传统方式实现:先读取、再发送,实际经过
1~4
四次
copy。
1
、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2
、第二次:将内核缓冲区的数据,
copy
到
application
应用程序的
buffer
;
3
、第三步:将
application
应用程序
buffer
中的数据,
copy
到
socket
网络发送缓冲区
(
属于操作系统内核的缓冲区)
;
4
、第四次:将
socket buffer
的数据,
copy
到网络协议栈,由网卡进行网络传输。
实际
IO
读写,需要进行
IO
中断,需要
CPU
响应中断
(
内核态到用户态转换
)
,尽管引入
DMA(Direct Memory Access,直接存储器访问
)
来接管
CPU
的中断请求,但四次
copy
是存在
“
不必要的拷贝
”的。
实际上并不需要第二个和第三个数据副本。数据可以直接从读缓冲区传输到套接字缓冲区。
实际上并不需要第二个和第三个数据副本。数据可以直接从读缓冲区传输到套接字缓冲区。
kafka的两个过程:
1、网络数据持久化到磁盘 (Producer 到 Broker) 页缓存
2、磁盘文件通过网络发送(Broker 到 Consumer)零拷贝
数据落盘通常都是非实时的,
Kafka
的数据并不是实时的写入硬盘,它充分利用了
现代操作系统分页存储来
利用内存提高
I/O
效率。
磁盘文件通过网络发送(
Broker
到
Consumer)
磁盘数据通过
DMA(Direct Memory Access
,直接存储器访问
)
拷贝到内核态
Buffer直接通过 DMA
拷贝到
NIC Buffer(socket buffer)
,无需
CPU
拷贝。
除了减少数据拷贝外,整个读文件
==>
网络发送由一个
sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。
Java NIO对sendfile的支持就是FileChannel.transferTo()/transferFrom()。
fileChannel.transferTo( position, count, socketChannel);
把磁盘文件读取OS内核缓冲区后的fileChannel,直接转给socketChannel发送;底层就是sendfile。消费者从broker读取数据,就是由此实现。
Java NIO对sendfile的支持就是FileChannel.transferTo()/transferFrom()。
fileChannel.transferTo( position, count, socketChannel);
把磁盘文件读取OS内核缓冲区后的fileChannel,直接转给socketChannel发送;底层就是sendfile。消费者从broker读取数据,就是由此实现。
具体来看,
Kafka
的数据传输通过
TransportLayer
来完成,其子类
PlaintextTransportLayer
通过Java NIO 的
FileChannel
的
transferTo
和
transferFrom
方法实现零拷贝。
注:
transferTo 和 transferFrom 并不保证一定能使用零拷贝,需要操作系统支持
。
Linux 2.4+
内核通过
sendfile
系统调用,提供了零拷贝。
2
页缓存
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘
I/O
的操作。 具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
Kafka
接收来自
socket buffer
的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用mmap
内存文件映射。
Memory Mapped Files简称mmap
,简单描述其作用就是:将磁盘文件映射到内存
,
用户通过修改内存就能修改磁盘文
件。
它的工作原理是直接利用操作系统的
Page
来实现磁盘文件到物理内存的直接映射。完成映射之后你 对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
通过
mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存)。使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销。
mmap
也有一个很明显的缺陷:
不可靠
,写到
mmap
中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush
的时候才把数据真正的写到硬盘。
Kafka
提供了一个参数
producer.type
来控制是不是主动
flush
;
如果
Kafka写入到mmap之后就立即flush
然后再返回
Producer
叫同步
(sync)
;
写入mmap之后立即返回Producer不调用flush
叫异步
(async)。
Java NIO
对文件映射的支持
Java NIO
,提供了一个
MappedByteBuffer
类可以用来实现内存映射。
MappedByteBuffer
只能通过调用
FileChannel
的
map()
取得,再没有其他方式。
使用
MappedByteBuffer
类要注意的是
mmap的文件映射,在full gc时才会进行释放
。当
close
时,需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner
方法。
当一个进程准备读取磁盘上的文件内容时:
1.
操作系统会先查看待读取的数据所在的页
(page)
是否在页缓存
(pagecache)
中,如果存在
(
命中)
则直接返回数据,从而避免了对物理磁盘的
I/O
操作;
2.
如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存,之后再将数据返回给进程。
如果一个进程需要将数据写入磁盘:
1.
操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页。
2.
被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。
对一个进程而言,它会在进程内部缓存处理所需的数据,然而这些数据有可能还缓存在操作系统的页缓存中,因此同一份数据有可能被缓存了两次。并且,除非使用Direct I/O
的方式, 否则页缓存很难被禁止。
当使用页缓存的时候,即使
Kafka
服务重启, 页缓存还是会保持有效,然而进程内的缓存却需要重建。这样也极大地简化了代码逻辑,因为维护页缓存和文件之间的一致性交由操作系统来负责,这样会比进程内维护更加安全有效。
Kafka
中大量使用了页缓存,这是
Kafka 实现高吞吐的重要因素之一。
消息先被写入页缓存,由操作系统负责刷盘任务。
消息先被写入页缓存,由操作系统负责刷盘任务。
3 顺序写入
操作系统可以针对线性读写做深层次的优化,比如预读
(read-ahead
,提前将一个比较大的磁盘块读入内存)
和后写
(write-behind
,将很多小的逻辑写操作合并起来组成一个大的物理写操作
)技术。
Kafka
在设计时采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消 息,并且也不允许修改已写入的消息,这种方式属于典型的顺序写盘的操作,所以就算 Kafka
使用磁盘作为存储介质,也能承载非常大的吞吐量。
mmap
和
sendfile
:
1. Linux
内核提供、实现零拷贝的
API
;
2. sendfile
是将读到内核空间的数据,转到
socket buffer
,进行网络发送;
3. mmap
将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
4. RocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。
Kafka
速度快是因为:
1. partition
顺序读写,充分利用磁盘特性,这是基础;
2. Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入;
3. Customer从broker读取数据,采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。