ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Oracle read by other session等待事件实战:从故障定位到根治优化

Oracle read by other session等待事件实战:从故障定位到根治优化 我至今还记得那次故障是怎么开始的。上午十点一刻平台刚进入大促预热期的流量爬升阶段监控大屏的告警几乎同时亮起——不是黄灯是红灯。应用方反馈订单查询接口的 P99 延迟从平时的 200ms 左右直接飙到 8 秒部分交易开始超时。我登录数据库看到的第一眼是活动会话数从日常的 200 骤增到 800 多AWR 里排名第一的等待事件赫然是read by other session占比超过 67%。对于很多 DBA 来说read by other session 很容易被当成一个普通的 Buffer Cache 竞争事件来处理。但经历过这次故障之后我的看法完全变了——这个等待事件一旦成为 Top 1往往说明系统里已经有一批会话在扎堆而且不是简单的偶发读冲突背后通常站着某个不合理的 SQL、不均衡的数据分布或者一个过热的对象。这篇文章就把这次故障的完整处理链路写出来从现象到机制从定位到根治希望能给同样被这个等待事件折磨过的朋友一点参考。1. 故障第一现场告警大屏和快速疯长的活动会话1.1 业务侧看到的现象与第一反应故障发生时平台正在做大促预热。运营这边刚投放了一批优惠券订单量比平时翻了三倍。最先察觉到问题的是应用组订单查询接口超时率突然上升从千分之一级别跳到百分之八紧接着客服系统开始出现用户下单后看不到订单的反馈因为订单列表查不出来前端一直转圈。应用组第一反应是怀疑应用服务的问题对服务实例做了扩容和重启但效果微乎其微。观察了十几分钟后他们才把问题抛到 DBA 这边要求重点排查数据库是否出现锁等待或者资源瓶颈。这个时间点距离实际故障爆发已经过去了将近二十分钟。1.2 数据库侧的第一份证据活动会话和等待事件分布我接手后的第一件事不是看慢 SQL而是先看全局会话状态的分布。当时系统是两节点的 Oracle RAC 11.2.0.4底层存储是全闪阵列平时单块读延迟基本稳定在 2ms 以内。我拉了一下活跃会话数select inst_id, count(*) from gv$session where wait_class Idle and status ACTIVE group by inst_id order by 2 desc;结果两个节点上活跃会话分别有 390 和 420平时这个数只有 100 左右。再按等待事件归一下类问题立刻清楚了read by other session 占了 67.8%平均等待时间 42ms。这个数字很说明问题——如果只是正常的磁盘单块读全闪阵列下平均等待不会超过 5ms。42ms 意味着大量会话挤在同一个或同一批数据块的读取路径上排队。我顺手拉了一小时的 AWR 快照对比指标9:00-10:00 基线10:00-11:00 故障时段DB Time3,200s18,600s平均活动会话55310Top 1 等待事件db file sequential read (28%)read by other session (67.8%)read by other session 平均等待3ms42ms到了这一步基本能确定数据库确实遇到了某种并发读阻塞而不是单纯的 CPU 或 IO 资源耗尽。1.3 第一轮快速排除存储、网络与 RAC 缓存融合做深入分析之前我习惯先把容易踩的坑排掉。存储层先看磁盘延迟、IOPS、队列深度都正常全闪盘没有明显饱和网络层也正常两个 RAC 节点的私网延迟没有波动。再单独看了一眼 gc buffer busy 和 gc cr block busy 这些 RAC 缓存融合相关的等待事件占比都很低基本可以排除跨节点数据块传输导致的叠加问题。这样问题就被锁定在从磁盘到 buffer cache 这一段路径上的并发竞争。2. read by other session 的等待机制一句话讲透它卡在哪2.1 一个数据块从磁盘到内存的正常读取路径要理解这个等待事件先要知道一次普通的数据块读取是怎么走的。假设会话 A 需要读取 43 号文件的第 189270 号数据块步骤如下会话 A 根据表的段头信息找到这个数据块的地址先在 buffer cache 里查找。如果 cache 里没有就需要从磁盘文件读取。会话 A 在 buffer cache 中分配一个缓冲区然后把物理 I/O 请求交给操作系统自己进入db file sequential read等待直到磁盘返回数据。磁盘返回后会话 A 把数据写入 buffer cache标记缓冲区可用再继续执行。整个过程看起来简单但有个前提buffer cache 中的数据块是多个会话共享的。也就是说任何会话在读取同一个块时都要先检查它是否已经在 cache 里。2.2 read by other session 的准确语义它卡在哪里现在场景变成会话 A 已经发现块不在 cache 里正在发起物理 I/O还没读完。此时会话 B、C、D 也来读同一个数据块它们在 buffer cache 里查找时同样没有命中但 Oracle 不会让 B、C、D 各自都去发一遍物理 I/O而是只让第一个会话负责读取后面的会话全部挂起等待第一个会话完成。这些被挂起等别人读盘的会话等待事件就是read by other session。换句话说read by other session 的会话本身并没有发起任何磁盘 I/O它是在等别人的 I/O 完成。等第一个人读完把数据块挂到 buffer cache 上后面排队的会话就能立刻从内存里取块继续跑。用一个不太严谨但好记的类比快递柜里同一个格子的包裹被别人取走了后来的人都得站在柜子前排成一排等柜员把下一个包裹从仓库搬过来。2.3 与相似等待事件的对比别再搞混很多 DBA 会把 read by other session 和另外几个等待事件搞混这里我列一个简单对照表等待事件自己是否发起了物理 I/O等待的本质db file sequential read是等自己的单块读完成db file scattered read是等自己的多块读完成read by other session否等别人的单块读完成然后共享数据buffer busy waits否等别人释放已经在内存中的缓冲区注意read by other session 是从 Oracle 11g 开始从 buffer busy waits 里拆分出来的11g 之前它被统称为 buffer busy waits。这种拆分让问题定位更精确如果大量会话卡在 read by other session说明争抢的焦点是从磁盘读入 cache这个动作如果卡在 buffer busy waits说明块已经在内存里但有人在改或同时在读缓冲区被占用了。两者的排查方向完全不同。2.4 为什么 read by other session 特别容易引发雪崩这个等待事件最危险的地方在于它造成的阻塞成本会随着并发数线性放大。正常情况下一个会话读一个块可能只等 2ms但当 100 个会话同时要读 10 个新块时只有 10 个会话真正在等磁盘剩下 90 个全部排在 read by other session 上。如果这些会话背后又是高频业务接口新的请求还在不断进来等待链就会越滚越大最终表现为活动会话数暴涨、接口响应时间失控。这也是为什么遇到这个等待事件不能只盯着存储性能找原因——问题往往出在并发模型和对象设计上。3. 热点块定位从会话、等待事件到具体对象的完整链路3.1 用 gv$session 抓当前正在等待的会话既然等待事件已经确认下一步就是把正在等待的会话和它们等的是哪个数据块捞出来。在 RAC 环境里用 gv$session单实例用 v$session 即可select inst_id, sid, serial#, sql_id, p1, p2, p3, seconds_in_wait from gv$session where event read by other session and wait_class Idle and status ACTIVE order by seconds_in_wait desc;这里的关键是 p1、p2、p3 三个参数。对于 read by other session 这个等待事件p1 是文件号file#p2 是块号block#p3 是块类型class。我当时查出来的结果非常有规律大部分会话的 p1 都等于 43p2 集中在 189250 到 189300 这个区间还有一小部分集中在 185400 附近。这意味着什么说明几百个会话不是分散等待而是全部挤在同一小段块范围上。这种扎堆特征基本排除了随机 IO 问题直接指向热点对象。3.2 把文件号和块号翻译成业务对象有了 file# 和 block#接下来用 dba_extents 把块号映射到段对象。这里有个小坑dba_extents 里存的是段的起始块号所以要判断某个目标块属于哪个段不能用等于要用范围判断select owner, segment_name, segment_type, partition_name from dba_extents where file_id 43 and 189270 between block_id and block_id blocks - 1;我当时执行后得到两条关键结果块 189250-189300 对应的是T_ORDER表也就是核心订单表。块 185400 附近对应的是IDX_T_ORDER_CRT_TIME索引一个基于 create_time 的普通索引。这个结果基本解释了现象既有表数据块的热点也有索引块的热点。问题范围进一步缩小到订单相关的读取路径上。3.3 借 ASH 把历史等待拉出来做定量分析当前会话只能反映抓取瞬间的快照要判断问题持续了多久、影响多大必须看 ASH 历史采样。我跑了这么一条select sql_id, count(*), round(sum(delta_time)/1000000, 1) as ash_secs from gv$active_session_history where sample_time sysdate - 1 and event read by other session group by sql_id order by 3 desc;结果第一名非常集中一个 SQL_ID 占了 ASH 时间里的绝大部分。这个 SQL 就是应用端负责查询待处理订单列表的接口语句。我把它执行计划拉出来一句一句拆解时根因基本就浮出水面了。3.4 三个关键事实对象、SQL、时机到这里定位阶段需要确认的三件事已经全部落地热点对象T_ORDER 表数据块 IDX_T_ORDER_CRT_TIME 索引块。热点 SQL查询待处理订单列表的接口 SQL高频轮询。热点时机大促预热期开始后某个大商家集中下单订单数据在短时间内集中写入同一个分区和时间窗口。4. 根因剖析为什么全体会话都堵在同一批数据块上4.1 业务侧的导火索大商家订单集中落在同一批块故障当天业务侧有个明显的异常某一个头部商家做了整点秒杀活动用户在几十秒内集中下单。订单表 T_ORDER 是按月范围分区存储的所有新订单都落入当前月分区的末尾数据块。按每块大约 100 行订单估算几万笔新订单会集中在几百个块里。正常情况下这也就是几百个块的访问压力不至于出大问题。真正致命的是这些块对 buffer cache 来说是新块——每次有一批新订单落盘后第一次被查询访问时都需要从磁盘读入内存。在高并发查询下这些冷块瞬间变成热块。4.2 SQL 侧的执行计划退化索引扫描范围被放大查询语句简化后长这样select * from t_order where order_status PENDING and create_time trunc(sysdate) - 2 order by create_time desc;这条 SQL 本意是查最近两天待处理的订单。问题出在执行计划优化器没有走 order_status 的过滤条件而是选择了IDX_T_ORDER_CRT_TIME这个 create_time 索引做范围扫描。因为 create_time 的范围是最近两天索引扫描范围非常大扫描过程中需要回表访问最近两天的订单数据块。再加上应用有 60 个服务实例每 5 秒轮询一次这个接口等价于每秒并发 12 次以上每次都去扫同一个两天时间窗的数据。大量回表和大量新块首次读取叠加在一起read by other session 不爆才怪。为什么优化器会选错执行计划事后检查发现订单表的统计信息在故障前几小时被一个批量 job 刷新过但刷新时采集到的数据分布和故障时的真实数据分布严重不一致。简单说统计信息过期导致优化器对 order_status PENDING 的选择率估算偏高认为走这个条件也会扫很多行不如走 create_time 索引。4.3 索引右侧叶块的热点为什么很难躲另一个容易被忽略但是真实存在的热点在IDX_T_ORDER_CRT_TIME索引本身。普通 B-tree 索引有个特点索引条目按键值顺序追加。create_time 是递增的新插入的索引条目永远落在索引最右侧的叶子块上。当并发插入和并发范围查询同时出现在同一个右叶子块上时这个块会成为整个索引的超级热点。这次故障中大量新订单插入时刻在推进 create_time 索引的最右块同时高频范围查询也在扫这个最右块。前面 read by other session 的等待里确实包含了这个索引块的等待。这也是为什么单纯优化表数据块还不够必须同时考虑索引设计。4.4 根因小结三层因素叠加复盘下来这次故障不是单一原因而是三层因素叠加数据分布层大商家秒杀导致订单数据在时间和空间上高度集中。SQL 和索引层缺少匹配等值 范围查询的复合索引优化器退化成大范围索引扫描 大面积回表。应用并发层高频轮询模式把瞬时并发放到最大60 个实例齐刷刷地打向同一个对象。这三层里任何一层单独存在都不会出大问题叠在一起就形成了一次典型的 read by other session 雪崩。5. 优化方案落地从应急止损到彻底消肿5.1 应急止血限流、杀会话与临时改执行计划故障处理的第一优先级永远是止血。当时我们做了三件事第一和应用组协调把订单查询接口的轮询频率从 5 秒改成 30 秒瞬时并发直接降到原来的六分之一。这一步立竿见影read by other session 的会话数在几分钟内开始回落。第二对确实长时间卡在等待中的会话做定点清理。注意是定点不是无差别 kill。我只处理了满足两个条件的会话等待时长超过 30 秒且对应的业务确实已经超时返回。清理脚本长这样begin for r in (select inst_id, sid, serial# from gv$session where event read by other session and seconds_in_wait 30) loop execute immediate alter system disconnect session || r.sid || , || r.serial# || immediate; end loop; end; /写这段脚本时候我心里很清楚kill 会话只是给后面争取时间如果根因不解决几分钟后还会继续堆积。第三临时用 hint 先让 SQL 走另一个可用的索引避开 create_time 大范围扫描带来的回表压力。这个操作也是过渡性的目的是先给业务一个可用的执行计划真正确认是否有效还需要看后续优化动作。5.2 应用 SQL 改写与增量轮询设计应急动作完成后我开始推动根本性修复。第一刀切在应用 SQL 上。原来的 SQL 是每次轮询都查最近两天的全部待处理订单这本身就存在设计缺陷。真正的业务诉求其实是拉取从上一次处理之后新产生的待处理订单完全没必要每次扫两天的数据。我给出的改写建议是把轮询改造成增量模式select * from t_order where order_status PENDING and create_time :last_pull_time order by create_time desc;这样每次查询只需要拉取上次游标之后新增的少量订单数据量从几十万行降到几百行。应用侧改动并不复杂却能从源头上砍掉绝大部分无效的物理读和逻辑读。5.3 长效手段复合索引与数据归档第二刀切在索引设计上。原 SQL 的过滤条件包含order_status等值匹配和create_time范围排序。最合适的方案是建立一个复合索引create index idx_t_order_status_ct on t_order(order_status, create_time) tablespace idx_ts online parallel 8; alter index idx_t_order_status_ct noparallel;这个索引带来的改变有两点一是让优化器可以用order_status PENDING先做等值过滤把扫描范围缩小到待处理订单这一小撮数据二是索引已经包含 create_time 排序可以直接按序输出避免大量回表排序。故障当天建完这个索引后那条 SQL 的执行计划立刻切换逻辑读从单次几万降到了几百。与此同时我还推动了一个数据归档方案对 create_time 超过 3 个月且状态为终态的订单做离线归档控制在线表体积。虽然这不是当天能完成的动作但对后续大促峰值稳定性是有实际意义的。5.4 优化效果等待事件与业务指标双双回落优化措施全部落地后我等了半个小时拉了一份新的 AWR 报告和故障时段做对比指标优化前10:00-11:00优化后11:30-12:15read by other session 平均每秒等待次数61012平均活跃会话31085P99 响应时间8s280msDB Time18,600s4,100sread by other session 基本从视图里消失数据库各项指标回到了故障前的正常水位。后续一整天直到大促预热结束这个等待事件都没有再抬头。这次处理也再次证明了一个原则等待事件只是报警器真正要下功夫的是报警器背后那根烧断的保险丝。6. 复盘沉淀把 read by other session 写进监控与评审基线6.1 在监控系统里单独建一条等待事件预警过去我们的数据库监控主要盯 CPU、内存、IO 和锁等待对具体等待事件的关注不够细化。这次故障后我在监控系统里单独为 read by other session 配置了两级告警等待次数每分钟超过 500 次且持续 5 分钟进入 P2 告警等待会话数超过 50 个或平均等待时间超过 20ms进入 P1 告警。同时把告警事件自动关联到热点对象定位脚本一旦触发可以直接输出文件号、块号、所属段对象、关联 SQL_ID这几项信息省去后期人工排查的时间。后来第二个月又遇到过一次类似苗头告警比业务反馈来得更早处理起来从容得多。6.2 大促前的热点扫描清单这次故障的导火索是大促所以事后复盘时我们专门做了一份大促前的检查清单核心就几条梳理所有高频轮询 SQL评估调用频率是否需要限制拉取核心表的 Top SQL 执行计划确认没有隐藏的全表扫描和大范围回表检查核心表数据倾斜风险特别是头部商家/头部用户可能造成的单点数据集中检查统计信息收集任务的执行时间确保不会因为批量任务导致核心表的统计信息过期。这份清单后来在大促前反复使用确实帮我们提前发现过几次潜在问题。6.3 把它沉淀进 SQL Review 和变更评审更长期的改进是把 read by other session 的风险意识固化到日常流程里。现在的 SQL Review 评审里多了两条硬性检查项一是新上线的高频查询是否可能产生大量物理读二是过滤条件和排序字段是否有匹配的复合索引而不是依赖单个字段索引碰运气。我后来跟团队反复强调一个观点read by other session 的英文名里有个 other但它从来不是别人的问题而是自己这一侧出现了并发扎堆的信号。监控和告警只是让我们看见它真正解决它还是要回到 SQL、索引和数据分布这些基本功上。这次故障能在一个多小时内稳住靠的也是对这块基本功的排查——先看懂等待事件再顺着热点块一路追到 SQL最后用索引和改写把它根掉。整个过程没有一步是玄学每一步都是可以复现的排查动作。
返回列表