Java

流量切换先于缓存预热怎么排查?发布为什么会触发读放大

发布后出现读放大,很多时候不是代码突然变重,而是流量已经切到新实例,缓存预热、本地状态和热点对象覆盖却还没跟上。先把顺序和时间线看清,比一开始就翻 diff 更有效。

  • 缓存预热
  • 发布排查
  • 读放大
  • 流量切换
  • Java
18 分钟阅读

很多发布后的性能问题,第一眼都会被怀疑成代码回归;但把时间线摊开以后,经常发现真正先出错的不是业务逻辑,而是切流顺序。

现场通常长这样:

  • 发布刚结束,接口 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 设计。本文只盯一条很具体的链路:流量已经切到新实例、新路径、新分片或新机房,但缓存和实例状态还没准备好,于是发布窗口把读路径推成了读放大。

讨论范围先卡在这里:

换句话说,这篇先帮你把“发布后变慢”里最容易误判成代码回归的那一段拆开:到底是不是流量先切过去了,而吸收这波流量的缓存和本地状态还没热起来。

哪几类现象最能说明“流量先切了,状态还没热”

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 时机 更偏向核对预热对象、时机和路径有没有对齐;这篇更聚焦顺序反了以后,发布窗口为什么会把读链直接点成放大链。

接下来就看哪个信号最扎眼

如果你已经确认问题和发布窗口绑得很紧,后面通常就是按现场最明显的信号继续拆:

最后收一下

发布触发读放大,很多时候不是代码突然变慢,而是流量已经到了,新实例上的缓存、本地状态和热点对象还没准备好。

先把切流时间、实例冷热状态、热点对象覆盖和回填承压这几处对齐,再去看代码 diff、SQL 或配置变更,通常更容易判断问题究竟是顺序失配,还是发布真的引入了更重的读路径。