流量切换先于缓存预热怎么排查?发布为什么会触发读放大
发布后出现读放大,很多时候不是代码突然变重,而是流量已经切到新实例,缓存预热、本地状态和热点对象覆盖却还没跟上。先把顺序和时间线看清,比一开始就翻 diff 更有效。
很多发布后的性能问题,第一眼都会被怀疑成代码回归;但把时间线摊开以后,经常发现真正先出错的不是业务逻辑,而是切流顺序。
现场通常长这样:
- 发布刚结束,接口 RT 就开始往上抬
- 数据库查询量跟着上来
- Redis 命中率短时波动
- 某些新实例明显比老实例更慢
- 读接口 timeout、连接池等待一起变差
这时候团队往往会先盯两件事:代码是不是写重了,数据库是不是刚好在高峰扛不住。
这两个方向都值得看,但还有一种非常高频的情况更容易漏掉:
流量已经切过去了,缓存预热、本地状态、热点对象覆盖和回填速度却还没准备好,于是发布窗口自己把读链推成了读放大。
也就是说,发布后的“变慢”不一定来自代码执行更慢,还可能来自:
- 新实例先接流量,再慢慢把缓存打热
- 预热对象和真实热点不完全一致
- Redis 预热了,但本地缓存、连接池、对象缓存、JIT、类加载状态没热起来
- TTL 和切流窗口对不齐,第一波流量正好撞上半热状态
这类问题最典型的后果不是直接挂,而是:
- 读请求 miss 变多
- 回源量上升
- 数据库读压力抬高
- 连接池和线程池周转变慢
- 上游继续重试,读放大越来越明显
如果你现在还停留在“预热做了但还是抖”的层面,先看上一跳 缓存预热做了还是抖怎么排查?先看预热范围、切流路径和 TTL 时机。如果你已经看到数据库、连接池和接口 RT 一起被拖坏,也要把数据库等待链一起接回来,看 数据库没打满,为什么 API 和连接池已经开始变慢?。
这里想拆开的,不是“发布后到底是不是代码回归”这个大问题,而是其中一段很常见、也很容易被忽略的时间差:流量先到了,吸收这波流量的缓存层和实例状态却还停在半热状态。
现场更像典型的发布型读放大吗
这张表先拿来判断,问题到底是不是紧跟切流窗口出现,还是其实更接近预热范围、TTL 或实例差异。
| 你现在看到的现象 | 更像什么 | 下一步 |
|---|---|---|
| 发布、灰度、扩容切流后读接口明显更慢 | 发布型读放大 | 先盯切流时间线和第一波 miss |
| 新实例比老实例更慢,但代码差异不明显 | 冷状态 + 切流顺序问题 | 先核对新实例是不是半热就接流量 |
| Redis 已预热,但新实例上的命中和 RT 仍然不稳 | 预热层和实例状态没一起热起来 | 先把实例状态和预热对象放在一起看 |
| 读接口越来越差,写接口影响不明显 | 读路径吸收层失效 | 先查读路径哪一层没接住 |
| 切流后数据库读压力、连接池 pending 一起上升 | 回源和回填链在放大 | 先把回源放大和回填承压一起看 |
| 还在判断是不是预热范围或 TTL 问题 | 先回预热 / TTL 页面,不要直接跳到发布结论 | 先看 缓存预热做了还是抖怎么排查?先看预热范围、切流路径和 TTL 时机 |
| 问题只发生在部分实例 | 还要联动实例差异页一起看 | 看完本文后接 同一个接口只有部分实例慢,优先怀疑什么? |
切流后先把哪些信息放到一张时间线上
如果问题就是在发布或切流后冒出来,这一轮先别急着把 diff 翻到底。先把切流时间点、新老实例差异、预热对象覆盖、本地缓存 / 连接池 / 对象缓存是否已热,以及回源 SQL RT 和 pending 放到一起。这样做不是为了证明谁背锅,而是尽快看清:到底是代码本身变重了,还是流量先到了、吸收这波流量的状态却没跟上。
如果问题就是发布后开始出现,先把“切流顺序”和“代码回归”拆开,后面判断会省很多弯路。
| 5 分钟内先看什么 | 想先回答的问题 | 若信号成立,优先去哪里 |
|---|---|---|
| 切流时间点和 RT / miss / 数据库读压力抬头时间 | 问题是不是切流一发生就被触发 | 若几乎同步抬头,就先顺着切流顺序往下核对 |
| 新实例 vs 老实例的本地缓存、对象缓存、连接池与冷启动状态 | 新实例是不是在半热状态就开始接流量 | 若新实例明显更冷,就按读取吸收层未热收敛 |
| 新版本 key 路径、组合 key、衍生 key 与灰度样本 | 切流后读的还是不是原来那批预热对象 | 若热点对象变了,就回看预热页而不是只盯发布代码 |
| 回源 SQL RT、连接池 pending、接口 timeout | 读放大还停在切流层,还是已经往等待链扩散 | 若等待链一起变差,就联动数据库过渡页和实例差异页 |
本文只盯发布窗口里的哪段链路
这里不打算替你做一篇“发布后接口变慢总排查”,也不重复展开预热对象和 TTL 设计。本文只盯一条很具体的链路:流量已经切到新实例、新路径、新分片或新机房,但缓存和实例状态还没准备好,于是发布窗口把读路径推成了读放大。
讨论范围先卡在这里:
- 如果你还在做“发布后接口慢了”的宽分诊,先回
最近一次发布后接口才变慢,应该先核对哪 5 类变化? - 如果你还在判断“预热对象、预热时机和真实流量路径有没有对齐”,先看上一跳
缓存预热做了还是抖怎么排查?先看预热范围、切流路径和 TTL 时机 - 如果你发现问题只发生在部分实例、部分灰度批次,要把实例差异页和发布漂移页一起拉进来,不要只拿缓存解释全部现象
换句话说,这篇先帮你把“发布后变慢”里最容易误判成代码回归的那一段拆开:到底是不是流量先切过去了,而吸收这波流量的缓存和本地状态还没热起来。
哪几类现象最能说明“流量先切了,状态还没热”
1. 问题是不是在切流后立刻出现,而不是缓慢出现
重点看:
- 哪一批实例何时开始接流量
- RT、miss、数据库读压力是立刻抬头,还是延后抬头
- 所有新实例都差,还是某一批次更差
这些现象主要回答:问题更像“冷状态 + 切流顺序”,还是更像后续 TTL / 回填问题。
2. 新实例和老实例的读路径状态是否明显不同
重点看:
- 本地缓存、对象缓存、配置缓存是否热起来了
- 连接池、HTTP 连接池、序列化缓存、JIT / 类加载是否还处在冷启动阶段
- 新老版本的 key 路径、配置、路由是否一致
这些现象主要回答:发布切流是不是把请求送到了“半热状态”的实例上。
3. 预热对象和真实切流后热点是否还一致
重点看:
- 预热的是主 key,还是把衍生 key、组合 key 一起预热了
- 新版本读路径是否新增了更重的读取组合
- 切流样本本身是否比旧流量更重
这些现象主要回答:问题是不是“切流之后读的就不是原来那批预热对象”。
4. 回填链是否已经被第一波 miss 打慢了
重点看:
- miss 后数据库 RT 是否显著抬高
- 连接池 pending、线程池队列、接口超时是否一起上升
- 同一个热点对象是否在回填完成前被重复读取
这些现象主要回答:问题是不是已经从“发布切流不对齐”演化成“读放大等待链”。
发布窗口里建议按哪条顺序查
发布后读放大最怕的是大家一窝蜂去看代码 diff,却没人先把切流和状态迁移顺序对齐。更合适的是先把时间线、实例状态和回填承压连起来看。
第一步:先对齐切流时间线
先看:
- 哪一批实例何时开始接流量
- 问题是在切流后立刻出现,还是延迟一段时间出现
- 是所有新实例都差,还是部分批次更差
这一步的目标是先判断:更像冷状态问题,还是更像 TTL / 回填问题。
第二步:再看新实例上的读取吸收层是否真的热起来了
重点核对:
- Redis 之外的本地缓存和对象缓存是否已建立
- 连接池、序列化缓存、JIT / 类加载是否处于冷态
- 新实例是否比老实例多承担了更重的首次构建成本
如果这里成立,问题就不是简单的“Redis 已预热”,而是上层状态并没跟着热起来。
第三步:再看预热对象和切流后真实热点是否对齐
重点看:
- 预热的是哪些对象、哪些 key
- 新版本真实最热的读请求是不是还打在同一批对象上
- 是否新增了更重的组合 key、衍生 key 或区域维度
如果这里对不上,问题更像“切流后读路径变了”,而不是“预热动作没做”。
第四步:再看 TTL 和切流窗口是否撞在一起
重点核对:
- 预热发生在切流前多久
- TTL 是否足够覆盖切流窗口和高峰窗口
- 预热后是否又发生了统一过期、批量删缓存或集中失效
如果这里成立,问题就不是发布本身,而是发布窗口刚好撞上半热甚至失效窗口。
第五步:最后把数据库、连接池和回填链一起拉回来
如果你已经看到数据库读压力、连接池 pending、接口 RT 和超时一起抬头,就不要只停在“切流顺序有问题”这层了。此时要把数据库等待链一起拉回来,因为读放大已经开始往下游扩散。
这类问题背后的常见根因
根因 1:流量切过去时,新实例上的读取吸收层还没热起来
Redis 只是其中一层。很多系统真正的读路径还依赖本地缓存、对象缓存、连接池、JIT、类加载和序列化缓存。切流太快时,这些状态来不及建立,读路径就会被整体打重。
根因 2:预热对象和切流后的真实热点不一致
缓存里确实有数据,但真实流量读的是组合 key、衍生 key、新维度或更重路径,结果还是大量 miss,数据库被重新打高。
根因 3:切流样本本身更重
有些问题不是预热没做好,而是灰度先吃到内部重度用户、大客户或更重的查询条件,导致新实例上的单位请求读取代价本来就更高。
根因 4:TTL 和切流窗口错位
预热做了,但 TTL 到切流时快过完了,或者预热和过期都太集中,导致发布窗口和失效窗口撞在一起,第一波流量就被重新打回数据库。
根因 5:回填链扛不住第一波 miss
即便前面几层都做得还行,只要 miss 后数据库回填慢、连接池紧、热点对象回填时间长,后续请求还是会继续落在空窗期里,读放大会被越放越大。
最容易误判的地方
- 发布后读接口变慢,就一定先怪代码回归;很多发布型读放大先是状态迁移顺序问题
- Redis 预热做了,就说明缓存已经准备好了;很多真实问题出在本地缓存、组合 key、实例冷状态和回填速度上
- 切流只是流量动作,不会改变读取代价;一旦切到半热状态实例,读成本本来就会明显升高
- 数据库查询量上升,就说明数据库自己先慢了;也可能只是读取吸收层失效,把更多读请求重新打回了数据库
- 只看缓存,不看实例差异、发布漂移和切流批次;发布型问题天然要和实例 / 发布链一起看
常见追问
1. 这类问题是先看预热范围,还是先看切流顺序?
通常先把切流时间线对齐,再看预热对象和真实热点是否重合,最后判断第一波 miss 有没有把回填链压坏。顺序决定故障会不会被点燃,覆盖决定点燃后会不会继续放大。
2. 为什么只有读接口越来越差,写接口却不明显?
因为先失效的通常是读取吸收层,读请求被重新打回数据库,而不是整个服务逻辑同时变重。写接口暂时没出问题,不代表发布窗口没有触发状态迁移。
3. 为什么新实例总比老实例更慢?
大概率还是冷状态:本地缓存、连接池、JIT、类加载、热点对象分布、路由落点都可能还没就位。新实例一旦过早接流量,首次读取成本自然会高于老实例。
4. 什么时候应该先暂停切流?
当新实例 RT、数据库读压力、连接池 pending、miss / 回源量一起抬头,而且影响范围还在扩散时,先暂停继续切流通常更划算。否则只是把更多请求送进半热状态实例。
5. 这篇和“预热做了还是抖”那篇怎么分工?
缓存预热做了还是抖怎么排查?先看预热范围、切流路径和 TTL 时机 更偏向核对预热对象、时机和路径有没有对齐;这篇更聚焦顺序反了以后,发布窗口为什么会把读链直接点成放大链。
接下来就看哪个信号最扎眼
如果你已经确认问题和发布窗口绑得很紧,后面通常就是按现场最明显的信号继续拆:
- 还在判断预热对象、切流路径和 TTL 时机有没有对齐:
缓存预热做了还是抖怎么排查?先看预热范围、切流路径和 TTL 时机 - 怀疑 TTL 或统一过期把切流窗口撞坏了:
缓存 TTL 设计不当,为什么会把高峰流量集中打回数据库? - 已经确认数据库回源、连接池和接口 RT 一起被读流量拖慢:
数据库没打满,为什么 API 和连接池已经开始变慢? - 问题只发生在部分实例、部分灰度批次:
同一个接口只有部分实例慢,优先怀疑什么? - 想先回到发布宽入口,重新核对是不是发布导致的状态漂移:
最近一次发布后接口才变慢,应该先核对哪 5 类变化? - 怀疑是发布后实例状态、配置、生效链出现漂移:
Spring Boot 服务发布后状态总是漂移,应该先查环境差异、探针、缓存还是配置?
最后收一下
发布触发读放大,很多时候不是代码突然变慢,而是流量已经到了,新实例上的缓存、本地状态和热点对象还没准备好。
先把切流时间、实例冷热状态、热点对象覆盖和回填承压这几处对齐,再去看代码 diff、SQL 或配置变更,通常更容易判断问题究竟是顺序失配,还是发布真的引入了更重的读路径。