Open jinhailang opened 5 years ago
根据近来使用 Nginx 缓存的实践,做一个总结,以及阐述了一般的缓存系统实现的关键和问题。
Nginx 缓存是比较传统的单机本地缓存(需要说明的是,根据 KEY 计算缓存路径的方式是一样的,也就是说多个 Nginx 进程是可以共享相同缓存资源的),比较简单,容易理解。但是,通过 Nginx 缓存的使用与理解,能够窥探到业界通用的缓存系统的设计与实现方法。Nginx 会对上游返回的 Response 进行缓存,存放在特定目录下(磁盘),Nginx 会启动一个专门的 Worker 进程对缓存进行管理,周期性的删除过期缓存等。当一个请求进来,首先会判断是否需要使用缓存(proxy_cache_bypass),如果需要使用缓存,则直接将请求代理到上游;否则根据 proxy_cache_key 计算出 KEY(32 位 Hash 值),根据这个值从对应的目录(可设置多级目录)下获取相同名称的资源。需要注意的是,磁盘上缓存的是整个响应,包括响应头和响应体,而且,此时,Nginx 不会再对请求头和响应头进行判断,只是直接读进内存,返回响应到客户端,这点也很重要。实例:
proxy_cache_bypass
proxy_cache_key
Path: /tmp/c/5a/3a1796923cc2b162a5e7f89dc4cf95ac
/tmp/c/5a/3a1796923cc2b162a5e7f89dc4cf95ac
▒q-\▒▒▒▒▒▒▒▒yq-\▒▒go▒ KEY: httphttpbin.org/cache/60 HTTP/1.1 200 OK Connection: close Server: gunicorn/19.9.0 Date: Thu, 03 Jan 2019 02:20:41 GMT Content-Type: application/json Content-Length: 219 Cache-Control: public, max-age=60 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true Via: 1.1 vegur { "args": {}, "headers": { "Accept": "*/*", "Connection": "close", "Host": "httpbin.org", "User-Agent": "curl/7.52.1" }, "origin": "103.126.92.86", "url": "http://httpbin.org/cache/60" }
缓存 KEY 的计算是缓存应用中非常关键的部分,一般的自然想到的就是根据请求 URL 来计算,即: $scheme$proxy_host$uri$is_args$args。
$scheme$proxy_host$uri$is_args$args
但是,这会导致几个问题:
$scheme$proxy_host$uri
http_accept_encoding
cookie
user_agent
根据标准的 HTTP 协议,服务端(源站)应该在响应头设置 Vary 字段,来显示指定这些影响缓存的头部字段,缓存系统需要支持这种协商机制,对不同的客户端请求返回不同的资源。但是,Vary 这个字段使用的比较少,甚至很多程序员都不清楚这个字段,而且,很多缓存系统也是不支持这个字段。
Vary
因此,KEY 的计算需要非常谨慎,最好的办法是根据具体的 Host 选择合适的字段来计算,即 proxy_cache_key 可配置化。一般为了简单快速实现,通用的方式主要有:
总之,通用实现的场景下,最重要的是,宁可多缓存几份或;者直接回源,也不能出现请求响应的资源不匹配的情况;
proxy_pass
有些请求响应(大多数动态资源)是不能缓存的,那么 Nginx 怎么会知道哪些响应应该被缓存呢?根据 HTTP 协议, 响应头 Cache-Control 和 Expires 控制缓存失效时间,也即,Nginx 只会缓存这种显式指定了缓存失效时间的响应。
Cache-Control
Expires
当然,在代理层是可以做很多事情的,比如,可以使用 add_header 来添加 Cache-Control 头,使得源站返回的响应能够被缓存,或者设置 proxy_no_cache 来强制不缓存某些响应。
add_header
proxy_no_cache
proxy_cache_purge
缓存系统必不可少的一个功能就是需要支持外部直接刷新的接口,因为很多场景下,需要将被缓存的资源提前失效,这个时候一般是通过 API 接口直接刷新的,也即请求对应的 PURGE 方法。缓存刷新模块看似简单,但是,在大流量的场景下,可能还要支持批量刷新功能,比如大型网站的更新迭代,往往有大量的资源需要更新,CDN 厂商经常会遇到这类需求。要保证缓存能够被快速,准确和稳定的刷新,还是挺有挑战的。一般缓存失效,磁盘上对应的文件并不会立即直接被删除掉,因为读写磁盘代价较大,而且缓存的资源实在太多了,一般先会在内存标记,然后待缓存管理进程(线程)周期性的轮询删除,配置命令 proxy_cache_path 属性 inactive 就是指定删除周期的。
PURGE
proxy_cache_path
inactive
Nginx 中可以使用 proxy_cache_purge 命令来支持 PURGE 请求刷新对应缓存资源。
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; } }
但是,这个命令只有商业版才会有,大部分开发者用的应该都是开源版。
This functionality is available as part of our commercial subscription.
在整个架构中,缓存系统往往是比较容易引起问题的模块,除了自身的问题外,因为缓存的使用还有一个与上下游协商的过程,也可能出现外部服务不规范导致的问题,一般比较多的问题已就是缓存未及时刷新,导致客户端使用了旧资源,以及 404 等错误状态未启用缓存(或过滤)导致缓存穿透,缓存刷新,过期不合理,导致缓存雪崩等。一般的使用场景直接使用 Nginx 自带的缓存就够了,但是,对于比较复杂的场景,要自建缓存系统才行,在 CDN 中缓存系统尤为重要。
以下,是我在项目中的使用实例:
proxy_cache_path /tmp levels=1:2 keys_zone=mcache:5m max_size=5g inactive=60m use_temp_path=off; ... location / { proxy_cache_key $scheme$proxy_host$uri$is_args$args$http_accept_encoding; # compatible with clients that do not support gzip. proxy_no_cache $cookie_nocache $arg_nocache$arg_comment; proxy_no_cache $http_pragma $http_authorization; proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment; proxy_cache_bypass $http_pragma $http_authorization; proxy_cache_bypass $http_cache_control; proxy_cache mcahe; proxy_pass http://test.com; proxy_set_header Host "myhost.com"; add_header X-Cache $upstream_cache_status; }
Nginx 缓存实践与实现讨论
根据近来使用 Nginx 缓存的实践,做一个总结,以及阐述了一般的缓存系统实现的关键和问题。
Nginx 缓存是比较传统的单机本地缓存(需要说明的是,根据 KEY 计算缓存路径的方式是一样的,也就是说多个 Nginx 进程是可以共享相同缓存资源的),比较简单,容易理解。但是,通过 Nginx 缓存的使用与理解,能够窥探到业界通用的缓存系统的设计与实现方法。Nginx 会对上游返回的 Response 进行缓存,存放在特定目录下(磁盘),Nginx 会启动一个专门的 Worker 进程对缓存进行管理,周期性的删除过期缓存等。当一个请求进来,首先会判断是否需要使用缓存(
proxy_cache_bypass
),如果需要使用缓存,则直接将请求代理到上游;否则根据proxy_cache_key
计算出 KEY(32 位 Hash 值),根据这个值从对应的目录(可设置多级目录)下获取相同名称的资源。需要注意的是,磁盘上缓存的是整个响应,包括响应头和响应体,而且,此时,Nginx 不会再对请求头和响应头进行判断,只是直接读进内存,返回响应到客户端,这点也很重要。实例:Path:
/tmp/c/5a/3a1796923cc2b162a5e7f89dc4cf95ac
缓存 KEY 的计算是缓存应用中非常关键的部分,一般的自然想到的就是根据请求 URL 来计算,即:
$scheme$proxy_host$uri$is_args$args
。但是,这会导致几个问题:
$scheme$proxy_host$uri
就够了;http_accept_encoding
,cookie
,user_agent
等等)的不同,返回不同的资源,因此计算的时候需要加入这些头部字段,否则会返回错误的资源,可能会导致比较严重的问题;根据标准的 HTTP 协议,服务端(源站)应该在响应头设置
Vary
字段,来显示指定这些影响缓存的头部字段,缓存系统需要支持这种协商机制,对不同的客户端请求返回不同的资源。但是,Vary
这个字段使用的比较少,甚至很多程序员都不清楚这个字段,而且,很多缓存系统也是不支持这个字段。因此,KEY 的计算需要非常谨慎,最好的办法是根据具体的 Host 选择合适的字段来计算,即
proxy_cache_key
可配置化。一般为了简单快速实现,通用的方式主要有:http_accept_encoding
一定要加入 KEY,否则会导致将压缩的资源缓存,响应给不接受压缩资源的客户端,出现乱码的情况。虽然现在大部分浏览器都是支持解压的。总之,通用实现的场景下,最重要的是,宁可多缓存几份或;者直接回源,也不能出现请求响应的资源不匹配的情况;
有些请求响应(大多数动态资源)是不能缓存的,那么 Nginx 怎么会知道哪些响应应该被缓存呢?根据 HTTP 协议, 响应头
Cache-Control
和Expires
控制缓存失效时间,也即,Nginx 只会缓存这种显式指定了缓存失效时间的响应。当然,在代理层是可以做很多事情的,比如,可以使用
add_header
来添加Cache-Control
头,使得源站返回的响应能够被缓存,或者设置proxy_no_cache
来强制不缓存某些响应。缓存系统必不可少的一个功能就是需要支持外部直接刷新的接口,因为很多场景下,需要将被缓存的资源提前失效,这个时候一般是通过 API 接口直接刷新的,也即请求对应的
PURGE
方法。缓存刷新模块看似简单,但是,在大流量的场景下,可能还要支持批量刷新功能,比如大型网站的更新迭代,往往有大量的资源需要更新,CDN 厂商经常会遇到这类需求。要保证缓存能够被快速,准确和稳定的刷新,还是挺有挑战的。一般缓存失效,磁盘上对应的文件并不会立即直接被删除掉,因为读写磁盘代价较大,而且缓存的资源实在太多了,一般先会在内存标记,然后待缓存管理进程(线程)周期性的轮询删除,配置命令proxy_cache_path
属性inactive
就是指定删除周期的。Nginx 中可以使用
proxy_cache_purge
命令来支持PURGE
请求刷新对应缓存资源。但是,这个命令只有商业版才会有,大部分开发者用的应该都是开源版。
小结
在整个架构中,缓存系统往往是比较容易引起问题的模块,除了自身的问题外,因为缓存的使用还有一个与上下游协商的过程,也可能出现外部服务不规范导致的问题,一般比较多的问题已就是缓存未及时刷新,导致客户端使用了旧资源,以及 404 等错误状态未启用缓存(或过滤)导致缓存穿透,缓存刷新,过期不合理,导致缓存雪崩等。一般的使用场景直接使用 Nginx 自带的缓存就够了,但是,对于比较复杂的场景,要自建缓存系统才行,在 CDN 中缓存系统尤为重要。
以下,是我在项目中的使用实例: