在Varnish架构上编程的注意点(翻译)

< DOCTYPE HTML PUBLIC -WCDTD HTML TransitionalEN>
原文作者:Poul-Henning Kamp
译文作者:YaoWeibin( yaoweibin2008@163.com)
 
当你开始涉及Varnish的源代码时,将会注意到Varnish不同于你运行的一般程序。
     这不是偶然的。
我长年工作于FreeBSD的内核,很少写用户级程序,当偶然写一些的时候,却总是发现人们还在1975年的方式进行编程。
当我接触Varnish项目时,起初并不是真的感兴趣。直到意识到,这其实是个很好的机会,来展现所有我关于如何使硬件和内核更好的工作的知识。现在项目已经进入了alpha阶段,对此我发现自己很享受。
 
1975年式的编程方式有何问题?
简而言之,电脑不再区分两种存储方式了。
曾经电脑的基本存储,起初是充满水银的声波延迟器(acoustic delaylines filled with mercury),其次是磁性电圈,再次是晶体管,到现在是动态随机访问内存。
然后,辅助存储出现了:卡带、磁带、硬盘。起先硬盘像房子一样大,继而是洗衣机一般大小,近来它已经变得如此小,女孩子可能会失望,硬盘怎么会像MP3一样了,看似不大牢靠。
人们的编程方式也大致如此。
    他们把变量放在内存,用硬盘存取数据。
以Squid为例,一个我见到过的1975年式的软件:你首先设定软件可以使用的内存大小和硬盘大小。然后它会花未知数量的时间来追踪哪些HTTP对象在RAM中,哪些在硬盘中,视访问的繁忙程度进行换入调出。
事实上,今天电脑只有一种存储,类似于硬盘的形式。操作系统和虚拟内存管理硬件已经把内存变成了硬盘的高速缓存了。
    所以Squid精心制作的内存管理机制效果会怎么样?它会和内核的内存管理打架,就像所有的国内战争一样,谁也不服谁。
事情是这样发生的:Squid在“RAM”中创建一个HTTP对象,并在创建后很快就被使用。过后它不再被命中,内核也注意到了。其他某些程序需要分配一些内存,内核就把这些不再使用的内存页面换到交换空间,然后这些内存用来放某个程序更有用的数据。但是,Squid不知道这点,它仍然认为这些HTTP对象还在RAM中,它们尝试去访问他们,但此时这些RAM已经被移作它用了。
这些虚拟内存都知道。
如果Squid什么都不做,事情还好些,但这就是1975年式的编程方式该踢屁股的地方。
过了一段时间,Squid也注意到这些对象不再被使用了,决定把他们从内存移到硬盘上,以便让其他更忙的数据来使用RAM。所以Squid创建一个文件,然后把这些HTTP对象写到文件中。
现在我们切换对到高速相机一步步来看:Squid调用了write(2),地址i是一个“虚拟地址”,内核记录为“不在内存”。
所以CPU的换页硬件会产生一个trap中断,告诉操作系统赶紧“请载入到内存”。
内核尝试找到一个空闲页面,如果没有,还需要占用其他在使用的页面,就像其他程序占用Squid的对象内存一样,把这些页面换到交换空间。当换出完成时,它会从交换空间读要读的换出页面,把它换入这个释放的RAM页面,修改页表,然后重试刚才的指令。
Squid对此毫无所知,因为Squid只是作了一个普通的页面访问。
    现在HTTP对象已经在RAM的某个页面中,Squid把它写到两个地方:一个拷贝到操作系统的页面空间;一个拷贝到文件系统。
Squid现在使用这块内存来做其他事情,到过了一会,HTTP对象又命中了,所以Squid需要它回来。
    首先Squid需要一些内存,所以它可能会决定把另一个HTTP对象换出到硬盘(重复上述过程),然后从文件系统把该对象读回到RAM,最后把数据发送到网络连接的socket中。
    还有其他像这样浪费你的工作的吗?
下面是Varnish做的:
    Varnish分配一些虚拟内存,告诉操作系统从某个文件换入到这些内存。当它需要发送HTTP对象到某个客户端时,只是简单的指向一段虚拟内存,让内核做其他的工作。
    当内核决定把内存用在其他地方的时候,页面会写回文件,RAM也会被重用。
当Varnish下次要用到这个虚拟内存时,操作系统会找到一个RAM页面,可能需要释放某个页面,然后把内容从文件读过来。
就是这样,Varnish不会真正尝试去控制哪些应被缓存到RAM中,哪些应被换出,内核有相应的代码和硬件支持会去做,而且做得很好。
    在硬盘中Varnish只有一个文件,而squid把每个对象都放在一个单独的文件中,毫无疑问每个对象都会浪费文件系统的命名空间(目录、文件名等等)。在Varnish中,我们需要做的是一个指向虚拟内存的指针和长度,内核会做其他东西。
虚拟内存意味着在处理数据大于物理内存时,编程更加容易,但人们似乎仍没有得其要领。
 
更多缓存?
    现在我们已经有越来越多的cache,硅工程师可以生产差不多主频达到4GHz的CPU,他们甚至在CPU和RAM之间放上一级、二级、有时是三级cache。RAM事实上是4级cache。当然也会有其他东西,比如写缓冲、流水线和页模式的存取,所有这些都为了能更快得从内存中读取数据。
虽然他们已逼近4GHz的极限,但随着硅材料尺寸减小,有越来越多的晶体管可以一起工作,多CPU的设计也在世界上变得越来越流行,尽管他们打乱了原有的编程模型。
多CPU系统没什么时髦的,就是程序可以每次使用多个CPU。但如何写这样的程序是很棘手的,现在仍然是这样。
写出在多核系统中运行优良的程序事实上更加棘手。
想象我有两个用来统计的计数值:
   unsigned    n_foo;
   unsigned    n_bar;
    当一个CPU在运行,并执行了n_foo++。
为了做到这些,它先读n_foo,然后把n_foo写回。它有可能把它load到CPU的寄存器中,也可能不会,这并重要。
读某个内存意味着检查它是否在CPU的一级cache中。除非它被频繁使用,一般是不会在的。然后检查二级cache,我们假定是cache miss。
    如果这是单CPU的系统,游戏也就在这里结束了,我们取出内存的数据,继续执行。
在多CPU的系统中,不管CPU共享针脚还是独有,我们首先必须检查其他CPU是否已经修改了cache中的n_foo值,所以需执行一个特殊的总线事务来查明。如果某个CPU回复说“是的,我把它修改过了",这个CPU会写回RAM。好的硬件设计是这样的,我们的CPU会在写操作的时候听总线获取n_foo的值,糟糕的设计是需要再做一次读内存。
现在CPU增加n_foo的值,然后写回。但不会直接写回内存,我们可能很快还需要它,所以这个修改过的值存在我们的一级cache中,直到返回到RAM中。
    现在想象另外一个CPU同时想做n_bar+++,它能这样做吗?不。Cache操作不是以字节计的,事实上是”行“的字节,典型的值是每行8到128字节。所以当第一个CPU忙于计算n_foo时,第二个CPU会抓取同一行cache数据,所以它必须等,尽管它们的是不同的变量。
    开窍了没?
是的,很邪恶。
那我们怎么做才好呢?
避免所有可以避免的内存操作。
下面是一些Varnish尽力去做的:
当我们处理某个HTTP请求或回复的时候,有一组的指针和工作空间。不是每次都调用malloc(3),我们为整个工作空间调用一次,然后从那里为每个HTTP头部分配空间。这样做的好处是通常一下子就可以释放头部,我们只需重设指向工作空间开始的指针值。
    当我们需要把某个HTTP头部从一个请求拷贝到另外的时候(或者是从某个回复到另外一个),不必拷贝整个字符串,我们只需拷贝指针。这是安全的,因为我们不会改变或者释放源头部,一个良好的例子就是把客户的请求拷贝到发送给后端服务器的请求中。
当一个新头部的生存期比源要长时,我们就需要进行拷贝了。例如当我们把头部存放在cache对象中。在那个时候,我们就在工作空间中创建新的头部,当我们知道它有多大时,我们只需一个简单的malloc(3)取得空间,然后把整个头部放进去。
我们也尝试重用那些可能在cache中内存。
    工作线程用的是”最近的线程最忙“的方式,当一个工作线程空闲时,它会被放到队列的头部。而头部的线程最有可能获得下一个请求,所以所有的内存还在cache中。堆栈、变量等都可以在cache中被重用,而不是去做昂贵的RAM读取。
我们也会为每个工作线程配备一个私有变量的集合,所有都在线程的堆栈中分配。这样的方式可以确定他们在RAM中占有一个页面,只要这个线程一直在它自己的CPU上运行,其他的CPU就不会用到这个页面。这样就不会在”cache行“上产生冲突。
    如果你对于所有这些听起来还很陌生,我需要你确保它是这样工作的:当处理某个cache命中的时候,我们使用不到18个系统调用,甚至大部分是为了统计之用而调用时钟。
这不是什么新技术,我们已经在内核中用了10多年,现在是你开始学习它们的时候了:)
欢迎来到Varnish,一个2006年的程序。
Poul-Henning Kamp,Varnish的设计和程序员。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值