nginx proxy cache 原理和配置实践
2019-05-04 15:42:17 阿炯

Web缓存坐落于客户端和"原始服务器(origin server)"中间,它保留了所有可见内容的拷贝。如果一个客户端请求的内容在缓存中存储,则可以直接在缓存中获得该内容而不需要与服务器通信。这样一来,由于web缓存距离客户端"更近",就可以提高响应性能,并更有效率的使用应用服务器,因为服务器不用每次请求都进行页面生成工作。在浏览器和应用服务器之间,存在多种"潜在"缓存,如:客户端浏览器缓存、中间缓存、内容分发网络(CDN)和服务器上的负载平衡和反向代理。缓存仅在反向代理和负载均衡的层面就对性能提高有很大的帮助。

并不是只有大规模的内容分发网络(CDN)可以在使用缓存中受益--缓存还可以提高负载平衡器、反向代理和应用服务器前端web服务的性能。通过缓存内容结果,可以更高效的使用应用服务器,因为不需要每次都去做重复的页面生成工作。此外,Web缓存还可以用来提高网站可靠性。当服务器宕机或者繁忙时,比起返回错误信息给用户,不如通过配置nginx将已经缓存下来的内容发送给用户。这意味着网站在应用服务器或者数据库故障的情况下,可以保持部分甚至全部的功能运转。


HTTP缓存机制

后端服务器会通过响应包头来定义缓存特性:


Origin Server定义的缓存特性

当然,缓存服务器可以通过设置一些参数来忽略或者重写后端服务器的缓存特性,但后端服务器的缓存特性也是极其重要的。

Expires:最原始的配置策略,即设置过期时间,但使用效率低下,目前绝大部分已经被Cache-Control(有兴趣的可以去看下http1.0和http1.1);

Cache-Control:定义缓存资源属性是private或者是public,并且设置缓存多久后过期,本例中,属性为public,60秒过期;

X-Accel-Expires:只有nginx能识别的缓存特性header,优先级大于上面两个header,可以设置此header,在nginx侧来重新定义缓存特性;

Etag和Last-Modified是捆绑生成的:有些场景下,你希望client端的浏览器长时间缓存,而缓存服务器只短时间缓存文件,以至于当后端服务器更新后,缓存服务器会及时同步,我们就可以使用最后两个header,Last-Modified表示最后修改时间,并声明一个ETag(哈希值),做为缓存内容的标签,具有唯一性;客户端访问请求带有If‑Modified‑Since或者If‑None‑Match header,并申明自己的客户端带有静态缓存文件,以及文件修改日期和ETag值,如果服务器端的版本和Etag值与客户端一致,则服务端会直接返回304 not modified,这个验证流程是非常快的,并且节省网络带宽;

如果Cache-Control设置为public,则客户端不会去验证资源的有效性,将会一直使用直到过期,同时public也代表资源可以被缓存在web proxy中;

如果Cache-Control包含must-revalidate,则客户端每一次访问请求资源都会去验证缓存是否有更新;

proxy_cache模块的工作原理如图所示:


nginx proxy cache最基本的配置:

proxy_cache_path   /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
server {
set $upstream http://ip:port
location / {
    proxy_cache  my_cache;
    proxy_pass    $upstream; }
}

配置项说明:

/path/to/cache : 本地路径,缓存文件存放地址;

levels : 默认所有缓存文件都放在同一个/path/to/cache下,从而影响缓存的性能,大部分场景推荐使用2级目录来存储缓存文件;

key_zone : 在共享内存中设置一块存储区域来存放缓存的key和metadata(类似使用次数),这样nginx可以快速判断一个request是否命中或者未命中缓存,1m可以存储8000个key,10m可以存储80000个key;

max_size : 最大cache空间,如果不指定,会使用掉所有disk space,当达到配额后,会删除最少使用的cache文件;

inactive : 未被访问文件在缓存中保留时间,本配置中如果60分钟未被访问则不论状态是否为expired,缓存控制程序会删掉文件,默认为10分钟;"需要注意的是,inactive和expired配置项的含义是不同的,expired只是缓存过期,但不会被删除,inactive是删除指定时间内未被访问的缓存文件";

use_temp_path : 如果为off,则nginx会将缓存文件直接写入指定的cache文件中,而不是使用temp_path存储,official建议为off,避免文件在不同文件系统中不必要的拷贝;

proxy_cache : 启用proxy cache,指定key_zone;


重要内容再说一遍

只需要两个命令就可以启用基础缓存:proxy_cache_path和proxy_cache,proxy_cache_path用来设置缓存的路径和配置,proxy_cache用来启用缓存。

proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m
use_temp_path=off;
server {
 ...
location / {
    proxy_cache my_cache;
    proxy_pass http://my_upstream;
}
}

proxy_cache_path命令中的参数及对应配置说明如下:

1.用于缓存的本地磁盘目录是/path/to/cache/

2.levels在/path/to/cache/设置了一个两级层次结构的目录。将大量的文件放置在单个目录中会导致文件访问缓慢,所以针对大多数部署,我们推荐使用两级目录层次结构。如果levels参数没有配置,则nginx会将所有的文件放到同一个目录中。

3.keys_zone设置一个共享内存区,该内存区用于存储缓存键和元数据,有些类似计时器的用途。将键的拷贝放入内存可以使nginx在不检索磁盘的情况下快速决定一个请求是`HIT`还是`MISS`,这样大大提高了检索速度。一个1MB的内存空间可以存储大约8000个key,那么上面配置的10MB内存空间可以存储差不多80000个key。

4.max_size设置了缓存的上限(在上面的例子中是10G)。这是一个可选项;如果不指定具体值,那就是允许缓存不断增长,占用所有可用的磁盘空间。当缓存达到这个上线,处理器便调用cache manager来移除最近最少被使用的文件,这样把缓存的空间降低至这个限制之下。

5.inactive指定了项目在不被访问的情况下能够在内存中保持的时间。在上面的例子中,如果一个文件在60分钟之内没有被请求,则缓存管理将会自动将其在内存中删除,不管该文件是否过期。该参数默认值为10分钟(10m)。注意,非活动内容有别于过期内容。nginx不会自动删除由缓存控制头部指定的过期内容(本例中Cache-Control:max-age=120)。过期内容只有在inactive指定时间内没有被访问的情况下才会被删除。如果过期内容被访问了,那么nginx就会将其从原服务器上刷新,并更新对应的inactive计时器。

6.nginx最初会将注定写入缓存的文件先放入一个临时存储区域,use_temp_path=off命令指示nginx将在缓存这些文件时将它们写入同一个目录下。我们强烈建议你将参数设置为off来避免在文件系统中不必要的数据拷贝。use_temp_path在nginx1.7版本和nginx Plus R6中有所介绍。

最终,proxy_cache命令启动缓存那些URL与location部分匹配的内容(本例中,为`/`)。你同样可以将proxy_cache命令添加到server部分,这将会将缓存应用到所有的那些location中未指定自己的proxy_cache命令的服务中。



关于缓存的那些疑问

陈旧总比没有强

nginx内容缓存的一个非常强大的特性是:当无法从原始服务器获取最新的内容时,nginx可以分发缓存中的陈旧(stale:即过期内容)内容。这种情况一般发生在关联缓存内容的原始服务器宕机或者繁忙时。比起对客户端传达错误信息,nginx可发送在其内存中的陈旧的文件。nginx的这种代理方式,为服务器提供额外级别的容错能力,并确保了在服务器故障或流量峰值的情况下的正常运行。为了开启该功能,只需要添加proxy_cache_use_stale命令即可:
location / {
    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
}

按照上面例子中的配置,当nginx收到服务器返回的error,timeout或者其他指定的5xx错误,并且在其缓存中有请求文件的陈旧版本,则会将这些陈旧版本的文件而不是错误信息发送给客户端。

缓存微调

nginx提供了丰富的可选项配置用于缓存性能的微调。下面是使用了几个配置的例子:
proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m
use_temp_path=off;
server {
location / {
    proxy_cache my_cache;
    proxy_cache_revalidate on;
    proxy_cache_min_uses 3;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_lock on;
    proxy_pass http://my_upstream;
}
}

这些命令配置了下列的行为:
1.proxy_cache_revalidate指示nginx在刷新来自服务器的内容时使用GET请求。如果客户端的请求项已经被缓存过了,但是在缓存控制头部中定义为过期,那么nginx就会在GET请求中包含If-Modified-Since字段,发送至服务器端。这项配置可以节约带宽,因为对于nginx已经缓存过的文件,服务器只会在该文件请求头中Last-Modified记录的时间内被修改时才将全部文件一起发送。

2.proxy_cache_min_uses设置了在nginx缓存前,客户端请求一个条目的最短时间。当缓存不断被填满时,这项设置便十分有用,因为这确保了只有那些被经常访问的内容才会被添加到缓存中。该项默认值为1。

3.proxy_cache_use_stale中的updating参数告知nginx在客户端请求的项目的更新正在原服务器中下载时发送旧内容,而不是向服务器转发重复的请求。第一个请求陈旧文件的用户不得不等待文件在原服务器中更新完毕。陈旧的文件会返回给随后的请求直到更新后的文件被全部下载。

4.当proxy_cache_lock被启用时,当多个客户端请求一个缓存中不存在的文件(或称之为一个MISS),只有这些请求中的第一个被允许发送至服务器。其他请求在第一个请求得到满意结果之后在缓存中得到文件。如果不启用proxy_cache_lock,则所有在缓存中找不到文件的请求都会直接与服务器通信。


跨多硬盘分割缓存

使用nginx,不需要建立一个RAID(磁盘阵列)。如果有多个硬盘,nginx可以用来在多个硬盘之间分割缓存。下面是一个基于请求URI跨越两个硬盘之间均分缓存的例子:
proxy_cache_path /path_of_hdd1 levels=1:2 keys_zone=my_cache_hdd1:10m max_size=10g
    inactive=60m use_temp_path=off;
proxy_cache_path /path_of_hdd2 levels=1:2 keys_zone=my_cache_hdd2:10m max_size=10g
    inactive=60m use_temp_path=off;
split_clients $request_uri $my_cache {
    50% "my_cache_hdd1";
    50% "my_cache_hdd2";
}
server {
location / {
    proxy_cache $my_cache;
    proxy_pass http://my_upstream;
}
}

上例中的两个proxy_cache_path定义了两个缓存(my_cache_hdd1和my_cache_hd22)分属两个不同的硬盘。split_clients配置部分指定了请求结果的一半在my_cache_hdd1中缓存,另一半在my_cache_hdd2中缓存。基于$request_uri(请求URI)变量的哈希值决定了每一个请求使用哪一个缓存,对于指定URI的请求结果通常会被缓存在同一个缓存中。

查看nginx缓存状态

如果直接从页面中查看,可以使用add_header指令:
add_header X-Cache-Status $upstream_cache_status;

上面的例子中,在对客户端的响应中添加了一个`X-Cache-Status`HTTP响应头,下面是$upstream_cache_status的可能值:
MISS--响应在缓存中找不到,所以需要在服务器中取得。这个响应之后可能会被缓存起来。
BYPASS--响应来自原始服务器而不是缓存,因为请求匹配了一个proxy_cache_bypass(见下面我可以在缓存中打个洞吗?)。这个响应之后可能会被缓存起来。
EXPIRED--缓存中的某一项过期了,来自原始服务器的响应包含最新的内容。
STALE--内容陈旧是因为原始服务器不能正确响应。需要配置proxy_cache_use_stale。
UPDATING--内容过期了,因为相对于之前的请求,响应的入口(entry)已经更新,并且proxy_cache_use_stale的updating已被设置。
REVALIDATED--proxy_cache_revalidate命令被启用,nginx检测得知当前的缓存内容依然有效(If-Modified-Since或者If-None-Match)。
HIT--响应包含来自缓存的最新有效的内容。

如果不想从页面中查看,可从日志中查看对应的页面:
log_format  main  '$remote_addr - $request_time - $upstream_response_time - $upstream_cache_status - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent $http_referer '
'$http_user_agent $http_x_forwarded_for $upstream_addr $upstream_status';

第4个字段即为缓存状态。

如何决定是否缓存

默认情况下,nginx需要考虑从原始服务器得到的Cache-Control标头。当在响应头部中Cache-Control被配置为Private,No-Cache,No-Store或者Set-Cookie,nginx不进行缓存。nginx仅仅缓存GET和HEAD客户端请求。你也可以参照下面的解答覆盖这些默认值。

Cache-Control头部可以使用proxy_ignore_headers命令来忽略,如下列配置:
location /images/ {
    proxy_cache my_cache;
    proxy_ignore_headers Cache-Control;
    proxy_cache_valid any 30m;
    ...
}

nginx会忽略所有/images/下的Cache-Control头。proxy_cache_valid命令强制规定缓存数据的过期时间,如果忽略Cache-Control头,则该命令是十分必要的。nginx不会缓存没有过期时间的文件,同理适用于Set-Cookie头部指令。使用proxy_cache_methods命令指定对应的对请求方法的缓存:
proxy_cache_methods GET HEAD POST;

这个例子中可以缓存POST请求,其他附加的方法可以依次列出来的,如PUT。

缓存的清理

采用nginx  proxy_cache_purge模块 ,该模块与proxy_cache成对出现,功能正好相反。
设计方法:在nginx中,另启一个server,当需要清理响应资源的缓存时,在本机访问这个server。
例如访问 127.0.0.1:8083/tmp-test/file2del.txt 即可清理该资源的缓存文件。
配置方法:
location /tmp-test/ {
    allow 127.0.0.1; //只允许本机访问
    deny all; //禁止其他所有ip
    proxy_cache_purge tmp-test $uri;  //清理缓存
}

proxy_cache_purge:缓存清理模块
tmp-test:指定的key_zone
$uri:指定的生成key的参数


缓存动态内容

提供的Cache-Control头部可以做到。缓存动态内容,甚至短时间内的内容可以减少在原始数据库和服务器中加载,可以提高第一个字节的到达时间,因为页面不需要对每个请求都生成一次。

缓存打洞(Punch a Hole)

使用proxy_cache_bypass命令:
location / {
    proxy_cache_bypass $cookie_nocache $arg_nocache;
}

这个命令定义了哪种类型的请求需要向服务器请求而不是尝试首先在缓存中查找。有些时候又被称作在内存中"打个洞"。在上面的例子中,nginx会针对nocache cookie或者参数进行直接请求服务器,如: http://www.freeoa.net/?nocache=true。nginx依然可以为将那些没有避开缓存的请求缓存响应结果。

使用哪些缓存键

nginx生成的键的默认格式是类似于下面的nginx变量的MD5哈希值: $scheme$proxy_host$request_uri,实际的算法有些复杂。
proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m
use_temp_path=off;
server {
location / {
    proxy_cache $my_cache;
    proxy_pass http://my_upstream;
}
}

按照上面的配置, http://www.example.org/my_image.jpg的缓存键被计算为md5("http://my_upstream:80/my_image.jpg")。

注意:$proxy_host变量用于哈希之后的值而不是实际的主机名(www.example.com)。$proxy_host被定义为proxy_pass中指定的代理服务器的主机名和端口号。

为了改变变量(或其他项)作为基础键,可以使用proxy_cache_key命令:使用Cookie作为缓存键的一部分
proxy_cache_key $proxy_host$request_uri$cookie_jessionid;

使用Etag头部

在nginx 1.7.3和nginx Plus R5及之后的版本,配合使用If-None-Match, Etag是完全支持的。

处理Pragma头部

当客户端添加了Pragma:no-cache头部,则请求会绕过缓存直接访问服务器请求内容。nginx默认不考虑Pragma头部,不过可以使用下面的proxy_cache_bypass的命令来配置该特性:
location /images/ {
    proxy_cache my_cache;
    proxy_cache_bypass $http_pragma;
}

处理Vary头部

在Nginx Plus R5、Nginx 1.7.7和之后的版本中是支持的。可以看看这篇文章: good overview of the Vary header


附:缓存和代理中常用的配置项

上文讲述了如何配置最基础的proxy cache,接下来会对常用的高级配置项进行梳理。

proxy_no_cache string;

Default: -
Context: http , server , location
config example:
proxy_no_cache  $cookie_nocache  $arg_nocache   $arg_comment;
proxy_no_cache  $http_pragma $http_authorization;

$cookie_nocache $arg_nocache...皆为变量,可以根据你访问的匹配策略来设置,其值只有2类,0和非0;
访问匹配策略例如:
if ($request_uri ~ ^/(login|register|password\/reset)/) { set $cookie_nocache 1; }
如果在此链式配置中,只要有一个值不为0,则不会cache;例如:proxy_no_cache $cookie_nocache(0) $arg_nocache(1) $arg_comment(0),不会被cache。`
注:一般会配合proxy_cache_bypass共同使用;


proxy_cache_bypass string;

Default: -
Context: http , server , location
config example:
proxy_cache_bypass  $cookie_nocache $arg_nocache$arg_comment;
proxy_cache_bypass  $http_pragma  $http_authorization;

定义在哪些情况下不从cache读取,直接从backend获取资源;配置方式同proxy_no_cache。


proxy_cache_key string;

Default: proxy_cache_key $scheme$proxy_host$request_uri;
Context: http, server, location

自定义cache key,例如:
proxy_cache_key "$host$request_uri $cookie_user";
默认值为:proxy_cache_key $scheme$proxy_host$uri$is_args$args;


proxy_cache_methods GET| HEAD|POST...;

Default: proxy_cache_methods GET HEAD;
Context: http, server, location

指定客户端那些方法被缓存,默认为GET|HEAD。


proxy_cache_purge string ...;

Default: -
Context: http, server, location
config example:
proxy_cache_path /data/nginx/cache keys_zone=cache_zone:10m;
map $request_method $purge_method {
PURGE   1;
default 0;
}
server {
...
location / {
    proxy_pass http://backend;
    proxy_cache cache_zone;
    proxy_cache_key $uri;
    proxy_cache_purge $purge_method;
}
}

定义缓存清除场景,同proxy_no_cache,proxy_cache_bypass链式配置方式,只要又一个不为0,则清除对应的cache key则会被清除,并返回204 response。注意,这里是删除内存中的cache key,而不是disk上的cache文件!disk的cache文件是由inactive控制;当purege request的cache key以通配符*结束时,所有匹配到通配符的cache入口的cachekey都会被删除。


proxy_cache_valid *[code...] time *;

Default: -
Context: http, server, location

设置不同相应码的缓存时间,当不指定响应码的时候,例如
proxy_cache_valid 5m;
只对响应码为200,301,302的访问请求资源设置缓存时间,此外可以个性化定制,例如:
proxy_cache_valid 200 302 10m; proxy_cache_valid 301 1h; proxy_cache_valid 404 1m; proxy_cache_valid any 1m;
此外,还可以在相应header里设置优先级更高的缓存有效时间:

"X-Accel-Expires",设置响应的缓存过期时间,以秒为单位;0为不缓存;
如果没有设置"X-Accel-Expires" header,则关于缓存的配置策略可能会在"Expires"或者"Cache-Control" header中;
如果header含有"Set-Cookie",则响应不会被缓存,类似的配置可以在"proxy_ignore_header"中可见;
header包含"Vary"并且设置为"*",则请求不会被缓存,如果"Vary"有具体的值,则对应的请求会被缓存;


proxy_ignore_headers field;

Default: -
Context: http, server, location

不缓存包含在field的响应header,可以设置的值有:"X-Accel-Redirect", "X-Accel-Expires", "X-Accel-Limit-Rate","X-Accel-Buffering", "X-Accel-Charset", "Expires", "Cache-Control", "Set-Cookie" (0.8.44), and "Vary"。如果上述的header field没有设置为忽略,则header filed中有"X-Accel-Expires", "Expires", "Cache-Control", "Set-Cookie", and "Vary"的话,响应会被缓存。


proxy_pass_headers field;

proxy_hide_headers field;

Default: -
Context: http, server, location


参考来源

NGINX缓存使用官方指南

nginx proxy_cache 缓存配置


Nginx Proxy Cache原理和最佳实践