Rust in Everything
基于 pingora-proxy 0.9.0

打开了缓存却什么都没缓存

主线位置:缓存主线第三步。第一次请求未命中,去上游;响应头回来时 response_cache_filter 决定这个响应能不能存。它说不能,第二次请求就还是未命中。整条主线见缓存状态机,可缓存性怎么判断见键、元数据与可缓存性。

flowchart TB
  rcf["request_cache_filter enable"] --> key["cache_key_callback"]
  key --> miss["查找 未命中"]
  miss --> up["上游响应头"]
  up --> resp{"response_cache_filter"}
  resp -->|"默认 Uncacheable"| dis["disabled 不写"]
  resp -->|"resp_cacheable"| write["写入存储"]
  dis --> again["下次还是去上游"]
  write --> hit["下次命中"]

实验 crate exp-pingora/pitb-lab,bin p23_cache(#23 到 #25 共用,环境变量选行为),脚本 tools/p23.sh 加客户端 tools/c23.sh,上游 tools/origin.py(debug);M2 Max、macOS 27、rustc 1.99.0,pingora 0.9.0,tokio 钉在 1.53.2(原因见怎么读这一卷)。代理在 19380,上游在 19381;上游的 /hello 回 hello #N (方法) 和 Cache-Control: max-age=60。存储是 MemCache,response_filter 把 session.cache.phase() 写进 x-cache 头。

现象

照着文档把该写的钩子都写了:request_cache_filter 里 enable,cache_key_callback 返回一个由 Host 和路径组成的键;response_cache_filter 没有覆盖:

fn request_cache_filter(&self, session: &mut Session, _: &mut ()) -> Result<()> {
  session.cache.enable(&*STORAGE, None, None, None, None);
  Ok(())
}

fn cache_key_callback(&self, session: &Session, _: &mut ()) -> Result<CacheKey> {
  // length-prefixed host and path
  // ...
  Ok(CacheKey::new(primary, ""))
}

GET /hello 两次:

hello #1 (GET)
  <- x-cache=disabled
hello #2 (GET)
  <- x-cache=disabled
## origin
origin #1 GET /hello
origin #2 GET /hello
## proxy
logging GET /hello phase=disabled reason=default status=Some(200) err=None
logging GET /hello phase=disabled reason=default status=Some(200) err=None

上游的响应写着 max-age=60,两次请求却都到了上游。x-cache 不是 miss 而是 disabled:缓存在这个请求里被关掉了,原因的名字是 default。

出处

ProxyHttp::response_cache_filter 的默认实现:

    fn response_cache_filter(
        &self,
        _session: &Session,
        _resp: &ResponseHeader,
        _ctx: &mut Self::CTX,
    ) -> Result<RespCacheable> {
        Ok(Uncacheable(NoCacheReason::Custom("default")))
    }

它不看响应,一律不缓存。cache_http_task 拿到 Uncacheable(reason) 就 disable(reason);phase() 变成 Disabled(Custom("default")),as_str() 打出来是 disabled,原因的 as_str() 是 default。request_cache_filter 的默认实现什么都不做,文档注释是「By default this filter does nothing which effectively disables caching」;response_cache_filter 是第二道关,同样默认关着,这一点注释里没有写,只有一行「Decide if the response is cacheable」。从 0.1.0 起就是这样。

修法

用 pingora-cache 自带的 resp_cacheable 按 Cache-Control / Expires 判断(解析规则见键、元数据与可缓存性):

// no Cache-Control and no Expires: not cacheable
const DEFAULTS: CacheMetaDefaults = CacheMetaDefaults::new(|_| None, 0, 0);

fn response_cache_filter(&self, session: &Session, resp: &ResponseHeader, _: &mut ()) -> Result<RespCacheable> {
  let cc = CacheControl::from_resp_headers(resp);
  let auth = session.req_header().headers.contains_key("authorization");
  Ok(resp_cacheable(cc.as_ref(), resp.clone(), auth, &DEFAULTS))
}

RCF=1:

hello #1 (GET)
  <- x-cache=miss
hello #1 (GET)
  <- x-cache=hit
## origin
origin #1 GET /hello
## proxy
logging GET /hello phase=miss reason=- status=Some(200) err=None
logging GET /hello phase=hit reason=- status=Some(200) err=None

第二次命中,body 还是 hello #1,上游只收到一次。

  • CacheMetaDefaults::new 的第一个参数是「没有 Cache-Control 时按状态码给的新鲜期」,|_| None 表示不给,也就是上游不说能缓存就不缓存;后两个是默认的 stale-while-revalidate / stale-if-error 秒数。
  • 第三个参数 authorization_present 为真时,resp_cacheable 要求响应显式允许共享缓存(public、s-maxage 或 must-revalidate)才存。
  • 这样只是「能缓存了」。默认实现不管请求方法、Set-Cookie 和 Vary,紧接着要处理,见下一章 POST 命中 GET 的缓存。

怎么确认没中招

  • 响应头 x-cache(或者自己在 logging 里打的 session.cache.phase())一直是 disabled,原因是 default:response_cache_filter 用的是默认实现。原因是 NeverEnabled 则说明 request_cache_filter 没有 enable。
  • 搜 fn response_cache_filter:开了缓存的 ProxyHttp 实现里必须有它。
  • 同一个 URL 连请求两次,看上游的访问日志是不是两条。

小结

  • 开缓存要两个钩子都改:request_cache_filter 里 enable,response_cache_filter 里判断可缓存性。
  • response_cache_filter 的默认实现一律 Uncacheable(Custom("default")),响应头写什么都不存;phase() 是 disabled,原因是 default。
  • 修法:用 resp_cacheable 加一个 CacheMetaDefaults;然后处理方法、Set-Cookie、Vary。

💬 关联讨论

发起讨论
加载中...