Nginx模塊fastcgi_cache的幾個(gè)注意點(diǎn)
在web項(xiàng)目中,大家都已經(jīng)非常熟悉其架構(gòu)流程了。都說Cache是萬金油,哪里不舒服抹哪里。這些流程中,幾乎每個(gè)環(huán)節(jié)都會(huì)進(jìn)行cache。從瀏覽器到webserver,到cgi程序,到DB數(shù)據(jù)庫,會(huì)進(jìn)行瀏覽器cache,數(shù)據(jù)cache,SQL查詢的cache等等。對于fastcgi這里的cache,很少被使用。去年年底,我對nginx的fastcgi_cache進(jìn)行摸索使用。在我的測試過程中,發(fā)現(xiàn)一些wiki以及網(wǎng)絡(luò)上沒被提到的注意點(diǎn),這里分享一下。
從瀏覽器到數(shù)據(jù)庫的流程圖
這里是我的NGinx配置信息
- #增加調(diào)試信息
- add_header X-Cache-CFC "$upstream_cache_status - $upstream_response_time";
- fastcgi_temp_path /dev/shm/nginx_tmp;
- #cache設(shè)置
- 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;
配置這些參數(shù)時(shí),注意每個(gè)參數(shù)的作用域,像fastcgi_cache_path參數(shù),只能在http配置項(xiàng)里配置,而 fastcgi_cache_min_uses這個(gè)參數(shù),可以在http、server、location三個(gè)配置項(xiàng)里配置。這樣更靈活的會(huì)每個(gè)域名、每個(gè)匹配的location進(jìn)行選擇性cache了。具體的參數(shù)作用域,參考FASTCGI模塊的官方WIKI。我為了調(diào)試方便,添加了一個(gè)『X-Cache-CFC』的http響應(yīng)頭,$upstream_cache_status 變量表示此請求響應(yīng)來自cache的狀態(tài),分別為:
·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子進(jìn)程)更新中
·STALE – expired, stale response was used due to proxy/fastcgi_cache_use_stale Cache已過期,響應(yīng)數(shù)據(jù)不合法,被污染
·HIT 命中cache
FASTCGI_CACHE $upstream_cache_status 結(jié)果為miss,一次也沒命中
程序代碼是Discuz!論壇, 隨便開啟測試了幾下,發(fā)現(xiàn)/dev/shm/nginx_cache/下沒有任何目錄建立,也沒有文件創(chuàng)建。調(diào)試的http header響應(yīng)頭里的X-Cache-CFC 結(jié)果一直是MISS。從服務(wù)器進(jìn)程上來看,Nginx cache manager process 跟Nginx cache loader process 進(jìn)程也正常運(yùn)行:
- 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
不知道為何會(huì)這樣,為何沒有cache成功,我以為我配置參數(shù)有問題,只好閱讀WIKI。發(fā)現(xiàn)fastcgi_ignore_headers 參數(shù)下解釋有這么一段
fastcgi_ignore_headers This 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”. |
也就是說這個(gè)參數(shù)的值,將會(huì)被忽略掉,同樣被忽略掉的響應(yīng)頭比如”X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”,而nginx配置中并沒有fastcgi_ignore_headers參數(shù)的設(shè)定,那么問題會(huì)不會(huì)出現(xiàn)在 FASTCGI響應(yīng)結(jié)果里包含了類似”X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”這幾個(gè)響應(yīng)頭呢?用strace抓包,看了下nginx與fpm進(jìn)程通訊的數(shù)據(jù)
- ####為了確保準(zhǔn)確抓到處理該http請求的進(jìn)程,我把nginx 、fpm都只開啟了一個(gè)進(jìn)程處理。
- //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
從抓取的數(shù)據(jù)包里可以看到,fpm確實(shí)返回了包含“Expires”、“Cache-Control”頭的http 響應(yīng)頭信息。那么疑問來了:
·nginx的fastcgi_cache沒緩存這條http響應(yīng),是因?yàn)轫憫?yīng)頭里包含“Expires”、“Cache-Control”的原因嗎?
·程序里并沒有輸出“Expires”、“Cache-Control” http header的代碼,這是誰輸出的呢?
·既然是fpm響應(yīng)的時(shí)候,就已經(jīng)有了,那么是php的core模塊,還是其他拓展模塊輸出的?
·“Expires:”時(shí)間為何是“Thu, 19 Nov 1981 08:52:00 GMT”?
疑問比較多,一個(gè)一個(gè)查起,先從Nginx的fastcgi_cache沒緩存這條http響應(yīng)查起。我根據(jù)測試環(huán)境nginx版本 1.1.9(ubuntu 12.04默認(rèn)的),到nginx官方下了對應(yīng)版本的源碼,搜索了fastcgi參數(shù)使用的地方,在httpngx_http_upstream.c找到了。雖然不能很流程的讀懂nginx的代碼,但粗略的了解,根據(jù)了解的情況加以猜測,再動(dòng)手測試實(shí)驗(yàn),也得出了結(jié)論,確定了nginx的 fastcgi_cache的規(guī)則。
- //ngx_http_upstream.c
- //line 3136 當(dāng)fastcgi響應(yīng)包含set-cookie時(shí),不緩存
- 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 當(dāng)響應(yīng)頭包含Expires時(shí),如果過期時(shí)間大于當(dāng)前服務(wù)器時(shí)間,則nginx_cache會(huì)緩存該響應(yīng),否則,則不緩存
- 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->hheaders_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 當(dāng)響應(yīng)頭包含Cache-Control時(shí),#####如果####這里有如果啊。。。
- //【注意】如果Cache-Control參數(shù)值為no-cache、no-store、private中任意一個(gè)時(shí),則不緩存...不緩存...
- //【注意】如果Cache-Control參數(shù)值為max-age時(shí),會(huì)被緩存,且nginx設(shè)置的cache的過期時(shí)間,就是系統(tǒng)當(dāng)前時(shí)間 + 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響應(yīng)http請求的結(jié)果中,響應(yīng)頭包括Expires、Cache-Control、Set-Cookie三個(gè),都會(huì)可能不被cache,但不只有這些,別忘了nginx配置中fastcgi_ignore_headers參數(shù)設(shè)定的部分。以及ngxin的X-ACCEL X- Accel-Redirect、X-Accel-Expires、X-Accel-Charset、X-Accel-Buffering等nginx自定義的響應(yīng)頭。由于這幾個(gè)不常用,我也沒深入研究。通過對nginx的ngx_http_upstream模塊代碼模糊理解,加猜測,以及寫了腳本測試驗(yàn)證,可以得到結(jié)論是正確的。即Nginx fastcgi_cache在緩存后端fastcgi響應(yīng)時(shí),當(dāng)響應(yīng)里包含“set-cookie”時(shí),不緩存;當(dāng)響應(yīng)頭包含Expires時(shí),如果過期時(shí)間大于當(dāng)前服務(wù)器時(shí)間,則nginx_cache會(huì)緩存該響應(yīng),否則,則不緩存;當(dāng)響應(yīng)頭包含Cache-Control時(shí),如果Cache- Control參數(shù)值為no-cache、no-store、private中任意一個(gè)時(shí),則不緩存,如果Cache-Control參數(shù)值為max- age時(shí),會(huì)被緩存,且nginx設(shè)置的cache的過期時(shí)間,就是系統(tǒng)當(dāng)前時(shí)間 + mag-age的值。
nginx fastcgi_cache 響應(yīng)expired
nginx fastcgi_cache hit命中
FASTCGI_CACHE $upstream_cache_status 結(jié)果為miss,一次也沒命中。
- //逐個(gè)測試,測試時(shí),注釋其他的
- 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響應(yīng)的時(shí)候,就已經(jīng)有了,那么是php的core模塊,還是其他拓展模塊輸出的?我精簡了代碼,只輸出一個(gè)“hello world”,發(fā)現(xiàn)也確實(shí)被緩存了。顯然,php腳本程序中并沒輸出http header 的“Expires”、“Cache-Control”,多次測試,最終定位到session_start函數(shù),翻閱源碼找到了這些代碼:
- //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 或者上面幾個(gè) ##默認(rèn)是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;
- }
- // ...
到了這里,知道原因了,是程序調(diào)用session_start時(shí),php的session拓展自己輸出的。session.cache_limit 參數(shù)來決定輸出包含哪種Expires的header,默認(rèn)是nocache,修改php.ini的session.cache_limit參數(shù)為 “none”即可讓session模塊不再輸出這些http 響應(yīng)頭?;蛟谡{(diào)用session_start之前,使用session_cache_limiter函數(shù)來指定下該參數(shù)值。那為什么要在使用 session時(shí),發(fā)Expires、Cache-Control的http response header呢?我猜測了下,需要session時(shí),基本上是用戶跟服務(wù)器有交互,那么,既然有交互,就意味著用戶的每次交互結(jié)果也可能不一樣,就不能 cache這個(gè)請求的結(jié)果,給返回給這個(gè)用戶。同時(shí),每個(gè)用戶的交互結(jié)果都是不一樣的,nginx也就不能把包含特殊Cache-Control的個(gè)人響應(yīng)cache給其他人提供了。
還有一個(gè)無聊的問題“Expires:時(shí)間為何是Thu, 19 Nov 1981 08:52:00 GMT”?我翻閱了session.c這段代碼的添加時(shí)間,版本,作者信息,在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兩個(gè)版本的變更,果然是這塊代碼。作者是sas,也就是Sascha Schumann, http://php.net/credits.php里可以看到他的大名。關(guān)于這個(gè)expires過期時(shí)間的問題,有人在stackoverflow也提問過,Why is “Expires” 1981?,別人說那天是他生日。這是真的么?如果那天是他生日的話,而他增加session.cache_limiter時(shí)是1999年,他才17歲,17歲呀。我17歲時(shí)在干嘛?還不知道電腦長啥樣,正在玩『超級瑪麗』呢。
好奇的不是我一個(gè)人,還有個(gè)帖子是epoch date — Expires: Thu, 19 Nov 1981 08:52:00也問了。另外兩個(gè)地址雖然沒問,也有人提到那天是他生日了。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 ,我無法訪問,被跳轉(zhuǎn)到首頁了。用http://web.archive.org找到了歷史快照,發(fā)現(xiàn)上下文關(guān)系不大,也不能證明是他生日。 我更是好奇的發(fā)了兩封郵件到他的不同郵箱里問他,不過,目前他還沒回復(fù)?;蛟S他沒收到、沒看到,或許懶得回了。N年后,“Expires:時(shí)間為何是Thu, 19 Nov 1981 08:52:00 GMT”這個(gè)日期,會(huì)不會(huì)又成了一段奇聞佳話了呢?
原文鏈接:http://www.cnxct.com/several-reminder-in-nginx-fastcgi_cache-and-php-session_cache_limiter/