ARTICLE DETAIL

资讯详情

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

PolarDB从节点异常排查复盘:从复制延迟到慢查询的根因与恢复

PolarDB从节点异常排查复盘:从复制延迟到慢查询的根因与恢复 大年初七开工第一天我人还没从节后综合征里缓过来手机就连续震了七八下直接被拉进了一个“PolarDB从节点异常”的应急群。群里消息一条比一条急“报表查不出来了”“只读地址连不上”“从节点是不是挂了”。那一刻脑子是懵的但手已经很诚实地打开了阿里云控制台。开年第一周就把自己这个“大能人”系列的第一篇写成了“丢人现眼”现场确实是我没想到的。不过回过头看这次事故虽然丢人却特别有复盘价值。从节点不能用这件事表面看是“数据库挂了”实际上链路长得很——可能是回放积压、慢查询打爆内存、连接地址配置、节点保护机制甚至节前遗留的大事务每个环节都能单独写一篇。这篇文章就把我这次完整的排查和恢复过程拆开讲一遍包括 PolarDB 从节点的工作原理、我是怎么一步步定位根因的、怎么止血恢复的以及后来做了哪些防护才敢保证下次不再“开年就丢人”。如果你也负责 PolarDB 集群或者正在用云数据库的只读节点分担查询压力这篇应该能帮你少踩几个坑。1. 开年第一天的下马威从节点状态直接飘红1.1 节后返工还没喝上咖啡就被拉进应急群先说下背景。我们这套 PolarDB MySQL 版集群是去年上线的主力业务库一主两从三个计算节点共享底下的分布式存储。主节点正常承接读写两个从节点主要是给报表、统计、运营后台这类查询业务用的业务侧通过只读连接串访问。年前最后一件事还是我亲自做的巡检当时一切正常假期里也没收到任何告警所以我默认它是稳的。结果开工第一天我甚至连工位都没坐热报表组的同事就在大群里喊“查不出数”。我一开始还以为只是报表 SQL 写得有问题等我自己用只读地址连了一下发现不对劲——连接能建但任何查询都卡在那里等十几秒就超时部分情况下直接报“节点不可用”之类的错误。再上控制台一看一个从节点的状态已经不是“运行中”了显示异常。那一刻我就意识到这已经不是“某条 SQL 慢”的级别了是从节点整体出问题了。1.2 第一轮信息收集现象、范围、影响遇到这种情况第一件事不是急着重启而是先把“影响面”摸清楚。我当时在应急群里快速确认了几个问题只有报表业务受影响还是主站的读写也受影响是从节点地址连不上还是连接上了但查询超时两个从节点都挂了还是只挂了一个最近一次变更是什么有没有节前遗留下来的批量任务信息收集的结果是主节点完全正常核心交易链路没受影响两个从节点里只挂了一个另一个虽然延迟比平时高但还能响应查询出问题的是业务直接连的那个只读地址而集群的只读负载均衡地址似乎已经把异常节点摘掉了走那个地址的请求有一部分还是好的。这个信息很关键。它让我判断出PolarDB 的集群地址本身是有健康检查机制的异常节点会被自动摘除但业务长期拿着“只读节点独立地址”直连就等于绕过了这层保护所以节点一挂直连的业务就立刻感知了。1.3 先分清“不能用”的三种程度我后来总结所谓“从节点不能用”其实可以分成三个层级处理方式完全不同第一种是“连不上”一般是网络、白名单、节点宕机或负载均衡把节点摘掉了第二种是“连得上但查询极慢/超时”通常是 CPU 打满、内存压力大、锁等待或回放积压第三种是“数据是旧的”就是复制延迟太高查出来的数据和主库对不上。这三种情况经常叠加出现。比如这次就是因为节点内部已经处在高负载的亚健康状态查询迟迟不返回才让业务侧感觉连不上。因此不要一看到“不能用”就重启节点先确认到底属于哪一种再动手。2. 理清 PolarDB 从节点的工作机制才知道该往哪查2.1 共享存储架构下从节点到底在干什么很多人一想到“从节点”就会拿传统 MySQL 主从复制那套逻辑来套觉得无非就是主库产生 binlog从库拉过来重放。但 PolarDB 不是这个逻辑它的存储层是共享分布式存储三个计算节点实际上访问的是同一份数据文件。主节点把 redo log 实时写入共享存储从节点要做的事情是读取这些 redo log在自己的内存里做并行回放把数据页更新到最新状态。所以从节点不是“另存一份数据”而是“在同一份数据上追赶最新状态”。这个架构的好处是存储成本低、从节点不占额外磁盘、可以快速添加只读节点但它的命门也在这——从节点的性能很大程度上取决于 redo 回放能不能跟上主节点的写入速度。一旦主库产生了超大事务或者某一段时间写入量猛增redo 体积会瞬间放大从节点回放就会出现积压也就是我们常说的“复制延迟”。2.2 从节点“不能用”的常见原因清单结合我这次踩坑的经历和平时的运维经验PolarDB 只读节点异常常见原因大致是下面几类大事务或 DDL 引起的 redo 积压从节点回放追不上延迟持续走高查询看到的数据越来越旧最终可能导致节点被保护从节点上跑了大量慢查询尤其像笛卡尔积、全表扫描、超大排序、无 limit 的深分页这些查询会把 CPU、内存打满影响回放线程的同时还让正常查询全部排队连接数打满应用侧连接池配置了过大的最大连接数或者有连接泄漏从节点连接数达到上限新连接直接被拒临时表空间或缓冲池内存不足大批量排序/分组操作把内存耗尽触发 OOM节点假死业务直连独立只读地址绕过了集群地址的健康检查节点异常时流量不会自动摘除导致故障影响被放大。2.3 为什么这次不是“复制延迟”一句话能解释的我在控制台第一眼确实看到了复制延迟的监控曲线在节前最后一天晚上开始往上翘到开工前已经到了一个很夸张的数字。我最初也以为只是延迟问题但后来发现只靠“延迟高”解释不了一个现象为什么另一个从节点延迟也不低但它还能正常查询说明除了回放积压还有别的东西在起作用。后来我逐步排查才发现出问题的那个从节点在延迟积压的同时还承接了大量报表查询这些查询里面有几个特别重的把节点的内存和 CPU 直接吃干了。也就是说真正的故障是“回放积压”和“查询资源争抢”两个问题在同一个节点上叠加。如果没有进一步看会话和慢查询只盯着延迟做文章可能永远找不到真正的导火索。3. 现场排查的完整链路控制台、监控、慢日志、长事务3.1 控制台初检状态、负载、延迟三块面板我的排查第一步永远是先看 PolarDB 控制台上的三块面板节点状态、负载监控、复制延迟。节点状态页面上那个出问题的只读节点显示“异常”点击进去能看到具体指标。负载监控里CPU 使用率已经持续 100% 很久内存使用率也接近上限连接数接近 max_connections 的阈值。复制延迟的曲线是一条陡峭的上升线看起来非常吓人。这里有一个经验看到 CPU 100% 和内存 100% 同时出现基本可以断定节点内部已经发生了资源竞争而不是单纯写入量大的问题。因为 redo 回放本身虽然消耗 CPU但通常不会把内存顶到这么高内存被打满大概率是有查询在大量使用临时表或排序缓冲。所以当时的第一个判断是有坏 SQL 在从节点上跑。3.2 实例会话与慢查询快速揪出“打死”从节点的 SQL接下来我在控制台的“会话管理”里或者在从节点上用 SQL 直接查当前会话列表SHOW FULL PROCESSLIST;这个命令很基础但对于快速定位问题特别有效。一眼扫过去我发现从节点上有十多个会话处于“Query”状态执行时间从几十分钟到几个小时不等。最夸张的一个查询执行时间已经超过两小时来源是一个报表统计任务。再看慢查询日志定位到了问题的另一部分几个 SQL 都有明显的性能问题有的一上来就做了三个大表的 JOIN 没有过滤条件有的对几十万行做 GROUP BY 排序还有一个查询在 ORDER BY 一个非索引字段导致排序缓冲区被反复打满临时表刷到磁盘又不断膨胀。这些查询平时可能也不快但也不至于把节点打死。可它们和 redo 回放抢 CPU、抢内存就成了压垮骆驼的最后一根稻草。节点内存一旦不足部分查询开始报错连接堆积最终新查询全部排队业务侧表现就是“从节点不能用”。3.3 大事务现场还原节前那个数据订正脚本解决了“谁在打节点”的问题还要解决“为什么 delay 会这么高”。我通过 cluster 事件和主节点的 binlog/redo 情况往前倒查。最后定位到节前最后一个工作日开发同学跑了一个数据订正脚本对一张核心业务大表做 UPDATE。本来这种订正是常规操作但问题出在脚本的写法上它没有做分批处理一条 UPDATE 语句直接更新了整张表的上千万行。单条大事务在主库执行了很久产生的 redo 日志量非常大从节点要等事务提交后把这部分日志完整回放回放期间又不断有新的业务写入产生新的 redo雪球越滚越大。所以从节点延迟飙高不是从开工那天才开始的而是从春节前那个大事务提交之后就已经埋下伏笔了。假期期间业务写入量低延迟没有爆炸节后查询量一上来问题瞬间暴露。3.4 根因定论回放积压与查询争抢的叠加效应到这里整条链路就清楚了。我把它理成一串因果节前一条超大 UPDATE产生了海量 redo log从节点回放积压开始累积假期中业务低峰延迟居高不下但没有造成明显故障开工第一天报表查询集中涌向从节点个别低效 SQL 直接打满 CPU 和内存回放线程和查询线程互相争抢资源回放更慢查询也全部超时节点进入亚健康状态业务直连只读地址的请求大量失败造成“从节点不能用”的群体感知。这里要特别提醒一点如果没有看会话列表只盯着“延迟高”很可能把问题误判成单纯的复制延迟然后傻傻地等它追平。但实际上节点已经被慢查询打满了延迟永远追不平问题永远不会自己好。4. 应急恢复全操作从 Kill 会话到延迟追平4.1 第一步立即止血干掉最耗资源的查询会话定位到问题之后恢复动作要快但也要有优先级。我做的第一件事不是重启节点而是先 kill 掉从节点上那几个跑了几个小时的低效查询会话。操作上直接在会话管理界面里找到对应的会话 ID执行 kill或者在命令行里执行KILL thread_id;这里有个细节kill 的时候要优先处理执行时间最长、状态是“Copy to tmp table”“Sorting result”“Sending data”的查询会话。这类会话往往占着大量 CPU 和内存kill 掉之后资源会立刻释放一部分。我一次性 kill 了五个问题会话节点的 CPU 使用率很快从 100% 降到了 60% 左右连接排队的情况明显缓解。注意不要在主节点上这么随意 kill。如果主节点有大量写入事务贸然 kill 可能会造成应用层报错需要和业务确认。但从节点是只读的kill 掉的是查询会话影响相对可控恢复业务是第一位的。4.2 第二步处理节点本能重启从节点前后kill 完慢查询之后节点能响应了但还有一个问题没解决——那些积压的 redo log 还需要时间回放。我观察了一下延迟下降的速度发现按照当时的回放速率要追平还需要不少时间。而且从节点在经历过内存高水位之后buffer pool 里的数据页很可能已经“不干净”了继续跑着效率也不高。这种情况下我选择在控制台对这个只读节点做一次重启。PolarDB 的从节点是共享存储架构重启不会丢失数据它会在启动后重新从共享存储读取 redo log 并回放相当于用一个新的内存状态去追赶往往比重负载状态下边查询边回放更快。不过这里要提醒重启前一定要确认有什么事不能中断。如果你有业务直连这个只读节点的独立地址重启期间连接会断开应用侧需要配置好重连机制。我们的应用连接池有自动重连所以影响窗口很短。4.3 第三步盯紧复制延迟等 redo 回放追平重启完成后节点状态恢复“运行中”但延迟可能仍然很高。这时候不要急着对外宣布“恢复了”要持续观察复制延迟的下降趋势。我在控制台打开了“复制延迟”监控同时用下面这类 SQL 在从节点上确认当前回放位置和主节点的差距SHOW REPLICA STATUS;虽然 PolarDB 的逻辑和传统主从不完全一样但这个命令依然能反映部分同步状态控制台监控里也有专门的延迟指标。重点看延迟曲线是不是在稳步下降。如果重启后延迟还是不动甚至往上走那就要怀疑回放线程本身出了问题可能需要提工单让云厂商介入了。我们这次重启后延迟从几万秒开始稳步下降配合低峰期大概花了不到一小时就追平了。追平之后我再用只读地址跑了一下核心报表查询返回正常数据也和主库一致业务侧确认恢复。至此应急阶段结束。4.4 业务验证与恢复确认恢复不只是“节点能查了”就结束。我让报表组的同事把早上跑失败的几个任务重新跑了一遍同时重点验证了几个之前卡死的 SQL。结果有一个 SQL 仍然跑得很慢只是这次不会把节点打死了。这也给我提了个醒如果不对烂 SQL 做治理类似的故障迟早还会再来一遍。5. 复盘总结与长效防护同样的坑不能踩两次5.1 大事务治理从源头拆分与限流这次事故最底层的根源是那条一次性更新上千万行的大事务。这类大事务对 PolarDB 这种 redo 回放架构的伤害比想象中更大。它不只是让主库锁表、产生大事务那么简单——redo 体积暴涨从节点的回放延迟会被瞬间拉高影响所有查询只读业务的实时性。经过这次我直接在团队里定了一条规矩任何 UPDATE、DELETE、INSERT...SELECT 操作都必须分批执行。批大小根据表的行宽来定一般每批 500 到 2000 行批与批之间至少 sleep 50 到 100 毫秒。示例写法类似于-- 伪代码示意按主键分批更新 DELIMITER $$ CREATE PROCEDURE batch_update() BEGIN DECLARE v_min_id BIGINT DEFAULT 0; DECLARE v_max_id BIGINT; SELECT MIN(id), MAX(id) INTO v_min_id, v_max_id FROM target_table WHERE update_flag 0; WHILE v_min_id v_max_id DO UPDATE target_table SET status 2, update_time NOW() WHERE id BETWEEN v_min_id AND v_min_id 1000 AND update_flag 0; SET v_min_id v_min_id 1001; DO SLEEP(0.1); END WHILE; END$$ DELIMITER ;而且要在任务调度平台里加超时和行数限制防止脚本失控。宁可多花一点时间跑完也不要为省时间把整个集群拖下水。5.2 只读侧查询规范超时、隔离、走对连接地址从节点上跑的查询尤其是报表类查询必须有个“紧箍咒”。我在 PolarDB 参数层面给只读节点设置了max_execution_time限制单条 SELECT 的最长执行时间防止一条烂 SQL 跑几个小时。当然这个参数要跟业务协商好阈值我们设置的是 60 秒超过直接报错让开发去优化 SQL而不是让它在从节点上耗着。同时建议所有查询业务尽量改动连接配置走 PolarDB 集群的只读负载均衡地址而不是长期直连某一个从节点的独立地址。集群地址会在节点异常时自动摘除流量虽然故障期间会有一小段时间影响但能够避免整个节点被打死后所有查询全部超时的极端情况。如果业务对实时性要求不高但又经常跑重查询更好的方案是单独再增加一个只读节点专门给重报表用和在线业务的常规查询做资源隔离。PolarDB 集群本身支持增加只读节点成本可控比所有查询挤在一两个节点上安全得多。5.3 告警与容量规划把“开年惊喜”变成日常可控最后是监控告警。复盘的时候我发现节前大事务提交之后复制延迟其实已经开始异常上升但我们的告警阈值设得太宽松假期期间根本没人关注一直拖到开工才爆出来。事故后我把这几条告警全部重新配置了一遍复制延迟超过 30 秒立即告警之前是 600 秒只读节点 CPU 超过 85% 持续 5 分钟告警只读节点内存使用率超过 85% 持续 5 分钟告警连接数达到 max_connections 的 80% 告警慢查询数量在 5 分钟内超过某个阈值告警。另外容量规划上我也重新评估了一遍。从节点的规格略低于主节点平时够用但一旦回放积压和重查询叠加就很容易崩溃。对于需要跑大量报表分析业务的集群建议从节点规格不要低于主节点规格条件允许的话可以稍微超出给回放线程和查询线程都留足余量。经过这一轮折腾从节点稳定运行至今没有再出过类似问题。我自己最大的体会是数据库故障很少是单一原因它往往是一连串小问题恰好叠在了一起。只看表面现象很容易做出错误的处置。把排查链路走完整把治理措施做到前面才能让“开年就丢人”这种事只发生一次。
返回列表