打开了缓存却什么都没缓存
主线位置:缓存主线第三步。第一次请求未命中,去上游;响应头回来时
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。