伯樂線上注:本文來自文章作者 @CFC4N 的投稿(原文連結)。
在web專案中,大家都已經非常熟悉其架構流程了。都說Cache是萬金油,哪裡不舒服抹哪裡。這些流程中,幾乎每個環節都會進行cache。從瀏覽器到webserver,到cgi程式,到DB資料庫,會進行瀏覽器cache,資料cache,SQL查詢的cache等等。對於fastcgi這裡的cache,很少被使用。去年年底,我對nginx的fastcgi_cache進行摸索使用。在我的測試過程中,發現一些wiki以及網路上沒被提到的注意點,這裡分享一下。
這裡是我的NGinx配置資訊
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#增加除錯資訊 add_header X-Cache-CFC "$upstream_cache_status - $upstream_response_time"; fastcgi_temp_path /dev/shm/nginx_tmp; #cache設定 fastcgi_cache_path /dev/shm/nginx_cache levels=1:2 keys_zone=cfcache:10m inactive=50m; fastcgi_cache_key "$request_method://$host$request_uri"; fastcgi_cache_methods GET HEAD; fastcgi_cache cfcache; fastcgi_cache_valid any 1d; fastcgi_cache_min_uses 1; fastcgi_cache_use_stale error timeout invalid_header http_500; fastcgi_ignore_client_abort on; |
配置這些引數時,注意每個引數的作用域,像fastcgi_cache_path引數,只能在http配置項裡配置,而fastcgi_cache_min_uses這個引數,可以在http、server、location三個配置項裡配置。這樣更靈活的會每個域名、每個匹配的location進行選擇性cache了。具體的引數作用域,參考FASTCGI模組的官方WIKI。我為了除錯方便,新增了一個『X-Cache-CFC』的http響應頭,$upstream_cache_status 變數表示此請求響應來自cache的狀態,分別為:
- MISS 未命中
- EXPIRED – expired, request was passed to backend Cache已過期
- UPDATING – expired, stale response was used due to proxy/fastcgi_cache_use_stale updating Cache已過期,(被其他nginx子程式)更新中
- STALE – expired, stale response was used due to proxy/fastcgi_cache_use_stale Cache已過期,響應資料不合法,被汙染
- HIT 命中cache
程式程式碼是Discuz!論壇, 隨便開啟測試了幾下,發現/dev/shm/nginx_cache/下沒有任何目錄建立,也沒有檔案建立。除錯的http header響應頭裡的X-Cache-CFC 結果一直是MISS。從伺服器程式上來看,Nginx cache manager process 跟Nginx cache loader process 程式也正常執行:
1 2 3 4 |
root 3100 1 0 14:52 ? 00:00:00 nginx: master process /usr/sbin/nginx www-data 3101 3100 0 14:52 ? 00:00:00 nginx: worker process www-data 3102 3100 0 14:52 ? 00:00:00 nginx: cache manager process www-data 3103 3100 0 14:52 ? 00:00:00 nginx: cache loader process |
不知道為何會這樣,為何沒有cache成功,我以為我配置引數有問題,只好閱讀WIKI。發現fastcgi_ignore_headers 引數下解釋有這麼一段
fastcgi_ignore_headers
Syntax: fastcgi_ignore_headers field …
Default:
Context: http
server
location
Reference: fastcgi_ignore_headersThis directive forbids processing of the named headers from the FastCGI-server reply. It is possible to specify headers like “X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”.
也就是說這個引數的值,將會被忽略掉,同樣被忽略掉的響應頭比如”X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”,而nginx配置中並沒有fastcgi_ignore_headers引數的設定,那麼問題會不會出現在FASTCGI響應結果裡包含了類似”X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”這幾個響應頭呢?用strace抓包,看了下nginx與fpm程式通訊的資料
1 2 3 4 5 6 |
####為了確保準確抓到處理該http請求的程式,我把nginx 、fpm都只開啟了一個程式處理。 //strace -ff -tt -s 1000 -o xxx.log -p PHPFPM-PID 14:52:07.837334 write(3, "\1\6\0\1\0\343\5\0X-Powered-By: PHP/5.3.10-1ubuntu3.5\r\nExpires: Thu, 19 Nov 1981 08:52:00 GMT\r\nCache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\nPragma: no-cache\r\nContent-type: text/html\r\n\r\nHello cfc4n1362034327\0\0\0\0\0\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 256) = 256 //strace -ff -tt -s 1000 -o xxx.log -p Nginx-PID 15:05:13.265663 recvfrom(12, "\1\6\0\1\0\343\5\0X-Powered-By: PHP/5.3.10-1ubuntu3.5\r\nExpires: Thu, 19 Nov 1981 08:52:00 GMT\r\nCache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\nPragma: no-cache\r\nContent-type: text/html\r\n\r\nHello cfc4n1362035113\0\0\0\0\0\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 4023, 0, NULL, NULL) = 256 |
從抓取的資料包裡可以看到,fpm確實返回了包含“Expires”、“Cache-Control”頭的http 響應頭資訊。那麼疑問來了:
- nginx的fastcgi_cache沒快取這條http響應,是因為響應頭裡包含“Expires”、“Cache-Control”的原因嗎?
- 程式裡並沒有輸出“Expires”、“Cache-Control” http header的程式碼,這是誰輸出的呢?
- 既然是fpm響應的時候,就已經有了,那麼是php的core模組,還是其他擴充模組輸出的?
- “Expires:”時間為何是“Thu, 19 Nov 1981 08:52:00 GMT”?
疑問比較多,一個一個查起,先從Nginx的fastcgi_cache沒快取這條http響應查起。我根據測試環境nginx版本1.1.9(ubuntu 12.04預設的),到nginx官方下了對應版本的原始碼,搜尋了fastcgi引數使用的地方,在httpngx_http_upstream.c找到了。雖然不能很流程的讀懂nginx的程式碼,但粗略的瞭解,根據瞭解的情況加以猜測,再動手測試實驗,也得出了結論,確定了nginx的fastcgi_cache的規則。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
//ngx_http_upstream.c //line 3136 當fastcgi響應包含set-cookie時,不快取 static ngx_int_t ngx_http_upstream_process_set_cookie(ngx_http_request_t *r, ngx_table_elt_t *h, ngx_uint_t offset) { #if (NGX_HTTP_CACHE) ngx_http_upstream_t *u; u = r->upstream; if (!(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_SET_COOKIE)) { u->cacheable = 0; } #endif return NGX_OK; } //line 3242 當響應頭包含Expires時,如果過期時間大於當前伺服器時間,則nginx_cache會快取該響應,否則,則不快取 static ngx_int_t ngx_http_upstream_process_expires(ngx_http_request_t *r, ngx_table_elt_t *h, ngx_uint_t offset) { ngx_http_upstream_t *u; u = r->upstream; u->headers_in.expires = h; #if (NGX_HTTP_CACHE) { time_t expires; if (u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_EXPIRES) { return NGX_OK; } if (r->cache == NULL) { return NGX_OK; } if (r->cache->valid_sec != 0) { return NGX_OK; } expires = ngx_http_parse_time(h->value.data, h->value.len); if (expires == NGX_ERROR || expires < ngx_time()) { u->cacheable = 0; return NGX_OK; } r->cache->valid_sec = expires; } #endif return NGX_OK; } //line 3199 當響應頭包含Cache-Control時,#####如果####這裡有如果啊。。。 //【注意】如果Cache-Control引數值為no-cache、no-store、private中任意一個時,則不快取...不快取... //【注意】如果Cache-Control引數值為max-age時,會被快取,且nginx設定的cache的過期時間,就是系統當前時間 + mag-age的值 if (ngx_strlcasestrn(p, last, (u_char *) "no-cache", 8 - 1) != NULL || ngx_strlcasestrn(p, last, (u_char *) "no-store", 8 - 1) != NULL || ngx_strlcasestrn(p, last, (u_char *) "private", 7 - 1) != NULL) { u->cacheable = 0; return NGX_OK; } p = ngx_strlcasestrn(p, last, (u_char *) "max-age=", 8 - 1); if (p == NULL) { return NGX_OK; } ... r->cache->valid_sec = ngx_time() + n; |
也就是說,fastcgi響應http請求的結果中,響應頭包括Expires、Cache-Control、Set-Cookie三個,都會可能不被cache,但不只有這些,別忘了nginx配置中fastcgi_ignore_headers引數設定的部分。以及ngxin的X-ACCEL X-Accel-Redirect、X-Accel-Expires、X-Accel-Charset、X-Accel-Buffering等nginx自定義的響應頭。由於這幾個不常用,我也沒深入研究。通過對nginx的ngx_http_upstream模組程式碼模糊理解,加猜測,以及寫了指令碼測試驗證,可以得到結論是正確的。即Nginx fastcgi_cache在快取後端fastcgi響應時,當響應裡包含“set-cookie”時,不快取;當響應頭包含Expires時,如果過期時間大於當前伺服器時間,則nginx_cache會快取該響應,否則,則不快取;當響應頭包含Cache-Control時,如果Cache-Control引數值為no-cache、no-store、private中任意一個時,則不快取,如果Cache-Control引數值為max-age時,會被快取,且nginx設定的cache的過期時間,就是系統當前時間 + mag-age的值。
1 2 3 4 5 6 7 8 9 10 |
//逐個測試,測試時,註釋其他的 header("Expires: ".gmdate("D, d M Y H:i:s", time()+10000).' GMT'); header("Expires: ".gmdate("D, d M Y H:i:s", time()-99999).' GMT'); header("X-Accel-Expires:30"); header("Cache-Control: no-cache"); header("Cache-Control: no-store"); header("Cache-Control: private"); header("Cache-Control: max-age=10"); setcookie('cfc4n',"testaaaa"); echo 'Hello cfc4n',time(); |
到了這裡,疑問1解決了。那麼疑問2、3呢?程式裡並沒有輸出“Expires”、“Cache-Control” http header的程式碼,這是誰輸出的呢?既然是fpm響應的時候,就已經有了,那麼是php的core模組,還是其他擴充模組輸出的?我精簡了程式碼,只輸出一個“hello world”,發現也確實被快取了。顯然,php指令碼程式中並沒輸出http header 的“Expires”、“Cache-Control”,多次測試,最終定位到session_start函式,翻閱原始碼找到了這些程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
//ext/session/session.c line:1190 左右 // ... CACHE_LIMITER_FUNC(private) /* {{{ */ { ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT"); CACHE_LIMITER(private_no_expire)(TSRMLS_C); } /* }}} */ //再到這裡3 或者上面幾個 ##預設是nocache CACHE_LIMITER_FUNC(nocache) /* {{{ */ { ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT"); /* For HTTP/1.1 conforming clients and the rest (MSIE 5) */ ADD_HEADER("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0"); /* For HTTP/1.0 conforming clients */ ADD_HEADER("Pragma: no-cache"); } /* }}} */ //這裡2 static php_session_cache_limiter_t php_session_cache_limiters[] = { CACHE_LIMITER_ENTRY(public) CACHE_LIMITER_ENTRY(private) CACHE_LIMITER_ENTRY(private_no_expire) CACHE_LIMITER_ENTRY(nocache) {0} }; static int php_session_cache_limiter(TSRMLS_D) /* {{{ */ { php_session_cache_limiter_t *lim; if (PS(cache_limiter)[0] == '\0') return 0; if (SG(headers_sent)) { const char *output_start_filename = php_output_get_start_filename(TSRMLS_C); int output_start_lineno = php_output_get_start_lineno(TSRMLS_C); if (output_start_filename) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cache limiter - headers already sent (output started at %s:%d)", output_start_filename, output_start_lineno); } else { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cache limiter - headers already sent"); } return -2; } for (lim = php_session_cache_limiters; lim->name; lim++) { if (!strcasecmp(lim->name, PS(cache_limiter))) { lim->func(TSRMLS_C); //這裡1 return 0; } } return -1; } // ... |
到了這裡,知道原因了,是程式呼叫session_start時,php的session擴充自己輸出的。session.cache_limit引數來決定輸出包含哪種Expires的header,預設是nocache,修改php.ini的session.cache_limit引數為“none”即可讓session模組不再輸出這些http 響應頭。或在呼叫session_start之前,使用session_cache_limiter函式來指定下該引數值。那為什麼要在使用session時,發Expires、Cache-Control的http response header呢?我猜測了下,需要session時,基本上是使用者跟伺服器有互動,那麼,既然有互動,就意味著使用者的每次互動結果也可能不一樣,就不能cache這個請求的結果,給返回給這個使用者。同時,每個使用者的互動結果都是不一樣的,nginx也就不能把包含特殊Cache-Control的個人響應cache給其他人提供了。
還有一個無聊的問題“Expires:時間為何是Thu, 19 Nov 1981 08:52:00 GMT”?我翻閱了session.c這段程式碼的新增時間,版本,作者資訊,在php官方版本庫中找到了這次提交的資訊:
Revision 17092 – (view) (download) (as text) (annotate) – [select for diffs]
Modified Sun Dec 12 14:16:55 1999 UTC (13 years, 2 months ago) by sas
File length: 28327 byte(s)
Diff to previous 16964
Add cache_limiter and cache_expire options. Rename extern_referer_check
to referer_check.
對比session.c兩個版本的變更,果然是這塊程式碼。作者是sas,也就是Sascha Schumann, http://php.net/credits.php裡可以看到他的大名。關於這個expires過期時間的問題,有人在stackoverflow也提問過,Why is “Expires” 1981?,別人說那天是他生日。這是真的麼?如果那天是他生日的話,而他增加session.cache_limiter時是1999年,他才17歲,17歲呀。我17歲時在幹嘛?還不知道電腦長啥樣,正在玩『超級瑪麗』呢。
好奇的不是我一個人,還有個帖子是epoch date — Expires: Thu, 19 Nov 1981 08:52:00也問了。另外兩個地址雖然沒問,也有人提到那天是他生日了。http://boinc.berkeley.edu/dev/forum_thread.php?id=2514、https://github.com/codeguy/Slim/issues/157,這些帖子都提到說原帖是http://www.phpbuilder.com/lists/php3-list/199911/3159.php ,我無法訪問,被跳轉到首頁了。用http://web.archive.org找到了歷史快照,發現上下文關係不大,也不能證明是他生日。 我更是好奇的發了兩封郵件到他的不同郵箱裡問他,不過,目前他還沒回復。或許他沒收到、沒看到,或許懶得回了。N年後,“Expires:時間為何是Thu, 19 Nov 1981 08:52:00 GMT”這個日期,會不會又成了一段奇聞佳話了呢?