通过FastCGI Cache实现服务降级

标签: 系统架构 | 发表时间:2014-11-29 07:31 | 作者:老王
出处:http://www.blogread.cn/it/

标签:   FastCGI   降级

   在自然界中,很多生物面临生死考验的时候,往往会做出惊人的反应,其中最为大家熟知的当属壁虎,危难关头,与其坐以待毙,不如断尾求生,通过自残来换取活下去的希望。对于互联网项目而言,同样存在着很多生死考验,比如:访问量激增;数据库宕机等等,此时如果没有合理的降级方案,那么结局必然是死路一条。

   任何问题一旦脱离了实际情况,便失去了讨论的意义。在继续之前,不妨先介绍一下案例的背景情况:一个PHP网站,以读为主,原本躲在CDN后面,运行很稳定,后来新增了很多强调实时性的需求,便去掉了CDN,进而导致系统稳定性受到影响。因为历史包袱重,所以完全废弃以前的架构显得并不现实,解决方案最好能够尽可能透明,不能对原有架构造成冲击,最终我选择了通过 FastCGI Cache实现服务降级的方案。

   关于FastCGI Cache,以前很多朋友已经做过分享,比如: 超群莿鸟栖草堂,概念性的东西我就不再赘述了,说点与众不同的:虽然使用了缓存,但出于实时性考虑,正常情况下缓存都是被穿透的,只有在出现异常情况的时候才查询,架构图如下:

Degradation

Degradation

   实现的关键点在于通过 error_page处理异常,并且完成服务降级:

limit_conn_zone $server_name zone=perserver:1m;

error_page 500 502 503 504 = @degradation;

fastcgi_cache_path /tmp
                   levels=1:2
                   keys_zone=degradation:100m
                   inactive=10d
                   max_size=10g;

upstream php {
    server 127.0.0.1:9000;
    server 127.0.0.1:9001;
}

server {
    listen 80;

    limit_conn perserver 1000;

    server_name *.xip.io;

    root /usr/local/www;

    index index.html index.htm index.php;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        set $cache_key $request_method://$host$request_uri;

        set $cache_bypass "1";
        if ($arg_degradation = "on") {
            set $cache_bypass "0";
        }

        try_files $uri =404;

        include fastcgi.conf;
        fastcgi_pass php;
        fastcgi_intercept_errors on;
        fastcgi_next_upstream error timeout;
        fastcgi_cache degradation;
        fastcgi_cache_lock on;
        fastcgi_cache_lock_timeout 1s;
        fastcgi_cache_valid 200 301 302 10h;
        fastcgi_cache_min_uses 10;
        fastcgi_cache_use_stale error
                                timeout
                                invalid_header
                                updating
                                http_500
                                http_503;
        fastcgi_cache_key $cache_key;
        fastcgi_cache_bypass $cache_bypass;

        add_header X-Cache-Status $upstream_cache_status;
        add_header X-Response-Time $upstream_response_time;
    }

    location @degradation {
        rewrite . $request_uri?degradation=on last;
    }
}

   插播一个小技巧:设置域名时用到了 xip.io,有了它就不用设置hosts了,方便调试。

   代码里用到的都是Nginx缺省包含的功能,我们可以看作是一个通用版,不过对照我们架构图中的目标就会发现:它没有实现全局激活缓存的功能。如何实现呢?最简单的方法就是通过单位时间内出错次数的多少来判断系统健康以否,设置相应的阈值,一旦超过限制就全局激活缓存,通过Lua我们可以实现一个定制版:

lua_shared_dict fault 1m;

limit_conn_zone $server_name zone=perserver:1m;

error_page 500 502 503 504 = @degradation;

fastcgi_cache_path /tmp
                   levels=1:2
                   keys_zone=degradation:100m
                   inactive=10d
                   max_size=10g;

upstream php {
    server 127.0.0.1:9000;
    server 127.0.0.1:9001;
}

init_by_lua '
    get_fault_key = function(timestamp)
        if not timestamp then
            timestamp = ngx.time()
        end

        return os.date("fault:minute:%M", timestamp)
    end

    get_fault_num = function(timestamp)
        local fault = ngx.shared.fault
        local key = get_fault_key(timestamp)

        return tonumber(fault:get(key)) or 0
    end

    incr_fault_num = function(timestamp)
        local fault = ngx.shared.fault
        local key = get_fault_key(timestamp)

        if not fault:incr(key, 1) then
            fault:set(key, 1, 600)
        end
    end
';

server {
    listen 80;

    limit_conn perserver 1000;

    server_name *.xip.io;

    root /usr/local/www;

    index index.html index.htm index.php;

    location / {
        rewrite_by_lua '
            if ngx.var.arg_degradation then
                return ngx.exit(ngx.OK)
            end

            local ok = true

            for i = 0, 1 do
                local num = get_fault_num(ngx.time() - i * 60)
                if num > 1000 then
                    ok = false
                    break
                end
            end

            if not ok then
                local query = "degradation=on"
                if ngx.var.args then
                    ngx.var.args = ngx.var.args .. "&" .. query
                else
                    ngx.var.args = query
                end
            end
        ';

        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        set $cache_key $request_method://$host$request_uri;

        set $cache_bypass "1";
        if ($arg_degradation = "on") {
            set $cache_bypass "0";
        }

        try_files $uri =404;

        include fastcgi.conf;
        fastcgi_pass php;
        fastcgi_intercept_errors on;
        fastcgi_next_upstream error timeout;
        fastcgi_cache degradation;
        fastcgi_cache_lock on;
        fastcgi_cache_lock_timeout 1s;
        fastcgi_cache_valid 200 301 302 10h;
        fastcgi_cache_min_uses 10;
        fastcgi_cache_use_stale error
                                timeout
                                invalid_header
                                updating
                                http_500
                                http_503;
        fastcgi_cache_key $cache_key;
        fastcgi_cache_bypass $cache_bypass;

        add_header X-Cache-Status $upstream_cache_status;
        add_header X-Response-Time $upstream_response_time;
    }

    location @degradation {
        content_by_lua '
            if ngx.var.arg_degradation then
                return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
            end

            local res = ngx.location.capture(
                ngx.var.request_uri, {args = "degradation=on"}
            )

            ngx.status = res.status
            for name, value in pairs(res.header) do
                ngx.header[name] = value
            end
            ngx.print(res.body)

            incr_fault_num()
        ';
    }
}

   说明:实际上真实案例中缓存键名的获取逻辑有点复杂,鉴于篇幅所限一切从简。

   当系统正常时,运行于动态模式,数据通过PHP-FPM渲染;当系统异常时,全局缓存被激活,运行于静态模式,数据通过缓存渲染。通过测试发现,系统在从正常切换到异常时,因为舍弃了PHP-FPM,所以RPS从一千跃升到一万。这让我想起儿时看圣斗士的情景:每当不死鸟一辉被敌人击倒后,他总能重新站起来,并爆发出更大的能量。

   此外需要说明的是:在发生故障的时候,如果出现大量缓存过期的情况,那么由于涉及到缓存的重建,所以依然会和PHP-FPM发生交互行为,这可能会影响性能,此时没有特别好的解决办法,如果Nginx版本够的话,可以考虑激活 fastcgi_cache_revalidate,如此一来,PHP-FPM一旦判断系统处于异常情况,那么可以直接返回304实现续费。

   …

   通过FastCGI Cache实现服务降级,这是一个完美的方案么?No!它甚至有些丑陋,比如说多台服务器时,会导致大量冗余的缓存,此外磁盘IO也需要注意。虽然这不是一个完美的方案,但是它简单,正符合我解决问题时的惯用打法:先用一个土鳖一点的解决方案缓解问题,再实现一个完美的架构解决问题。稍后我会考虑使用Memcached,加上一致性哈希来替换FastCGI Cache,实现一个相对完美的服务降级方案。

您可能还对下面的文章感兴趣:

  1. 漫漫降级路 [2013-05-01 18:38:46]
  2. 降级论 [2012-07-04 14:08:56]

相关 [fastcgi cache 服务] 推荐:

通过FastCGI Cache实现服务降级

- - IT技术博客大学习
标签:   FastCGI   降级.    在自然界中,很多生物面临生死考验的时候,往往会做出惊人的反应,其中最为大家熟知的当属壁虎,危难关头,与其坐以待毙,不如断尾求生,通过自残来换取活下去的希望. 对于互联网项目而言,同样存在着很多生死考验,比如:访问量激增;数据库宕机等等,此时如果没有合理的降级方案,那么结局必然是死路一条.

Cache应用中的服务过载案例研究

- - 美团点评技术团队
简单地说,过载是外部请求对系统的访问量突然激增,造成请求堆积,服务不可用,最终导致系统崩溃. 本文主要分析引入Cache可能造成的服务过载,并讨论相关的预防、恢复策略. Cache在现代系统中使用广泛,由此引入的服务过载隐患无处不在,但却非常隐蔽,容易被忽视. 本文希望能为开发者在设计和编写相关类型应用,以及服务过载发生处理时能够有章可循.

Guava cache

- - 孟飞阳的博客
Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制. 整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好.    Guava Cache有两种创建方式:.   通过这两种方法创建的cache,和通常用map来缓存的做法比,不同在于,这两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值.

Java Cache系列之Guava Cache

- - BlogJava-首页技术区
然而作为工具库中的一部分,我们自然不能期待Guava对Cache有比较完善的实现. 因而Guava中的Cache只能用于一些把Cache作为一种辅助设计的项目或者在项目的前期为了实现简单而引入. 在Guava CacheBuilder的注释中给定Guava Cache以下的需求:. 对于这样的需求,如果要我们自己来实现,我们应该怎么设计.

巧用query cache

- - OurMySQL
   收到一用户反馈其应用日志中狂报错误,获取连接超时:. 同时应用报错超出了数据库的最大连接数:max connections:. 这种情况很有可能是有慢sql占用了连接池中的连接没有释放,导致后续进来的请求迟迟获取不到连接池中的连接,导致请求报错,登录数据库排查发现如下sql出现执行非常的慢:.

Cache-control使用Cache-control:private学习笔记

- - Web前端 - ITeye博客
网页缓存由 HTTP消息头中的Cache-control控制,常见取值有private、no-cache、max-age、must- revalidate等,默认为private. 其作用根据不同的重新浏览方式,分为以下几种情况:. 值为private、no-cache、must-revalidate,那么打开新窗口访问时都会重新访问服务器.

MySQL Query Cache 小结

- Eneri - Sky.Jian 朝阳的天空
最近经常有人问我 MySQL Query Cache 相关的问题,就整理一点 MySQL Query Cache 的内容,以供参考. 顾名思义,MySQL Query Cache 就是用来缓存和 Query 相关的数据的. 具体来说,Query Cache 缓存了我们客户端提交给 MySQL 的 SELECT 语句以及该语句的结果集.

从free到page cache

- xiao - 博客园-MrDB's 技术随笔
我们经常用free查看服务器的内存使用情况,而free中的输出却有些让人困惑,如下:. 先看看各个数字的意义以及如何计算得到:. free命令输出的第二行(Mem):这行分别显示了物理内存的总量(total)、已使用的 (used)、空闲的(free)、共享的(shared)、buffer(buffer大小)、 cache(cache的大小)的内存.

MySQL Query Cache 小结

- - Sky.Jian 朝阳的天空
最近经常有人问我 MySQL Query Cache 相关的问题,就整理一点 MySQL Query Cache 的内容,以供参考. 顾名思义,MySQL Query Cache 就是用来缓存和 Query 相关的数据的. 具体来说,Query Cache 缓存了我们客户端提交给 MySQL 的 SELECT 语句以及该语句的结果集.

oracle 11g 之 result cache

- - CSDN博客数据库推荐文章
oracle 11g 之 result cache.   今天是2013-10-12,打算最近时间研究一下shared pool的相关原理以及awr报告分析. 今天学习一下在oracle 11g shared pool中新增的一个cache 那就是result cache.      现在开始研究一下result cache,对于oracle 11g 分为client result cache以及server result cache,前者在client进行内存的分配,后者对于数据库server进行内存分配,现在看一下server result cache(如下皆是server result cache内容).