<audio id="audio" title="33 | 加餐3：定位应用问题，排错套路很重要" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a3/89/a386559811a46f95445534120cc39889.mp3"></audio>

你好，我是朱晔。

咱们这个课程已经更新13讲了，感谢各位同学一直在坚持学习，并在评论区留下了很多高质量的留言。这些留言，有的是分享自己曾经踩的坑，有的是对课后思考题的详细解答，还有的是提出了非常好的问题，进一步丰富了这个课程的内容。

有同学说，这个课程的案例非常实用，都是工作中会遇到的。正如我在开篇词中所说，这个课程涉及的100个案例、约130个小坑，有40%来自于我经历过或者是见过的200多个线上生产事故，剩下的60%来自于我开发业务项目，以及日常审核别人的代码发现的问题。确实，我在整理这些案例上花费了很多精力，也特别感谢各位同学的认可，更希望你们能继续坚持学习，继续在评论区和我交流。

也有同学反馈，排查问题的思路很重要，希望自己遇到问题时，也能够从容、高效地定位到根因。因此，今天这一讲，我就与你说说我在应急排错方面积累的心得。这都是我多年担任技术负责人和架构师自己总结出来的，希望对你有所帮助。当然了，也期待你能留言与我说说，自己平时的排错套路。

## 在不同环境排查问题，有不同的方式

要说排查问题的思路，我们首先得明白是在什么环境排错。

- 如果是在自己的开发环境排查问题，那你几乎可以使用任何自己熟悉的工具来排查，甚至可以进行单步调试。只要问题能重现，排查就不会太困难，最多就是把程序调试到JDK或三方类库内部进行分析。
- 如果是在测试环境排查问题，相比开发环境少的是调试，不过你可以使用JDK自带的jvisualvm或阿里的[Arthas](https://github.com/alibaba/arthas)，附加到远程的JVM进程排查问题。另外，测试环境允许造数据、造压力模拟我们需要的场景，因此遇到偶发问题时，我们可以尝试去造一些场景让问题更容易出现，方便测试。
- 如果是在生产环境排查问题，往往比较难：一方面，生产环境权限管控严格，一般不允许调试工具从远程附加进程；另一方面，生产环境出现问题要求以恢复为先，难以留出充足的时间去慢慢排查问题。但，因为生产环境的流量真实、访问量大、网络权限管控严格、环境复杂，因此更容易出问题，也是出问题最多的环境。

接下来，我就与你详细说说，如何在生产环境排查问题吧。

## 生产问题的排查很大程度依赖监控

其实，排查问题就像在破案，生产环境出现问题时，因为要尽快恢复应用，就不可能保留完整现场用于排查和测试。因此，是否有充足的信息可以了解过去、还原现场就成了破案的关键。这里说的信息，主要就是日志、监控和快照。

日志就不用多说了，主要注意两点：

- 确保错误、异常信息可以被完整地记录到文件日志中；
- 确保生产上程序的日志级别是INFO以上。记录日志要使用合理的日志优先级，DEBUG用于开发调试、INFO用于重要流程信息、WARN用于需要关注的问题、ERROR用于阻断流程的错误。

对于监控，在生产环境排查问题时，首先就需要开发和运维团队做好充足的监控，而且是多个层次的监控。

- 主机层面，对CPU、内存、磁盘、网络等资源做监控。如果应用部署在虚拟机或Kubernetes集群中，那么除了对物理机做基础资源监控外，还要对虚拟机或Pod做同样的监控。监控层数取决于应用的部署方案，有一层OS就要做一层监控。
- 网络层面，需要监控专线带宽、交换机基本情况、网络延迟。
- 所有的中间件和存储都要做好监控，不仅仅是监控进程对CPU、内存、磁盘IO、网络使用的基本指标，更重要的是监控组件内部的一些重要指标。比如，著名的监控工具Prometheus，就提供了大量的[exporter](https://prometheus.io/docs/instrumenting/exporters/)来对接各种中间件和存储系统。
- 应用层面，需要监控JVM进程的类加载、内存、GC、线程等常见指标（比如使用[Micrometer](https://micrometer.io/)来做应用监控），此外还要确保能够收集、保存应用日志、GC日志。

我们再来看看快照。这里的“快照”是指，应用进程在某一时刻的快照。通常情况下，我们会为生产环境的Java应用设置-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=…这2个JVM参数，用于在出现OOM时保留堆快照。这个课程中，我们也多次使用MAT工具来分析堆快照。

了解过去、还原现场后，接下来我们就看看定位问题的套路。

## 分析定位问题的套路

定位问题，首先要定位问题出在哪个层次上。比如，是Java应用程序自身的问题还是外部因素导致的问题。我们可以先查看程序是否有异常，异常信息一般比较具体，可以马上定位到大概的问题方向；如果是一些资源消耗型的问题可能不会有异常，我们可以通过指标监控配合显性问题点来定位。

一般情况下，程序的问题来自以下三个方面。

第一，程序发布后的Bug，回滚后可以立即解决。这类问题的排查，可以回滚后再慢慢分析版本差异。

第二，外部因素，比如主机、中间件或数据库的问题。这类问题的排查方式，按照主机层面的问题、中间件或存储（统称组件）的问题分为两类。

主机层面的问题，可以使用工具排查：

- CPU相关问题，可以使用top、vmstat、pidstat、ps等工具排查；
- 内存相关问题，可以使用free、top、ps、vmstat、cachestat、sar等工具排查；
- IO相关问题，可以使用lsof、iostat、pidstat、sar、iotop、df、du等工具排查；
- 网络相关问题，可以使用ifconfig、ip、nslookup、dig、ping、tcpdump、iptables等工具排查。

组件的问题，可以从以下几个方面排查：

- 排查组件所在主机是否有问题；
- 排查组件进程基本情况，观察各种监控指标；
- 查看组件的日志输出，特别是错误日志；
- 进入组件控制台，使用一些命令查看其运作情况。

第三，因为系统资源不够造成系统假死的问题，通常需要先通过重启和扩容解决问题，之后再进行分析，不过最好能留一个节点作为现场。系统资源不够，一般体现在CPU使用高、内存泄漏或OOM的问题、IO问题、网络相关问题这四个方面。

对于CPU使用高的问题，如果现场还在，具体的分析流程是：

- 首先，在Linux服务器上运行top -Hp pid命令，来查看进程中哪个线程CPU使用高；
- 然后，输入大写的P将线程按照 CPU 使用率排序，并把明显占用CPU的线程ID转换为16进制；
- 最后，在jstack命令输出的线程栈中搜索这个线程ID，定位出问题的线程当时的调用栈。

如果没有条件直接在服务器上运行top命令的话，我们可以用采样的方式定位问题：间隔固定秒数（比如10秒）运行一次jstack命令，采样几次后，对比采样得出哪些线程始终处于运行状态，分析出问题的线程。

如果现场没有了，我们可以通过排除法来分析。CPU使用高，一般是由下面的因素引起的：

- 突发压力。这类问题，我们可以通过应用之前的负载均衡的流量或日志量来确认，诸如Nginx等反向代理都会记录URL，可以依靠代理的Access Log进行细化定位，也可以通过监控观察JVM线程数的情况。压力问题导致CPU使用高的情况下，如果程序的各资源使用没有明显不正常，之后可以通过压测+Profiler（jvisualvm就有这个功能）进一步定位热点方法；如果资源使用不正常，比如产生了几千个线程，就需要考虑调参。
- GC。这种情况，我们可以通过JVM监控GC相关指标、GC Log进行确认。如果确认是GC的压力，那么内存使用也很可能会不正常，需要按照内存问题分析流程做进一步分析。
- 程序中死循环逻辑或不正常的处理流程。这类问题，我们可以结合应用日志分析。一般情况下，应用执行过程中都会产生一些日志，可以重点关注日志量异常部分。

对于内存泄露或OOM的问题，最简单的分析方式，就是堆转储后使用MAT分析。堆转储，包含了堆现场全貌和线程栈信息，一般观察支配树图、直方图就可以马上看到占用大量内存的对象，可以快速定位到内存相关问题。这一点我们会在[第5篇加餐](https://time.geekbang.org/column/article/230534)中详细介绍。

需要注意的是，Java进程对内存的使用不仅仅是堆区，还包括线程使用的内存（线程个数*每一个线程的线程栈）和元数据区。每一个内存区都可能产生OOM，可以结合监控观察线程数、已加载类数量等指标分析。另外，我们需要注意看一下，JVM参数的设置是否有明显不合理的地方，限制了资源使用。

IO相关的问题，除非是代码问题引起的资源不释放等问题，否则通常都不是由Java进程内部因素引发的。

网络相关的问题，一般也是由外部因素引起的。对于连通性问题，结合异常信息通常比较容易定位；对于性能或瞬断问题，可以先尝试使用ping等工具简单判断，如果不行再使用tcpdump或Wireshark来分析。

## 分析和定位问题需要注意的九个点

有些时候，我们分析和定位问题时，会陷入误区或是找不到方向。遇到这种情况，你可以借鉴下我的九个心得。

**第一，考虑“鸡”和“蛋”的问题。**比如，发现业务逻辑执行很慢且线程数增多的情况时，我们需要考虑两种可能性：

- 一是，程序逻辑有问题或外部依赖慢，使得业务逻辑执行慢，在访问量不变的情况下需要更多的线程数来应对。比如，10TPS的并发原先一次请求1s可以执行完成，10个线程可以支撑；现在执行完成需要10s，那就需要100个线程。
- 二是，有可能是请求量增大了，使得线程数增多，应用本身的CPU资源不足，再加上上下文切换问题导致处理变慢了。

出现问题的时候，我们需要结合内部表现和入口流量一起看，确认这里的“慢”到底是根因还是结果。

**第二，考虑通过分类寻找规律。**在定位问题没有头绪的时候，我们可以尝试总结规律。

比如，我们有10台应用服务器做负载均衡，出问题时可以通过日志分析是否是均匀分布的，还是问题都出现在1台机器。又比如，应用日志一般会记录线程名称，出问题时我们可以分析日志是否集中在某一类线程上。再比如，如果发现应用开启了大量TCP连接，通过netstat我们可以分析出主要集中连接到哪个服务。

如果能总结出规律，很可能就找到了突破点。

**第三，分析问题需要根据调用拓扑来，不能想当然。**比如，我们看到Nginx返回502错误，一般可以认为是下游服务的问题导致网关无法完成请求转发。对于下游服务，不能想当然就认为是我们的Java程序，比如在拓扑上可能Nginx代理的是Kubernetes的Traefik Ingress，链路是Nginx-&gt;Traefik-&gt;应用，如果一味排查Java程序的健康情况，那么始终不会找到根因。

又比如，我们虽然使用了Spring Cloud Feign来进行服务调用，出现连接超时也不一定就是服务端的问题，有可能是客户端通过URL来调用服务端，并不是通过Eureka的服务发现实现的客户端负载均衡。换句话说，客户端连接的是Nginx代理而不是直接连接应用，客户端连接服务出现的超时，其实是Nginx代理宕机所致。

**第四，考虑资源限制类问题。**观察各种曲线指标，如果发现曲线慢慢上升然后稳定在一个水平线上，那么一般就是资源达到了限制或瓶颈。

比如，在观察网络带宽曲线的时候，如果发现带宽上升到120MB左右不动了，那么很可能就是打满了1GB的网卡或传输带宽。又比如，观察到数据库活跃连接数上升到10个就不动了，那么很可能是连接池打满了。观察监控一旦看到任何这样的曲线，都要引起重视。

**第五，考虑资源相互影响。**CPU、内存、IO和网络，这四类资源就像人的五脏六腑，是相辅相成的，一个资源出现了明显的瓶颈，很可能会引起其他资源的连锁反应。

比如，内存泄露后对象无法回收会造成大量Full GC，此时CPU会大量消耗在GC上从而引起CPU使用增加。又比如，我们经常会把数据缓存在内存队列中进行异步IO处理，网络或磁盘出现问题时，就很可能会引起内存的暴涨。因此，出问题的时候，我们要考虑到这一点，以避免误判。

**第六，排查网络问题要考虑三个方面，到底是客户端问题，还是服务端问题，还是传输问题。**比如，出现数据库访问慢的现象，可能是客户端的原因，连接池不够导致连接获取慢、GC停顿、CPU占满等；也可能是传输环节的问题，包括光纤、防火墙、路由表设置等问题；也可能是真正的服务端问题，需要逐一排查来进行区分。

服务端慢一般可以看到MySQL出慢日志，传输慢一般可以通过ping来简单定位，排除了这两个可能，并且仅仅是部分客户端出现访问慢的情况，就需要怀疑是客户端本身的问题。对于第三方系统、服务或存储访问出现慢的情况，不能完全假设是服务端的问题。

**第七，快照类工具和趋势类工具需要结合使用。**比如，jstat、top、各种监控曲线是趋势类工具，可以让我们观察各个指标的变化情况，定位大概的问题点；而jstack和分析堆快照的MAT是快照类工具，用于详细分析某一时刻应用程序某一个点的细节。

一般情况下，我们会先使用趋势类工具来总结规律，再使用快照类工具来分析问题。如果反过来可能就会误判，因为快照类工具反映的只是一个瞬间程序的情况，不能仅仅通过分析单一快照得出结论，如果缺少趋势类工具的帮助，那至少也要提取多个快照来对比。

**第八，不要轻易怀疑监控。**我曾看过一个空难事故的分析，飞行员在空中发现仪表显示飞机所有油箱都处于缺油的状态，他第一时间的怀疑是油表出现故障了，始终不愿意相信是真的缺油，结果飞行不久后引擎就断油熄火了。同样地，在应用出现问题时，我们会查看各种监控系统，但有些时候我们宁愿相信自己的经验，也不相信监控图表的显示。这可能会导致我们完全朝着错误的方向来排查问题。

如果你真的怀疑是监控系统有问题，可以看一下这套监控系统对于不出问题的应用显示是否正常，如果正常那就应该相信监控而不是自己的经验。

**第九，如果因为监控缺失等原因无法定位到根因的话，相同问题就有再出现的风险**，需要做好三项工作：

- 做好日志、监控和快照补漏工作，下次遇到问题时可以定位根因；
- 针对问题的症状做好实时报警，确保出现问题后可以第一时间发现；
- 考虑做一套热备的方案，出现问题后可以第一时间切换到热备系统快速解决问题，同时又可以保留老系统的现场。

## 重点回顾

今天，我和你总结分享了分析生产环境问题的套路。

第一，分析问题一定是需要依据的，靠猜是猜不出来的，需要提前做好基础监控的建设。监控的话，需要在基础运维层、应用层、业务层等多个层次进行。定位问题的时候，我们同样需要参照多个监控层的指标表现综合分析。

第二，定位问题要先对原因进行大致分类，比如是内部问题还是外部问题、CPU相关问题还是内存相关问题、仅仅是A接口的问题还是整个应用的问题，然后再去进一步细化探索，一定是从大到小来思考问题；在追查问题遇到瓶颈的时候，我们可以先退出细节，再从大的方面捋一下涉及的点，再重新来看问题。

第三，分析问题很多时候靠的是经验，很难找到完整的方法论。遇到重大问题的时候，往往也需要根据直觉来第一时间找到最有可能的点，这里甚至有运气成分。我还和你分享了我的九条经验，建议你在平时解决问题的时候多思考、多总结，提炼出更多自己分析问题的套路和拿手工具。

最后，值得一提的是，定位到问题原因后，我们要做好记录和复盘。每一次故障和问题都是宝贵的资源，复盘不仅仅是记录问题，更重要的是改进。复盘时，我们需要做到以下四点：

- 记录完整的时间线、处理措施、上报流程等信息；
- 分析问题的根本原因；
- 给出短、中、长期改进方案，包括但不限于代码改动、SOP、流程，并记录跟踪每一个方案进行闭环；
- 定期组织团队回顾过去的故障。

## 思考与讨论

1. 如果你现在打开一个App后发现首页展示了一片空白，那这到底是客户端兼容性的问题，还是服务端的问题呢？如果是服务端的问题，又如何进一步细化定位呢？你有什么分析思路吗？
1. 对于分析定位问题，你会做哪些监控或是使用哪些工具呢？

你有没有遇到过什么花了很长时间才定位到的，或是让你印象深刻的问题或事故呢？我是朱晔，欢迎在评论区与我留言分享你的想法，也欢迎你把这篇文章分享给你的朋友或同事，一起交流。
