通過FastCGI Cache實(shí)現(xiàn)服務(wù)降級
在自然界中,很多生物面臨生死考驗(yàn)的時(shí)候,往往會做出驚人的反應(yīng),其中最為大家熟知的當(dāng)屬壁虎,危難關(guān)頭,與其坐以待斃,不如斷尾求生,通過自殘來換取活下去的希望。對于互聯(lián)網(wǎng)項(xiàng)目而言,同樣存在著很多生死考驗(yàn),比如:訪問量激增;數(shù)據(jù)庫宕機(jī)等等,此時(shí)如果沒有合理的降級方案,那么結(jié)局必然是死路一條。
任何問題一旦脫離了實(shí)際情況,便失去了討論的意義。在繼續(xù)之前,不妨先介紹一下案例的背景情況:一個(gè)PHP網(wǎng)站,以讀為主,原本躲在CDN后面,運(yùn)行很穩(wěn)定,后來新增了很多強(qiáng)調(diào)實(shí)時(shí)性的需求,便去掉了CDN,進(jìn)而導(dǎo)致系統(tǒng)穩(wěn)定性受到影響。因?yàn)闅v史包袱重,所以完全廢棄以前的架構(gòu)顯得并不現(xiàn)實(shí),解決方案***能夠盡可能透明,不能對原有架構(gòu)造成沖擊,最終我選擇了通過FastCGI Cache實(shí)現(xiàn)服務(wù)降級的方案。
關(guān)于FastCGI Cache,以前很多朋友已經(jīng)做過分享,比如:超群、莿鳥棲草堂,概念性的東西我就不再贅述了,說點(diǎn)與眾不同的:雖然使用了緩存,但出于實(shí)時(shí)性考慮,正常情況下緩存都是被穿透的,只有在出現(xiàn)異常情況的時(shí)候才查詢,架構(gòu)圖如下:
實(shí)現(xiàn)的關(guān)鍵點(diǎn)在于通過error_page處理異常,并且完成服務(wù)降級:
- 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;
- }
- }
插播一個(gè)小技巧:設(shè)置域名時(shí)用到了xip.io,有了它就不用設(shè)置hosts了,方便調(diào)試。
代碼里用到的都是Nginx缺省包含的功能,我們可以看作是一個(gè)通用版,不過對照我們架構(gòu)圖中的目標(biāo)就會發(fā)現(xiàn):它沒有實(shí)現(xiàn)全局激活緩存的功能。如何實(shí)現(xiàn)呢?最簡單的方法就是通過單位時(shí)間內(nèi)出錯(cuò)次數(shù)的多少來判斷系統(tǒng)健康以否,設(shè)置相應(yīng)的閾值,一旦超過限制就全局激活緩存,通過Lua我們可以實(shí)現(xiàn)一個(gè)定制版:
- 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
- ngxngx.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()
- ';
- }
- }
說明:實(shí)際上真實(shí)案例中緩存鍵名的獲取邏輯有點(diǎn)復(fù)雜,鑒于篇幅所限一切從簡。
當(dāng)系統(tǒng)正常時(shí),運(yùn)行于動態(tài)模式,數(shù)據(jù)通過PHP-FPM渲染;當(dāng)系統(tǒng)異常時(shí),全局緩存被激活,運(yùn)行于靜態(tài)模式,數(shù)據(jù)通過緩存渲染。通過測試發(fā)現(xiàn),系統(tǒng)在從正常切換到異常時(shí),因?yàn)樯釛壛薖HP-FPM,所以RPS從一千躍升到一萬。這讓我想起兒時(shí)看圣斗士的情景:每當(dāng)不死鳥一輝被敵人擊倒后,他總能重新站起來,并爆發(fā)出更大的能量。
此外需要說明的是:在發(fā)生故障的時(shí)候,如果出現(xiàn)大量緩存過期的情況,那么由于涉及到緩存的重建,所以依然會和PHP-FPM發(fā)生交互行為,這可能會影響性能,此時(shí)沒有特別好的解決辦法,如果Nginx版本夠的話,可以考慮激活fastcgi_cache_revalidate,如此一來,PHP-FPM一旦判斷系統(tǒng)處于異常情況,那么可以直接返回304實(shí)現(xiàn)緩存續(xù)期。
…
通過FastCGI Cache實(shí)現(xiàn)服務(wù)降級,這是一個(gè)***的方案么?非也!它甚至有些丑陋,比如說多臺服務(wù)器時(shí),會導(dǎo)致大量冗余的緩存,此外磁盤IO也需要注意。雖然這不是一個(gè)***的方案,但是它簡單,正符合我解決棘手問題時(shí)的慣用打法:先用一個(gè)土鱉一點(diǎn)的方案緩解問題,再用一個(gè)***的方案解決問題。稍后我會考慮使用Memcached,加上一致性哈希來替換FastCGI Cache,實(shí)現(xiàn)一個(gè)相對***的服務(wù)降級方案。