ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JVM与连接池参数失衡引发的线上事故:从GC异常到P99飙升的完整复盘

JVM与连接池参数失衡引发的线上事故:从GC异常到P99飙升的完整复盘 1. 事件背景一次本不该发生的“低峰期事故”4月9日凌晨1点40分我的手机在床头柜上连续震动了几次。这个时间点来消息基本没好事——果然值班群里的告警截图显示线上订单服务的P99延迟从平时的80ms直接飙到了3.2秒错误率从0.02%爬到了11.7%。更让人头疼的是我们当时刚好在0点30分开始做一次常规发版按计划只发布了8台机器中的2台也就是25%的灰度批次。一开始所有人的第一反应都一样是不是新代码捅了娄子毕竟改动里有一条和订单状态流转相关的逻辑虽然自测、联调、预发环境都跑过一遍但线上环境的数据分布和流量特征总归不一样。我当时的第一反应也是回滚但再看了几分钟监控以后我反而冷静下来了——因为出问题的请求分布压根没有集中在刚发版的这两台机器上6台老版本机器的错误率也在同步猛涨。这个细节很关键如果真是新代码引入的Bug不可能出现“新旧版本一起挂”的情况。于是决策从“立刻回滚”变成了“先定位再决定”。事后回看整个排障过程我有几个特别想分享给同行的体会凌晨的故障处理最忌讳的就是凭第一直觉做动作回滚本身也有成本如果根因不在发版代码里回滚完问题依旧存在反而会浪费最宝贵的排查窗口期。告警信息不能只看“有没有”要看“分布在哪”。同样的错误率上涨集中在新版本机器和均匀分布在所有机器上指向的完全是两类问题。监控指标的粒度决定了你的定位速度。如果只盯着“接口平均耗时”你什么都看不出来必须把线程数、GC、连接池、数据库慢查询这些底层指标并列着看才可能拼出完整画面。这起事故从发生到彻底确认根因前后花了大约50分钟。复盘的时候我们拉了完整的工单记录、监控截图和操作日志发现整个排查链路里其实有好几个地方是可以提前规避的——不管是监控盲区、参数配置的隐患还是发布节奏的设计单独拿出来看都不是致命问题但叠在一起就变成了一场40分钟的线上故障。这篇文章的复盘不会只停留在“发生了什么”我会把当时完整的时间线、每一步的判断依据、以及最终确认的根因链路全部展开也会把我们在事后做的改进整理成可以直接抄作业的清单。如果你负责的服务也依赖JVM、连接池这类基础组件哪怕技术栈不完全一样里面涉及的排查思路和配置经验应该都能给你一些参考。先简单交代一下当时的系统形态。这套订单服务是标准的Spring Boot Dubbo架构部署在8台4核8G的容器上JVM堆内存设置的是4G使用CMS垃圾回收器。服务依赖一个MySQL主库承接订单表读写和一个Redis集群做基础缓存。数据库连接池用的是Druid配置了maxActive20。服务承接的流量不算高日常峰值QPS大概在300左右所以给到连接池和JVM的资源其实是比较宽裕的。正因为量级不算大所以出事之后所有人都很意外——按常理说这种量级下连“性能瓶颈”的边都摸不到。但恰恰是这种“觉得够用就行”的心态埋下了后面几个坑。2. 第一轮排查的时间线从异常定位到监控盲区被逐步揭开故障处理讲究一个“时间线越清晰复盘越有价值”。我现在把当天从告警到初步定位的过程按时间顺序整理出来每一个节点标上我当时的判断依据这样比直接抛出结论更有参考意义。2.1 告警触发后的前5分钟大家都在盯表象凌晨1点40分告警群弹出第一条消息订单服务的“接口成功率”低于99%阈值具体数字为98.3%。紧接着1点42分“P99耗时”告警触发从基线的80ms涨到1秒以上。1点45分我登录跳板机连上监控面板第一眼看到的是“订单创建接口QPS”还在正常范围但“异常数”在快速增长。这时候团队里已经有人在群里问要不要先回滚刚才发的新版本我刚才说了我的判断是先看错误分布的来源。所以操作的顺序是先看网关层有没有5xx状态码的集中爆发判断是不是入口流量有问题。再打开服务的监控面板按“版本号”维度看两台新版本机器和六台老版本机器的错误率曲线是否一致。如果一致说明和代码变更无关问题出在更底层。这三步花了大概4分钟。结果非常明确——8台机器的错误率曲线几乎是重合的没有任何一台单独恶化。这说明问题不在刚发布的代码里于是“回滚”这个选项暂时被排除排查方向转向了基础设施和中间件。2.2 第5到第15分钟数据库和Redis先被排除凌晨的故障排查优先级最高的永远是有没有直接依赖的“外部存储”。因为服务内部出问题通常会导致单机或单集群异常而存储中间件出问题会导致大面积雪崩。所以我们第二轮的排查目标直接锁定了MySQL和Redis。当时用到的命令和工具如下# 查看数据库当前活跃连接数 mysql SHOW STATUS LIKE Threads_connected; mysql SHOW STATUS LIKE Threads_running; # 查看是否有长时间未提交的事务 SELECT * FROM information_schema.innodb_trx\G # 慢查询日志检查在线开启临时开关 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;MySQL这边的指标很干净Threads_connected稳定在80到100之间远低于max_connections我们设置的是500Threads_running基本在3到5之间徘徊慢查询日志里只有几条毫秒级的普通查询没有任何超过1秒的SQL。这说明数据库层面没有锁等待、没有连接打满、没有慢SQL堆积。Redis这边用了redis-cli --latency测了一下实时延迟均值稳定在0.3ms以内内存和连接数也都在正常水位。两个最核心的依赖都被排除了排查方向开始收敛到应用本身。2.3 第15到第30分钟从JVM监控中找到“毛刺”数据库和Redis没问题那问题大概率出在Java应用进程自身。我们当时的车是自建监控系统JVM的指标采集粒度是30秒一次Grafana面板里能看到Heap、Old Gen、GC次数和耗时。打开面板的瞬间我看到了当天最核心的一条线索——Old Gen的占用曲线出现了一个非常明显的上升台阶。具体表现是这样的凌晨0点30分之前Old Gen一直稳定在1.5G到1.8G之间很平稳。0点30分开始发版之后Old Gen以肉眼可见的速度往上爬。到1点30分左右Old Gen已经逼近3.5G接近4G堆内存的可用上限。1点42分CMS GC开始频繁触发每次Full GC耗时从平时的200ms左右暴涨到2到3秒。这里有个背景信息必须补充我们这套服务用的是4G堆内存Old Gen理论上最多占2.8G左右但实际运行时Old Gen超过2.5G就已经非常危险因为CMS的并发回收需要预留足够的空间否则会出现“Concurrent Mode Failure”并退化到Serial Old收集器那种STW停顿在低延迟服务里是毁灭性的。1点42分前后监控面板上的“Full GC次数”从每10分钟0到1次突然变成了每10分钟8到10次单次GC耗时普遍超过1秒。服务的P99耗时被GC停顿直接顶到了3秒以上错误率飙升的逻辑闭环到这里就通了——应用的业务线程在GC STW期间被集体冻结所有请求全部超时部分请求被服务框架的异步超时机制标记为失败。但问题是——底层逻辑通了之后新的问题又冒出来了为什么Old Gen会在凌晨这个低流量时段快速上涨流量没涨代码也没变内存怎么可能平白无故被撑爆3. 根因定位的完整链路从GC参数到连接池参数的连锁反应到这一步前面所有的排查都还只算“表象”真正的高潮在于搞清楚Old Gen为什么会在凌晨这个绝对低峰期被填满。这一段是整个复盘里最有含金量的部分我尽量把每一步的判断逻辑拆细一点。3.1 GC日志里的“异常对象”char[] 和 byte[] 占据了半壁江山为了搞清楚内存是被什么东西吃掉的我登录到那台问题最严重的机器上拉取了最近一小时的GC日志。# 查看GC日志文件重点看并发回收前后的堆占用变化 grep CMS gc.log | tail -100 # 如果GC日志没开可以用jstat手动拉取实时数据 jstat -gcutil 18211 1000 10GC日志里出现了一个非常典型的现象每次CMS GC的日志里Heap after GC的Old Gen部分有接近1.5G的内存无法被释放而且这部分内存的组成非常单一通过jmap -histo:live命令查看后排名前两位的是[C也就是char[]占用了约800MB。[B也就是byte[]占用了约500MB。char[]和byte[]是Java应用里最常见的两类数组——前者对应String的内部实现后者对应IO缓冲、网络帧、序列化结果等。如果你的服务在低峰期出现这两类对象的堆积最常见的可能性有两个要么是代码里有大字符串/大数组被缓存了而且无法回收要么是连接池或IO层的缓冲对象长时间驻留。结合当时的场景我们第一时间排除了“大对象缓存”——因为流量没涨如果有大对象被缓存不可能选择在凌晨这个时间点集中暴露。于是排查方向转入了连接层。3.2 被忽略的连接池参数maxActive20 背后的隐性压力这里必须引入一个很多人容易忽略的细节Druid连接池的“物理连接”和“业务线程”之间的关系。我们当时的Druid配置大概是这样的spring: datasource: druid: maxActive: 20 minIdle: 5 initialSize: 5 maxWait: 60000 testWhileIdle: true timeBetweenEvictionRunsMillis: 60000表面上看起来maxActive20对于QPS不到500的服务绰绰有余——毕竟正常的单次数据库操作只占用连接几十毫秒20个连接理论上能支撑每秒几百次查询。但这里的问题不在于“够不够用”而在于“当连接不够用的时候连接池会怎么表现”。在故障发生前的几个小时里由于业务方做了一轮数据补录白天的时候数据库出现过几次慢查询单次耗时达到2到3秒。这些慢查询不致命但它们在Druid连接池里占着连接不放导致连接池的“活跃连接数”一直处于中高位水平。等到凌晨流量回落后连接池里的连接虽然被逐步释放了但物理连接的TCP缓冲区里积累了大量未完全清空的半关闭连接——Druid默认会通过testWhileIdle定期检测空闲连接的可用性但对于已经被数据库主动断开的“死连接”检测的频次和灵敏度并不足以完全避免资源黑洞。更关键的是发布过程中新版本服务启动时需要建立一批新的数据库连接这会让连接池进入“扩容”状态。而连接池扩容时如果底层TCP建连因为某种原因变慢比如网络抖动、或者数据库端的back_log队列堆积Druid会持续尝试创建新连接甚至进入“获取连接超时”的循环。每一轮尝试都会创建一批byte[]缓冲对象作为网络读写缓冲区这些对象在连接创建失败后并不能立刻被GC回收——因为它们被连接池内部的ConnectionHolder对象强引用着要等池子完成清理才能释放。这个过程在白天慢查询夜间发版的组合下就像把一个本来只有20根管道的排水系统突然塞进了大量半堵塞的管道。连接获取变慢业务线程等待变长线程数堆积线程栈上挂着的业务对象包括String、byte[]、事务上下文全部变成GC Roots的强引用目标Old Gen占用自然就一路上涨。3.3 雪崩的启动按钮发布动作恰好踩中了GC的“回收临界点”到这里根因的逻辑链已经有了但还有一个关键问题没解释为什么这台机器偏偏在0点30分发布之后才开始出现Old Gen快速上涨如果是连接池的问题为什么凌晨之前没爆这个问题的答案在复盘时被我们总结为“三重巧合叠加”。发布动作本身不是根因但它恰好成了压垮骆驼的最后一根稻草第一重巧合新版本服务启动时Spring容器初始化要加载大量Bean定义其中包含了Dubbo服务的注册、配置中心的拉取、以及连接池初始化。这些对象在启动完成后大部分会变成不可达垃圾但因为在0点30分这个时间点老版本机器的Old Gen已经因为白天的慢查询积累到了一个较高水位约2.0G新版本服务的启动动作又额外增加了约200MB的短期对象把堆推向了2.5G这个临界点。第二重巧合CMS的并发回收有一个“提前触发阈值”——当Old Gen使用率达到-XX:CMSInitiatingOccupancyFraction设定的百分比时后台线程就开始并发标记。我们的设置是68%。当Old Gen达到2.7G左右4G堆的68%附近时CMS开始频繁进入并发回收阶段而并发回收期间业务线程依然在正常处理请求连接池的动态扩容又在持续产生新的不可回收对象导致“一边回收、一边增长”的拉锯战。第三重巧合凌晨的流量虽然低但恰好有一批定时批处理任务在0点30分同时触发这批任务会批量查询近一周的订单数据每条查询结果集都偏大涉及订单明细和商品快照且都在同一个事务里执行。这批任务运行期间数据库连接被长时间占用连接池可用连接数骤降业务请求开始排队最终把线程池打满触发Tomcat的拒绝策略返回大量错误。可以想象一下白天积累的半死连接、深夜启动的容器、高峰期错峰执行的批处理任务、加上CMS回收阈值设置不当——这四个条件任何一个单独出现都不可怕但当它们在同一时间窗口内叠到一起时就变成了一场系统性雪崩。每个环节单独看都有“合理的解释”但合在一起就超出了任何单一监控指标的预警能力。3.4 用一次“内存Dump”实锤根因逻辑分析归逻辑分析最终确认根因我们还是用了最传统也最扎实的手段——jmap导出堆内存快照然后离线分析。# 在问题机器上导出堆快照注意这一步会触发一次Full GC低峰期操作风险可控 jmap -dump:live,formatb,file/tmp/heap_0409.hprof 18211 # 把快照拉回本地用MAT或者JProfiler分析 scp rootX.X.X.X:/tmp/heap_0409.hprof /Users/me/heapdump/结果和预想完全一致。MAT的Histogram界面里byte[]的前三大引用路径分别指向Druid的DruidConnectionHolder及其内部的ConnectionBuffer。NettyDubbo底层通信框架的ChannelOutboundBuffer。Tomcat的NioEndpoint里的SocketWrapper。这些路径把“连接池残留连接 网络层缓冲 发布时的建连风暴”三个嫌疑对象全部实锤了。至于char[]主要来源是Druid连接池在初始化时创建的全量SQL预编译缓存PSCache——我们把poolPreparedStatements开到了true默认的缓存数量是每个连接缓存20条SQL。在慢查询变多时这些预编译对象的体积会变大而连接池迟迟不清理空闲连接这部分内存就变成了“沉默的钉子户”。到这一步我已经可以很确定地说事故的直接原因不是某个Bug或者某条慢SQL而是一组“配置参数在极端组合下的行为失衡”。这些参数单看都合理但缺少一个全局视角的统筹。4. 那些“加了等于没加”的监控措施和参数陷阱复盘过程中我们花了很长时间去讨论一个让人脸红的问题为什么这些隐患卧底了半年监控系统完全没发现不是没有监控而是监控的设计存在几个典型的“盲区叠加”。如果你也负责线上系统的稳定性这part应该会有不少共鸣。4.1 “平均耗时正常”不等于“可用性正常”我们当时的核心监控指标是“接口平均耗时”和“TP99耗时”。这两项指标在白天流量正常时表现得非常好——平均耗时在50ms左右TP99在120ms以内。但问题在于平均耗时会被大量低延迟请求稀释。即使有10%的请求因为连接获取变慢而耗时3秒平均耗时也只会上涨到350ms左右除非你把监控阈值卡死到50ms以下否则很容易被掩盖。那天的情况更是极端——凌晨本身就处于流量低谷期错误的请求数量虽然在报警阈值以上但绝对数量相比白天要少得多。如果只看“总请求成功率”零点到一点这个时段甚至还没达到我们设置的99.5%的告警阈值。真正触发告警的其实是P99耗时这个指标因为那一批批处理请求把尾部延迟顶得实在太狠了。这里给一个非常具体的建议监控必须同时盯“均值、中位数、P99、P99.9、最大值”这五个指标而且告警规则要分开配置。P99.9和最大值的告警阈值可以比均值放宽10倍以上但一旦触发就必须立刻人工介入。只配置“平均值”的监控系统等于给故障穿了一件隐身衣。4.2 GC监控和连接池监控两个边缘指标的手动补课我们用自建的监控系统采集了JVM的堆内存使用率和GC次数但监控面板太粗糙——只展示了堆内存的“总量”和“已用百分比”没有按“Eden/Survivor/Old Gen”分段展示更没有对单次GC耗时设置独立告警。CMS GC单次耗时超过1秒这件事在当时的监控面板上是看不到的因为它的展示维度是“每分钟GC总耗时”而低峰期的GC总耗时本身就很低。连接池的监控就更粗糙了——我们只关注Druid的activeCount是否接近maxActive恰恰没有关注关键的“等待获取连接的线程数”。在慢查询导致的连接池拥堵场景下activeCount可能一直保持在15/20左右看起来离上限还有距离但等待获取连接的线程数已经悄悄飙升到了50。这是一个非常典型的“指标接近上限才告警指标还没到上限就出事”的例子。经过这次事故我们把Druid的Web监控页面/druid/index.html的指标完整接入了自建监控重点盯三个指标waitThreadCount等待获取连接的线程数阈值设为5超过就告警。activePeak连接池活跃连接数的峰值用来辅助判断峰值时段的连接压力。discardCount连接被丢弃的次数这个数值如果持续增长说明连接池内部在不断地“清理死连接”往往伴随着底层网络的不稳定。4.3 “发布窗口”里的三次老生常谈每次复盘都会聊发布流程但聊归聊真正能落实的其实不多。这次的故障里发布动作本身不算直接原因但它确实起到了“扳机”的作用——因为它改动了连接池初始化的时间点让连接池的“半死连接清理”和“慢查询残留”刚好撞在一起。我们的发布方式是按批次滚动每批2台间隔10分钟。这个设计原本是为了“小步快跑有问题就止损”但问题在于发布窗口内新批次启动时前一批次机器正在处理的请求会重新路由到剩下未发布的机器上。如果你的发布批次没有等比扩容剩余机器的连接池和线程池需要在短时间内承接双倍流量。尤其在这个案例里剩余机器自身的连接池本来就已经处于“亚健康”状态半死连接、PSCache膨胀多承接一点流量就会把延迟拉长延迟拉长导致连接获取时间更长于是形成一个微型正反馈。这个问题的解法其实不复杂但需要很强的执行纪律发布前检查剩余机器的连接池活跃数、P99耗时是否低于基线发布批次之间增加一次“观察哨”窗口至少观察5分钟再决定是否继续。这些动作我们把它们固化成了一份checklist贴在了发布工具的首页每次上线必须逐项确认。5. 事后整改清单哪些参数和策略值得直接抄作业复盘会最怕的就是“会上痛心疾首会后涛声依旧”。这次我们定了一套可以量化验证的整改方案并且后续两周内逐步落地这里我把改动内容整理成表方便直接参考。5.1 连接池参数的重新校准参数原值调整后调整理由maxActive2050给低峰期批处理任务预留足够连接避免因为20个连接被占满就直接雪崩minIdle510提前保持一定数量的空闲连接减少动态建连带来的内存和CPU开销initialSize510和minIdle对齐启动时就完成连接建立避免启动阶段触发集中建连maxWait600003000原值太长应用会傻等60秒才放弃获取连接导致线程被拖死3秒足够覆盖绝大多数正常耗时超时后直接失败返回配合重试策略更健康testWhileIdletruetrue保留但把验证一次连接的SQL从SELECT 1改成SELECT id FROM order WHERE id -1避免极端情况下验证语句本身被数据库解析优化器卡住poolPreparedStatementstruefalse关闭PSCache这块内存收益不大但风险不小尤其在高版本MySQL下预编译对象体积比想象中大keepAlivefalsetrue时长60秒开启连接保活减少数据库主动断开连接导致的“半死连接”积累这里面有一个我特别想强调的点maxWait3000这个调整刚开始团队里有争议觉得3秒太短可能导致正常流量下的握手失败率上升。但实际上连接获取超过3秒本身就是一个极其危险的信号——说明连接池已经被严重堵塞了与其让大家排队傻等不如快速失败并触发重试。我们还配合了spring的重试机制对非幂等写操作做了Safe Idempotent处理这样就算个别请求失败也能在业务层自动补一次不会给用户可感知的影响。5.2 JVM参数的调整从CMS切换到G1并显式设置Region大小这次的根因里有很大一块是CMS在堆接近临界点时“一边回收一边增长”的拖沓表现。虽然这个事故的直接诱因不是GC算法本身但CMS在面对“突发性大对象分配连接池缓冲膨胀”时的表现确实不如G1来的从容。我们当时的JVM参数不全我把调整后的关键参数列出来-Xms4g -Xmx4g -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:G1HeapRegionSize4m -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/jvm/ -XX:PrintGCDetails -XX:PrintGCDateStamps -XX:PrintGCApplicationStoppedTime -Xloggc:/data/logs/gc.logG1和CMS相比最大的优势在于它对“碎片化”的容忍度更高。CMS在回收后容易产生大量内存碎片导致后续大对象分配直接触发Full GC或Serial Old回收而G1会把堆划分为多个Region在回收时会主动进行拷贝和压缩碎片化问题显著改善。对于连接池这种需要持续分配大小不等的byte[]缓冲的场景G1的整体表现会更平稳。G1HeapRegionSize4m这个参数是我后来特意加上的。G1默认会根据堆大小自动计算Region大小4G堆通常对应2M或4M但我们有一些连接池的缓冲对象和Dubbo的协议帧通常会超过2M如果Region太小大对象会直接分配到Humongous区域触发连续的标记和回收开销反而拖慢GC效率。手动固定4M后这块的GC耗时下降非常明显。5.3 批处理任务和发布窗口的“错峰策略”这个属于流程层面的整改但技术上也可以做一些保证。批处理任务原本由XXL-Job的默认调度线程池执行每天0点30分准时跑。我们后来调整成了“按订单ID分片”的方式将全量批处理拆成了5个分片分别延迟5分钟执行。另外加了一个“智能熔断”规则如果某个分片执行期间检测到数据库Threads_running超过20就主动让出CPU并延长下一次重试的间隔。发布窗口这边我们把“观察哨”机制做成了工具链的硬性步骤每批发布完CICD流水线会强制暂停等待监控平台的“健康分”评分达到80分以上才继续下一批。健康分的计算维度包括P99耗时、错误率、GC耗时、连接池等待线程数四个指标任何一个不合格就自动阻断发布通道。这个机制上线之后发布导致的事故数量下降了60%以上。5.4 监控盲区的补丁三个“新增看板”和两个“新增告警”监控的整改不再只是加几个Dashboard的问题而是重新设计了一套针对“亚健康状态”的告警规则。新增的看板有三个连接池健康看板展示maxActive、activeCount、waitThreadCount、discardCount、keepAlive连接数五个指标时间粒度细化到10秒。JVM分段看板把Eden、Survivor、Old Gen、MetaSpace分成四条独立曲线同时展示每次GC的耗时柱状图和GC后存活对象大小。发布影响看板在发布窗口内自动把“发布批次”和“错误率/耗时/GC/JVM”四张图叠放在一起方便定位发布动作和系统指标之间的因果关系。新增的告警规则有两条waitThreadCount连续3个采样点30秒超过5触发WARNING连续3分钟超过5触发CRITICAL。GC耗时单次超过800ms即触发WARNING无论该次GC是否引起了业务指标波动。这些规则上线后的第一个月就提前抓到了两次类似的隐患——都是连接池等待线程数先涨其他指标还没反应的阶段。这就是“把故障扼杀在苗头里”的典型效果比等P99飙到3秒再救火要从容得多。6. 复盘这件事本身也是需要“复盘”的最后这一段不聊技术细节了想聊聊“复盘”这个动作本身。我做技术复盘这十多年最深的感受是复盘的价值不在于总结出一个“责任人”而在于梳理出一套“可复用的防御机制”。如果一场复盘会开完得到的结论只是“运维不到位”“代码写得不够健壮”这类标签那这场复盘基本是无效的。我们这次复盘会开了两个多小时最终沉淀下来的不是“谁背锅”而是三张清单故障时间线精确到分钟的人机交互记录所有操作和判断依据都写了备注。行动项清单每一项都带负责人、截止日期和验收标准后续每周技术周会上滚动跟踪。系统脆弱点清单明确标注了系统里哪些环节在什么条件下可能出问题哪些监控指标是“滞后指标”哪些是“先行指标”。我个人还有一个习惯复盘会结束前会问在场每一个人一个问题“如果在故障发生的瞬间你重来一次你会怎么做”这个问题不追求一个标准答案但能逼着所有人跳出“事后诸葛亮”的心态把注意力放在“当时的信息条件下最优的决策路径是什么”。很多时候我们能做的其实不是优化“决策”而是优化“获取信息的方式”——比如这次如果waitThreadCount有监控我们根本不需要花30分钟看GC日志。另外这种复盘blog写出来的意义在我看来是给其他团队提供一个“提前避坑”的机会。互联网公司的技术栈大同小异很多低概率、高影响的事故模式都是可以跨团队触类旁通的。如果你读到这里发现自己服务的连接池参数还停留在“默认值没改过”的状态或者你的监控面板里还没有“GC单次耗时”和“连接池等待线程数”这两个指标那我建议你尽快把这些补上——大概率不会立刻用上但一旦用上省下的就是一顿凌晨三点的手忙脚乱。我自己的一个习惯是每隔一段时间把线上机器当作“已经出了事故”来演练一遍排障路径看自己能不能在15分钟内定位到最可能的原因。这种主动的“假想复盘”比任何告警规则都更能锻炼团队的应急手感。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进