nginx proxy cache 介绍
2019-04-20 21:03:54 阿炯

Nginx非常适合做网页缓存,这点与Varnish有些类似,有许多管理员转向Varnish,因其确实有用。但Nginx也有如下优点:
Nginx能非常有效地直接处理静态内容。在静态文件和Nginx在同一主机的情况下,这种特性尤为有用。
当放置在应用服务器前端时,Nginx确实能够担当缓存服务器的角色。

虽然Varnish作为网页缓存服务器拥有比Nginx更丰富的缓存相关的特性,但是Nginx仍然是一个不错的选择。如果您的流量需要为缓存添加一层基础设施,但不需要引入学习和维护的新技术的开销,Nginx可能更适合。除了其他HTTP服务器/监听器之外,Nginx还可以缓存代理到FastCGI和uWSGI进程的请求结果,甚至缓存负载平衡请求("upstream")的结果。一个很好的用例是缓存内容管理系统(CMS)的结果,大多数用户不需要网站的动态方面 - 他们只是想看到内容。缓存服务器的主要优点是我们在我们的应用服务器上放置的负载较少。对缓存的静态或动态资产的请求无需甚至到达应用程序(或静态内容)服务器 - 我们的缓存服务器本身可以处理许多请求!不过在继续阅读下文之前,建议先看一下本站的http协议之缓存

nginx proxy cache 原理



proxy_cache_path 缓存文件路径

proxy_cache_path path_of_nginx/proxy_cache levels=1:2 keys_zone=content:20m inactive=1d max_size=100m;

levels 设置缓存文件目录层次;levels=1:2 表示两级目录
keys_zone 设置缓存名字和共享内存大小
inactive 在指定时间内没人访问则被删除,默认是1天
max_size 最大缓存空间,如果缓存空间满,默认覆盖掉缓存时间最长的资源
proxy_temp_path : 使用temp_path存储,如果不使用,则配置在max_size后 use_temp_path=off;

#levels设置目录层次,第一层目录只有一个字符,是由levels=1:2设置,总共二层目录,子目录名字由二个字符组成。如果启用了反向代理的gzip压缩,则本地磁盘中存储的文件是解压后的文件。


server节点添加
location  ^~ / {
    proxy_pass http://192.168.56.1:8080/;
    proxy_cache content; #根keys_zone后的内容对应
    proxy_cache_valid  200 304 301 302 10d;   #哪些状态缓存多长时间
    #proxy_cache_valid  any 1d;    #其他的缓存多长时间,或者不缓存
    #proxy_cache_methods GET;  # 默认是get和head
    proxy_cache_key $host:$server_port$uri$is_args$args;   #通过key来hash,定义KEY的值
    #[host]192.168.56.2 [server_port]8080 [uri]/cache_ehcache-2.10.0_web/expire [is_args] [args]-
    proxy_cache_min_uses 3; #只要统一个url,不管间隔多久,总次数访问到达3次,就开始缓存。
    proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment; # 如果任何一个参数值不为空,或者不等于0,nginx就不会查找缓存,直接进行代理转发
}

配置proxy_cache的一些关键参数:
proxy_temp_path   path_of_nginx/proxy_cache/proxy_temp_dir;

proxy_cache_path  /dev/shm/proxy_cache_dir levels=1:2 keys_zone=cache_one:200m inactive=2d max_size=2g;

proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_404;

proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;


可以进入到具体缓存目录里,可查看文件,正是我们页面看到的内容。另外缓存的时间单位,有s,m,d。

proxy_cache_min_uses这个参数,是指同一个url,不管时间间隔多长,是否在一个缓存周期外,只要总次数到达proxy_cache_min_uses次数,就会触发缓存功能。后续缓存失效以后,只要访问一次,又会缓存。

proxy_cache_bypass是绕开缓存,其中可以通过cookie,和url的get参数进行控制。取得cookie参数的前缀是$cookie_,取得url参数的前缀是$arg_,其他的杜泽可以自行领略。

注意:当在运行时,删除proxy_cache_path缓存目录时,则永远无法缓存。

所有的缓存,都遵循源策略,就像CDN一样。Cache-Control以下4个属性,出现任意一个,则不缓存。
no-cache, no-store, max-age=0, s-maxage=0

其中max-age和s-maxage大于0的时候,则根据源站策略进行缓存,proxy_cache_valid的缓存设置时间失效。
当 max-age=3, s-maxage=5,同时配置的时候,nginx缓存时间以s-maxage时间为主。

遵循源和proxy_cache_path里面的时间有冲突的时候,策略如下:
当proxy_cache_path时间大于遵循源的时候,遵循源时间到期,请求时候,重新设置并覆盖磁盘上的文件;
当proxy_cache_path时间小于遵循源的时候,则proxy_cache_path到期的时候,重新到源站获取数据,遵循源策略失效。


Origin Server是什么

源服务器是拥有真正的静态文件或动态生成的HTML的服务器,它们有两个责任:
请求时提供动态和静态内容
通过HTTP缓存头决定如何缓存文件(和潜在的动态内容)

源服务器最终负责提供文件和控制如何缓存文件。客户端可以请求不被缓存的资源。缓存服务器"必须"遵守HTTP规范。此外,客户端请求可缓存资源时,必须遵循从源服务器返回的缓存参数,这可能包括不缓存结果的指令。

location ~* \.(?:manifest|appcache|html?|xml|json)$ {
  expires -1;
}

# Feed
location ~* \.(?:rss|atom)$ {
  expires 1h;
  add_header Cache-Control "public";
}

# Media: images, icons, video, audio, HTC
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
  expires 1M;
  access_log off;
  add_header Cache-Control "public";
}

# CSS and Javascript
location ~* \.(?:css|js)$ {
  expires 1y;
  access_log off;
  add_header Cache-Control "public";
}

上述配置禁用manifest,appcache,html,xml和json文件的缓存。它将RSS和ATOM订阅文件缓存1小时,Javascript和CSS文件1年,以及其他静态文件(图像和媒体)1个月。缓存全部设置为"public",所以任何系统都可以缓存它们。 将它们设置为私有将限制它们被私有缓存(例如我们的浏览器)缓存。所以源服务器本身没有进行任何缓存,只是说文件应该根据文件扩展名进行缓存。

以.html结尾的文件不会被缓存,我们可以看到响应如下:

# GET curl request and select response headers
$ curl -X GET -I 127.17.0.18:9000/index.html
HTTP/1.1 200 OK
Server: nginx/1.4.6 (Ubuntu)
Date: Fri, 05 Sep 2014 23:24:52 GMT
Content-Type: text/html
Last-Modified: Fri, 05 Sep 2014 22:16:24 GMT
Expires: Fri, 05 Sep 2014 23:24:52 GMT
Cache-Control: no-cache

请注意,Expires与Date相同,表示该请求立即到期,即告诉客户端不要缓存。 响应还返回头信息Cache-Control:no-cache来表明不缓存响应内容。 这完全遵循上述配置设置的.html文件的规则。接下来,我们可以尝试获取一个需要缓存的文件:

# GET curl request and select response headers
$ curl -X GET -I 127.17.0.18:9000/css/style.css
HTTP/1.1 200 OK
Server: nginx/1.4.6 (Ubuntu)
Date: Fri, 05 Sep 2014 23:25:04 GMT
Content-Type: text/css
Last-Modified: Fri, 05 Sep 2014 22:46:39 GMT
Expires: Sat, 05 Sep 2015 23:25:04 GMT
Cache-Control: max-age=31536000
Cache-Control: public

可以看到这个css文件在当前日期的1年后过期! 缓存规则的设置max-age(过期时间)大约为1年(以秒为单位),并允许公共缓存。这也遵循了上述设置的.css文件的规则。


接下来说下缓存服务器。

缓存服务器(通常)是"前端",它从客户端收到初始HTTP请求。然后它会处理请求本身(如果它具有所请求资源的新缓存副本),或者将请求传递给Origin Server来实现。如果请求发送到Origin Server,则由Cache Server读取源服务器的响应头,以确定响应是缓存还是简单传递。一些较大的Web应用程序除了缓存服务器之外还使用负载平衡器,从而导致高度分层的基础架构。

缓存服务器的职责:
确定HTTP请求是否接受缓存响应,并且缓存中有一个新项目可以响应
如果请求不应被缓存,或者缓存的项目是否过期,则向源服务器发送HTTP请求
响应来自其缓存或源服务器的HTTP响应为适当的。

最后说下客户端。客户端可以拥有自己的本地(私有)缓存 - 例如每个浏览器都有一个本地缓存。我们的浏览器可能会缓存一个响应本身(通常是图像,CSS和JS文件),因此,如果静态文件在其本地缓存中已经有新版本,那么浏览器根本不会向缓存服务器发送请求。

实现本地缓存的客户端具有以下职责:
发送请求
缓存响应
决定从本地缓存中提取请求或发出HTTP请求以检索它们

可以添加必要的指令,实现从源服务器中获取Nginx缓存响应,在上面定义的配置中,增加额外的缓存指令:
location / {
    proxy_cache my_zone;
    add_header X-Proxy-Cache $upstream_cache_status;
    include proxy_params;
    proxy_pass http://172.17.0.18:9000;
}

以下解释一下重要的缓存指令

proxy_cache_path

这是保存缓存文件的路径。levels指令设置缓存文件如何保存到文件系统。如果没有定义,缓存文件直接保存在指定的路径中。如果这样定义(1:2),缓存文件将根据其md5哈希值保存在缓存路径的子目录中。

keys_zone是缓存区域的名称。这里它被命名为my_zone,并为缓存key和其他元数据提供了10MB的存储空间,尽管这并不限制可以缓存的文件数量!它只是设置元数据的存储空间。文档声称1MB区可以存储约8000个key和元数据。

最后,我们设置了inactive指令,它告诉Nginx在60分钟内清除任何没有访问的缓存。请注意,这里60m是60分钟,而key_zone的10m是10兆字节。如果未显式设置,则inactive指令默认为10分钟。

使用inactive使Nginx有机会"忘记"关于不常被请求的缓存资源。这样一来,Nginx缓存可以让您最大程度的降低成本 - 最需要的资源会保留在缓存中(并遵循愿服务器所指示的缓存规则)。

proxy_cache_key

这是用来区分缓存文件的key。 默认值为$scheme$proxy_host$uri$is_args$args,但是我们可以根据需要进行更改。

proxy_cache_key也可以设置为类似"$host$request_uri $cookie_user"(带引号)这样的形式,也可以包括cookies信息。

Cookie确实会影响缓存,所以请谨慎设置!如果Cookie被并入缓存密钥,可能会意外地Nginx为每个独立cookie(每个站点访问者)都创建了重复缓存的文件。

这意味着将Cookie并入key确实会降低缓存的有效性。 针对每个用户的缓存,是私有缓存(Web浏览器)的目的,而不是正在构建的"公共"缓存服务器的目的。 但在某种情景下确实需要引入Cookie,那么proxy_cache_key的这个选项就很有用了。

proxy_cache

在location块内,nginx以proxy_cache my_zone指令定义缓存区域。

在location中使用proxy cache:

location ^~ /static/  {
... ...
#定制proxy_cache的key,去除imei和sn等个性化参数。
set $custom_proxy_cache_key $host$uri$is_args$args;
include vhosts/customize_proxy_cache_key;

#忽略Expires、Set-Cookie头部
proxy_ignore_headers Expires Set-Cookie;
proxy_cache cache_one;
proxy_cache_valid 200 304 10m;
proxy_cache_key $custom_proxy_cache_key;
add_header X-Proxy-Cache $upstream_cache_status;
expires 10m;
... ...
}

这里有几个要注意的地方:

定制cache的key时,一定要注意:谨慎使用值变化范围比较大的参数。因为这里使用参数的值作为cache的key的,当值变化返回很大的时候,一方面会导致缓存文件变得很大,另一方面缓存也就失去意义。这里还有一个知识点:源服务器是通过Set-Cookie来告诉浏览器cookie的信息,包括cookie值,path,以及域。只要浏览器查看请求满足本地cookie的域,就把这个cookie携带入头部传给server。这里忽略掉这个头部才能使nginx proxy_cache 生效。

上边customize_proxy_cache_key具体配置如下:
set $custom_params $query_string;
#nginx缓存key去除imei
if ( $custom_params ~ ^(.*)(&imei=[^&]+)(.*)$) {
set $a $1;
set $c $3;
set $custom_params "${a}${c}";
}
set $custom_proxy_cache_key $host$uri$is_args$custom_params;

这里就将参数中的imei和sn用户唯一值的参数去除,保证cache健康。配置完成后,重置nginx则会让nginx cache启用。


还添加了一个有用的header(头信息)来通知我们资源是否从缓存提供。这可以通过add_header X-Proxy-Cache $upstream_cache_status指令完成。这将设置一个名为X-Proxy-Cache的响应头,值为HIT,MISS 或 BYPASS。

保存配置文件后,重新加载Nginx的配置(sudo service nginx reload),并再次尝试HTTP请求。

首次获取CSS文件

X-Proxy-Cache: MISS

因为之前没有请求过该文件,所以这里返回的cache状态为MISS。缓存服务器需要向源服务器请求资源,再试一次:
Last-Modified: Fri, 05 Sep 2014 22:46:39 GMT
Expires: Sat, 05 Sep 2015 23:50:12 GMT
X-Proxy-Cache: HIT

可以看到通过X-Proxy-Cache头信息看到缓存状态是HIT。

Expires头信息保持不变,因为Nginx只是从缓存中返回资源。当缓存服务器返回到源服务器获取新文件时,那些头信息将会更新。Nginx将忽略客户端的Cache-Control请求头。但有些Web客户端不想使用缓存项目,我们希望缓存服务器能够支持这种要求。

例如,使用浏览器打开网页时,按住SHIFT,同时单击重新加载按钮,这时浏览器将发送一个Cache-Control: no-cache。 这要求缓存服务器不提供资源的缓存版本,但目前的设置会将之忽略。为了在请求时适当地绕过缓存,可以在location块中将proxy_cache_bypass $http_cache_control指令添加到缓存服务器中:

location / {
    proxy_cache my_zone;
    proxy_cache_bypass  $http_cache_control;
    add_header X-Proxy-Cache $upstream_cache_status;
    include proxy_params;
    proxy_pass http://172.17.0.18:9000;
}

nginx reload 之后,可以看到设置生效了:
$ curl -X GET -I 172.17.0.13/css/style.css
...
X-Proxy-Cache: HIT # A regular request which is normally a cache HIT ...

$ curl -X GET -I -H "Cache-Control: no-cache" 172.17.0.13/css/style.css
...
X-Proxy-Cache: BYPASS  # ... is now bypassed when told to

proxy_cache_bypass指令告知Nginx遵守HTTP请求中的Cache-Control请求头。

如果使用Nginx来缓存FastCGI进程的结果,可以将FastCGI进程视为源服务器,将Nginx作为缓存服务器,缓存了从PHP-FPM返回的HTML结果。

这是一个使用fastcgi_cache的示例:
fastcgi_cache_path /tmp/cache levels=1:2 keys_zone=freeoa:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

server {
    set $no_cache 0;
    # Example: Don't cache admin area
    # Note: Conditionals are typically frowned upon :/
    if ($request_uri ~* "/(admin/)"){
        set $no_cache 1;
    }

    location ~ ^/(index)\.php(/|$) {
    fastcgi_cache fideloper;
    fastcgi_cache_valid 200 60m; # Only cache 200 responses, cache for 60 minutes
    fastcgi_cache_methods GET HEAD; # Only GET and HEAD methods apply
    add_header X-Fastcgi-Cache $upstream_cache_status;
    fastcgi_cache_bypass $no_cache;  # Don't pull from cache based on $no_cache
    fastcgi_no_cache $no_cache; # Don't save to cache based on $no_cache

    # Regular PHP-FPM stuff:
    include fastcgi.conf; # fastcgi_params for nginx < 1.6.1
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
    }
}

这里的缓存设置选项很多,对于使用FastCGI缓存,请注意以下两点:
用fastcgi_cache替换proxy_cache的所有实例
用fastcgi_cache_valid 200 60m来设置PHP请求响应的到期时间。


Nginx让缓存延期

当 Nginx 使用 proxy cache 的文件作为响应时,它会更新其中的一些内容,比如 Date 响应头;但大部分响应头都不会得到更新,比如 Expires 和 Cache-Control。众所周知,Cache-Control 可以通过 max-age=xxx 或者 s-maxage=xxx 指令设置缓存的有效时间。跟 Expires 响应头不同,这一时间是相对的。假设上游服务器返回 Cache-Control: public; max-age=3600,那么 Nginx 会缓存该响应一小时。如果在这一小时到期之前,有Client 访问了这个页面,它会获取到同样的 Cache-Control 响应头,因此会再缓存多一小时。所以总体上该响应会被缓存两小时。

这听起来很让人惊讶。但仔细想想,其实也不算什么严重的问题。首先设置 max-age=3600 时,大多数情况下并不要求其严格地在一小时后过期。其次算是一般的多层缓存固有的弊端:缓存数据的最大过期时间,取决于各级缓存 TTL 的总和。如果想要避免,可以选择根据外层数据剩下的 TTL 设置当前 TTL;或者提供主动 purge 的操作,从最内层开始逐层清理数据。

当然在某些时候下这一行为会带来一些问题。假设开启了 proxy_cache_use_stale,在上游服务器出问题时使用过期的内容代替正常的响应。这种情况下,缓存只是作为一个临时救急的方案使用,并不希望 Client 多缓存更多的时间。否则会有上游应用的开发者抱怨,为何上游服务器已经正常了,用户刷新页面看到的还是旧数据。作为解决办法可以在 Nginx 的 header filter 阶段,通过 Lua 代码或者 Nginx C module,把 Cache-Control: max-age=... 修改成 Cache-Control: no-cache。这么一来,Client 会在使用缓存之前先验证下,如果 Nginx 返回 304 状态码,那么该缓存会被继续使用;如果上游已经 OK 了且更新了响应,那么 Client 就会重新请求,避免使用过期的内容。

这里需要强调下,no-cache 并非如字面上的意义表示不缓存,而是要求 Client 在使用该缓存之前,需要先验证下被缓存的内容是否还是最新的。MDN 的说法是:
Forces caches to submit the request to the origin server for validation before releasing a cached copy.

对应的,RFC 7234 的说法:
The "no-cache" request directive indicates that a cache MUST NOT use a stored response to satisfy the request without successful validation on the origin server.

如果要想让 Client 不缓存响应的内容,按 MDN 上的说法,需要用 Cache-Control: no-cache, no-store, must-revalidate

仔细看了下 no-cache/no-store/must-revalidate 这三项指令的介绍,似乎 no-store 就能让 Client 不用这个缓存,因为 no-store 要求:
The cache should not store anything about the client request or server response.

另外 must-revalidate 要求在使用过期缓存前验证下该内容是否是最新的,而 no-cache 也是要求重新验证的,那为什么需要两个都一起用呢?

通过 Google 找到了这个 Stackover 上的这个问答,其中解释了为何不单单用 no-store:因为臭名昭著的 IE6 浏览器在处理 no-store 时有 bug。但可惜的是,这个回答没有给出这一论断的证据,比如 IE 的 bug report 之类。MDN 在给出 Cache-Control: no-cache, no-store, must-revalidate 这个例子的时候,也没有提及更多的上下文。


Nginx 中缓存静态文件速览

配置 nginx、设置 HTTP 头部过期时间,用 Cache-Control 中的 max-age 标记为静态文件(比如图片、CSS 和 Javascript 文件)设置一个时间,这样用户的浏览器就会缓存这些文件。这样能节省带宽,并且在访问网站时会显得更快些(如果用户第二次访问你的网站,将会使用浏览器缓存中的静态文件)。可以参考 expires 指令手册来设置 HTTP 头部过期时间,这个标记可以放在 "http {}"、"server {}"、"location {}" 等语句块或者 "location {}" 语句块中的条件语句中。一般会在 "location" 语句块中用 "expires" 指令控制你的相关的静态文件,就像下面一样:
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 365d;
}

在上面的例子中,所有后缀名是 .jpg、.jpeg、.png、.gif、.ico、.css 和 .js 的文件会在浏览器访问该文件之后的 365 天后过期。因此要确保 location {} 语句块仅仅包含能被浏览器缓存的静态文件。然后重启 nginx 进程,可以在 "expires" 指令中使用以下的时间设置:
●"off" 让 "Expires" 和 "Cache-Control" 头部不能被更改。
●"epoch" 将"Expires" 头部设置成 1970 年 1 月 1 日 00:00:01。
●"max" 设置 "Expires" 头部为 2037 年 12 月 31 日 23:59:59,设置" Cache-Control "的最大存活时间为 10 年
●没有" @ "前缀的时间意味着这是一个与浏览器访问时间有关的过期时间。可以指定一个负值的时间,就会把" Cache-Control "头部设置成" no-cache"。例如:"expires 10d" 或者" expires 14w3d"。
●有" @ "前缀的时间指定在一天中的某个时间过期,格式是 Hh 或者Hh:Mm,H 的范围是 0 到 24,M 的范围是 0 到 59,例如:"expires @15:34"。

可以用以下的时间单位:
●"ms": 毫秒
●"s": 秒
●"m": 分钟
●"h": 小时
●"d": 天
●"w": 星期
●"M": 月 (30 天)
●"y": 年 (365 天)

例如:"1h30m" 表示一小时三十分钟,"1y6M" 表示一年六个月。

注意如果要用一个在将来很久才会过期的头部,当组件修改时就要改变组件的文件名。因此给文件指定版本是一个不错的方法。例如,如果有个 javascript.js 文件 并且要修改它,可以在修改的文件名字后面添加一个版本号。这样浏览器就要下载这个文件时如果没有更改文件名,浏览器将从缓存里面加载(旧的)文件。除了把基于浏览器访问时间设置" Expires" 头部(比如 "expires 10d")之外,也可以通过在时间前面的" modified "关键字,将 "Expires" 头部的基准设为文件修改的时间(请注意这仅仅对存储在硬盘的实际文件有效)。
expires modified 10d;

要测试配置是否有效,可以用火狐浏览器的开发者工具中的网络分析功能,然后用火狐访问一个静态文件(比如一张图片)。在输出的头部信息里,应该能看到 Expires 头部和有 max-age 标记的 Cache-Control 头部(max-age 标记包含了一个以秒为单位的值,比如 31536000 就是指今后的一年)


nginx 的 Http 头部模块(HttpHeadersModule)。