阿里妹导读:软件工程领域存在一个共识:维护代码所花费的时间要远多于写代码。而整个代码维护过程中,最惊心动魄与扣人心弦的部分,莫过于问题排查(Trouble-shooting)了。特别是那些需要 7x24 小时不间断维护在线业务的一线服务端程序员们,大大小小的问题排查线上救火早已成为家常便饭,一不小心可能就吃成了自助餐 —— 竖着进躺着出,吃不了也兜不住。本文分享作者在服务端问题排查方面的一些经验,包括常见问题、排查流程、排查工具,结合实际项目中发生过的惨痛案例进行现身说法。
Know Your Enemy:知己知彼,百战不殆。
逻辑缺陷:e.g. NPE、死循环、边界情况未覆盖。
性能瓶颈:e.g. 接口 RT 陡增、吞吐率上不去。
内存异常:e.g. GC 卡顿、频繁 FGC、内存泄露、OOM
并发/分布式:e.g. 存在竞争条件、时钟不同步。
数据问题:e.g. 出现脏数据、序列化失败。
安全问题:e.g. DDoS 攻击、数据泄露。
环境故障:e.g. 宿主机宕机、网络不通、丢包。
操作失误:e.g. 配置推错、删库跑路(危险动作,请勿尝试..)。
医生:小王你看,这个伤口的形状,像不像一朵漂浮的白云?
病人:...再不给我包扎止血,就要变成火烧云了。
发布期间开始报错,且发布前一切正常?啥也别管,先回滚再说,恢复正常后再慢慢排查。
应用已经稳定运行很长一段时间,突然开始出现进程退出现象?很可能是内存泄露,默默上重启大法吧。
只有少数固定机器报错?试试隔离这部分机器(关闭流量入口)。
单用户流量突增导致服务不稳定?如果不是惹不起的金主爸爸,请勇敢推送限流规则。
下游依赖挂了导致服务雪崩?还想什么呢,降级预案走起。
隔离一两台机器:将这部分机器入口流量关闭,让它们静静等待你的检阅。
Dump 应用快照:常用的快照类型一般就是线程堆栈和堆内存映射。
所有机器都回滚了,咋办?别慌,如果你的应用监控运维体系足够健全,那么你还有多维度的历史数据可以回溯:应用日志、中间件日志、GC 日志、内核日志、Metrics 指标等。
关联近期变更:90% 以上的线上问题都是由变更引发,这也是为什么集团安全生产的重点一直是在管控“变更”。所以,先不要急着否认(“肯定不是我刚加的那行代码问题!”),相信统计学概率,好好 review 下近期的变更历史(从近至远)。
全链路追踪分析:微服务和中台化盛行的当下,一次业务请求不经过十个八个应用处理一遍,都不好意思说自己是写 Java 的。所以,不要只盯着自己的应用不放,你需要把排查 scope 放大到全链路。
还原事件时间线:请把自己想象成福尔摩斯(柯南也行),摆在你面前的就是一个案发现场,你需要做的是把不同时间点的所有事件线索都串起来,重建和还原整个案发过程。要相信,时间戳是不会骗人的。
找到 Root Cause:排查问题多了你会发现,很多疑似原因往往只是另一个更深层次原因的表象结果之一。作为福尔摩斯,你最需要找到的是幕后凶手,而不是雇佣的杀人犯 —— 否则 TA 还会雇人再来一次。
尝试复现问题:千辛万苦推导出了根因,也不要就急着开始修 bug 了。如果可以,最好能把问题稳定复现出来,这样才更有说服力。这里提醒一点:可千万别在生产环境干这事(除非你真的 know what you're doing),否则搞不好就是二次伤害(你:哈哈哈,你看,这把刀当时就是从这个角度捅进去的,轨迹完全一样。用户:...)。
修复也是一种变更,需要经过完整的回归测试、灰度发布;切忌火急火燎上线了 bugfix,结果引发更多的 bugs to fix。
修复发布后,一定要做线上验证,并且保持观察一段时间,确保是真的真的修复了。
最后,如果问题已经上升到了故障这个程度,那就拉上大伙好好做个故障复盘吧。整个处理过程一定还有提升空间,你的经验教训对其他同学来说也是一次很好的输入和自查机会:幸福总是相似的,故障也是。
手里只有锤子,那看什么都像钉子。作为工程师,你需要的是一整套工具箱。
老板:既要快,又要稳,还要好。哦,工资的事你别担心,下个月一定能发出来。
问:要跑出最快的圈速,是车手重要,还是赛车重要? 答:全都重要。
吞吐率(Throughput):系统单位时间内能处理的工作负载,例如:在线 Web 系统 - QPS/TPS,离线数据分析系统 - 每秒处理的数据量。
响应时间(Response Time):以 Web 请求处理为例,响应时间(RT)即请求从发出到收到的往返时间,一般会由网络传输延迟、排队延迟和实际处理耗时几个部分共同组成。
可伸缩性(Scalability):系统通过增加机器资源(垂直/水平)来承载更多工作负载的能力;投入产出比越高(理想情况是线性伸缩),则说明系统的可伸缩性越好。
系统层面:tsar、top、iostat、vmstat
网络层面:iftop、tcpdump、wireshark
数据库层面:SQL explain、CloudDBA
应用代码层面:JProfiler、Arthas、jstack
有些事,你可以选择不做。
业务层面:e.g. 流程精简、需求简化。
编码层面:e.g. 循环内减少高开销操作。
架构层面:e.g. 减少没必要的抽象/分层。
数据层面:e.g. 数据清洗、提取、聚合。
有些事,你可以找人一起做。
方式:单机并行(多线程)、多机并行(分布式)。
优点:充分利用机器资源(多核、集群)。
缺点:同步开销、线程开销、数据倾斜。
同步优化:乐观锁、细粒度锁、无锁。
线程替代(如协程:Java WISP、Go routines、Kotlin coroutines)。
数据倾斜:负载均衡(Hash / RR / 动态)。
有些事,你可以放手,不用死等。
避免过度积压:Back-pressure(Reactive思想)。
有些事,你可以合起来一起做。
减少等待延迟:Timeout 触发提交,控制延迟上限。
游戏的本质:要么有闲,要么有钱。
案例:缓存、CDN、索引、只读副本(replication)。
案例:数据压缩(HTTP/2 头部压缩、Bitmap)。
程序 = 数据结构 + 算法
多了解一些“冷门”的数据结构 :Skip list、Bloom filter、Time Wheel 等。
一些“简单”的算法思想:递归、分治、贪心、动态规划。
共享经济 & 小区超市
案例:线程池、内存池、DB 连接池、Socket 连接池。
案例:TLB(ThreadLocalBuffer)、多级缓存(本地局部缓存 -> 共享全局缓存)。
升级红利:内核、JRE、依赖库、协议。
调参大师:配置、JVM、内核、网卡。
SQL 优化:索引、SELECT *、LIMIT 1。
业务特征定制优化:e.g. 凌晨业务低峰期做日志轮转。
Hybrid 思想(优点结合):JDK sort() 实现、Weex/RN。
稳住,我们能赢。—— by [0 杀 10 死] 正在等待复活的鲁班七号
优点:数据真实(客户端角度)
缺点:数据不全面(单一客户数据)
优点:覆盖所有调用数据。
缺点:缺失客户端链路数据。
父母:一个人在外漂了这么多年,也该找个人稳定下来了。
集群部署
数据副本
多机房容灾
接入层:DNS、VipServer、SLB。
服务层:服务发现 + 健康检查 + 剔除机制。
应用层:无状态设计(Stateless),便于随时和快速切换。
计划生育、上学调剂、车牌限号、景区限行... 人生处处被流控。
类型:QPS 流控、并发度流控。
工具:RateLimiter、信号量、Sentinel。
粒度:全局、用户级、接口级。
热点流控:避免意料之外的突增流量。
上午买的股票熔断,晚上家里保险丝熔断... 淡定,及时止损而已。
目的:防止连锁故障(雪崩效应)。
工具:Hystrix、Failsafe、Resilience4j。
功能:自动绕开异常服务并检测恢复状态。
流程:关闭 → 打开 → 半开。
没时间做饭了,今天就吃外卖吧... 对于健康问题,还是得少一点降级。
关闭非核心功能:停止应用日志打印
牺牲数据时效性:返回缓存中旧数据
牺牲数据精确性:降低数据采样频率
钉钉不回怎么办?每 10 分钟 ping 一次,超过 1 小时打电话。
超时时间设置:全链路自上而下规划
Timeout vs. Deadline:使用绝对时间会更好
消息去重
异步重试
指数退避
双 11 如何避免女友败家?提前把自己信用卡额度调低。
目的:防止资源被异常流量耗尽
资源类型:线程、队列、DB 连接
设限方式:资源池化、有界队列
超限处理:返回 ServiceUnavailable / QuotaExceeded
双 12 女友还是要败家?得嘞刷你自个的卡吧,别动我的。
目的:防止资源被部分异常流量耗尽;为 VIP 客户提供服务质量保证(QoS)。
隔离方式:队列划分、独立集群;注意处理优先级和资源分配比例。
女友哭着说再让我最后剁一次手吧?安全第一,宁愿心疼也不要肉疼。
Switch:类型安全;侵入性小。
DUCT:自动/手动调整 HSF 节点权重。
前人栽树,后人乘凉。
前人挖坑,后人凉凉。
编码:简洁度、命名一致性、代码行数等。
架构:组件耦合度、层次清晰度、职责单一性等。
需要变更代码或配置时,是否简单优雅、不易出错。
日志、监控是否完善;部署、扩容是否容易。
软件生命周期:维护周期 >> 开发周期。
破窗效应、熵增定律:可维护性会趋向于越来越差。
遗留系统的危害:理解难度,修改成本,变更风险;陷入不断踩坑、填坑、又挖坑的循环。
无规矩,不成方圆。
编码:推荐《Java 开发手册》,另外也推荐 The Art of Readable Code 这本书。
日志:无盲点、无冗余、TraceID。
测试:代码覆盖度、自动化回归。
别灰心,代码还有救。
何时重构:任何时候代码中嗅到坏味道(bad smell)。
重构节奏:小步迭代、回归验证。
重构 vs. 重写:需要综合考虑成本、风险、并行版本维护等因素。
推荐阅读:Refactoring: Improving the Design of Existing Code。
相信数据的力量。
系统数据:监控覆盖、Metrics 采集等,对于理解系统、排查问题至关重要。
业务数据:一致性校验、旧数据清理等;要相信,数据往往比代码要活得更久。
技术是第一生产力。
死守阵地 or 紧跟潮流? 需要综合评估风险、生产力、学习成本。
当前方向:微服务化、容器化。
三 结语
Truth lies underneath the skin - 真理永远暗藏在表象底下。