ARTICLE DETAIL

资讯详情

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

稳定性治理实战:从疑难故障排查到SLO度量体系建设

稳定性治理实战:从疑难故障排查到SLO度量体系建设 不出意外的话每个做后端、做SRE、做基础架构的同学职业生涯里都会遇到那么一两个让你饭都吃不香的线上疑难故障。我盯着的系统去年一年下来光是大大小小的稳定性事件就复盘了不下二十次。折腾得多了我最大的感受是稳定性治理这事儿真不是跑个监控、配几个告警、写几篇复盘报告就完事的。那些真正折腾人的“疑难杂症”往往不是技术方案有多难而是整套应急机制、排查思路、甚至是团队协作的隐性习惯出了漏洞。这篇文章我打算换个聊法不整那些“稳定性治理体系白皮书”之类的虚词就把我这一年踩过的坑、复盘过的经典疑难问题以及如今沉淀下来的实战打法掰开揉碎了讲给你听。这其中会涉及具体的现象、完整的排查链路、以及很多“如果不是自己亲手查根本不会信”的根因。内容偏长但对正在做稳定性或者被线上疑难问题折腾得够呛的同行应该有点参考价值。1. “大师课”式的稳定性治理为什么把线上搞得越来越乱先聊个扎心的话题。我发现很多团队对稳定性的理解停留在“监控系统越来越重告警群里越来越吵故障复盘会越开越多线上该挂还是挂”。坦白讲稳定性的第一性原理是尽可能缩短从“故障发生”到“恢复业务”的时间而不是追求一套理论完美的架构。所谓的“大师课”打法动不动就上微服务、上ServiceMesh、上全链路压测结果肺活量不够还没开跑就岔气了。我接手这个系统的时候线上主要在跑一套核心交易链高峰期QPS大概在两万左右支撑全国几千家门店的进销存。按说这个体量论架构复杂度不低但论单机压力也没大到离谱。真正的问题是什么是组里没有任何一个人能讲清楚用户一个请求从前端拦截器到最终数据库落库这条路径上到底有多少超时点是未受控的。治理前我们线上有大概三千多条告警规则平均每天告警一千两百多次。这个数字听起来好多规则但实际情况是业务同学早就把告警群屏蔽了因为百分之九十九的告警都是无效的。真正的稳定性危机就在这儿不是没有监控而是监控数据没有转化为有效的决策依据。我当时干的第一件蠢事就是继续加告警规则结果除了让值班同学疯了之外毫无用处。后来我反思真正的稳定性治理控制面要做的不是建立一套多么牛的监控矩阵而是把故障的逃逸半径尽量压缩。怎么压缩靠的是对核心链路做减法对非核心路径做隔离对每一份流量做容量预算对每一次变更做灰度兜底。很多团队为什么要做稳定性治理就是因为救火救累了想制度化地把火苗提前灭掉。但制度化的前提不是写个《稳定性保障手册》喊口号而是先把系统现状盘清楚。我们的CPU经常被一张统计报表的临时大查询干到80%以上复杂的慢SQL在订单表上建了八九个单列索引MySQL优化器选错索引就能把整个库拖垮。所以我后来把所有号称“稳定性治理”的动作全部收敛到三个字“防、控、恢”。“防”是事先把变量控制住尽可能杜绝已知风险“控”是故障发生时的熔断降级把爆炸半径尽量切小“恢”是依赖预案和演练第一时间恢复业务而不是坐在电脑前现查日志。这套认知才是我后面所有疑难问题排查的一个底层框架。如果你现在发现自己每天的工作就是在告警群里别人、到处救火我真心建议你把监控大盘关掉先坐下来梳理一下你的系统最怕什么网络抖动怕不怕DB CPU飙高怕不怕代码里那个循环依赖的Bean导致启动变慢怕不怕把这几个最怕的场景对应出可执行的预案比你在开源社区找二十个监控采集器管用得多。2. 疑难问题的“疑难”到底难在哪偶发、退化与跨域的三重门在这两年跟各种疑难问题打交道的过程中我发现大家挂在嘴边的“疑难问题”其实细化下来有三种完全不同的底层逻辑处理方式也完全不同。如果一刀切地去排查很容易陷入“查了一整天但毫无头绪”的泥潭。2.1 第一类疑难偶发性问题永远复现不了的才是真Bug最让人崩溃的不是问题多严重而是这问题它“看心情”出现。比如我遇到过一个线上支付回调偶发报错频率大概是每天三到五次每次持续几秒钟日志里没有任何堆栈只有一句“connection reset”。我们找了两周一开始怀疑是Nginx超时调了所有跟超时相关的参数没用后来怀疑是数据库连接池问题把连接池参数翻了个底朝天还是没用。这类偶发问题的根子多数情况不在显式的报错点上而在系统微不可查的资源竞争或者JVM停顿上。后来怎么查出来的我们给线上所有核心接口的响应时间加上了P99.99的分位统计然后发现出问题的时间点P99.99会突然跳到十秒以上而这个时间点恰好对应后台数据同步任务的某批大事务提交。数据库刷脏页、行锁等待导致了个别请求的连接被数据库侧强杀。这问题如果你只会看平均耗时永远看不出来。所以偶发性问题排查的第一条原则不要盯着报错那一秒的日志看要把视角拉长到分钟级甚至小时级对比同期所有资源指标找到异常的共振点。所谓疑难很多时候就是数据粒度不够细。2.2 第二类疑难性能退化系统没挂但越来越慢这是另一种疑难系统表面看起来一切正常QPS没掉错误率为零CPU也不高但用户就是反馈页面转圈圈。这种“温水煮青蛙”式的性能退化排查起来比直接宕机更痛苦因为所有指标看上去都非常健康。有一次我们一个核心接口正常响应时间是80毫秒用户开始反馈卡顿的时候平均值已经到300毫秒了。但监控大盘上服务器CPU只有20%数据库CPU只有30%看起来余量充足。后来通过链路追踪看到问题出在一个Redis大Key上。这个Key是某商家全部商品信息的聚合缓存刚开始只有一百多K后来数据涨到五兆多。读取这个Key的反序列化时间指数级上升直接拖垮了整个购物车接口。这种退化的核心难在容量模型是动态变化的它不会立刻给出一个报错让你去查但会钝刀割肉。要解决这类问题常规监控不够必须得有长期视角的容量巡检比如定期分析热点Key、慢查询、JVM内存水位。等用户骂声起来了再查本质上已经算事故了。2.3 第三类疑难跨域问题谁的系统都是好的放到一起就坏了最考验架构师功力的是那种拆分到单个服务上看每个服务响应都很快一走到完整链路就超时。这种跨域问题排查看不到任何明确“凶手”日志分散在两三个子系统里全链路TraceId断层排查成本极高。我有个印象很深的案例。两个服务A和B通过HTTP调用A调B超时B的日志里却没有这个请求的完整记录。两边开发各执一词A说是B响应慢B说压根没收到多少请求。四个小时查下来最后发现是两边服务器的时钟偏差了大概三十秒导致A记录的TraceId在B服务里的异步日志落库时由于按时间分表数据被扔到了另一张表里自然就“查无此请求”。这种问题没有跨域视角和统一的时钟基线排查基本靠猜。所以对于疑难问题不能只会拿着官方文档对着自己的代码查得有从上往下俯视整个调用链的能力能把分散的指标在时间轴上强对齐。跨域问题最忌讳的就是各团队只盯着自己的服务日志反复看当你发现两边的“事实”对不上时大概率是底层基础设施层面出了一些你平时根本不会关注的小变量。3. 一次CPU伪飙高引发的血案完整复盘整个“不可能”故障行了方法论聊太多容易飘直接上盘硬菜。这个案例是我年初处理的绝对是典型的“稳定性治理疑难问题”——无论从哪个角度看它都不该发生但它就是虎视眈眈地在你眼皮子底下发生了。当时凌晨两点多告警群里突然刷出一条消息某核心应用集群的CPU使用率直接从15%暴增到98%持续了将近五分钟才自动恢复。按常规逻辑CPU飙高要么是有死循环要么是GC垃圾回收过于频繁要么是流量突增。我第一反应是查流量结果监控一看流量毫无波澜平稳得跟心电图上的直线似的再看GC日志老年代GC频率没增加新生代也没啥异常就是CPU高到离谱用arthasJava诊断工具去看线程栈发现大量线程阻塞在同一个SocketInputStream.read方法上。这时候团队里有经验的同事已经懵了因为线程都阻塞在IO读取上按理说CPU应该极低才对怎么反而高了呢这就像一个人在那儿躺着不动你测他体温却高烧四十二度违背生理常识。我静下来把线程栈的样本多抓了十几份仔细对比后发现阻塞在IO读的线程数量其实只有十个左右但真正消耗CPU的是几个名不见经传的GC线程他们正以极高的频率在执行YoungGC。线程栈里看到大量的“G1 Young Generation Pause”而每次YoungGC的耗时都在夸张的几百毫秒。这就有意思了YoungGC频繁说明对象分配速率极高但业务流量没涨为什么对象分配速率会高我马上开了一把jstat对着GC数据一顿输出发现Eden区新生代伊甸园区的大小被配置成了极小值好像是部署新版本的时候有人不小心把一个调优参数覆盖了。原本Eden区应该至少两个G结果被谁改成了两百兆。对象稍微一多Eden就满满就触发YoungGC而YoungGC又要扫描整个根集合导致CPU飙高同时垃圾回收时间过长业务线程都等待着看起来就像Socket一直在阻塞。整个根因链路梳理清楚没有神秘的死循环没有传说中的流量攻击就是一个参数被误改引发的连锁反应。但就是这么个参数如果没有足够的耐心从线程栈看到GC日志再看到JVM配置你绝对抓不到真凶。这个案例对我的冲击非常大。稳定性治理中最容易被忽略的就是JVM参数的版本管理。我们当时所有配置都存在配置中心里但JVM参数是写在启动脚本里的启动脚本们既不归代码仓库管也没有走变更审批流程。一个同事在调试本地环境时把他的小堆参数带到了生产启动命令里整整跑了三天才暴雷。经历了这事我们做了两个硬性约定第一所有Java应用的JVM参数全部抽离到配置平台任何变更必须有审批记录第二线上启动前必须跑一次JVM参数巡检脚本校验关键指标的最小阈值不合规直接拒绝启动。类的问题以后再没出现过但这类治理经验的爆发点往往就是一次让你措手不及的线上事故。4. 磁盘I/O高、连接池耗尽、还有那个让人头秃的“幽灵”内存泄漏刚才聊了CPU的问题这里再集中把另外三个高频疑难场景一次性说透基本覆盖了我日常排查故障的80%的时长。4.1 磁盘I/O成谜日志里藏着一个“无限循环”的打点有一次线上应用频繁出现接口超时告警我查监控发现数据库各项指标正常CPU也不高但是磁盘I/O读写等待时间突破了百分之五十。一开始怀疑是RDS云数据库那边在做备份检查了下没有怀疑是慢查询刷盘量太大看了慢查询日志也没明显增长。后来我登录到应用服务器执行了一个iotop命令看到有个Java进程在疯狂写盘每秒写大概一百多兆。但是这个应用的业务逻辑本不该有这么大的日志输出。进到日志目录好家伙一个application.log文件已经膨胀到了三十几个G。打开日志一看某个切面里的一段Debug日志被错误地遗留在了生产环境而且这段日志在打印的时候会遍历一个常量Map对象Map里有几万个枚举值。高并发情况下每秒光打印日志产生的字符串拼接都能干出上百兆的临时对象文件写入又要同步落盘I/O自然被拖垮。这里有个几乎没人注意的隐患那就是日志框架的异步丢弃策略。我们当时用的Logback配置里开启了异步但没设置丢弃阈值导致日志量大了之后异步队列塞满业务线程直接参与写日志性能瞬间恶化。现在很多团队都在往可观测性平台接日志但在日志采集还么有完全改造完之前打印日志本身就是一种性能开销。特别是带有复杂参数拼接的大日志必须慎之又慎。4.2 数据库连接池耗尽一个慢接口拖死了整条业务链经典的连接池耗尽问题特性症状是报“GetConnectionTimeoutException”但排查它的过程经常比异常本身更气人。大多数情况下第一个背锅的是数据库——DBA把慢查询日志翻烂了也没发现哪个SQL能跑好几秒。我调的另一个案例现象是晚间高峰所有接口集体超时数据库CPU只有40%。后来通过动态线程池监控看到业务线程都阻塞在一个获取数据库连接的地方。而连接池的状态是活跃连接数占满空闲连接为零。为什么活跃连接会满我们抓了一把线程栈发现大约有六十个线程全都卡在同一个Mapper接口的调用上那个接口本身是一个很简单的单行查询。但为什么一个单行查询会卡住继续往下查发现这个单行查询的SQL是动态拼接的慢日志里没有却会在特定参数条件下生成一个不走索引的查询计划。这种查询在本地几百条数据下秒回线上几千万数据下直接全表扫而且行锁彼此等待最终把连接池活活憋死。那为什么数据库CPU才40%呢因为大部分等待在线程池那侧数据库其实早就干完活了只是应用侧拿不到连接大家全在排队。这类问题的治理核心不是让你把连接池调大调大只会把雪崩推迟二十分钟而是要建立数据库连接“获取前拦截”的机制。我们在MyBatis拦截器里加了执行计划分析碰到全表扫自动熔断同时把核心接口的SQL全部收敛到标准化枚举禁止动态拼接。4.3 “幽灵”内存泄漏为什么内存总在一个不可名状的地方慢慢涨内存泄漏的疑难程度在所有问题里能排进前三尤其是堆内存总用量一直缓慢爬升、触顶之后就FullGC但FullGC之后只回落到百分之七八十下不去了。我遇到过一个小程序每天OOM一次重启就好了。一开始用MATEclipse Memory Analyzer分析堆转储发现有个静态Map占了百分之六十的内存。队伍里没经验的开发第一反应是把这个Map清掉。但清掉Map里的数据内存也不降因为Map持有的是Class对象的引用而Class对象是JVM的Metaspace管着的不是堆内的。真正的幽灵在“直接内存”Direct Memory。我们的网关应用用了Netty高性能网络通信框架Netty的PooledDirectBuffer分配了不少堆外内存。代码里在读取文件的时候新建了一个ByteBuffer但没主动释放。开发认为GC会回收堆外内存——这是最大的误解堆外内存的释放靠的是Cleaner机制Java对DirectByteBuffer的回收机制一旦Cleaner线程被某些特殊类加载器阻塞内存就彻底失联了。最终状态是堆内正常系统可用内存越来越少频繁触发swap服务响应开始忽快忽慢。这东西排查起来常规的jstat、jmap根本看不出名堂必须用Native Memory Tracking才能看到堆外内存的真实水位。5. 稳定性的度量体系没有北极星指标的治理都是耍流氓聊完了具体问题得上升到机制了。很多团队为什么治理失败我认为是因为他们缺了一套能真实反映系统稳定性的度量体系。你连稳定还是不稳定都是靠感觉治理就成了无本之木。5.1 用SLO把“好与坏”定义清楚之前我们谈稳定性往往用可用性几个九来衡量。但对于互联网业务来说单纯用请求成功比例做指标颗粒度太粗。我比较推荐的是Google SRE那套方法论落地的裁剪版核心接口必须定义SLO按三个维度去度量——可用性请求成功率、时延P95/P99、饱和度当前容量水位。比如说交易链路这个SLO我们定义目标值是99.95%即每个月只允许大概21分钟的不可用时间。别小看这个数字当你真的为这21分钟去较真时你会发现很多原来觉得“无所谓”的小毛病都得揪出来改掉。同时SLO不能只是写个大数字挂在墙上。我们每个季度会用错误预算机制做决策如果这个月错误预算消耗过快说明系统的稳定性在恶化这时候所有新功能必须暂停上线全部人力投入稳定性修复直到错误预算回补。这招极其得罪项目经理但极其有效。5.2 告警质量与分级别让狼来了消耗掉团队的信任没有体验过凌晨三点被无关告警吵醒的人不足以谈稳定性的告警治理。一个失败的监控体系特征是告警很多但从没人真正按告警去做应急响应。我们后来做了很痛苦的减法通过下面的维度重新梳理了规则告警严重级别对应的响应要求以前乱得一团浆糊。现在整理的等级大概是这样不同业务可微调:P0紧急核心链路成功率低于99%或所有可用性SLO失守立即启动应急响应要求10分钟内拉起电话会议相关接口人必须接入不解决不下线P1严重非核心接口大面积超时或有明确的慢SQL风险增强需要立即介入排查值班同学需要在15分钟内确认是否影响核心业务视结果决定是否升级P2警告单机异常或某指标超过安全水位但未影响业务进入工单池由对应负责人当天处理完毕P3信息日常巡检信息如扩容完成、定期巡检结果只需要记录在案不需要打扰任何人。在做了这轮分类之后我们告警总量直接降到了原来的百分之八真正P0级别的告警一个月都未必有一次。这个结果让我自己都挺意外原来大家被无止境的告警压得根本喘不过气每天在无效告警的海洋里捞针自然而然就会漏掉真正的鱼雷。告警降噪这件事不是让你把阈值调高糊弄自己而是让你把每一条告警都变成有明确处置动作的信号灯。5.3 容量水位经常被忽略的稳定性隐形指标大多数团队对容量的理解停留在“QPS到了瓶颈就扩容”的阶段。但真正的容量治理是需要在每个版本上线前估算它带来的额外资源开销的。我在复盘那些疑难问题时发现很多底层故障的诱因其实是容量水位规划不足。举个例子某次我们做一次中间件升级升级之后连接数从每个节点的两百飙升到五百。幸好提前做了水位监控在达到阈值前完成了扩容。如果不幸没有监控按原来的容量规划多出来的连接数会直接打满ulimit限制导致应用无法新建连接雪崩式宕机。容量治理的实操层面我目前做两件事第一定期全链路压测并不只是为了得到一个“单机最大QPS”的数字更重要的是找到每个节点的拐点然后按拐点的百分之七十作为生产运行的安全水位第二建立成本与稳定性的联动机制扩容不再只是基础设施的事业务研发需要为自己的接口增长的资源消耗负责。这一点执行下来最大的变化是大家写代码的时候开始注意内存分配了不再动不动就缓存一堆不用的对象。6. 稳定性治理的真正抓手预案、变更、演练一个都不能少有了度量体系还得有落地动作。我是那种不太信“靠自觉”的人稳定性这东西必须有硬性的流程和工具去卡住关键环节。我在这块主要抓三样预案、变更、演练。听起来都是老生常谈但真正做扎实的团队凤毛麟角。6.1 变更管理稳定性头号杀手不是代码Bug是变更大数据统计下来绝大部分线上故障都是由变更触发的。我们团队内部流行一句话“如果不change就不会break。”但业务要迭代变更无法完全避免所以关键是建立一套安全可控的变更流程。我们的做法很简单全量变更必须过三道闸第一道闸是自动化的静态检查和灰度策略所有应用必须支持按流量比例灰度没有灰度发布平台的应用不允许上线核心链路第二道闸是变更窗口限制核心交易链路的变更只允许在周二和周四的白天进行避开业务高峰留足观察期第三道闸是快速回滚每个变更都必须提前准备回滚方案回滚预案与实际脚本不允许临时写必须随变更单一起提交。这套流程执行了半年变更导致的事故占比直线下降了约七成。很多研发一开始觉得麻烦催着上线时不理解但经历了一两次因为不规范变更导致的P0故障后大家都开始主动遵守了毕竟谁也不想凌晨爬起来开电话会议。6.2 预案与故障演练让肌肉记忆先于大脑思考预案这个东西百分之九十的团队都写过但大部分烂在文档中心吃灰。故障演练的意义就是把这些白纸黑字的预案变成每个值班兄弟的肌肉记忆。第一次做机房级故障演练时场面是相当混乱的。我们模拟了一个核心机房网络断网原本预案要求流量在五分钟内切换到备机房。结果实际操作时发现DNS切换脚本里有几个IP写死了备机房的数据库延迟没达标流量切过去老接口报错。整个团队手忙脚乱了大概四十分钟才成功完成切换。要是真碰上故障这四十分钟的业务损失不可估量。从那以后我们的故障演练从每年一次变成了每季度一次而且每次都要换不同的故障场景数据库主从切换、缓存集群崩溃、依赖第三方接口长时间无响应、甚至整个K8s节点宕机。每次演完都要开个小复盘把预案中与现实脱节的细节逐一修正。这些平时看起来毫不起眼的操作真到了故障发生时就是救命的稻草。6.3 稳定性文化的最后一块拼图复盘会到底该怎么开最后聊一聊在稳定性治理里最容易被做变味的一环——复盘。我非常反感追责式的复盘那种会上大家互踢皮球、会后出一份推诿报告的打法除了制造内耗没有任何技术价值。我的复盘会流程是这样固定的第一步以时间线方式完整回放故障现象不讨论任何“谁对谁错”只还原技术事实第二步用“5个为什么”法连续追问多个为什么找到根因和触发条件这一步要求必须区分“直接原因”和“深层原因”很多团队的复盘直接死在第一步因为只记录了一句“代码Bug”而没有继续追为什么代码有Bug仓促上线了第三步将所有行动项分为“立即修复”、“短期改进”和“长期治理”三类每个行动项都要有明确的Owner和截止日期第四步两到四周后专项跟进行动项是否真的落地没落地视为复盘无效。我用这套流程把很多疑难问题的技术债转化成了实实在在的治理项。比如前面提到的JVM参数配置就是因为一次复盘才被提为最高优先级治理项的。最后说点实在的现在网上关于稳定性治理的文章很多往往是一堆概念和框架看着高大上落到自己团队却很难执行。这一年多深挖疑难问题并复盘总结下来我最大的心得是稳定性治理没有什么灵丹妙药它更像是一个持续对抗熵增的过程。你不需要一开始就追求上百项监控指标也不必非得搞一套多复杂的混沌工程平台。你只需要定好真正的北极星指标盯着SLO把所有变更和预案都管起来然后再敢于把最难啃的疑难问题拿上台面做深度复盘就已经超过了大多数团队。如果这篇文章里的某一个排查思路、某一条治理经验能让你在处理线上故障时少走一些弯路那我这大几千字就没白写。稳定性的路没有终点出了问题不可怕可怕的是同样的问题换个马甲再次出现而我们依旧束手无策。愿大家的系统都能长命百岁少点惊心动魄的凌晨三点。
返回列表