Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nginx 缓存实践与实现讨论 #44

Open
jinhailang opened this issue Jan 3, 2019 · 0 comments
Open

Nginx 缓存实践与实现讨论 #44

jinhailang opened this issue Jan 3, 2019 · 0 comments

Comments

@jinhailang
Copy link
Owner

jinhailang commented Jan 3, 2019

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

▒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"
}

proxy_cache_key

缓存 KEY 的计算是缓存应用中非常关键的部分,一般的自然想到的就是根据请求 URL 来计算,即: $scheme$proxy_host$uri$is_args$args

但是,这会导致几个问题:

  • 同样的请求,参数顺序可能不一样,这会导致同一份资源被多次缓存;
  • 在后端服务中,URI 表示资源路径,很多情况下,相同 URI 对应的资源是相同的,即:$scheme$proxy_host$uri 就够了;
  • 服务端会根据某些头部字段(比如: http_accept_encoding, cookie, user_agent 等等)的不同,返回不同的资源,因此计算的时候需要加入这些头部字段,否则会返回错误的资源,可能会导致比较严重的问题;

根据标准的 HTTP 协议,服务端(源站)应该在响应头设置 Vary 字段,来显示指定这些影响缓存的头部字段,缓存系统需要支持这种协商机制,对不同的客户端请求返回不同的资源。但是,Vary 这个字段使用的比较少,甚至很多程序员都不清楚这个字段,而且,很多缓存系统也是不支持这个字段。

因此,KEY 的计算需要非常谨慎,最好的办法是根据具体的 Host 选择合适的字段来计算,即
proxy_cache_key 可配置化
。一般为了简单快速实现,通用的方式主要有:

  • 在 rewrite 阶段对参数进行排序;
  • 使用缓存一般都会同时开启资源压缩,节省带宽和磁盘空间,因此,头部字段 http_accept_encoding 一定要加入 KEY,否则会导致将压缩的资源缓存,响应给不接受压缩资源的客户端,出现乱码的情况。虽然现在大部分浏览器都是支持解压的。

总之,通用实现的场景下,最重要的是,宁可多缓存几份或;者直接回源,也不能出现请求响应的资源不匹配的情况

proxy_pass

有些请求响应(大多数动态资源)是不能缓存的,那么 Nginx 怎么会知道哪些响应应该被缓存呢?根据 HTTP 协议, 响应头 Cache-ControlExpires 控制缓存失效时间,也即,Nginx 只会缓存这种显式指定了缓存失效时间的响应。

当然,在代理层是可以做很多事情的,比如,可以使用 add_header 来添加 Cache-Control 头,使得源站返回的响应能够被缓存,或者设置 proxy_no_cache 来强制不缓存某些响应。

proxy_cache_purge

缓存系统必不可少的一个功能就是需要支持外部直接刷新的接口,因为很多场景下,需要将被缓存的资源提前失效,这个时候一般是通过 API 接口直接刷新的,也即请求对应的 PURGE 方法。缓存刷新模块看似简单,但是,在大流量的场景下,可能还要支持批量刷新功能,比如大型网站的更新迭代,往往有大量的资源需要更新,CDN 厂商经常会遇到这类需求。要保证缓存能够被快速,准确和稳定的刷新,还是挺有挑战的。一般缓存失效,磁盘上对应的文件并不会立即直接被删除掉,因为读写磁盘代价较大,而且缓存的资源实在太多了,一般先会在内存标记,然后待缓存管理进程(线程)周期性的轮询删除,配置命令 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;
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant