在自然界中,很多生物面临生死考验的时候,往往会做出惊人的反应,其中最为大家熟知的当属壁虎,危难关头,与其坐以待毙,不如断尾求生,通过自残来换取活下去的希望。对于互联网项目而言,同样存在着很多生死考验,比如:访问量激增;数据库宕机等等,此时如果没有合理的降级方案,那么结局必然是死路一条。
任何问题一旦脱离了实际情况,便失去了讨论的意义。在继续之前,不妨先介绍一下案例的背景情况:一个PHP网站,以读为主,原本躲在CDN后面,运行很稳定,后来新增了很多强调个性化的需求,便去掉了CDN,进而导致系统稳定性受到影响。因为历史包袱重,所以完全废弃以前的架构显得并不现实,解决方案最好能够尽可能透明,不能对原有架构造成冲击,最终我选择了通过FastCGI Cache实现服务降级的方案。
关于FastCGI Cache,以前很多朋友已经做过分享,比如:超群、莿鸟栖草堂,概念性的东西我就不再赘述了,说点与众不同的:虽然使用了缓存,但出于个性化的考虑,正常情况下缓存都是被穿透的,只有在出现异常情况的时候才查询,架构图如下:
实现的关键点在于通过error_page处理异常,并且完成服务降级:
limit_conn_zone $server_name zone=perserver:1m; error_page 500 502 503 504 = @failover; fastcgi_cache_path /tmp levels=1:2 keys_zone=failover: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_failover = "1") { 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 failover; 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; } location @failover { rewrite . $request_uri?failover=1 last; } }
插播一个小技巧:设置域名时用到了xip.io,有了它就不用设置hosts了,方便调试。
代码里用到的都是Nginx缺省包含的功能,我们可以看作是一个通用版,不过对照我们架构图中的目标就会发现:它没有实现全局激活缓存的功能。如何实现呢?最简单的方法就是通过单位时间内出错次数的多少来判断系统健康以否,设置相应的阈值,一旦超过限制就全局激活缓存,通过Lua我们可以实现一个定制版:
lua_shared_dict status 1m; limit_conn_zone $server_name zone=perserver:1m; error_page 500 502 503 504 = @failover; fastcgi_cache_path /tmp levels=1:2 keys_zone=failover: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 status = ngx.shared.status local key = get_fault_key(timestamp) return tonumber(status:get(key)) or 0 end incr_fault_num = function(timestamp) local status = ngx.shared.status local key = get_fault_key(timestamp) if not status:incr(key, 1) then status:add(key, 0, 600) status:incr(key, 1) 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_failover 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 = "failover=1" 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_failover = "1") { 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 failover; 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; } location @failover { rewrite_by_lua ' incr_fault_num() ngx.req.set_uri_args(ngx.var.args .. "&failover=1") ngx.req.set_uri(ngx.var.uri, true) '; } }
补充:Nginx的商业支持里提供了删除缓存的功能,但社区版里没有包含此功能,好在逻辑简单,你可以自己实现,当然也可以使用第三方模块,比如 nginx_cache_purge。
当系统正常时,运行于动态模式,数据通过PHP-FPM渲染;当系统异常时,全局缓存被激活,运行于静态模式,数据通过缓存渲染。通过测试发现,系统在从正常切换到异常时,因为舍弃了PHP-FPM,所以RPS从一千跃升到一万。这让我想起儿时看圣斗士的情景:每当不死鸟一辉被敌人击倒后,他总能重新站起来,并爆发出更大的能量。
此外需要说明的是:在发生故障的时候,如果出现大量缓存过期的情况,那么由于涉及到缓存的重建,所以依然会和PHP-FPM发生交互行为,这可能会影响性能,此时没有特别好的解决办法,如果Nginx版本够的话,可以考虑激活fastcgi_cache_revalidate,如此一来,PHP-FPM一旦判断系统处于异常情况,那么可以直接返回304实现缓存续期。
…
通过FastCGI Cache实现服务降级,这是一个完美的方案么?非也!它甚至有些丑陋,比如说多台服务器时,会导致大量冗余的缓存,此外磁盘IO也需要注意。虽然这不是一个完美的方案,但是它简单,正符合我解决棘手问题时的惯用打法:先用一个土鳖一点的方案缓解问题,再用一个完美的方案解决问题。稍后我会考虑使用Memcached,加上一致性哈希来替换FastCGI Cache,实现一个相对完美的服务降级方案。
good
楼主大神,恕我愚昧,我想请问的是这个conf中似乎没有生成缓存的机制啊。还是说bypass一个页面的同时会生成缓存么?
缓存是自动生成的。实际上我在真实项目里的实现比本文要复杂很多,这里只是做了一个演示,在细节上有裁剪。
楼主我又来了,最近和你文中所提一样需要根据条件强制bypass缓存,但是屡次三番失效。最后得出的结论是,非LUA代码中不应该用rewrite来强制加query string,而应该通过set $args $args&failover=on;来实现,否则有可能会和其他重写规则有冲突。
或许你把有冲突的场景描述出来比较好。
即便是1年半已经过去了,老王的这篇文章咀嚼起来仍相当有营养。实践的过程中我发现如果fastcgi_cache_lock on时候,对同一个fastcgi_cache_key的并发请求需要等待时间fastcgi_cache_lock,默认是5s,老王这里设定的是1s。这就使得本可以并行的请求变成串行,请问有更优的解决办法吗?fastcgi_cache_lock off?
你的意思是说如何规避大并发时锁的堵塞么?如果对数据实时性要求不太高,可以使用 fastcgi_cache_use_stale updating 设置,这样堵塞时可以使用旧数据应付一下。
Pingback引用通告: 通过FastCGI Cache实现服务降级 | phper
老王,穿透缓存使用fastcgi_cache_bypass不如直接关掉fastcgi cache。压测hello world的话,RPS差一个数量级。
赞
我用ab压测,ab -c10 -n50000
1. fastcgi_cache_bypass 1
2. fastcgi_cache off
这两个结果没有明显区别,都在2306-2400之间(用的一个vps压测的)