当你接触到Varnish源码,你就会发现Varnish并不是你的那些常见的普通的应用。
这绝不是偶然。
在FreeBSD内核方面我花费了好多年时间,极少有闯入用户空间编程的时候,但是当我有这样的机会时,却总是发现人们的编程方式就像仍然在1975年。
因此当我开始Varnish项目时,我真的不感兴趣,直到我想到我可以尝试将我所知道的硬件和内核运作方面的一些知识充分发挥作用,我才意识到这是一个很好的机会,现在我们进展到了alpha阶段,我可以说我非常喜欢它了。
那么1975年的编程有什么问题?最简短的答案就是,计算机不再有两种存储结构了。
曾经的主存储器,一开始是充满水银的声波延迟器(acoustic delaylines filled with mercury),再到小磁性线圈,再到晶体管触发器,直到现在的动态随机访问内存。
之后辅助存储器出现了:卡带、磁带、硬盘。起先硬盘像房子一样大,继而是洗衣机一般大小,近来它已经变得如此小,女孩子可能会失望,硬盘怎么会像口袋里的MP3一样了,可惜不能听。
人们的编程方式也差不多这样。
他们把变量放在“内存”,用“硬盘”存取数据。
以 Squid 为例,一个我曾经看到过的 1975 风格的程序:你告诉它能够使用的 RAM 和磁盘空间。然后它会花费大把的时间来跟踪哪些 HTTP 对象在 RAM 中和哪些在磁盘中,并根据传输的运作模式来回移动它们。
而事实上当今的计算机只有一种存储系统,它通常是某类(存储)盘,操作系统和虚拟内存管理硬件将 RAM 转换成这类(存储)盘存储的缓存。
所以 Squid 精心设计的内存管理机制是在与内核精心设计的内存管理机制作对,且和任何一场内战一样,一事无成。
事情是这样的:Squid 在“RAM”中创建了一个 HTTP 对象,接着很快就被使用了几次。一段时间后它没有再被命中过且内核注意到了这点。然后某人因为某些用途尝试向内核获取内存,内核便决定将内存中那些暂时没有使用到页面拉出到交换空间并很明智地将它们(缓存RAM)用在某程序中确实要处理的数据上。但这是在 Squid 毫不知情的情况下发生的。Squid 依然以为这些 HTTP 对象还是在内存里,而它们将会是(译者注:就是在未来某个时间内核会将它们再次从交换空间拉回内存)。很快,Squid 想访问它们,但直到那一刻,那段 RAM 正在用于其他的处理工作。
虚拟内存就是这么一回事。
如果 Squid 做点别的,情况会好很多,但这就是 1975 风格编程的开始。
一段时间后,Squid 也会注意到这些对象是没用的,并打算将它们转移到磁盘中,以让空出来的 RAM 可用于更频繁被用到的数据。Squid 便出来创建一个文件,然后将这个 HTTP 对象写入这个文件。
现在我们切换到慢速回放:Squid 调用 write(2),我所提供的地址是一个“虚拟地址”,内核已经将其标志为“不在家(译者注:不在物理内存中,已被转移到交换空间)”。
所以 CPU 硬件分页单元会发出一个软中断(trap),操作系统中所谓的中断就会告诉它“请恢复一下内存”。
内核尝试寻找一个空闲的页面,如果没有,它会拿将某个不怎么使用的页面,很可能是另一个很少使用的 Squid 对象,将写入到磁盘上的页面池(交换空间)。当那些写入完成后,它会从页面池的另一个数据被分页出去的地方读入到现在不使用的 RAM 页面,再修改分页表,并重试刚执行失败的指令。
Squid 对此一无所知,对 Squid 而言这只是一次常规的内存访问罢了。
所以现在 Squid 让这个对象出现在内存中的页面上,还将其写到磁盘上,这就出现了两个副本:一个在操作系统的页面空间里,另一个在文件系统中。
Squid 现在将这段 RAM 作其他用途,但一段时间后,这个 HTTP 对象被命中了,所以 Squid 需要将它取出来。
首先,Squid 需要得到一些 RAM,因此可能打算将其他 HTTP 对象放到磁盘上(重复上述的步骤),然后从文件系统的文件中读入这个 HTTP 对象,再然后向网络连接套接字发送这个 HTTP 对象的数据。
这听起来是不是在给你做无用功啊?
这是 Varnish 的做法:
Varnish 先分配一些虚拟内存,并告知操作系统将这段内存备份到磁盘上的一个文件的存储空间中。当需要向客户端发送对象时,它只要提交那块虚拟内存空间,剩下的就交给内核即可。
如果/当内核认为它需要 RAM 作其他用途时,那个页面会被写到后备文件并将这段 RAM 用于其他地方。 当 Varnish 下次提交这段虚拟内存时,操作系统会查找一个 RAM 页面,也可能会释放一个,并从后备文件中读入其内容。
仅此而已。Varnish 并不会去控制哪些内容缓存在 RAM 中或哪些不是,内核代码和硬件维护程序会处理好这些事情,并且的确处理好了。