关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

缓存命中率为什么一直上不去:缓存键是怎么把请求打散的

发布时间:2026-09-23

命中率卡在 60% 死活不动,加内存、加 TTL 都没用,问题多半在键上

先说个很常见的现场:一个内容站,日请求量几千万级,CDN 或源站前反代的命中率长期卡在 60% 上下。运维的第一反应通常是"缓存不够大",于是把内存从 32G 加到 128G;没明显变化,又觉得是"缓存有效期太短",把 TTL 从 10 分钟调到 1 小时;还是没变化,再加一层节点、把边缘节点数量翻倍。几轮下来,机器成本上去了,命中率曲线几乎是平的。

这种时候,问题大概率不在缓存本身,而在缓存键。说得再直白一点:你的缓存不是"装不下",而是被切碎了。同一个页面、同一张图、同一个接口响应,因为键里多带了几个维度,被拆成了几十甚至上百个不同条目,每一份都只被命中一两次,然后就因为冷门被淘汰。你加再大的内存,也只是给这些碎片多准备了一点存放空间,复用率还是零。

这篇要讲的就是这件事。先把几条结论摆在这:

命中率不是"调"出来的,是被缓存键设计"决定"的。容量和 TTL 只能影响边缘,键的维度数量才是主因。

缓存键里每多一个维度,就在把原本可复用的条目切分成 N 份。查询参数、Cookie、Vary 头、设备类型、语言、AB 分桶、时间戳、用户标识,每加一个都是一次乘法。

排查顺序不能反。先数清楚键里到底有几个维度,再谈容量和 TTL;顺序反了就是白花钱。

命中率提高之后,真正省下来的不是带宽,是源站的并发与计算。带宽账单可能只降一点点,但源站 CPU 和回源 QPS 的下降是决定性的。

改缓存键等于做一次全量失效。优化动作本身就可能引发故障,节奏比方案更重要。

先别急着动手,确认你说的"命中率"到底在算什么

很多人一上来就改配置,却没确认过自己看的那个数字是怎么算的。这事儿看着基础,实际是绝大多数误判的根源。

分母里混进了不可缓存请求,命中率天然就上不去

命中率 = 命中次数 ÷ 总请求次数。问题是"总请求次数"里装的到底是什么?如果一个站的请求构成是这样的:60% 是静态资源与可公开复用的页面,40% 是 POST 提交、带 token 的用户接口、实时库存查询、个性化推荐接口——那么这 40% 从定义上讲就不可能命中共享缓存。就算前面那 60% 做到了 100% 命中,你的整体命中率天花板也只有 60%。

于是就出现了很荒诞的一幕:团队为了把 58% 提到 62% 折腾两个月,实际上可缓存层早就跑到 96% 了,剩下的全是"分母里的沙子"。真正该做的动作是把可缓存层和全部请求分开统计,看两个数字:可缓存层命中率(这是你能优化的部分)和不可缓存请求占比(这是业务形态决定的,要么接受,要么改架构)。

怎么分层?最简单的做法是按请求方法与路径前缀分组:GET 且不带用户凭证、响应头里没有 no-store/private 的,算可缓存层;POST、PUT、DELETE,以及带了 Authorization 头或会话 Cookie 的,单独归到不可缓存层。两组分别算命中率,别混。

字节命中率和请求命中率是两回事

还有一个容易混的点:请求命中率(request hit ratio)和字节命中率(byte hit ratio)。前者按次数算,后者按流量算。大文件下载站经常见到这种情况——请求命中率只有 40%,但字节命中率 85%,因为命中的那部分全是几百 MB 的包,回源的都是小 JSON。反过来也成立:一个小图标为主的页面,请求命中率 95%,字节命中率可能只有 60%。

你关心源站压力,就看请求命中率;你关心回源带宽账单,就看字节命中率。两个数字报给不同的人,别拿错。

一次请求的缓存键,默认由哪些东西拼出来

缓存键,说白了就是"这条缓存叫什么名字"。两个请求算出来的键一样,第二个就能直接用第一个的结果;不一样,就得重新回源、重新存一份。

大多数人对缓存键的想象是"URL 嘛",其实 URL 只占一部分。典型的键至少由这几块拼起来:

Host(或站点标识):同一个 IP 上跑了十个域名,a.com/logo.pngb.com/logo.png 必须分开。

协议:http 和 https 一般是两个键,除非你显式合并。

路径/article/123/article/124 当然是两回事。

查询串:这是第一个大坑,下面单独讲。/article/123?from=wx/article/123?from=weibo 在很多默认配置下是两个键。

部分请求头:哪些头进键,取决于缓存组件的配置和源站返回的 VaryAccept-Encoding 进键很常见(gzip 和 br 要分开存),但 User-Agent 进键就是灾难。

部分 Cookie:如果配置了按 Cookie 分键,那么登录用户的每一次会话都可能是一份。

缓存组件自定义的维度:设备类型(PC/移动/平板)、语言、地域、AB 分桶 ID、灰度标识。这些往往是"为了业务方便"加进去的,加的时候没人算过代价。

你可以现在就打开自己的缓存配置,把进键的维度一项项列出来。列完做个乘法:假设查询串有 8 种常见取值、设备 3 种、语言 2 种、AB 分桶 4 组、编码 2 种,理论上同一个页面最多能产生 8×3×2×4×2 = 384 个键。你以为在缓存一个页面,实际在缓存 384 份副本,其中大部分只被访问过一两次。

五个把键切碎的地方,逐个拆开看

一、查询串:营销参数和时间戳是最常见的元凶

看这几个真实世界里到处都是的 URL:

/article/1234?utm_source=weixin&utm_medium=social&utm_campaign=0915

/article/1234?from=timeline&isappinstalled=0

/product/567?timestamp=1727092834

/api/config?_=1727092834

这四个 URL 指向的内容完全一样,但如果缓存键默认包含完整查询串,它们就是四个独立的缓存条目。更糟的是,timestamp_= 这种每次请求都变的参数,会让这个 URL 永远不可能被第二次命中——缓存里存着几百份一模一样的副本,命中率却是 0。

这类参数的来源通常是三个:市场投放团队加的渠道追踪参数、前端框架为避免浏览器缓存自动拼的随机数、以及历史遗留的埋点参数。它们对后端返回的内容没有任何影响,却完整参与了键的构造。

处理方式有三种,按推荐程度排:

参数白名单(最推荐):明确列出参与键构造的参数,比如 idpagesizelang,其余全部丢弃。白名单的好处是新增的营销参数不会意外污染键——新来一个 utm_term,不用改配置,自动被忽略。

参数黑名单:列出要忽略的参数(utm_*fromtimestamp_),其余保留。维护成本比白名单高,因为黑名单永远追不上新增参数的速度,但改动小、上线快,适合先止血。

参数归一化:把参数按名字排序后再拼进键,这样 ?a=1&b=2?b=2&a=1 会算成同一个键。这条通常和白名单配合用。

如果业务代码一时改不动(这是常态),在 CDN 或反代层做参数过滤就行,不需要动后端。Nginx 里可以用 proxy_cache_key 只取需要的几个参数,边缘节点一般也提供"忽略指定参数""保留指定参数"的配置项。

二、Vary 头:Vary: User-Agent 是教科书级的杀手

Vary 是源站告诉缓存"我这个响应还取决于哪些请求头"的字段。缓存看到 Vary: Accept-Encoding,就会把 gzip、br、identity 三种响应分开存,这是完全合理的——响应体确实不一样。

问题出在 Vary: User-Agent。UA 字符串有多少种?成千上万种,光 Android 机型就能列出几千个,同一个 Chrome 版本在不同系统上报出来的 UA 都不一样,而且每次浏览器升级还会产生新的。一旦 UA 进了键,你的缓存等于按客户端型号切碎,每个条目几乎只有同型号的少数用户能命中。

更要命的是这事儿经常不是故意的。某个历史版本的代码里,为了给老版本 IE 返回不同的 CSS,加了一句 Vary: User-Agent;后来兼容逻辑早就删了,这个头还挂在那儿。或者某个框架默认给所有响应加 Vary: User-Agent,没人注意。

还有两种情况值得单独提:

Vary: Cookie——比 UA 更狠,等于每个用户一份缓存,共享缓存直接退化成个人缓存。

Vary: *——按规范含义是"响应依赖于任何请求头之外的因素,这个响应不可被缓存复用"。大部分缓存组件看到 Vary: * 会直接放弃缓存,效果等价于不缓存。

处理方式:先全站抓一遍响应头,看看哪些路径返回了 Vary: User-AgentVary: Cookie,逐个确认是不是真的需要。如果业务上已经在用响应式布局(同一个 HTML 适配所有屏幕),那这个头就是纯负担,直接摘掉。如果确实要区分 PC 和移动版,正确做法是在边缘做设备识别,只往键里加一个二值的设备标记(pc / mobile),而不是把整条 UA 塞进去——两种结果,而不是几万种。

三、Cookie 与登录态:两件不同的事,别搞混

Cookie 进键这件事有两个完全不同的场景,处理方式也相反,混在一起讨论就容易出错。

第一种:公开内容被登录态污染。一个新闻详情页,未登录用户看到的内容完全一致,但因为所有请求都带着会话 Cookie(哪怕是空会话),而缓存配置又按 Cookie 分键,于是每个用户一份。这种的处理很直接——把登录态从键里摘出去。判断依据是这个响应到底公不公共:如果响应体里没有用户名、没有购物车数量、没有个性化推荐,那它就是可共享的,键里就不该有 Cookie 的事。

工程上的常见落法是分层:边缘缓存只认 URL 和几个必要维度,对公开路径完全忽略 Cookie;真正需要个性化的部分(右上角的用户名、购物车角标)用 AJAX 单独请求,或者前端用本地存储渲染。这个模式叫"页面缓存 + 片段回源",改一次前端,命中率能上一个台阶。

第二种:内容本身就是私有的。用户中心、订单详情、账户余额——这些响应的内容因人而异,本来就不该进共享缓存。这时候要做的不是优化键,而是把它从可缓存层里挪出去:响应头明确带上 Cache-Control: private, no-store,让缓存直接跳过,同时在统计口径里把它归到不可缓存层。

这两种搞混的后果很实际:要么你把私有内容缓存了,造成用户看到别人的数据(严重事故);要么你把公开内容按用户切碎了,命中率永远起不来(慢性浪费)。判断标准只有一个——这个响应体的内容,换一个用户来请求,会不会不一样

四、URL 归一化差异:同一个资源,长得不一样

这一类和业务无关,纯粹是"字符串长得不同"导致的碎片,处理起来也最省心,收益却经常不小。

Host 大小写与默认端口Example.COMexample.com,理论上等价,字符串上不同。

路径尾斜杠/category/news/category/news/,很多站点两者都能访问且返回同样内容,但是两个键。

URL 编码差异/img/%E5%9B%BE.jpg/img/图.jpg 是同一个资源;空格写成 %20 还是 +,也会造成差异。更隐蔽的是双重编码——%2520 这种。

参数顺序:前面提过,?a=1&b=2?b=2&a=1,语义相同,字符串不同。

多余参数?a=(空值)和 ?a=1&b= 这类空值参数,很多是前端拼接时留下的尾巴,应该清掉。

处理方式是在缓存键生成前做一次规范化:Host 转小写并去掉默认端口、路径去掉尾斜杠(或统一补上)、参数排序并过滤空值、URL 解码后再统一编码。这些逻辑在反代层做一次就够了,不需要每个业务都改。

五、Range 分片:大文件按字节区间缓存

视频、安装包、大图这类文件,客户端普遍用 Range 头做分片请求(拖进度条、断点续传都是这个)。这时候缓存是按字节区间存片的:Range: bytes=0-1048575Range: bytes=1048576-2097151 是两个不同的缓存对象。

这带来两个后果。一是分片边界不一致会造成重复回源:客户端 A 请求 0-1000,客户端 B 请求 500-1500,如果缓存组件不做分片对齐,这两段都要回源,而且存下来的碎片很难被后续请求完整复用。

二是整片请求和分片请求不互通:一个不带 Range 的完整请求回源取回了整个文件,后面带 Range 的请求未必能直接从中取片,取决于缓存组件的实现。

处理方式:如果用的是成熟的分片缓存方案,开启分片对齐(把请求对齐到固定的片大小,比如 1MB 或 2MB 边界),让不同客户端的 Range 请求落在同一组片上。同时确认分片缓存对大于某个阈值的文件才启用,小文件走整体缓存更省事。这一项的收益集中在大文件分发场景,做视频或软件下载的站点值得单独看一眼。

切碎来源速查:影响、处理与改动风险对照

切碎来源 典型表现 对命中率的影响 处理方式 改动风险
查询串参数 ?utm_source=?from=?timestamp= 进键,同一页面无数副本 高,时间戳类参数可使该路径命中率归零 参数白名单为主,黑名单兜底,配合参数排序 低,但要确认被忽略的参数确实不影响响应
Vary 响应头 Vary: User-AgentVary: Cookie,按客户端/用户切碎 极高,共享缓存退化为按型号或按用户存储 摘掉不必要的 Vary;确需区分时只保留二值维度 中,可能返回不适配的页面版本,需回归验证
Cookie 与登录态 公开页面按会话分键,或私有内容误入共享缓存 高,前者压低命中率,后者可能引发串号 摘出登录态,个性化片段单独请求;私有内容标记 no-store 高,涉及用户数据隔离,必须逐路径确认
URL 归一化差异 Host 大小写、尾斜杠、编码方式、参数顺序不同 中,取决于外部链接的整洁程度 键生成前统一规范化:小写、去尾斜杠、排序、解码重编 低,纯字符串处理,不影响响应内容
Range 分片 大文件按字节区间存片,区间不同即不同键 中大文件分发场景下明显 开启分片对齐,小文件走整体缓存 中,改动分片策略会同时影响回源形态
自定义业务维度 设备类型、语言、AB 分桶、灰度标识进键 取决于维度基数的乘积,常被低估 逐维度评估必要性,合并低价值分桶,能砍就砍 中高,涉及 AB 实验与灰度逻辑,需业务方确认

命中率和回源量不是线性关系:为什么 90% 提到 95% 比 50% 提到 55% 值钱得多

这一段是很多人算不明白的地方,也是"加内存没效果"的另一半原因。先看一个假设算例(以下数字为假设算例,非实测数据,仅用于说明关系)。

假设算例:同样 5 个百分点,收益差出一倍

假设某内容站日均请求 1 亿次,其中可缓存层 7000 万次,不可缓存层(POST、带 token 接口、个性化 API)3000 万次。假设全天流量的 60% 集中在 8 小时高峰内,据此估算回源峰值 QPS = 日回源量 × 0.6 ÷(8 × 3600)。

命中率 50%:可缓存层回源 3500 万,加不可缓存 3000 万,日回源 6500 万次,峰值回源 QPS ≈ 1354。

命中率 55%:可缓存层回源 3150 万,日回源 6150 万次,峰值回源 QPS ≈ 1281,比上一步降约 73。

命中率 90%:可缓存层回源 700 万,日回源 3700 万次,峰值回源 QPS ≈ 771。

命中率 95%:可缓存层回源 350 万,日回源 3350 万次,峰值回源 QPS ≈ 698,比上一步同样降约 73。

看出问题了吗?都是提升 5 个百分点,绝对回源量都只降了 350 万次——因为每 1 个百分点对应的可缓存回源量是固定的 70 万次。但相对效果完全不同:在 50% 那一段,峰值 QPS 从 1354 降到 1281,降幅 5.4%;在 90% 那一段,从 771 降到 698,降幅 9.5%。

更关键的是绝对值落在了哪一侧。假设源站单台实机承载能力是 800 QPS:命中率 50% 时你需要 2 台,且已经接近满载,一次大促就可能打穿;命中率 90% 时 771 QPS 刚好 1 台能扛;提到 95% 后 698 QPS,留出约 13% 的余量。90%→95% 这一步,直接决定了你是买 2 台还是 1 台,以及高峰期会不会雪崩。而 50%→55% 那一步,你还是得买 2 台,什么都没变。

省下来的是并发与计算,不是带宽

再说清楚收益的性质。回源量下降 350 万次,如果平均响应体是 20KB,一天省下的回源流量是 70GB——按带宽计费算,这点钱可能连一台机器的月租都抵不上。所以如果你拿"省带宽"去给命中率优化立项,多半算不过账。

真正省下来的是源站的并发槽位和 CPU 时间。一次回源请求,源站要做的是:接收连接、解析请求、查会话、查数据库或缓存、渲染模板、序列化、返回。这些步骤里,数据库连接和模板渲染是大头,一个动态页面回源可能消耗几十毫秒的 CPU 时间,而边缘命中一次的成本几乎为零。回源 QPS 从 1354 降到 698,意味着源站少了一半的数据库查询和渲染开销,这部分省下来的是实打实的机器数量。

还有一层是非线性的:排队论里,服务系统的响应时间大致按 1/(1-利用率) 增长。当源站利用率从 70% 涨到 90%,响应时间不是涨 28%,而是涨好几倍,还会引发重试放大、连接堆积、雪崩。所以高命中率区间的价值不在于省了多少资源,而在于把你从"高利用率的危险区"拉回安全区。这也是为什么 90% 往上那几个百分点最值钱。

顺带回答"为什么加内存没用"

如果你的键被切碎了,缓存里堆的是大量只被访问一两次的冷碎片。加内存的实际效果是让这些碎片活得更久一点——但它们本来就没人会再访问第二次,活得再久也不会产生命中。内存只能拯救"因为容量淘汰而失效"的条目,救不了"因为键太碎而天生只有一个请求"的条目。

这也解释了为什么 90% 往上的优化特别难:剩下的 10% 回源,往往不是容量问题(热数据早就装下了),而是长尾问题——无数个只来一次的键。这时候唯一有效的手段就是改键,把长尾合并回主干。

改一次缓存键,等于做一次全量失效

这一点必须单独拎出来讲,因为它是"优化引发故障"的经典路径。

改键规则之后,存量缓存条目的键名全变了。新请求算出的新键,在缓存里一个都找不到,于是全部回源。这不是渐进的,是瞬时的——改键生效的那一刻,你的缓存命中率会直接掉到 0,回源量瞬间等于全部可缓存请求量,而你的源站平时只按 10% 的回源比例准备容量。这就是打爆。

所以改键不能当成一次普通配置下发,要当成一次变更来安排:

选在低峰做。业务低峰期的绝对请求量最小,全量失效时的回源峰值也最低。别在上午十点或者大促前改。

先灰度。按路径灰度是最简单有效的方式:先对一两个不重要、量也不大的路径前缀应用新键规则,观察半小时到一小时的回源曲线,确认回源量、源站负载、错误率都正常,再逐步扩大范围。按节点灰度也可以,一个节点一个节点切。

盯着回源曲线,而不是命中率曲线。改键之后命中率必然先跌,这是预期内的,看它没意义。要看的是回源 QPS 有没有超过源站的安全水位、源站 CPU 和数据库连接池有没有打满、错误率和超时率有没有抬头。命中率会在缓存重新填满之后自然回升,通常几十分钟到几个 TTL 周期。

回源侧提前留余量。改键前把源站的容量临时扩上去,或者临时降低一部分非关键请求的 TTL 压力。最稳妥的做法是源站与反代层分开部署,让回源 Origin 这一层有足够的 CPU 和带宽冗余来扛住重建缓存期间的全量回源冲击。像一万网络这类深耕 IDC 19 年(成立于 2007 年)的服务商,裸金属产品线里有 E5-2620 32G/1T ¥999 起这一档(以官网实时价为准),用来单独放源站或反代层比较合适——无虚拟化开销、资源独占,重建缓存那半小时不至于被邻居拖累;如果只需要临时的弹性扩容,一万云 ¥25 起(以官网实时价为准)可以拿来顶一阵。具体规格与实时报价以官网为准。

准备回滚。键规则的改动要能一键回退。注意一个细节:回滚同样是一次全量失效,来回切两次对源站是两次冲击,所以灰度阶段宁可慢一点,也别频繁来回。

还有个容易被忽略的点:改键之后,缓存的条目总量会下降(碎片合并了),但单条目的访问频率会上升。如果你的缓存组件有单键热点保护或者 QPS 限制,合并之后可能出现新的热点键,这一点在灰度时也要留意。

浏览器、边缘、源站前反代,三层的键不该是同一套

很多团队把一套缓存规则复制到所有层,这是省事,也是浪费。三层的定位完全不同,键的设计就该不一样。

浏览器缓存:这一层是私有的,每台设备一份,不存在共享问题。它的键就是完整 URL,不需要考虑别的维度。这一层要关心的不是命中率,而是"缓存有效期内不要重复请求"和"失效时能快速更新"。做法通常是内容指纹(文件名带 hash)+ 长 TTL,HTML 用短 TTL 或 no-cache + ETag 协商。

边缘节点:这是共享缓存,也是键设计的主战场。前面讲的所有原则都适用——维度越少越好,能砍就砍。边缘还承担一个特殊职责:把外部带进来的脏参数在这里清洗掉,别让它们继续往后传。

源站前反代:这一层最接近源站,键可以比边缘更"宽"一点。原因有两个:一是它的容量通常更大、更可控,二是它面对的请求已经被边缘清洗过一轮,参数的多样性已经收敛。有些真正的业务维度(比如 AB 分桶)在边缘被合并掉之后,可以在这一层重新展开,前提是这一层扛得住。

反过来说,一个常见的错误是把所有维度都压到边缘层——设备识别、语言判断、AB 分桶全在边缘做键,结果边缘被切得粉碎,而源站前反代这一层明明有容量却只接到边缘漏下来的残羹。正确的思路是:越靠近用户、节点越多、单节点容量越小的那一层,键越要简单;越靠近源站、容量越大的那一层,可以承担更多维度。

做分发时还要估一下回源带宽的账:回源带宽 ≈ 回源 QPS × 平均响应体大小 × 8。这个数字决定了源站出口要开多大的端口、边缘到源站这一段需要什么样的链路。用前面假设算例的数字,700 QPS × 20KB × 8 ≈ 112 Mbps,这是重建缓存期间的量级,平时的十分之一都不到。选型时按峰值估,但要留一倍余量。节点与带宽怎么配,可以拿这个口径去找服务商对——一万网络这类多节点布局(华南、华东、华北等,BGP 多线)的服务商,通常能按你给的回源峰值和分布区域给出对应的端口与带宽组合,比自己拍脑袋准。具体以官网实时方案与签约合同为准。

怎么查出"我的键到底被谁切碎了":看这四个东西

只讲方法和观察指标,不给你编数据。你按这套去查,能拿到属于你自己的结论。

一、缓存键采样

把缓存组件实际使用的键打出来采样(Nginx 可以把 $proxy_cache_key 写进访问日志,多数 CDN 也提供缓存键的调试响应头)。然后做一件事:把采样出来的键按"去掉某个维度后"重新分组,看合并率

具体做法是,取一段时间内的一批键,分别做几次处理:去掉所有查询参数后去重、去掉 UA 相关维度后去重、去掉设备标记后去重。每做一次,条目数下降多少,就说明那个维度贡献了多少碎片。哪一步降得最狠,那个维度就是主犯。这个方法不需要任何猜测,直接量化。

二、回源 URL 去重后的分布

把回源请求的 URL 拉出来,按"规范化后的路径"分组,看每个路径下有多少个不同的查询串变体。如果某个路径下有几百个变体,而这些变体的响应体大小完全一致(这点可以从响应日志里核对),那基本可以断定是无效参数在作怪。

再看变体的长尾分布:统计每个变体出现的次数,看看有多少变体只出现了 1 次。这个"只出现一次的键占比"是非常直观的碎片指标,占比越高说明键越碎。

三、命中率按路径分组

整体命中率是一个被平均掉的数字,没什么用。按路径前缀分组统计,你通常会发现命中率极不均衡:静态资源路径 98%,详情页 40%,某个列表页 15%。优先处理量最大、命中率最低的那几个路径,收益集中。一个占 30% 流量、命中率 20% 的路径,比十个占 1% 流量的路径加起来都值钱。

四、慢请求集中在哪些参数组合

把响应时间 P95 以上的请求拉出来,按参数组合分组。如果慢请求高度集中在某些参数上(比如带 timestamp 的、带完整 UA 分键的、Range 分片没对齐的),那就直接指向了问题来源。慢请求和缓存未命中高度相关,这个列表本身就是线索。

这四项做下来,你手里会有三样东西:键的维度清单、每个维度的碎片贡献度、以及按路径排好序的优化优先级。有这三样,改什么、先改哪个、改完能涨多少,都能估出来。

被问得最多的几个缓存键问题

把 Vary: User-Agent 去掉,会不会导致移动端用户拿到桌面版页面?

取决于你的页面是怎么适配的。如果用的是响应式布局(同一份 HTML 靠 CSS 适配不同屏幕),那响应体对所有设备都一样,去掉 Vary 完全没问题,反而应该去掉。如果后端确实在做服务端自适应(根据 UA 返回结构不同的 HTML),那直接去掉就会出问题——移动端可能拿到桌面版的结构。

这种情况的正确做法不是"去掉 Vary",而是把整条 UA 换成一个二值标记:在缓存层做一次设备识别,只往键里放 pcmobile。从几万种收敛到两种,命中率的提升是数量级的,适配逻辑又完全保留。改完记得真机回归几款主流机型,别只看模拟器。

营销参数怎么在不改业务代码的情况下从键里剔除?

在缓存层做,不需要动后端。边缘 CDN 一般都有"忽略指定查询参数"或"仅保留指定查询参数"的配置项,填一份 utm_sourceutm_mediumutm_campaignfromtimestamp 之类的清单就行。自建反代的话,Nginx 里把 proxy_cache_key 改成只取白名单参数拼出来的字符串,或者用一个 map 把要忽略的参数过滤掉。

推荐用白名单而不是黑名单——黑名单永远追不上市场同学新加的参数,白名单是"没列进来的都不算",天然免疫。改完记得观察一个周期,确认被忽略的参数确实没影响响应内容(比如某些灰度参数可能是真的影响返回的)。

命中率多少算合格?有没有一个通用标准?

没有通用标准,别信任何"应该达到 90%"的说法,那取决于你的业务构成。看两个数字才有意义:一是可缓存层命中率,这个做到 90% 以上是比较健康的状态,85% 以下通常说明键有问题;二是不可缓存请求占比,这个由业务形态决定,一个重交互的 Web App 有 40% 不可缓存很正常,一个内容站有 40% 就说明架构有问题。

如果你的整体命中率是 60%,但可缓存层已经是 95%,那你的活已经干完了,剩下的 40% 是业务本身决定的。这时候该做的不是继续优化缓存,而是去问业务:"这 40% 的接口能不能做成可缓存的?"

为什么加了缓存内存,命中率一点没涨?

因为你的问题不是容量。加内存只能减少"因为空间不够被提前淘汰"造成的未命中,这类未命中的特征是:键本身是热的(会被反复请求),只是存不下。而键被切碎的情况完全不同——那些碎片条目本来就只有一两次访问,就算给它无限的空间,也不会产生第二次命中。

判断方法很简单:看缓存的淘汰原因统计。如果是空间淘汰(eviction)占比高,加内存有用;如果大部分条目是自然过期(expire)或者根本没被第二次访问过,加内存没用。前者加内存,后者改键。

改缓存键会不会把源站打爆?

会,如果不做准备的话。改键等于全量失效,生效瞬间所有存量缓存作废,回源量直接等于全部可缓存请求量。平时源站按 10% 回源准备容量,突然来 100%,不爆才怪。

规避方式前面讲过:低峰操作、按路径或按节点灰度、盯着回源 QPS 和源站 CPU 而不是命中率、提前给源站扩容留出余量、准备一键回滚。还有一条容易被忘的——回滚本身也是一次全量失效,来回切两次就是两次冲击,所以灰度阶段要稳,别反复横跳。

登录用户的页面到底能不能缓存?

分两部分看。页面的主体内容如果对所有登录用户都一样(比如一篇新闻的正文),那它是可缓存的,键里不该带用户标识。页面的个性化部分(用户名、头像、未读消息数、购物车角标)不可缓存,应该用独立的接口异步获取,或者前端从本地存储渲染。

如果整个页面确实因人而异(订单页、个人中心),那就明确标记 Cache-Control: private, no-store,让它别进共享缓存,同时在统计口径里归到不可缓存层——别让它污染你的命中率分母。最忌讳的是中间状态:内容其实是一样的,但因为 cookie 进了键,每个用户存了一份。

POST 请求要不要算进命中率?

不算。POST 按语义是非幂等的写操作,绝大多数缓存组件默认就不缓存 POST,把它放进分母只会拉低数字、干扰判断。同样该排除的还有:带 Authorization 头的接口、带用户会话 Cookie 的个性化接口、实时性要求高的查询(库存、价格、余额)。

正确的做法是分层统计:可缓存层单独出一个命中率,这个数字才是你能优化、也该负责的部分。全部请求出一个整体命中率,用来跟老板汇报成本。两个数字各管各的,别混在一起看。

参数排序和参数过滤,先做哪个?

先做过滤,收益大得多。参数顺序不同造成的碎片,通常只出现在少数几个多参数路径上,而且外部链接的参数顺序往往比较固定;而营销参数和时间戳是全局性的,几乎每个带外链的页面都中招。

过滤做到位之后,排序自然就包含进去了——如果你用白名单重建键,重建的过程本身就是有序的,排序是副产品。所以实际操作上,一次把白名单 + 排序一起做了,比分开做两轮更省事,也少一次全量失效。

我的判断:命中率是设计出来的,不是调出来的

写到这里,态度应该很清楚了:缓存命中率的上限,在你设计缓存键的那一刻就定死了,后面所有的调参都只是在这个上限附近波动。

见到命中率上不去,正确的动作顺序是——先分层看分母,把不可缓存请求摘出去;再数一遍键里有几个维度,逐个评估必要性;然后按路径排优先级,从量最大、命中率最低的路径下手;改之前算清楚全量失效的冲击,低峰灰度、盯着回源曲线;最后才是容量和 TTL。

反过来,一上来就加内存、加节点、调 TTL,是在给一个设计问题买硬件解药。钱花了,曲线不动,然后得出"我们业务特殊"的结论——这个结论基本都是错的。

再说一遍那句话:缓存键里每多一个维度,就在把原本可复用的条目切分成 N 份。先数维度,再看容量。顺序反了就是白花钱。

文中这些口径来自哪里,哪些数字必须自己复核

本文涉及的缓存键机制(查询串参与键构造、Vary 头的处理方式、Cache-Control 的 private/no-store 语义、Range 分片缓存的行为),依据的是 HTTP 缓存相关公开技术规范与主流缓存组件(Nginx、Varnish、主流 CDN)的公开文档,具体实现细节因组件版本而异,落地前请以你使用的组件官方文档为准。

文中"假设算例"一节的全部数字(日请求 1 亿次、可缓存层 7000 万、不可缓存层 3000 万、峰值 QPS、单台 800 QPS 承载能力、20KB 平均响应体)均为为说明非线性关系而构造的假设值,不是实测数据,不代表任何真实站点。请按你自己的请求量、可缓存占比、响应体大小和单机承载能力重新计算。

文中提到的服务商与价格锚点(一万网络,深耕 IDC 19 年,成立于 2007 年;裸金属 E5-2620 32G/1T ¥999 起;一万云 ¥25 起)仅为资源选型参考,以官网实时价为准,具体规格、计费方式、带宽与端口组合以签约时最新报价与合同为准,可前往 https://www.idc10000.net/ 查阅对应产品页面或提交工单确认。

所有排查方法(缓存键采样、回源 URL 去重分布、命中率按路径分组、慢请求参数组合分析)为通用方法说明,文中未给出任何实测结果,你需要在自己的环境上跑一遍才能得到属于你的结论。


上一篇:2026 RabbitMQ消息队列服务器租用配置手册:内存/磁盘/带宽选型与避坑全解

下一篇:同一地区差25倍:哥伦比亚三档套餐到底对应三种什么业务