拓冰建站拓冰建站
首页 / 资讯中心 / 正文

稳定性治理实战:从时间轮毛刺到连接池故障的排查复盘

一点重要提示所有涉及系统名称、参数值、代码片段均为生产环境通用形态请结合自身实际进行调整。以下是我在稳定性治理这个方向上踩过的坑、扒过的日志、复盘过的故障全文约7000字按话题展开。做稳定性治理这些年我慢慢有一个体会真正让系统稳下来的往往不是多高明的架构设计而是那些看起来不起眼的边界条件——比如一个定时任务的时区写法、一条连接池的空闲回收策略、一次线程池拒绝策略的选择。这些细节平时没人注意一旦出问题就是线上P0、P1整个团队跟着救火。我所在的团队负责一套日活千万级的中型业务系统高峰期QPS大概两万上下。因为业务属性特殊对稳定性的要求很高既不能卡顿也不能丢数据。这几年我们陆陆续续做了一轮又一轮稳定性治理既有从零搭建监控体系的阶段也有针对疑难故障逐个击破的攻坚期。这篇文章把其中几个典型的疑难问题、排查过程和最终的治理方案完整复盘一遍希望能帮到正在做同样事情的同行。1. 稳定性治理的范畴界定与衡量标准先搞清楚稳到底指什么很多团队一提到稳定性治理就想到加监控、搞容灾、做大促保障这没错但如果连稳的定义都不清晰治理工作就会变成无头苍蝇。我习惯先把稳定性拆成两个维度系统维度和业务维度。系统维度看的是基础设施与中间件的健康度比如CPU、内存、磁盘IO、网络带宽、GC停顿、连接池占用率、线程池活跃度。业务维度看的是核心链路的功能正确性比如下单成功率、支付回调延迟、消息积压量、数据一致性偏差。两套维度缺一不可我见过不少团队只盯着系统指标结果系统指标全绿、用户却在投诉下单失败——因为问题出在业务逻辑层的异常分支没被监控到。1.1 稳定性衡量指标的落地实践指标定义这件事不能拍脑袋。我们在治理初期定了一套自己的SLO体系这里直接分享出来可以参考层级核心指标目标值说明系统层核心服务可用性≥99.99%按分钟粒度计算排除计划内发布系统层P99响应时间≤300ms按核心接口单独统计系统层错误率≤0.05%排除由客户端取消引发的4xx业务层订单创建成功率≥99.95%排除用户主动取消的请求业务层消息投递成功率≥99.99%含MQ重试后的最终到达业务层数据对账差异率0每日T1对账必须完全一致这里最容易被忽略的是排除项。刚开始我们没有排除客户端取消的错误码导致错误率指标一直虚高研发同学产生监控疲劳看到告警也不当回事。后来做了精细化收敛把预期内的错误如参数校验失败、用户取消、并发冲突单独打点标注错误率指标才真正具备参考价值。1.2 故障等级的划分逻辑没有故障等级就没法确定响应优先级。我们参照行业内通行做法把故障按影响面分成P0到P4五级。P0的定义是核心链路整体不可用或资损比如支付服务宕机、数据库主库写不可用、消息大面积积压超过阈值。P1是核心功能部分不可用比如某个接口成功率低于99%、非核心服务雪崩。P2是功能受限但可用比如某个非核心页面加载缓慢、部分用户偶发超时。P3以下是通知级问题。这套划分标准最大的价值在于统一了团队内部的语言。以前值班同学接到告警判断不了该不该半夜拉人现在对着标准判断三分钟内就能定级并通知相应的人。2. 疑难问题复盘一时间轮算法引发的定时任务毛刺这个案例是我印象最深的一次疑难问题排查前后花了一周多时间问题隐藏得非常深属于典型的你觉得它不可能但它就是发生了的问题。背景是这样的我们有一个延迟队列服务内部用时间轮算法管理几百万个延迟任务。任务到期后触发回调回调里会做一些业务补偿操作。这个服务平时很稳定但每隔三四天就会出现一次响应毛刺——P99从正常的几十毫秒突然飙到两秒以上持续几分钟后自行恢复。监控上CPU、内存都正常GC也正常像是什么都没发生过。2.1 排查链路从网络层一路查到业务代码第一轮排查我把目光放在网络层。我们需要确认毛刺是发生在服务内部还是外部链路。通过RPC调用链追踪定位到毛刺是服务自身耗时变长不是下游依赖变慢。于是开始怀疑是不是JVM层面出了问题于是加了详细的GC日志把CMS的各个阶段全部打出来同时开启JIT编译日志。等了三天毛刺再次出现GC日志显示一切正常JIT也没有重编译的迹象。接着怀疑是不是锁竞争。用jstack在毛刺期间连续抓了十几份线程快照发现大量线程WAITING在同一个对象的monitor上。这个对象是延迟任务回调的分发器锁理论上所有任务在到期时都会抢这把锁去触发回调。看到这个现象我第一反应是锁竞争太激烈直接把分发改成分段锁。但改完后毛刺还是如期而至说明问题不在锁本身而是有线程持有锁的时间异常长。2.2 根因定位3600秒刻度上的第一个槽位真正定位到问题是因为一次偶然的日志分析。我们把所有回调执行的耗时打了百分位分布发现毛刺期间有一批任务的执行耗时超过10秒而这些任务都有同一个特征它们都落在时间轮的同一批刻度槽中。这里需要解释一下时间轮的实现。我们用的是层级时间轮精度做到秒级。每个刻度槽中维护一个任务链表通过一个指针每秒前进一格。正常情况下每个刻度槽里的任务会在该秒内全部处理完毕耗时不过几十毫秒。但有一个特殊场景当时间轮启动时第0个刻度槽也就是3600秒刻度上的第一个槽位会积压一批历史遗留任务——这些任务在服务重启前就已经到期但没有来得及执行。服务重启后时间轮会把这批到期任务统一转移到第0个槽位。我们的问题就出在这里。时间轮的推进逻辑是先处理当前槽位的所有任务然后再推进指针。如果当前槽位任务积压过多处理时间超过了1秒指针就没法按时推进导致整个时间轮卡住。在卡住期间新进来的任务挂到后续槽位指针恢复推进后会一次性把多个槽位的任务全部连带执行造成处理峰值表现就是每隔一段时间出现一次响应毛刺。2.3 解决方案槽位限流与超时摘除修复方案分三步走。第一步给每个槽位的任务处理加超时机制单个任务执行超过3秒直接摘除丢到单独的补偿队列异步处理不允许它阻塞时间轮推进。第二步给槽位处理加并发限制每秒钟最多处理N个任务宁可在当前秒内处理不完也不允许积压到下一秒爆发。第三步重启后的历史遗留任务不再直接丢进第0槽位而是先进入一个暂存队列等到整个时间轮运行稳定后再以低速率匀速灌入。这套改造上线后毛刺现象彻底消失P99稳定在50ms以内延迟队列的积压率也降了一个数量级。这个案例给我的最大教训是中间件类服务的内部调度机制往往比业务代码更容易产生周期性系统性疾病因为它在长时间运行中会积累各种边界状态而这些状态很难在测试环境被提前暴露。3. 疑难问题复盘二连接池假空闲导致的跨机房可用性故障第二个案例涉及的是连接池治理这个问题隐蔽性极高而且有一个非常迷惑性的表象——监控面板上看不到连接数异常但实际上可用连接已经趋近于零。背景是这样的我们有跨机房的双活架构两个机房各部署一套应用数据库主库在A机房B机房通过专线访问。某天B机房的应用突然出现大量数据库超时错误率从0.04%飙到15%持续了将近二十分钟然后自动恢复。整个过程A机房完全正常B机房的应用没有任何发版和配置变更。3.1 为什么监控数据一切正常却故障了第一反应肯定是看连接池监控。我们的监控面板确实显示了连接池的各项指标——总连接数、活跃连接数、空闲连接数、等待获取连接的线程数。但奇怪的是活跃连接数一直在正常范围内等待获取连接的线程数却是满的。这说明有大量不在池中的连接被拿走了但监控系统认为它们仍然是空闲连接。后来查看了具体的连接池日志发现数据库驱动连接存活检测的周期设置有问题。我们把连接的testWhileIdle周期设置得比数据库wait_timeout还长导致连接池里的连接已经超过wait_timeout被数据库踢掉了但连接池仍然认为它是健康的、可用的。业务线程获取到这些幽灵连接后执行SQL就会抛异常重试机制又会去获取新的连接但新连接建立的时间较长专线延迟数据库握手导致大量线程堆积在等待状态。3.2 排查与根因的确认过程这个问题的排查过程也很有代表性。一开始我以为是专线网络抖动导致的建连失败但ping和丢包率都正常两边网管也确认专线没有异常告警。后来又怀疑是数据库连接数打满但查了数据库端max_connections活跃连接只有几百远低于上限。最终是通过抓取数据库端的连接关闭日志发现问题——数据库在wait_timeout到期时主动断开了大量来自B机房的连接断开时间点与故障开始时间完全吻合。再回查应用端的连接池配置才确认问题源于连接池的存活检测周期配置错误。我们用的是HikariCP它的connectionTimeout、idleTimeout、maxLifetime、keepaliveTime这几个参数配置上有很多讲究。HikariCP的maxLifetime必须要比数据库wait_timeout短官方推荐的算法是数据库wait_timeout减去30秒。同时keepaliveTime需要小于maxLifetime这样才能保证在连接被数据库踢掉之前有足够的时间来验证和重建。3.3 连接池参数的最佳实践与动态化方案修复并不复杂把maxLifetime调整为比wait_timeout短60秒开启keepaliveTime并设置为30秒同时把connectionTimeout设定为2秒让业务线程在无法获取连接时快速失败而不是无限等待。但这里还有一层治理思考。生产环境的数据库wait_timeout不是一成不变的DBA可能因为某些原因调整它比如为了清理僵尸连接把wait_timeout调小。如果应用端的连接池参数写死DBA一调参数故障就来了。我们的做法是连接池参数支持通过配置中心动态调整每次调整后自动触发一次健康自检——模拟从连接池获取连接执行一条SELECT 1如果失败则告警并自动回滚配置。这个案例说明了连接治理的两个关键点一是连接池的每个时间参数都不是孤立的它们和数据库端的参数存在联动关系二是稳定性治理一定要关注跨团队变更的联动影响DBA改一个参数对应用来说可能就是一次故障。4. 容量治理与限流降级实战一次大促前的压力测试复盘大促保障是稳定性治理的重头戏这一部分把我认为最核心的容量评估方法和限流降级设计一起讲清楚。很多团队在大促前只知道压测一下看看能扛多少QPS然后根据结果加机器。这种思路有严重盲区——没有做全链路逐层容量评估导致扩容后的瓶颈从应用层转移到了数据库、缓存或其他中间件上。4.1 容量评估的核心方法论逐层拆解与冗余预留我们用的方法是自顶向下逐层拆解加单点瓶颈预判。以一次核心链路为例一条用户请求会经过网关、应用A、应用B、Redis、数据库五个节点。我们对每一个节点单独做压测得到单机水位和最大吞吐量然后计算出各节点在当前流量模型下的资源使用率。分享一个简化版的计算逻辑。假设应用A是核心服务单机压测结果显示单机最大QPS是1500当前单机日常负载是400QPS那么单机冗余率1500-400/1500约等于73%。大促预估峰值是20000QPS需要的实例数至少是20000除以1500等于14台这只是满足峰值的下限。但我们不会按这个数字来部署实际会额外预留30%到50%的冗余所以最终部署20台。数据库和缓存的评估逻辑也是类似的但要注意的是容量模型不是线性的。数据库的连接数是核心瓶颈一个实例的连接数上限假设是300每个连接能支撑的QPS取决于SQL复杂度和IO能力这个数据要通过真实压测得到拍脑袋估的往往虚高。我们在大促前专门针对数据库做了只读压测和写入压测找出了数据库在混合读写场景下的真实吞吐上限。4.2 限流降级设计中容易被忽略的细节限流降级设计里我踩过的坑是限流器本身变成新的瓶颈。最早我们用的是单机内存限流Guava RateLimiter每台机器配置独立的速率阈值。问题在于流量在网关层不是完全均匀分发的某台机器可能比其他机器多接到30%的流量结果就是这台机器触发限流而其他机器还有余量导致整体吞吐反而低于预期。后来换成了分布式限流用RedisLua脚本实现每次请求从Redis获取当前窗口的计数并判断是否放行。分布式限流的优点是全局限流公平但它的性能损耗比单机限流大单次判断增加了1到2毫秒的RT这个成本在高QPS场景下不小。最终我们采用了两层限流方案网关层做集群限流防止整体流量超过系统承载上限应用层做单机快速失败限流防止单机异常拖垮其他机器。每层的阈值相差20%左右给各层留出缓冲。降级设计上最容易忽略的是降级开关本身的高可用。降级开关我们存在配置中心但如果配置中心本身挂了降级开关就永远处于关闭状态关键时刻没法降级。所以我们把降级开关做成了三级同步配置中心是主数据源本次发布会同步一份到本地缓存本地缓存过期时间设置为1分钟同时保留一个本地properties文件的兜底开关即使配置中心和本地缓存都不可用还有一份静态配置能生效。5. 稳定性治理的日常运营机制从应急响应到主动防御故障排查和容量评估做得再好如果没有一套日常运营机制来承托稳定性的成果很难长期维持。这一部分聊一聊我们的稳定性运营体系尤其是故障演练和变更管控这两个环节我认为是投入产出比最高的。5.1 故障演练的设计思路与执行要点很多人对故障演练有误解觉得就是把服务停一下看看告警能不能发出来这种演练练了等于没练。我们的故障演练是围绕真实可能发生的故障场景来设计的每年大促前做一轮完整演练覆盖范围包括应用实例宕机随机杀掉一个Pod验证容器编排能否快速拉起新实例以及流量摘除是否及时生效。数据库主备切换DBA在预发环境模拟主库故障验证应用连接池的failover逻辑是否正常工作以及切换期间的写请求是否会有损。缓存节点故障模拟Redis集群中一个分片不可达验证请求是否全部打到数据库以及数据库是否能承受这波流量。消息队列积压向MQ中灌入正常流量5倍的消息验证消费端的扩容机制和消息堆积告警是否及时。演练执行中有一个很重要的细节每一次演练都要有明确的预期结果和通过标准。比如消息队列积压的演练预期结果一是积压量在30分钟内从500万降到0预期结果二是堆积告警在积压开始后1分钟内触发预期结果三是消费端的自动扩容逻辑生效消费速率提升到正常状态的5倍。三个预期结果全部满足才算演练通过任何一个不满足都要写复盘报告。我们做过几轮之后发现故障演练最大的价值不是验证已经有的能力而是能在演练中发现那些你以为有但实际上没有的能力。比如有一次演练我们发现某个核心服务的熔断器虽然配置了但熔断恢复策略写的是半开状态立即恢复导致熔断触发后会在1秒内被打穿随时可能被大流量再次打死。这种问题在生产环境可能永远不会主动暴露但一旦真正发生故障代价是不可估量的。5.2 变更管控与发布门禁的实操经验日常稳定性运营中最容易引发故障的动作就是变更包括发版、配置变更、数据库变更、资源调整。我们建立了变更审批和发布门禁机制所有变更必须在工单系统里提前申请说明变更影响面和回滚方案由值班负责人和架构师审批。看起来流程变长了但这套机制在实际运行中帮我挡住了至少三次潜在故障。发布门禁的核心逻辑是自动化校验先行。发布流程里嵌入了几个自动检查点第一是静态代码扫描检查是否有明显的安全隐患和性能问题第二是依赖安全检查第三是集成自测第四是发布后的金丝雀验证——先发布一台机器观察十五分钟核心指标确认P99错误率和成功率都正常后才发布全量。有一个我特别想分享的经验发布后的业务指标回归非常重要不能只看系统指标。有一次我们发布了一个看起来人畜无害的配置变更只是调整了某个开关的默认值系统指标在发布后半小时内全部正常但实际上那个开关改动影响了某个非核心接口的数据返回格式直到第二天业务方反馈某个页面的用户标签没了才被发现。后来我们在发布门禁中增加了一步关键业务指标对比发布前后各跑一次核心业务场景的自动化用例对比输出结果这个问题才被提前拦住。5.3 稳定性数据化的复盘机制最后聊复盘。我们的复盘不是追责会而是数据化复盘。每次线上故障或严重隐患后必须在一周内输出一份完整的复盘报告包含这几个固定模块故障时间线和关键节点、根因分析必须用5Why法追到物理根因、影响范围和时间、修复动作和验证结果、同类问题排查清单、后续预防措施和责任人。一个关键的经验是复盘报告要能够指导未来——每条预防措施都要明确可执行的验收标准不能只写加强监控四个字就完事。加强监控要具体到在XX服务的XX面板增加XX指标的分钟级监控并设置告警阈值为XX。这样的复盘报告才能真正推动系统变好而不是纸上谈兵。我经历过一个比较典型的例子一次因为缓存穿透引发的数据库压力过高故障复盘报告的预防措施写了三件事。第一在缓存访问层增加空值缓存策略对数据库查不到的数据也写一个短暂的空缓存默认三秒过期防止同样key反复打到数据库。第二对热点key开启本地缓存兜底即虽然分布式缓存里有数据但为了防穿透单机也会短暂持有最近访问过的一部分key。第三数据库连接池增加总连接数的独立告警一旦连接占用率达到80%持续两分钟立刻告警。这三条措施每条都有明确的验收标准后续跟踪确认全部落地后同类问题再也没出现过。6. 遇到瓶颈时的一些思路拓展与进阶经验稳定性治理做到一定阶段会进入平台期——该做的监控都做了该建的机制都建了但总感觉缺少一个系统性的框架来指导后续工作。我分享一下最近一两年的一些进阶思路。6.1 从单点治理走向链路治理早期的稳定性治理往往是单点的比如把DB慢查询优化掉、把接口超时时间调短、把一个缓存过期策略改对。这些动作本身有价值但很难从整体上提升系统的稳定性。接下来值得投入的方向是链路治理——把一条完整业务链路涉及的每个节点、每个依赖、每种异常分支都梳理出来形成一份链路地图然后针对链路上的每一个薄弱环节做稳定性设计。链路治理的具体做法是从上游流量入口开始把整条链路的所有依赖、中间件、外部调用、定时任务、消息消费都画在一张图上每个节点标记出它的SLO、最大容量、依赖关系、降级方案和责任人。然后针对这张图做两个动作一是链路上最小可用集设计——如果所有非核心依赖全部降级这条链路还能不能保证最基础的功能可用二是链路容量地图——给每个节点标记出容量水位当上游流量增长时哪个节点会最先成为瓶颈这两个动作能帮助团队从会修单点故障进化到能设计链路韧性。6.2 关注数据一致性对应用稳定性的隐性影响实践中另一个容易被忽略的稳定性维度是数据一致性。一个系统如果缓存和数据库之间的一致性做得不好用户看到的数据时好时坏短期看不会产生故障告警但长期看会让用户对系统失去信任这在业务上其实是最严重的慢性稳定性问题。我们在这块踩过很大的坑。一次缓存更新逻辑的bug导致缓存和数据库数据不一致长达几小时期间用户反复刷新页面看到的是不同的数据最终靠短时全量缓存淘汰才恢复。这次事件后我们把所有缓存更新逻辑统一改为先更新数据库再删缓存再延迟双删的策略同时加了一张数据变更流水表用异步任务做缓存与数据库的定期对账发现不一致立即告警并自动修复。这套机制上线后数据一致性类的问题从用户投诉才发现变为系统自动发现自动修复大大减少了类似的隐性故障。给做稳定性的同行一个建议不要把稳定性治理完全局限在性能、容量、高可用这些硬指标上数据一致性、逻辑正确性这些软指标同样值得纳入治理体系。6.3 建立稳定性文化的几点建议因为文章篇幅有限文化建设不能展开太多但我想特别强调一点稳定性治理的最大阻力往往不是技术难度而是团队意识的参差不齐。我的做法是每个月组织一次稳定性分享会每次由一个同学分享一个最近发现的隐患或一次印象深刻的线上问题复盘。为了让分享会有干货每次分享必须有三分之一的篇幅讲这个问题发生之前我们为什么没发现。坚持了半年之后团队的整体稳定性意识有了明显提升——新同学写的代码开始主动考虑边界条件老同学做技术方案时也会主动提一句这个方案挂在XX节点上的话稳定性有没有风险。这种意识上的改变比任何监控工具和治理平台都更值钱。工具可以买平台可以搭但判断力只能靠一次次真实的故障复盘和高质量的经验分享来积累。最后说一点个人体会。稳定性治理没有终点它是一个持续对抗系统复杂性熵增的过程。每次你觉得终于稳了的时候系统里很可能已经孕育着下一个隐患。我能给同行的最大建议是保持对线上数据的好奇心和对故障的敬畏心用数据化的方式定义问题、验证修复把每一次故障变成一次系统能力的确定性提升。如果这篇文章里的某个案例或方法能帮你少走一次弯路那我就很欣慰了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门