×

Nginx缓存陷阱

2021-04-10 04:22:47 Falcon

接手的一个老系统,nginx做proxy,apache做upstream。某个请求总是返回MySQL连接错误,但是手动连接却没有问题。进一步排查发现nginx配置了cache,也就是某次请求upstream返回了错误,被nginx缓存住了,虽然后来upstream恢复了正常,但cache仍然继续返回错误。为了解决这个问题,我需要搞清两个问题:

  1. 如何控制什么样的响应被缓存
  2. 如何控制缓存时间

1 测试配置文件

相应的配置做了注释,并提示了注意事项。

nginx.conf

# 配置缓存目录 /data/nginx/cache,必须确保目录存在,并且nobody用户可以写入(这里假设以nginx以nobody用户运行)
# sudo -u nobody touch /data/nginx/cache/test 测试目录可用

# keys_zone 配置的是缓存元数据的内存大小,max_size 是缓存所用磁盘大小
# inactive 的含义是2h内没有访问的cache,被自动清除
# use_temp_path=off upstream过来的响应直接写入缓存目录,避免二次拷贝

proxy_cache_path /data/nginx/cache levels=2:2 keys_zone=content_cache:512m max_size=10g inactive=2h use_temp_path=off;

conf.d/test.cache.conf

# cache
server {
  listen       80;

  # 必须为on,否则cache不生效
  proxy_buffering on;

  # 方便查看缓存是否生效,也可以把这个加入access log,方便统计
  add_header X-Cache-Status $upstream_cache_status;
  # cache 使用的 key
  proxy_cache_key $proxy_host$request_uri;

  location /t1 {
    # 返回200的请求被缓存120小时
    proxy_cache_valid 200 120h;
    proxy_cache content_cache;

    rewrite /t1/(.+)$ /$1 break;
    proxy_pass http://127.0.0.1:9090;
  }

  location /t2 {
    # 返回200的请求被缓存,缓存时间由upstream控制
    proxy_cache_valid 200 0h;
    proxy_cache content_cache;

    rewrite /t2/(.+)$ /$1 break;
    proxy_pass http://127.0.0.1:9090;
  }
}

# upstream
server {
  listen 9090;
  default_type "text/plain";

  location /static {
    return 200 "CACHED-STATIC@$request_uri";
  }

  location /max-age {
    add_header Cache-Control "max-age=86400";
    return 200 "CACHED-MAX-AGE@$request_uri";
  }
}

2 测试

2.1 proxy控制缓存时间

t1 配置了 proxy_cache_valid 200 120h 只要 127.0.0.1/t1/ 返回200 (static或max-age),就会被缓存120h。这就带来一个问题:很多upstream,因为框架的原因,或者开发自身的原因,当(可恢复的)错误发生时,仍然返回了200,导致错误响应被缓存。

2.2 upstream控制缓存时间

t2 配置了 proxy_cache_valid 200 0h 只有访问 127.0.0.1/t2/max-age 才会被缓存,static不会,因为upstream只给max-age配置了缓存。

2.3 两种方式对比

对于静态内容,或者不可恢复的错误(返回非200),我觉得都行,proxy控制可能更简单。对于动态内容,我觉得upstream控制比较好。缓存可以极大提升性能,可是缓存了错误的内容,恢复起来非常麻烦,动态内容upstream控制,可以降低风险。

3 确保缓存正确

因为缓存的存在,保证客户端获取的数据正确(过期的不算错误)尤为重要。为此可以在返回的数据中加入md5(由服务端生成,通过header或body返回),当md5校验失败时,重新获取(通过约定特殊的参数或header,以跳过缓存 proxy_cache_bypass ,同时服务端可以监控这些特殊参数或header发现问题)。

4 proxy_cache_key 的选择

proxy_cache_key 默认使用host + uri,通常这不会有问题,但是碰到upstream根据客户端的能力返回不同的内容的时候,就有问题了。例如:根据客户端是否 Accept webp 返回webp格式的图片,根据客户端的 Accept-Encoding 返回压缩或不压缩的数据。这时虽然uri一样,但是返回给客户端的东西是不同的,此时根据uri缓存就有问题。

4.1 Vary

此时upstream可以使用 Vary ,例如: Vary:Accept nginx 会为每类Accept缓存一份数据。

4.2 自定义 proxy_cache_key

Vary 是个很简洁的方法,但 Accept 的种类可能多达几十种,就会缓存几十份数据,但两份就够了,webp和非webp,这是极大的浪费和性能损失。此时自定义 proxy_cache_key 也许更好,虽然更复杂。

set $cachekey "$proxy_host$request_uri";
if ($http_accept ~* "image/webp") {
  set $cachekey "$proxy_host$request_uri/webp";
}
proxy_cache_key $cachekey;
本文收录于