问题引入

遇到一个很奇怪的问题,后端站点使用 etag 和 last-modified 为静态资源增加了客户端缓存,经过 NGINX 代理之后,etag 和 last-modified 标记到客户端就消失了,导致客户端无法使用浏览器缓存。

排查和结论

1、是否因为 gzip 导致 etag 丢失,在某些版本(1.3.3~1.7.3)的 NGINX 中,当开启 gzip 时,会导致 etag 头丢失。

官方给出的解释是压缩以后,文件大小可能被修改,会导致文件发生变化,故考虑将 etag 头丢掉。

源码分析:./release-1.3.3/src/http/modules/ngx_http_gzip_filter_module.c

1
2
3
4
5
6
7
8
ngx_str_set(&h->key, "Content-Encoding");
ngx_str_set(&h->value, "gzip");
r->headers_out.content_encoding = h;
r->main_filter_need_in_memory = 1;
ngx_http_clear_content_length(r);
ngx_http_clear_accept_ranges(r);
# remove etag
ngx_http_clear_etag(r);

自 NGINX 1.7.3 版本以后,当在 gzip 中遇到 etag 头,会将强 etag 自动转换为弱 etag(weak etag),如果遇到弱 etag,则不作处理原样返回。

源码分析:./release-1.7.3/src/http/modules/ngx_http_gzip_filter_module.c

1
2
3
4
5
6
7
8
ngx_str_set(&h->key, "Content-Encoding");
ngx_str_set(&h->value, "gzip");
r->headers_out.content_encoding = h;
r->main_filter_need_in_memory = 1;
ngx_http_clear_content_length(r);
ngx_http_clear_accept_ranges(r);
# 弱化 etag 标记
ngx_http_weak_etag(r);

2、是否因为引入了 sub_filter 模块导致的 etag 头丢失,自 1.5.1 版本起 NGINX 引入了一个配置项允许在替换期间保留原始响应中的 last-modified 头字段,以便于响应缓存。但默认情况下,在 sub_filter 模块处理过程中修改响应内容时,将删除 last-modified 头字段。

配置项目 sub_filter_last_modified on | off;
默认值 sub_filter_last_modified off;
可出现的位置 http,server,location

源码分析:./release-1.5.11/src/http/modules/ngx_http_sub_filter_module.c

1
2
3
4
5
6
7
if (r == r->main) {
ngx_http_clear_content_length(r);
ngx_http_clear_etag(r);
if (!slcf->last_modified) {
ngx_http_clear_last_modified(r);
}
}

自 NGINX 1.7.3 优化了 sub_filter 模块对 etag 和 last-modified 的处理逻辑。

源码分析:./release-1.7.0/src/http/modules/ngx_http_sub_filter_module.c

1
2
3
4
5
6
7
8
9
10
11
if (r == r->main) {
ngx_http_clear_content_length(r);
# sub_filter_last_modified off
if (!slcf->last_modified) {
ngx_http_clear_last_modified(r);
ngx_http_clear_etag(r);
# sub_filter_last_modified on
} else {
ngx_http_weak_etag(r);
}
}

clear 处理函数分析

以 nginx-release-1.7.0 为例,会将 hash 置 0,标记置空。

源码位置:./release-1.7.3/src/http/ngx_http_core_module.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define ngx_http_clear_last_modified(r)                                       \
\
r->headers_out.last_modified_time = -1; \
if (r->headers_out.last_modified) { \
r->headers_out.last_modified->hash = 0; \
r->headers_out.last_modified = NULL; \
}

#define ngx_http_clear_etag(r) \
\
if (r->headers_out.etag) { \
r->headers_out.etag->hash = 0; \
r->headers_out.etag = NULL; \
}

建议

1、后端使用客户端强缓存(例如 cache-control)方式能避免该问题。
2、调整 NGINX 版本,避免 NGINX 版本特性。