Clickhouse系列-番外-零拷贝

本文将向读者详细说明第三章中提到的无序存储时,每次读取需要读取4k的底层细节。第三章的附录已将向读者说明了“这个原因是因为操作系统在读取磁盘时,依据数据局部性原理,会按照页为单位读取,每页的大小默认是 4k。“本番外将向读者由此深入到一个计算机领域常用的一个优化——零拷贝技术。

在linux系统中,提供了3套API供应用执行文件操作:

  1. 系统调用
  2. 标准I/O
  3. mmap

第一种系统调用,是操作系统对外直接提供的文件API,提供对文件的字节读写操作。操作系统在其内部实现了页缓存机制,对应用端透明,操作系统根据访问页情况自行调整缓冲区大小。

第二种标准I/O,也就是大名鼎鼎的<stdio.h>。其通过流的方式实现对文件的操作。开发stdio的原因是因为系统调用的页缓存太大(16K~128K),而一些简单的应用,并不需要这么大的缓存,同时也由于调用系统调用涉及到CPU由用户模型向内核模式的切换,时间消耗比较大,因此开发了标准IO,可以看成是对内核的缓冲。

第三种mmap,就是所谓的零拷贝了。第二章标准IO适合简单的程序使用,但是对于数据库这样的对性能要求高的程序就会有个知名的缺点。标准IO本质是对第一种方式的缓冲,是通过在用户空间复制一份数据实现的,标准IO会将第一种系统调用read()生成的数据复制到用户空间一份,后续操作都在用户空间上进行操作,等待合适时机写会内核。此时,数据就发生了两次拷贝:即内核系统调用read()将数据复制到内核空间的内存上,和标准io将数据复制到用户空间。这也势必带来了性能损耗,幸好内核提供了mmap,支持应用将文件地址直接映射到当前进程的内存空间,这样应用就可以直接操作内存,由内核负责将内容同步到磁盘。

大部分数据库都使用mmap实现零拷贝,避免标准IO出现的两次拷贝的情况。不过mmap将文件内容映射到内存,而操作系统的内存管理单元(MMU)管理内存最小单位是页,因此mmap必须按照页的整数倍组织映射大小。这就是第三章计算中出现4K的原因。

使用mmap还有一个好处就是,除了少数的一些缺页异常,对mmap的读写都在用户空间进行。不会产生系统调用。此外,操作mmap还可以通过madvise()系统调用按需控制内核是否使用预读机制从而控制页缓存的大小。

mmap是现代应用程序应用非常广的一项技术,kafka的commitlog、postgresql的存储引擎……这些知名数据库都在大量应用零拷贝技术。但依然像我之前强调的那样,使用mmap也不是没有缺点,主要缺点是必须按照页的整数倍来组织大小,容易出现空间浪费。因此在处理大文件或者文件大小正好是pagesize的整数倍时,使用mmap会获得很大的性能提升。