首先谈谈遇到的问题:

一个 Nginx 架在一个后端服务的前面,Nginx proxy_pass 到它并开启 proxy_cache,假设这个后端服务总是会吐 Etag 响应头。

在应用中,我们发现当 Nginx 的 proxy_cache 成功将后端的页面 cache 住时,浏览器多次对该页面发起请求,会命中 Nginx 的 cache,但即使浏览器请求带了 If-None-Match 请求头,Nginx 却不会响应 304,而是响应200。

这样带来的问题是,即使 Nginx 的 cache 将请求阻挡在后端应用之外,但是:

  1. 命中后每次响应 200 导致 Nginx 所在的服务器和客户浏览器双方都有流量损耗
  2. 更重要的是增长了服务响应时间。因为,如果是304的话,nginx不需要向浏览器吐数据,只用告诉浏览器用本地的缓存就好了。

排查过程涉及个版本的测试和代码比对,这里就省略了,下面说下结论:

一. nginx-1.7.3 之前的版本:

  • Nginx 很早就支持 Etag,但是 nginx-1.7.3 之前不支持弱etag
  • nginx-1.7.3 之前版本在 proxy_cache 时,会有上面提到的问题。
  • 之所以会遇到上面的问题,是因为 Nginx 里面有很多 filter 模块,比如 ssl、xsl、gzip等等,这些 filter 模块本质上算是对响应做出了修改。所以,Nginx 为了严格遵循 Etag 的本意,这种情况下就认为 Etag 应该失效。而早期版本又不支持弱 Etag 验证,所以干脆就不承认 Etag,每次都返回 200,而不是304。

二.nginx-1.7.3及其之后的版本:

首先我们看看 nginx-1.7.3 的changelog:

Changes with nginx 1.7.3 08 Jul 2014
*) Feature: weak entity tags are now preserved on response
modifications, and strong ones are changed to weak.
*) Feature: cache revalidation now uses If-None-Match header if
possible.
*) Feature: the “ssl_password_file” directive.
*) Bugfix: the If-None-Match request header line was ignored if there
was no Last-Modified header in a response returned from cache.
*) Bugfix: “peer closed connection in SSL handshake” messages were
logged at “info” level instead of “error” while connecting to
backends.
*) Bugfix: in the ngx_http_dav_module module in nginx/Windows.
*) Bugfix: SPDY connections might be closed prematurely if caching was
used.

可以看到,主要有两点:

  • 增加了弱 Etag 检验功能:对于那些修改了响应的 filter 模块,nginx 启用弱 Etag 检验。
  • cache 住的文件,其验证会优先采用 If-None-Match 校验,即 Etag 校验。

那么,在nginx-1.7.3及其以后的版本,其Etag功能可以描述如下:

  1. Etag 功能得到增强,既有强 Etag,又有弱 Etag。其实,但是所谓的强弱,只是为了遵循标准而分出来的两种说法而已。
  2. 对于 Nginx 本地的静态文件,是强 Etag 验证。
  3. 对于 Nginx 本地的非静态内容,不做 Etag 验证。这里可以用 nginx-lua 模块 ngx.say 来简单验证。
  4. 对于 proxy_cache 缓存住的文件,无论该文件是后端的静态文件,或是后端动态产生的页面,只要后端吐出了 Etag 响应头,则 Nginx 对客户端过来的请求,都会启动 Etag 校验。即,第一次请求,Nginx 会将后端吐出的 Etag 头传给客户端,客户端后面再请求时,会带上 If-None-Match 请求头,如果校验通过,会直接返回 304。(这就解决了我们的问题)
  5. 注意,上面这一点又可以分为两种情况,第一种是如果 Nginx 在将 cache 住的内容吐给浏览器时,如果Nginx 不启用 filter 模块来修改响应,则 Etag 的强弱跟后端传过来的相同。第二种情况,如果 Nginx 在将 cache 住的内容吐给浏览器时,如果启用了 filter 模块,即响应头或体被修改,那么 Nginx 会将后端的强 Etag 转换为弱 Etag。例如:如果后端本来返回的 Etag 为 ETag: "12345",则Nginx会将其弱化,吐给浏览器,即改为 Etag: W/"12345",浏览器下次请求的 If-None-Match 请求头也变为 If-None-Match: W/"12345"