Spiga

使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制

2011-10-02 23:10 by T2噬菌体, 4995 visits, 收藏, 编辑

为了提高性能,几乎所有互联网应用都有缓存机制,其中Memcache是使用非常广泛的一个分布式缓存系统。众所周知,LAMP是非常经典的Web架构方式,但是随着Nginx的成熟,越来越多的系统开始转型为LNMP(Linux+Nginx+MySQL+PHP with fpm),这是因为Nginx采用基于事件机制的I/O多路复用思想设计,在高并发情况下其性能远远优于默认采用prefork模式的Apache,另外,相对于Apache,Nginx更轻量,同时拥有大量优秀的扩展模块,使得在Nginx上可以实现一些美妙的功能。

传统上,PHP中使用memcache的方法是使用php-memcachephp-memached扩展操作memcache,然而在Nginx上有构建更高效缓存机制的方法,本文将首先介绍这种机制,然后介绍具体的操作步骤方法,最后将对这种机制和传统的PHP操作memcache的性能进行一个benchmark。

Nginx的Memc和SR Cache模块

缓存策略的改进

我们知道,Nginx的核心设计思想是事件驱动的非阻塞I/O。Nginx被设计为可以配置I/O多路复用策略,在Unix系统中传统的多路复用是采用select或poll,但是这两个方法的问题是随着监听socket的增加,性能会下降,因为在linux内核中是采用轮询的方式判断是否可以触发事件,换句话说算法的复杂度为O(N),而在较新的linux内核中引入了复杂度为O(1)的epoll,因此Nginx在Linux下默认采用epoll,而在FreeBSD下默认采用kqueue作为I/O策略。

即便是这样,传统的缓存策略仍可能造成效率低下,因为传统上是通过PHP操作memcache的,要执行PHP代码,Nginx就必然要和FastCGI通信,同时也要进入PHP的生命周期,因此SAPI、PHP Core和Zend Engine的一系列逻辑会被执行。更糟糕的是,fpm和PHP可能会阻塞,因此破坏了Nginx的非阻塞性。下图展示了在memcache命中时整个处理过程。image

可以看到,即使memcache命中,还是要进入PHP的生命周期。我们知道,目前很多互联网应用都使用RESTful规范进行设计,在RESTful应用下,普遍使用uri和查询参数作为缓存的key,因此一种更高效的缓存策略是Nginx直接访问memcache,并用$uri和$args等Nginx内置变量设定缓存key规则,这样,当缓存命中时,Nginx可以跳过通过fastcgi和PHP通信的过程,直接从memcache中获取数据并返回。memc-nginx和srcache-nginx正是利用这种策略提高了缓存的效率。下图是这种高效缓存策略的示意图(当memcache命中时)。

image

个人博客已迁移至www.codinglabs.org,本文全文最新地址为http://www.codinglabs.org/html/nginx-memc-and-srcache.html

模块介绍

memc-nginxsrcache-nginx模块均为前淘宝工程师agentzh(章亦春)开发。其中memc模块扩展了Nginx标准的memcache模块,增加了set、add、delete等memcache命令,而srcache则是为location增加了透明的基于subrequest的缓存层。两者配合使用,可以实现上一节提到的高效缓存机制。关于两个模块的详细信息可以参考它们Nginx官网的wiki(memc wikisrcache wiki)页。

安装及配置

下面以LNMP环境介绍如何使用这两个模块构建缓存层。

因为Nginx并不支持模块动态加载,所以要安装新的模块,必须重新编译Nginx。首先下载两个模块(memc下载地址srcache下载地址),另外,为了发挥出缓存的最大性能,建议将memcache的upstream配置为keep-alive,为了支持upstream的keep-alive需要同时安装http-upstream-keepalive-module

将模块下载并解压到合适的目录,这里我Nginx使用的版本是1.0.4,与相关模块一起解压到了/home/zhangyang/downloads,如下图所示。

image

其中红框框起来的是我们需要用到的模块。进入nginx目录,执行下列命令:

./configure --prefix=/usr/local/nginx --add-module=../memc-nginx-module --add-module=../srcache-nginx-module --add-module=../ngx_http_upstream_keepalive
make
make install

这里我将nginx安装到/usr/local/nginx下,你可以根据自己的需要更改安装路径,另外,我只列出了本文必要的configure命令,你也可以增加需要的configure选项。

然后需要对nginx进行配置,nginx默认主配置文件放在安装目录的conf下,例如我的主配置文件为/usr/local/nginx/conf/nginx.conf。

这里我只贴出相关的配置:

#Memcache服务upstream
upstream memcache {
    server localhost:11211;
    keepalive 512 single;
}

server {
    listen       80;
    server_name  localhost;

    #memc-nginx-module
    location /memc {
        internal;

        memc_connect_timeout 100ms;
        memc_send_timeout 100ms;
        memc_read_timeout 100ms;

        set $memc_key $query_string;
        set $memc_exptime 300;
                                           
        memc_pass memcache;
    }

    location / {
        root   /var/www;
        index  index.html index.htm index.php;
    }

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        charset        utf-8;
        default_type   text/html;

        #srcache-nginx-module
        set $key $uri$args;
        srcache_fetch GET /memc $key;
        srcache_store PUT /memc $key;

        root           /var/www;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include        fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

下面解释一下其中几个点。

上文说过,memc-nginx是一个标准的upstream模块,因此首先需要定义memcache的upstream。这里我在本机上启动了一个memcache服务,端口为默认的11211,keepalive指令是http-upsteram-keepalive-module提供的功能,这里我们最大保持512个不立即关闭的连接用于提升性能。

下面是为memc-nginx-module配置location,我们配置为/memc,所有请求都通过请求这个location来操作memcache,memc-nginx-module存取memcache是基于http method语义的,使用http的GET方法表示get、PUT方法表示set、DELETE方法表示delete。这里我们将/memc设为internal表示只接受内部访问,不接收外部http请求,这是为了安全考虑,当然如果需要通过http协议开放外部访问,可以去掉internal然后使用denyallow指令控制权限。比较重要的是$memc_key这个变量,它表示以什么作为key,这里我们直接使用Nginx内置的$query_string来作为key,$memc_exptime表示缓存失效时间,以秒记。这里统一设为300(5分钟),在实际应用中可以根据具体情况为不同的内容设置不同的过期时间。

最后我们为“~ \.php$”这个location配置了缓存,这表示所有以“.php”结尾的请求都会结果被缓存,当然这里只是示例需要,实际中一般不会这么配,而是为特定需要缓存的location配置缓存。

srcache_fetch表示注册一个输入拦截处理器到location,这个配置将在location进入时被执行;而srcache_store表示注册一个输出拦截器到location,当location执行完成并输出时会被执行。注意srcache模块实际可以与任何缓存模块进行配合使用,而不必一定是memc。这里我们以$uri$args作为缓存的key。

经过上述配置后,相当于对Nginx增加了如下逻辑:当所请求的uri以“.php”结尾时,首先到memcache中查询有没有以$uri$args为key的数据,如果有则直接返回;否则,执行location的逻辑,如果返回的http状态码为200,则在输出前以$uri$args为key,将输入结果存入memcache。

更多配置

上一节给出了使用memc和srcache构建缓存层的最基本方法,实际应用中可能需要更多灵活的配置,例如为不同的location配置不同的缓存参数,根据返回内容而不是返回的http状态码确定是否缓存等等。可以有很多的方法实现这些需求,例如,srcache还支持两个指令:srcache_fetch_skip和srcache_fetch_skip,这两个指令接受一个参数,当参数已定义且非0时,则进行相应操作,否则不进行。例如,如果配置了srcache_fetch_skip $skip,这条指令,那么只有当$skip的值为非0时,才将结果缓存,如果配合ngx_lua模块的set_by_lua指令,则可以实现复杂的缓存控制。如:

location /xxxx {
    set $key ...;
    set_by_lua $skip '
        if ngx.var.cookie_foo == "bar" then
            return 1
        end
        return 0
    ';
 
    srcache_fetch_skip $skip;
    srcache_store_skip $skip;
 
    srcache_fetch GET /memc $key;
    srcache_store GET /memc $key;

    # proxy_pass/fastcgi_pass/...
}

这表示对/xxxx这个location的访问,只有存在cookie “foo”且值为“bar”时缓存机制才起作用。关于ngx_lua的更多内容请参考其主页

另外,我最近在春哥(章亦春在淘宝的昵称)的微博上看到他目前正在完善srcache的功能,为其实现更多RFC2616的缓存行为标准。关于这个模块的最新动态可以关注其github主页。

Benchmark

下面对使用memc和srcache构建的缓存机制进行一个简单的benchmark,并与使用PHP操作memcache的策略进行一个对比。为了简单起见,我们的测试PHP脚本不去访问I/O,而仅仅是调用phpinfo函数输出PHP相关信息。

测试一共分三组进行:第一组在Nginx和PHP中均不开启缓存,第二组仅使用PHP memcache缓存,第三组仅使用Nginx memcache缓存。三组都用ab程序去压,并发数为20,请求次数为10000。

这里的测试环境是我的一个虚拟机,操作系统为Ubuntu10,内存512M。Nginx采用epoll,单worker进程,memcache最大并发数为1024,最大使用内存64m。

不开启缓存

这一组我们不开启缓存,PHP程序非常简单:

<?php

phpinfo();

?>

测试结果如下:

image

PHP memcache缓存策略

第二组我们用PHP操作缓存,测试脚本为:

<?php

$memc = new Memcached;
$memc->addServer('localhost', 11211) or die('Connect to memcache server failed!');

$output = $memc->get('my_key');
if(empty($output))
{
    ob_start();
    phpinfo();
    $output = ob_get_contents();
    ob_end_clean();

    $memc->set('my_key', $output, 300);
}

echo $output;*/

?>

测试结果如下:

image

Nginx memcache缓存策略

最后,我们将PHP脚本回归到不使用缓存的版本,并配置好memc和srcache缓存机制。测试结果如下:

image

结果对比分析

为了直观,我取“每秒处理请求数”、“平均每个请求处理时间”和“吞吐率”作为评价指标,制作了一张图表。

image

我想看到图表,结论已毋需我多言。在各项指标上使用memc和srcache构建的缓存机制都大大优于使用PHP操作memcache。其中每秒处理请求数(并发度)和吞吐率都是其9倍左右,而平均个请求所用时间仅有传统策略的1/8。

这里要特别说明一下,这里之所以PHP memcache策略比不使用缓存优势不明显,是因为我们的PHP脚本不涉及I/O操作,如果其中存在如数据库存取,PHP memcache的优势还是有的,但不论如何,Nginx memcache策略在性能上的优势是其无法比拟的。

另外,除了性能优势外,使用这种策略还可以简化PHP逻辑,因为缓存这一层都放在Nginx中了,PHP就从缓存操作中解放了出来,因此是一举多得。

如果你的系统也构建在LNMP上(或LAMP)上,不妨使用本文提到的方法替代传统的缓存策略,尽情享受性能上的提升。

Creative Commons License

本文基于署名-非商业性使用 3.0许可协议发布,欢迎转载,演绎,但是必须保留本文的署名张洋(包含链接),且不得用于商业目的。如您有任何疑问或者授权方面的协商,请与我联系

Add your comment

22 条回复

  1. #1楼 hqlulu[未注册用户]2011-10-03 01:52
    这个做法也还是不错,只是想讨论说几句:

    nginx负责读写memcache的确是挺好的,后端代码不用修改。但如果php可以修改代码,php负责把输出的内容ob_get_contents拿到再写入memcache,是不是增加了nginx的稳定性?在可以简单稳定的方式的时候可以避免增加第三方模块的风险?

    另外,是否可以用/dev/shm的目录作为写入,使用自带的proxy_store/proxy_cache处理缓存逻辑?
    不知道这样相对来说速度如何呢?

    PS: 判断也许可以简单实现?
    set $skip 0;
    if xx {
    set $skip 1;
    }
     回复 引用   
  2. #2楼 agentzh[未注册用户]2011-10-03 05:00
    @hqlulu ngx_srcache 和 ngx_memc 在淘宝网和去哪儿网的一些产品中已经稳定服务近两年了,如果你有发现它们中的不稳定的地方,欢迎通过官方渠道向我们报告。我们会在第一时间予以修复 :)

    另外,使用 php 是否会更稳定只能通过具体测试来验证。据我所知,大多数常见的 php 实现在单机 1000 并发以上完全没有任何“稳定性”可言 :)

    nginx 的 ngx_proxy 和 ngx_fastcgi 等模块使用的基于文件的 http cache 有它们的应用场景。对于那些需要在集群内全局共享缓存以及磁盘很慢的应用场景而言,并不适用。其缓存粒度一般在机器级别。使用 /dev/shm 这样的 tmpfs 分区作为缓存目录的根,在实际操作中有不少困难,一旦空间超过上限,就悲剧了。Apache2 的 mod_disk_cache 亦有此问题,虽然有工具可以定时 purge,但周期并不容易确定好。

    最后,你那个 if/set 对于 ngx_srcache 并不适用。“Nginx 的 if 是邪恶的。”你需要理解它是如何工作的才能写对。具体细节可以参见我这一篇东西:http://agentzh.blogspot.com/2011/03/how-nginx-location-if-works.html (需要翻墙)
     回复 引用   
  3. #3楼 陆敏技      2011-10-03 09:03
    LAMP啊LAMP,你让ASP.NET情何以堪
     回复 引用 查看   
  4. #4楼 韬光      2011-10-03 09:52
    我觉得你这里nginx的作用就是相当于varnish或者squid,是对整个页面做一个缓存,但是如果是对页面后台某些业务逻辑上的做缓存的话,可能就不是很适合,应该还是需要php自己的缓存策略,不知道是否可以这样理解?
     回复 引用 查看   
  5. #5楼 liseen.wan[未注册用户]2011-10-03 11:11
    @韬光
    嗯, 差不多, 粒度肯定是要比在PHP层更大一点, 是页面级别的。
     回复 引用   
  6. #6楼 agentzh[未注册用户]2011-10-03 11:57
    @韬光 更小粒度的缓存就直接上 ngx_lua 和 Lua 代码啦!呵呵。仍然可以保留 C10K 的特性。"No PHP 运动",呵呵。
     回复 引用   
  7. #7楼[楼主] T2噬菌体      2011-10-03 12:56
    @韬光
    srcache是对整个返回内容进行缓存,因为是output filter嘛。。局部缓存可以用ngx_lua直接调用memc。
     回复 引用 查看   
  8. #8楼[楼主] T2噬菌体      2011-10-03 12:58
    引用陆敏技:LAMP啊LAMP,你让ASP.NET情何以堪

    ASP.NET一样可以用啊,memc和srcache只是对nginx的扩展,与具体编程语言无关。mono一样支持fastcgi nginx(http://www.mono-project.com/FastCGI_Nginx),配好后就可以在asp.net应用中使用这种机制了。
     回复 引用 查看   
  9. #9楼 佳贛      2011-10-03 16:48
    ASP.NET能運行在Nginx上嗎?
     回复 引用 查看   
  10. #10楼[楼主] T2噬菌体      2011-10-03 17:04
    @佳贛
    没听说过有个东西叫Mono吗,看这里http://www.mono-project.com/FastCGI_Nginx。nginx是webserver,只是通过fastcgi协议与脚本通信,至于fastcgi后面的处理脚本是什么,nginx根本不关心。
     回复 引用 查看   
  11. #11楼 1606437153      2011-10-03 21:26
    good
     回复 引用 查看   
  12. #12楼 xx-cheng      2011-10-04 00:08
    thaks share!
     回复 引用 查看   
  13. #13楼 陆敏技      2011-10-04 18:25
    引用T2噬菌体:
    ASP.NET一样可以用啊,memc和srcache只是对nginx的扩展,与具体编程语言无关。mono一样支持fastcgi nginx(http://www.mono-project.com/FastCGI_Nginx),配好后就可以在asp.net应用中使用这种机制了。

    俺的本意是指:那么多好的框架,都是跑在非WINDOWS上面,即便是基本的反向代理或者说HTTP加速器之类,都是在其它平台上,这有点悲剧。我们搭ASP.NET的站点,却还要弄LINUX平台做加速和缓存,微软悲剧。当然,反对者可能会说某些框架有WIN版本或者微软有自己的框架。
     回复 引用 查看   
  14. #14楼 GUO Xingwang      2011-10-04 18:33
    很好的文章 不错
     回复 引用 查看   
  15. #15楼[楼主] T2噬菌体      2011-10-04 21:09
    引用陆敏技:
    引用T2噬菌体:
    ASP.NET一样可以用啊,memc和srcache只是对nginx的扩展,与具体编程语言无关。mono一样支持fastcgi nginx(http://www.mono-project.com/FastCGI_Nginx),配好后就可以在asp.net应用中使用这种机制了。

    俺的本意是指:那么多好的框架,都是跑在非WINDOWS上面,即便是基本的反向代理或者说HTTP加速器之类,都是在其它平台上,这有点悲剧。我们搭ASP.NET的站点,却还要弄LINUX平台做加速和缓存,微软悲剧。当然,反对者可能会说某些框架有WIN版本或者微软有自己的框...

    我从来都不觉得平台会成为多大的纠结,现阶段微软的产品线确实不适合用于互联网应用,不管是成本还是产品的丰富性,Unix更适合互联网产品。完全可以在Windows下做.net开发然后部署在mono下,为什么非要部署在windows上呢。
     回复 引用 查看   
  16. #16楼 陆敏技      2011-10-05 08:40
    @T2噬菌体
    我前端和后端部署了那么多的非WIN平台,支持了可扩展,也提升了性能,然后到了应用服务器这里,为了非WIN而非WIN,用了MONO。。。如果应用服务器真用了ASP.NET开发的,这一层该用WIN部署还是用WIN部署,除非是碰到了非技术性上的需求。MONO本身我也很喜欢,当然我们都知道,为什么会有MONO,那还是微软的原因。LVS,SQUID,VARNISH,MEMCACHED,NOSQL,Hadoop,TFS等等,这些东西都是无一不发源于非WIN平台,而这些目前几乎成了开发高性能可扩展网站的首选框架,我的纠结在于:WINDOWS作为吸引优秀框架的能力太弱了,当然,这仍旧是非技术性能力。
     回复 引用 查看   
  17. #17楼 佳贛      2011-10-05 11:46
    是否有人详细的比较过ASP.NET运行在Linux+Mono与Windows+IIS相比
    稳定性跟效能是否靠谱?

    另外Nginx好像也能布署在Windows上,有人试过Windows+IIS+Nginx的组合吗?
     回复 引用 查看   
  18. #18楼[楼主] T2噬菌体      2011-10-05 13:34
    @佳贛
    至少目前没有不稳定的消息吧,mono还是很靠谱的。
     回复 引用 查看   
  19. #19楼 liexusong[未注册用户]2011-10-12 17:36
    如果要涉及逻辑方面,memc怎么处理啊?
     回复 引用   
  20. #20楼[楼主] T2噬菌体      2011-10-12 20:26
    引用liexusong:如果要涉及逻辑方面,memc怎么处理啊?

    用ngx_lua
     回复 引用 查看   
  21. #21楼 Lanisle      2011-10-21 13:21
    "个人博客已迁移至www.codinglabs.org"的实际链接有误,不应该为dot com。
     回复 引用 查看   
  22. #22楼 sunjing      2011-11-03 12:45
    您好!我是看啦你的面向接口编程详解 发布在UML软件组织结构上面的文件 对于您说的
    ///弹涂鱼出场了
    17 //我要扮演鱼
    18 //我要扮演爬树高手
    19 //我要扮演可以吃的美味,貌似没有人愿意扮演这个
    public class Goby : IFish, IClimbable, IEatable
    {
    }

    就是想请问一下 会不会出现这样的继承结构 在特定情况下面可以有满足的吗?
    public inteface IFish;
    public abstract AbstractEatable implements IFish;
    pulbic abstract AbstractClimbable extends AbstractEatable;
    public class Goby extends AbstractClimbable;

    这边有个订单继承结构
    IPluginPanel(插件面板)---AbstractPluginPanel--
    <1>PluginPanelOrder
    <1>采购订单面板
    <2>PluginSearchOrder
    我比较不清楚的地方 就是按理插件订单面板应该把订单的责任融合在一起。

    如果您有能空闲时间给我答复,我会非常感激,要是你没有时间给出回答,还是很感谢你,你那篇文件写的不错
    我的qq:284312195
     回复 引用 查看