2016年的一个深夜,我正在家看电视,手机突然响了——运维群里@我,说线上服务挂了,用户大面积报错。
我当时心里一紧,赶紧打开电脑,连上VPN,开始排查。
那是我第一次独立处理线上重大故障,说实话,当时有点慌。日志翻了半天没找到原因,服务重启了也没用,用户还在不断报错。最后折腾了两个多小时,才定位到是数据库连接池满了,导致所有请求都超时。
从那以后,我经历了不少线上故障,也慢慢积累了一套排查思路。现在再遇到线上问题,我已经能比较从容地应对了。
今天就来聊聊,线上出Bug了该怎么办,我的排查思路和方法论。
一、第一步:止损优先
线上出问题,第一步不是找原因,而是止损——先让服务恢复正常,减少影响范围和损失。
很多新手一遇到线上问题就慌了,开始翻日志、改代码、查原因,结果查了半天没查到,用户还在不断报错,影响越来越大。
正确的做法是:先止损,再排查。
常见的止损手段:
- 回滚:如果是刚发布的版本导致的问题,第一时间回滚到上一个稳定版本。这是最快、最有效的止损手段。
- 重启服务:如果是内存泄漏、死锁、连接池满等问题,重启服务可能能临时恢复。但是重启只是临时手段,不是根本解决方案,重启后还要继续排查根因。
- 降级:如果某个非核心功能出问题,可以先把这个功能降级(关闭、返回默认值、用缓存),保证核心功能正常。
- 限流:如果是流量突增导致服务扛不住,可以先限流,保护服务不被打垮。
- 切流量:如果有多台服务器或多个机房,可以把出问题的服务器/机房的流量切走,先保证其他服务器/机房正常。
- 熔断:如果依赖的下游服务出问题,可以熔断,避免级联故障。
止损的原则是:先恢复服务,再找原因。不要为了找原因而让服务一直挂着,那样损失会越来越大。
当然,止损的时候也要注意:
- 不要盲目重启,重启可能会丢失现场(如内存中的堆栈信息、日志),如果需要保留现场,可以先dump再重启
- 回滚前确认回滚版本是稳定的,不要回滚到一个也有问题的版本
- 降级、限流、切流量这些操作,要提前做好预案,不要临时想办法
二、第二步:信息收集
止损之后,服务暂时恢复了,接下来要做的是信息收集——收集尽可能多的信息,为后续的定位做准备。
信息收集的几个维度:
1. 现象
- 具体的错误信息是什么?(错误码、错误提示、异常堆栈)
- 影响范围有多大?(所有用户还是部分用户?所有功能还是部分功能?)
- 什么时候开始的?(精确到分钟,方便对照发布时间、变更时间)
- 是持续性的还是偶发性的?(一直报错还是偶尔报错?)
2. 日志
日志是排查问题最重要的信息来源。
- 应用日志:查看应用的错误日志、异常堆栈,找到报错的具体位置和原因
- 访问日志:Nginx/Apache的访问日志,查看请求量、状态码分布、响应时间
- 数据库日志:MySQL的慢查询日志、错误日志,查看是否有慢SQL、死锁、连接错误
- 系统日志:操作系统的日志(/var/log/messages、dmesg),查看是否有OOM、磁盘满、网络错误
- 中间件日志:Redis、MQ、缓存等中间件的日志
看日志的技巧:
- 先看错误发生的时间点,找到对应的日志
- 用grep搜索关键词(错误码、异常类名、请求ID)
- 看日志的上下文,不要只看报错的那一行
- 对比正常时的日志和异常时的日志,找出差异
3. 监控
监控数据能帮你快速了解系统的整体状态。
- 服务器监控:CPU、内存、磁盘、网络、负载
- 应用监控:QPS、响应时间、错误率、线程数、连接数
- 数据库监控:连接数、慢查询数、QPS、锁等待
- 中间件监控:Redis的内存、命中率、连接数;MQ的堆积数
看监控的技巧:
- 看问题发生时间点的监控曲线,是否有异常波动
- 对比正常时的监控数据和异常时的,找出差异
- 看多个监控指标之间的关联(如CPU飙升是否伴随着QPS飙升)
4. 用户反馈
- 用户报的具体错误是什么?
- 用户的操作路径是什么?(做了什么操作后出现的问题)
- 用户的环境是什么?(什么浏览器、什么设备、什么网络)
- 是所有用户都有问题,还是特定用户才有问题?
用户反馈能帮你快速复现问题,尤其是一些特定场景下才出现的Bug。
5. 变更记录
- 问题发生前,是否有代码发布?
- 是否有配置变更?
- 是否有基础设施变更?(服务器、数据库、网络)
- 是否有依赖的第三方服务变更?
很多线上问题,都是由变更引起的。对照问题发生时间和变更时间,能快速定位问题来源。
三、第三步:问题定位
收集了足够的信息之后,接下来就是问题定位——找到Bug的根本原因。
定位问题的几种常用方法:
1. 复现
能复现的问题,就解决了一半。
- 尝试在测试环境复现问题
- 如果测试环境复现不了,尝试在预发布环境或灰度环境复现
- 根据用户的操作路径,一步步复现
- 如果是偶发性问题,尝试多次复现,找到规律
复现问题的时候,要注意:
- 尽量模拟和线上一样的环境和数据
- 记录复现的步骤和条件
- 如果复现不了,不要硬试,换其他方法
2. 日志分析
日志是定位问题最直接的方法。
- 找到报错的异常堆栈,定位到具体的代码行
- 看异常的类型和消息,判断问题的性质
- 看异常发生前的日志,了解上下文
- 如果有多个异常,看第一个异常(后续的异常可能是连锁反应)
常见的异常类型:
- NullPointerException(空指针):某个对象为null,调用了它的方法
- SQLException(数据库异常):SQL执行出错,可能是SQL语法错、数据约束冲突、连接失败
- IOException(IO异常):文件读写、网络通信出错
- TimeoutException(超时异常):请求超时,可能是下游服务慢、网络慢
- OutOfMemoryError(内存溢出):内存不够用了,可能是内存泄漏、加载了大数据
3. 二分法
如果不知道问题出在哪里,可以用二分法快速缩小范围。
- 代码二分:如果是刚发布的版本导致的问题,可以回滚到中间版本,看是否有问题,逐步缩小范围
- 时间二分:如果不知道问题什么时候开始的,可以查不同时间点的日志/监控,逐步缩小时间范围
- 模块二分:如果不知道哪个模块出问题,可以逐个关闭/开启模块,看问题是否复现
二分法的核心是:每次排除一半的可能性,快速定位到问题所在。
4. 对比法
对比正常和异常的差异,找到问题所在。
- 对比正常服务器和异常服务器的配置、日志、监控
- 对比正常用户和异常用户的数据、操作路径
- 对比正常时间段和异常时间段的日志、监控
- 对比正常版本和异常版本的代码差异(git diff)
对比法能帮你快速找到"什么变了",而"变了的东西"往往就是问题的原因。
5. 调试工具
如果日志和监控都找不到问题,可以用调试工具深入分析。
- jstack:查看Java线程堆栈,定位死锁、线程阻塞
- jmap/jstat:查看JVM内存使用,定位内存泄漏
- tcpdump/wireshark:抓包分析网络请求,定位网络问题
- strace:跟踪系统调用,定位IO问题
- gdb:调试C/C++程序
- Chrome DevTools:调试前端问题
- 数据库执行计划:EXPLAIN分析慢SQL
调试工具能帮你深入到系统内部,看到日志和监控看不到的细节。
四、常见的线上Bug类型
根据我的经验,线上Bug常见的有以下几类:
1. 空指针(NullPointerException)
最常见的Bug。某个对象为null,但是代码调用了它的方法或属性。
常见原因:
- 从数据库/缓存/接口查询的数据为null,没有做判空
- 链式调用中某个环节返回null
- 并发情况下,对象被另一个线程置为null
排查方法:看异常堆栈,定位到具体的代码行,检查哪个对象可能为null。
预防:对可能为null的对象做判空处理,使用Optional(Java 8+)或空对象模式。
2. 死锁
两个或多个线程互相等待对方释放锁,导致都无法继续执行。
常见原因:
- 锁的获取顺序不一致(线程A先锁X再锁Y,线程B先锁Y再锁X)
- 数据库事务中的锁等待
- 分布式锁的使用不当
排查方法:用jstack查看线程堆栈,找到BLOCKED状态的线程,分析锁的依赖关系。
预防:统一锁的获取顺序,设置锁超时时间,避免在锁内做耗时操作。
3. 内存泄漏
内存用了不释放,导致内存占用越来越高,最终OOM。
常见原因:
- 静态集合类(如static Map/List)只加不删
- 各种连接(数据库连接、网络连接)没关闭
- 监听器/回调没注销
- 缓存只加不淘汰
- 内部类持有外部类引用(Java)
排查方法:用jmap dump内存,用MAT/VisualVM分析,找到占用内存最多的对象,分析引用链。
预防:及时释放资源,使用弱引用/软引用,缓存设置淘汰策略,定期内存泄漏检测。
4. 性能问题
服务响应慢、QPS上不去、CPU/内存占用高。
常见原因:
- 慢SQL:没有索引、索引失效、大表全表扫描
- N+1查询:循环中查询数据库
- 大对象:一次性加载大量数据到内存
- 同步调用:应该异步的操作用了同步
- 锁竞争:锁的粒度太大,导致线程等待
- 资源不足:CPU、内存、带宽不够
排查方法:
- 看监控,找到瓶颈在哪里(CPU、内存、磁盘IO、网络)
- 看慢查询日志,找到慢SQL,用EXPLAIN分析
- 用jstack看线程堆栈,找到线程阻塞在哪里
- 用压测工具复现性能问题
预防:SQL加索引,避免N+1查询,大操作异步化,缓存热点数据,定期性能测试。
5. 数据不一致
主从数据不一致、缓存和数据库不一致、分布式系统数据不一致。
常见原因:
- 主从延迟
- 缓存更新不及时或缓存失败
- 分布式事务失败
- 并发写入冲突
- 数据迁移/同步出错
排查方法:
- 对比主库和从库的数据
- 对比缓存和数据库的数据
- 看数据操作的日志,找到不一致的时间点
- 分析并发场景下的数据操作
预防:
- 主从延迟监控,延迟过大时读主库
- 缓存更新策略(先更数据库再删缓存,或用双写)
- 分布式事务用TCC、Saga、消息事务等方案
- 并发写入用乐观锁或分布式锁
6. 并发问题
多线程/多进程环境下的数据竞争、竞态条件。
常见原因:
- 共享变量没有同步
- 检查-执行不是原子操作(check-then-act)
- 死循环、活锁
- 线程池配置不当
排查方法:
- 代码审查,检查共享变量的访问是否有同步
- 用jstack看线程状态
- 压测复现并发问题
预防:
- 共享变量用synchronized/ReentrantLock同步
- 使用原子类(AtomicInteger等)
- 使用并发集合(ConcurrentHashMap等)
- 尽量避免共享状态,用不可变对象
五、第四步:修复和验证
定位到问题之后,接下来就是修复和验证。
修复的原则:
- 最小改动:修复Bug时,尽量做最小的改动,不要顺手重构、不要改无关的代码。改动越大,引入新Bug的风险越大。
- 根因修复:要修复根本原因,而不是表面现象。比如空指针,不要只加个判空就完事,要搞清楚为什么这个对象会是null,从源头上避免。
- 兼容性:修复时要考虑兼容性,不要破坏现有的功能和接口。
- 可回滚:修复的代码要可回滚,如果修复后出现新问题,能快速回滚。
验证的步骤:
- 本地验证:在本地开发环境复现问题,修复后验证问题是否解决。
- 测试环境验证:部署到测试环境,跑一遍相关的测试用例,确保修复有效且没有引入新问题。
- 预发布/灰度验证:部署到预发布环境或灰度环境,观察一段时间,确认没有问题。
- 全量发布:确认没有问题后,全量发布。
- 线上监控:发布后密切监控,观察错误率、响应时间、日志等,确认问题彻底解决。
验证的时候要注意:
- 不要只验证修复的那个场景,还要验证相关的场景,避免修复一个Bug引入另一个Bug
- 偶发性问题要多次验证,确保不是"碰巧没出现"
- 发布后至少观察30分钟到1小时,确认稳定后再下班/休息
六、第五步:事后复盘
问题解决之后,不要就这么过去了,要做事后复盘——总结经验教训,避免类似问题再次发生。
复盘的内容:
- 问题回顾:问题是什么、什么时候发生的、影响范围有多大、持续了多长时间。
- 时间线:从问题发生到发现、到止损、到定位、到修复、到验证,每个时间点做了什么。
- 根因分析:问题的根本原因是什么(技术原因、流程原因、人为原因)。
- 做得好的地方:这次处理中,哪些地方做得好,值得保持。
- 做得不好的地方:哪些地方做得不好,需要改进。比如:
- 发现不及时(监控没覆盖到) - 止损慢(没有回滚预案) - 定位慢(日志不全、没有链路追踪) - 沟通不畅(信息不同步)
- 改进措施:针对做得不好的地方,制定具体的改进措施,明确责任人和时间节点。
复盘的原则:
- 对事不对人:复盘是为了改进,不是为了追责
- 深挖根因:不要停留在表面,要找到根本原因
- 可执行:改进措施要具体、可执行,不要写空话
- 跟踪落实:改进措施要跟踪落实,不要复盘完就忘了
七、预防:如何减少线上Bug
最后,聊聊如何从源头上减少线上Bug。
1. 代码质量
- Code Review:每一行代码都要经过Review
- 单元测试:核心逻辑要有单元测试,覆盖率尽量高
- 集成测试:关键流程要有集成测试
- 静态代码检查:用SonarQube、FindBugs等工具检查代码质量
2. 测试
- 测试环境要尽量和线上一致
- 测试用例要覆盖正常场景、异常场景、边界场景
- 回归测试:每次发布前跑一遍回归测试
- 灰度测试:新功能先灰度,观察没问题再全量
3. 发布
- 小步快跑:每次发布的改动尽量小,不要一次发太多
- 发布时间:尽量在业务低峰期发布
- 回滚预案:每次发布前都要准备好回滚预案
- 发布后观察:发布后密切监控,至少观察30分钟
4. 监控
- 全面的监控:服务器、应用、数据库、中间件都要监控
- 告警:设置合理的告警阈值,问题早发现
- 日志:详细的日志,方便排查问题
- 链路追踪:分布式系统要有链路追踪,方便定位问题
5. 架构
- 高可用:避免单点故障,多机房、多副本
- 容错:降级、限流、熔断,保护系统
- 可扩展:水平扩展,应对流量增长
- 松耦合:模块之间松耦合,一个模块出问题不影响其他模块
6. 应急
- 应急预案:常见故障要有应急预案
- 应急演练:定期做故障演练,验证应急预案
- On-call:建立On-call机制,问题有人及时处理
八、写在最后
线上出Bug,是每个程序员都会遇到的事。区别在于,遇到线上问题时,你是慌慌张张、越搞越糟,还是从容不迫、高效解决。
我的排查思路总结起来就是五步:
- 止损优先:先恢复服务,再找原因
- 信息收集:收集日志、监控、用户反馈、变更记录
- 问题定位:用复现、日志分析、二分法、对比法、调试工具定位根因
- 修复验证:最小改动修复,多环境验证
- 事后复盘:总结经验教训,持续改进
这套思路不是万能的,但是能帮你在遇到线上问题时,有一个清晰的框架,不慌不乱、高效排查。
最后,想说的是:线上Bug不可怕,可怕的是不从Bug中学习。每一个线上Bug,都是一次成长的机会。认真对待每一个Bug,深入分析根因,做好预防,你会越来越强。
愿每一个程序员,都能少遇到线上Bug,遇到了也能从容应对。愿每一个系统,都能稳定运行,永不宕机。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录