上个月给公司一个内部工具做了一次小升级,结果上线当天下午,运维群里就炸了。有人在吼“页面加载要五六秒”,有人在问“是不是数据库扛不住了”。我第一反应是去查慢查询日志,结果数据库那边一切正常,CPU和内存都稳稳的。那问题出在哪?我盯着Chrome开发者工具的Network面板看了半天,发现一个静态资源文件居然花了3秒才下载完。那个文件才200KB,按理说不应该这么慢。
我登录到服务器上,先看了下Nginx的access log,发现这些静态资源的请求都没有被正确缓存。更奇怪的是,同一个用户在短时间内反复请求同一个JS文件,每次都是200而不是304。这就意味着Nginx压根没告诉浏览器“你可以缓存这个东西”。
我查了一下当前Nginx的版本,是1.18.0。配置里关于静态资源的部分长这样:
“`
location /static/ {
root /var/www/app;
expires 30d;
}
“`
看起来没什么大问题对吧?expires设了30天,按说浏览器收到响应头里应该有Cache-Control: max-age=2592000。但我用curl看了一下实际返回的响应头:
“`
$ curl -I https://example.com/static/js/app.abc123.js
HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Length: 204800
Cache-Control: max-age=2592000
Expires: Thu, 01 Jan 2026 00:00:00 GMT
“`
等等,Cache-Control是有的,expires也正常。那为什么浏览器每次都要重新请求?我又看了一眼请求头,发现浏览器每次都在发Cache-Control: no-cache。这不对劲,除非前端代码里强制设置了不缓存。我翻了前端打包配置,确实有个地方写了fetch的时候带上了{ cache: ‘no-cache’ }。但这不是主要原因,因为就算浏览器主动发no-cache,如果服务端返回304,也只会产生一次请求验证,不会真的重新下载文件。
问题在于我后端API返回的响应头里没有ETag。Nginx默认对静态文件是会自动生成ETag的,用的是文件的inode和修改时间。但我这个项目里,前端资源是通过一个Python脚本打包后直接丢到static目录里的,每次构建都会重新生成文件,文件名带了hash。按理说文件内容变了,ETag也会变,浏览器应该能正确处理。但实际情况是,某些情况下Nginx返回的ETag是弱校验器,格式是W/”xxxxx”,而有些浏览器对这个弱ETag的处理行为不一致。
我决定直接关掉ETag,改用Last-Modified加强缓存策略。修改后的配置是这样的:
“`
location /static/ {
root /var/www/app;
expires 30d;
add_header Cache-Control “public, immutable, max-age=2592000”;
etag off;
if_modified_since off;
}
“`
等等,if_modified_since off的意思是关掉Nginx自己对If-Modified-Since的处理,让后端去处理?不对,这里静态资源是Nginx直接serve的,没有后端。正确的做法是保留Last-Modified,但让expires足够长,这样浏览器在过期之前根本不会发请求去验证。我改成了:
“`
location /static/ {
root /var/www/app;
expires 30d;
add_header Cache-Control “public, immutable, max-age=2592000”;
etag off;
}
“`
expires 30d会自动生成Last-Modified头,而immutable这个指令告诉浏览器,在过期之前连条件请求都不要发。这个指令是2017年进入RFC的,Chrome和Firefox都支持,但Safari支持得晚一些。不过没关系,就算不支持immutable,max-age=2592000也足够让浏览器缓存30天。
我重新reload了Nginx配置:
“`
nginx -s reload
“`
然后再次用curl验证:
“`
$ curl -I https://example.com/static/js/app.abc123.js
HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Length: 204800
Cache-Control: public, immutable, max-age=2592000
Expires: Thu, 01 Jan 2026 00:00:00 GMT
Last-Modified: Tue, 15 Nov 2025 10:30:00 GMT
“`
没有ETag了,Last-Modified正常。我清空了浏览器缓存,重新打开页面,所有静态资源在第一次加载后,后续刷新都直接from disk cache。加载时间从5秒降到了1.2秒。
你以为这就完了?并没有。第二天运维又找过来了,说Nginx的错误日志里出现大量这样的报错:
“`
2025/11/16 14:32:18 [crit] 12345#12345: *6789 open() “/var/www/app/static/js/app.abc123.js” failed (24: Too many open files)
“`
这个报错我之前见过,是Nginx的worker_connections设置得太高,但系统的文件描述符上限没跟上。我查了一下当前的配置:
“`
worker_connections 1024;
“`
这个值对于内部工具来说其实够用了,但我们这次优化之后,大量请求被缓存命中,Nginx处理请求的速度变快了,单位时间内打开的连接数反而变多了。加上系统默认的ulimit -n是1024,刚好撞上线。
解决方案是修改系统的文件描述符限制:
“`
$ ulimit -n 65535
“`
然后修改Nginx配置:
“`
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
}
“`
再reload一次。这次错误日志安静了。
回头想想这次优化的坑,其实有两个教训。第一个是不要盲目相信expires配置,它确实能生成缓存头,但如果你没有显式指定Cache-Control的public和immutable,有些中间代理或者浏览器可能会自作主张。第二个是Nginx的worker_connections不是越大越好,它必须和系统的文件描述符上限匹配,不然高并发下直接crash给你看。
现在这个内部工具的页面加载速度稳定在1秒以内,运维群也没再响过。