nginx+NFS vs nginx+memcached

前面我写了SuperCache的介绍之后,两位朋友都表示他们在生产环境中使用了NFS,我直觉是NFS性能并不如memcached好,因为NFS牵扯了一个文件系统,但是NFS可以很好利用nginx的gzip_static。于是我做了这个评测。

我准备了三台服务器,一台服务器A用于提供memcached和NFS服务,B服务器上有nginx,挂载了A服务器上的NFS,最后通过C服务器对B服务器进行压力测试。三台服务器都是某云服务上的三台VPS,4核8G内存。

为了模拟真实环境,我首先给NFS和memcached准备了200,000条4000字节的html文件,路径格式为/articles/.html,内容是一些随机字符。

首先单纯就写入的速度来看:

写入速度明显是memcached占优,这是很容易理解的。因为NFS走了文件系统,而memcached减少了文件系统所需的诸如磁盘操作以及索引等等额外的东西。

然后我使用了siege对其进行测试

NFS

NFS服务器端/etc/exports配置

使用NFS的nginx配置:

NFS后端的结果如下:

我还额外对所有文件进行了gzip压缩(过程非常缓慢),然后启用了gzip_static:

结果效率并没有提升,原因不明。

memcached

使用memcached的nginx配置:

memcached后结果如下:

访问速度上基本没有太大差异,memcached略胜一筹

结论

NFS基于文件系统,Memcached完全存与内存。

所以,在需要频繁更新缓存的场景中,NFS受限于IO和网络条件,更新的效率较低。
由于NFS不具备memcached这种自动过期的功能,必须额外写脚本进行Sweep,可能不够方便
同时局限于文件系统的结构,清理过程中需要遍历文件也是十分消耗时间和磁盘IO的。

但如果不需频繁更新,NFS基于文件系统的特性更能节省内存。

另外我对NFS配置不够了解,对于NFS如何做高可用性不太清楚,但如果是memcached的话,则相对比较方便,可以在upstream中添加多个节点,并配置memcached_next_upstream等选项。

还有本文没有测试的:

  • 如果AB机器是同一台服务器,那么结果如何
  • NFS有一些配置选项,比如async选项,我在这里配置的是sync选项,是否对性能有所影响

Facebook对memcached的提升

如果你翻阅过一些关于大型网站扩展(Scaling)的资料,那么你可能听说过一个叫memcached的东西。memcached是一个高性能、分布式的内存对象缓存系统。我们Facebook可能是世界上最大的memcached用户了。我们利用memcached来减轻数据库的负担。memcached确实很快,但是我们还要让他更快、更高效。我们使用了超过800台服务器,提供超过28TB的内存来服务于用户。在过去的一年里,随着Facebook的用户量直线上升,我们遇到了一系列的扩展问题。日益增长的需求使得我们必须对操作系统和memcached进行一些修改,以获得足够的性能来为我们的用户提供最好的体验。

因为我们有好几千台机器,每个都运行了几百个Apache进程甚至更多,最终导致到memcached进程的TCP链接有几十万个。这些链接本身并不是什么大问题,但是memcached为每个TCP链接分配内存的方法却很成问题。memcached为每个链接使用单独的缓存进行数据的读写。当达到几十万链接的时候,这些累计起来达好几个G——这些内存其实可以更好地用于存储用户数据。为了收复这些内存,我们实现了一个针对TCP和UDP套接字的每线程共享的链接缓存池。这个改变使每个服务器可以收回几个G的内存。

虽然TCP上我们改进了内存的使用效率,但我们还是转向了UDP,目的是让get(获取)操作能降低网络流量、让multi-get(同时并行地获取几百个键值)能实现应用程序级别的流量控制。我们发现Linux上到了一定负载之后,UDP的性能下降地很厉害。这是由于,当从多个线程通过单个套接字传递数据时,在UDP套接字锁上产生的大量锁竞争导致的。要通过分离锁来修复内核恐怕不太容易。所以,我们使用了分离的UDP套接字来传递回复(每个线程用一个答复套接字)。这样改动之后,我们就可以部署UDP同时后端性能不打折。

另一个Linux中的问题是到了一定负载后,某个核心可能因进行网络软终端处理会饱和而限制了网络IO。在Linux中,网络中断只会总是传递给某个核心,因此所有的接受软终端的网络处理都发生在该内核上。另外,我们还发现某些网卡有过高的中断频率。我们通过引入网络接口的“投机”轮询解决了这两个问题。在该模型中,我们组合了中断驱动和轮询驱动的网络IO。一旦进入网络驱动(通常是传输一个数据包时)以及在进程调度器的空闲循环的时候,对网络接口进行轮询。另外,我们也用到了中断(来控制延迟),不过网络中断用到的数量大大减少(一般通过大幅度提升中断联结阈值interrupt coalescing thresholds)。由于我们在每个核心上进行网络传输,同时由于在调度器的空闲循环中对网络IO进行轮询,我们将网络处理均匀地分散到每个核心上。

最后,当开始部署8核机器的时候,我们在测试中发现了新的瓶颈。首先,memcached的stat工具集依赖于一个全局锁。这在4核上已经很令人讨厌了,在8核上,这个锁可以占用20-30%的CPU使用率。我们通过将stats工具集移入每个线程,并且需要的时候将结果聚合起来。其次,我们发现随着传递UDP数据包的线程数量的增加,性能却在降低。最后在保护每个网络设备的传送队列的锁上发现了严重的争用。数据包是由设备驱动进行入队传输和出队。该队列由Linux的“netdevice”层来管理,它位于IP和设备驱动之间。每次只能有一个数据包加入或移出队列,这造成了严重的争用。我们当中的一位工程师修改了出队算法,实现了传输的批量出队,去掉了队列锁,然后批量传送数据包。这个更正将请求锁的开销平摊到了多个数据包,显著地减少了锁争用,这样我们就能在8核系统上将memcached伸展至8线程。

做了这些修改之后,我们可以将memcached提升到每秒处理20万个UDP请求,平均延迟降低为173微秒。可以达到的总吞吐量为30万UDP请求/s,不过在这个请求速度上的延迟太高,因此在我们的系统中用处不大。对于普通版本的Linux和memcached上的50,000 UDP请求/s而言,这是个了不起的提升。

我们希望尽快将我们的修改集成到官方的memcached仓库中去,我们决定在这之前,先将我们对memcached的修改发布到github上。

Rails内置缓存引擎与线程安全

升级到Rails 2.1之后,使用Rails内置的缓存之后,发现使用mem_cache_store总是报错,于是干脆研究了Rails内置缓存引擎的代码。阅读的结果发现,Rails的内置引擎只应该应用于进程模式,尤其是mem_cache_store,因为mem_cache_store是每进程建立一个到memcached的链接,即便是读取,也需要写入套接字,所以为了同步,必须使用锁。因此即使在读取缓存的时候,也会出现争用的情况。mem_cache_store也必须像Rails2.2中的ActiveRecord一样实现一个到memcached的线程池,或者使用异步链接,否则是发挥不出memcached的效率的。

我研究了另外几个cache_store,

  • memory_store则根本连锁机制都没有,但对应有个多线程的synchronized_memory_store,使用了Monitor
  • drb_store没有用过,可能Drb本身有一些同步机制,但估计也可能会出现与mem_cache_store一样的问题;
  • file_store应该是在多线程中应该比较理想的,因为使用了文件系统自身的同步机制,使用了File.atomic_write,无论是多线程还是多进程都能共享同一个file_store

所以,Rails的线程安全还有很长一段路要走。

PS:前面的我说的我遇到的mem_cache_store的错误是Rails.cache.fetch误加了:raw => true 参数。

Rails + Memcached = Undefined Class/Module?

问题描述:
当使用memcached并将Model对象保存在其中时,若要取出这个缓存对象时,可能会找不到Model的类,并抛出“Undefined Class/Module SomeClass”的错误。
例如:

解决方案
要解决这个问题,可以在引用到该对象之间,先引用其类。比如,在前面的代码前面加入要引用的类Genre:

还有更好地方法是在Controller的before_filter中加载所依赖的所有Model:

结论
这个问题应该是Rails的一个Bug,不知道在2.0中有没有解决。我猜测是因为Rails中很多类信息是lazy load的,而从memcached中取出时没有附带类信息,也不知道如何加载,而通过上面的方法预先加载所需的类和其相关的类的信息,便解决了这个问题。