这篇文章我们来学习下经常出现在各种数据库与中间件设计中的零拷贝和顺序写.
零拷贝
零拷贝技术就是在文件传输时, 减少CPU执行的数据拷贝次数. 那如何减少数据的拷贝次数呢? 我们来学习下在不同Linux内核版本中文件传输的过程.
kernel1.0版本
文件传输代码:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
从应用角度来看只有两步:
- 调用read()函数, 将文件数据copy到缓冲区.
- 调用write()函数, 将文件数据从缓冲区copy到socket中进行网络传输.
但实际上从操作系统的角度来看有四步:
- 调用read()函数, 将文件数据copy到内核缓冲区.
- 文件数据从内核缓冲区copy到用户进程缓冲区.
- 调用write()函数, 将文件数据从用户进程缓冲区copy到内核中与socket相关的缓冲区.
- 文件数据从socket相关的缓冲区copy到相关协议引擎.
在kernel1.0版本下, 文件数据实际上进行了四次拷贝.
内核缓冲区是为了在操作系统级别上提高磁盘IO效率, 优化磁盘写操作. 用户进程缓冲区为了减少系统调用次数, 从而降低操作系统在用户态与核心态切换所耗费的时间.
kernel2.0版本
kernel2.0版本引入sendfile系统减少了一次数据拷贝.
文件传输代码:
sendfile(socket, file, len);
流程:
- 调用sendfile系统, 将文件数据copy到内核缓冲区.
- 文件数据从内核缓冲区copy到内核中与socket相关的缓冲区.
- 文件数据从socket相关的缓冲区copy到相关协议引擎.
在kernel2.0版本下, 文件数据进行了三次拷贝.
kernel2.4版本
在kernel2.4版本之后, 文件描述符结果被改变, sendfile实现了更简单的方式, 再次减少了一次copy操作.
文件传输代码:
sendfile(socket, file, len);
流程:
- 调用sendfile系统, 将文件数据copy到内核缓冲区.
- 文件数据从内核缓冲区copy到相关协议引擎.
在kernel2.4版本下, 文件数据只进行了两次拷贝.
顺序写
我们经常在日志写入技术里看到"顺序写"这个概念, 顺序写号称与内存写性能持平, 这怎么可能呢? 一个是写入磁盘, 一个是写入内存, 性能怎么可能做到持平, 下面我们就来学习下.
随机读写与顺序读写
因为磁盘是机械结构, 每次读写之前都会先寻址, 这个寻址的过程是非常耗费时间的. 随机读写每次都要进行寻址, 而顺序读写只用寻址一次, 从减少寻址次数这一步就极大提升了IO性能.
顺序读写有个缺点是无法删除数据, 使用一个偏移量(offset)来标记读写位置, 所以顺序读写的文件一般都是定时/定量删除重建, 如Kafka, Elasticsearch, MySQL等中间件的日志文件.
Memory Mapped Files
内存映射文件技术使一个磁盘文件与物理内存的一个缓冲区之间建立映射关系, 然后从缓冲区中读写数据就相当于读写磁盘文件, 极大的提升了IO性能.
原理:
- 使一个磁盘文件与物理内存的一个缓冲区之间建立映射关系.
- 用户进程执行写操作时写入到缓冲区就返回成功.
- 操作系统在特定的时候(或在用户进程主动调用flush的时候)把数据真正的写入硬盘.
从这里就可以知道内存映射文件技术性能高的原因了, 因为它省去了flush步骤, 即
- 将数据从用户进程缓冲区拷贝到内核缓冲区.
- 将数据从内核缓冲区写入磁盘.
顺序写磁盘+Memory Mapped Files就是"顺序写"为什么性能这么高的原因, 但同时也有一个明显的缺陷, 即数据并没有真正的写入磁盘, 还是有丢失数据的风险, 所以针对不同的应用场景有不同使用方式, 比如Kafka的数据储存就是上面描述的顺序写方式, 但Elasticsearch的Transaction Log在写入用户进程缓存后会立即调用flush.(7.x版本)