开源HTTP加速器 Varnish

Varnish是一款高性能HTTP加速器,采用现代化架构设计。它通过优化内存管理和利用虚拟内存特性,减少不必要的数据复制和系统调用,显著提高缓存效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Varnish是一款高性能的开源HTTP加速器,挪威最大的在线报纸 Verdens Gang (http://www.vg.no) 使用3台Varnish代替了原来的12台squid,性能居然比以前更好。

Varnish 的作者Poul-Henning Kamp是FreeBSD的内核开发者之一,他认为现在的计算机比起1975年已经复杂许多。在1975年时,储存媒介只有两种:内存与硬盘。但现在计算 机系统的内存除了主存外,还包括了cpu内的L1、L2,甚至有L3快取。硬盘上也有自己的快取装置,因此squid cache自行处理物件替换的架构不可能得知这些情况而做到最佳化,但操作系统可以得知这些情况,所以这部份的工作应该交给操作系统处理,这就是 Varnish cache设计架构。


Varnish Cache 的架构笔记

当你接触到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 中或哪些不是,内核代码和硬件维护程序会处理好这些事情,并且的确处理好了。


Varnish 也只是使用了一个磁盘文件,而 Squid 却将每个对象放到各自独立的文件中。HTTP 对象不需要像文件系统对象那样,所以对于每个对象都在文件系统命名空间(目录、文件名和诸如此类的东西)上浪费时间没有任何意义,在 Varnish 中所需要的就是一个虚拟内存的指针和一个长度值,剩下的就是内核的事了。

虚拟内存就是为了在实际数据量大于物理内存容量的时候让编程变得更容易而出现的,而人们却仍然不明白。

更多缓存

可是现在我们已经有越来越多的cache,硅工程师可以生产出差不多主频达到4GHz的CPU,他们甚至在CPU和RAM( 事实上是4级cache)之间能放上一级、二级、有时是三级的cache。当然也会有其他的一些东西,比如写缓冲、流水线和页模式的存取,所有这些都为了能更快得从内存中读取数据。

由于它们已逼近4GHz的极限,但随着硅材料尺寸减小,可以有越来越多的晶体管一起工作,多CPU的设计也开始在世界上变得越来越流行,尽管作为一个编程模型它们实际上很糟糕。

多CPU系统没什么新鲜的东西,不过写程序时可以每次使用多个CPU很棘手,而且现在仍然是这样。

在多核系统上写出运行优良的程序事实上更加棘手

假设我有两个用来统计的计数值:

        unsigned    n_foo;
        unsigned    n_bar;

现在一个CPU在运行,并要执行n_foo++。 

为了做到这个,它先读n_foo,然后把n_foo写回。它有可能把它加载到CPU的寄存器中,也可能不会,这并不是很重要。

要读某个内存地址意味着要检查它是否在CPU的一级cache中。除非它被频繁使用,一般都不会在的。然后检查二级cache,我们假定也是cache失中。

如果这是单CPU的系统,游戏在这里 结束了,我们会从RAM内存取出数据并继续执行。

在多CPU的系统中,无论CPU共享插座还是独有并不要紧,我们首先必须检查其它CPU的cache中是否具有一个修改的n_foo拷贝,所以需要执行一个特殊的总线事务来查明。如果某个CPU回复说“是的,我把它修改过了",这个CPU就会写回RAM。好的硬件设计中,我们的CPU会在写操作的时候监听总线,糟糕的设计是需要之后再执行一次读内存。

现在CPU可以增加n_foo的值,然后写回。但很可能不会直接写回内存,我们可能很快再次需要它,所以这个修改过的值存在我们的一级cache中,直到某个时刻它最终到达RAM中。


现在想象有另一 个CPU同时想做n_bar+++,它能这样做吗?不行。Cache操作不是以字节计的,而是按照”行“的字节数计算的,典型的值是每行8到128字节。所以当第一 个CPU忙于计算n_foo时,第二个CPU试着抓取同一行cache数据时,它必须要等待,尽管它是一个不同的变量。

开始领会了么?

是的,这很糟糕。

我们如何处理?

如果可以,就不要操作内存。

这有一些 Varnish 的做法:

当需要处理一个 HTTP 请求或响应时,我们持有一个指针数组及一个工作空间(译者注:预分配的一块内存空间)。我们不会在处理每个 HTTP 报头时都调用 malloc(3)。我们一次性调用它来获取整个工作空间,然后在上面抓取存储所有 HTTP 报头的空间。这样做的好处是我们通常会一次性释放全部 HTTP 报头,而我们要做的只是将指针重新设置成工作空间的起始位置即可。

当需要将 HTTP 报头从一个请求复制到另一个请求(或从从一个响应复制到另一个响应)时,我们不复制字符串,而只是复制指针。假设我们不改变或翻译源 HTTP 报头,这就万无一失了。一个很好的例子就是从客户端请求复制到我们将发送给后台的请求中。


当新的 HTTP 报头比源 HTTP 报头有更长的生命周期时,就必需复制了。例如当我们将 HTTP 报头存储在已缓存的对象时。但那样的话我们要在工作空间中构建新的 HTTP 报头,且一旦知道它的大小,我们只要简单地调用一次 malloc(3) 来获取空间并将全部 HTTP 报头放到那里就行了。

我们还可以试试重用那些可能在缓存中的内存。

工作线程以“最近最忙的”方式调度,当工作线程空闲时它就会跑到队列的前面,这样它就最有可能接受到下一个请求,以使所有已经缓存、拥有栈空间和变量等的内存可以在缓存中被重用,而不是从 RAM 中昂贵地撷取。


我们也可以给每个工作线程一个它最有可能用到的私有变量集合,全部变量都在线程的栈里分配。那样我们就可以保证它们是在本线程正在自己的 CPU 运行时没有其他 CPU 会想触碰到的 RAM 中占用的一个页面。那样它们就不会竞争缓存块(cachelines)。

如果对你来说所有这些听起来都很陌生,那就让我向你担保它是可行的:我们在处理一个缓存命中时花费少于 18 个系统调用,而且那些调用甚至只是为了统计数据而获取时间戳罢了。

这些技术也并不新鲜,我们已经在内核中应用了超过 10 年,现在轮到你们来学习它们了:-)

如此,欢迎进入 Varnish,一个 2006风格架构的程序。

Poul-Henning Kamp,Varnish 架构师兼程序员。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值