> For the complete documentation index, see [llms.txt](https://rowkey-books.gitbook.io/pragmatic-java-engineer/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://rowkey-books.gitbook.io/pragmatic-java-engineer/online-debug.md).

# 附录F: 如何应对在线故障

系统环境：CentOS 6.5 && JDK 1.8.0\_121

## 应对思路

第一时间上报给自己的直属领导或者相关负责人，由其把控后续流程，并在可能的情况下及时周知所有可能受影响方：问题、影响范围、解决方案、预计恢复时间等。

1. 根据经验来分析。如果应急团队中有人对相应的问题有经验，并能确定能够通过某种手段恢复系统的正常运行，那么应该第一时间恢复（回滚等），同时务必要保留现场，以备后续对问题的定位和修复；如果没有人有经验，则需要使用比较粗暴的办法保证服务可用，如定时重启、限流、降级等。
2. 业务负责人、技术负责人、核心研发人员、架构师、运维工程师以及运营人员对问题的原因进行快速分析。分析的过程需要首先考虑系统近期的变化，包括以下几方面：
   * 系统最近是否进行了发布上线工作？
   * 服务的使用方是否有运营活动？
   * 网络是否有流量的波动？
   * 最近的业务量是否上升？
   * 运营人员是否在系统了做了变动？
   * 依赖的基础平台和资源是否进行了发布上线？
   * 依赖的其他系统是否进行了发布上线？

## 可能的原因

* **DNS、网络、CDN故障**
* **代码Bug**: 逻辑不严谨、连接未释放
* **代码性能**：循环外部调用、未使用批量读取、正则循环等
* **内存泄漏**：本地缓存
* **异常流量/攻击**：DDOS
* **业务量提升**：容量预估失误
* **外部系统问题**: 数据库、搜索引擎、分布式缓存、消息队列等中间件的性能问题
* 应用的性能问题

## 分析步骤

* DNS是否正常？可以通过**ping**命令来测试。
* 网络是否正常？可以通过**ping、telnet**命令来测试。
* 是否是CDN缓存的问题？如果使用了CDN缓存可能会出现内容不一致的问题。
* 根据日志输出的异常信息定位问题，需要区分Tomcat中的catalina.out（标准输出和错误）和localhost.xx.log（应用初始化的日志，错误则无法启动）
* 磁盘是否已满(`df -h`)？磁盘满了则需要删除多余日志。
* 流量是否有异常？通过限流、降级、扩展服务结点、架构优化等手段解决。
* 外部系统问题则需要针对外部系统（数据库、搜索引擎、分布式缓存、消息队列）进行故障排查、性能优化等。

以上步骤未发现问题，就需要从应用的CPU、内存、IO方面做进一步分析。

## CPU分析

* 使用top、vmstat、ps等命令定位CPU使用率高的线程：`top -p [processId] -H`。
* `jstack [pid]`打印繁忙进程的堆栈信息
* 通过`printf %0x [processId]`转换进程id为16进制，在堆栈信息中查找对应的堆栈信息
* `jstat -gcutil [pid]`，查看GC的情况是否正常，是否GC引起了CPU飚高
* JVM加入`-XX:+PrintCompilation`参数，查看是否是JIT编译引起了CPU飚高

CPU繁忙可能的原因：线程中有无限空循环、无阻塞、正则匹配或者单纯的计算；发生了频繁的GC；多线程的上下文切换；JIT编译

## 内存分析

* **堆外内存**：JNI、Deflater/Inflater、DirectByteBuffer。通过vmstat、top、pidstat等查看swap和物理内存的消耗状况。通过Google-preftools来追踪JNI、Deflater这种调用的资源使用状况。
* **堆内存**：

  * 查看JVM内存使用状况：`jmap -heap <pid>`。
  * 查看JVM内存存活的对象：`jmap -histo:live <pid>`。
  * 把heap里所有对象都dump下来，无论对象是死是活：`jmap -dump:format=b,file=xxx.hprof <pid>`
  * 先做一次Full GC，再dump，只包含仍然存活的对象信息：`jmap -dump:format=b,live,file=xxx.hprof <pid>`
  * 使用**Eclipse MAT**或者**jhat**打开堆dump的文件，根据内存中的具体对象使用情况分析
  * VJTools中的**vjmap**可以分代打印出堆内存的对象实例占用信息

  堆内存中包括创建的对象、全局集合、缓存、ClassLoader、多线程等，都有可能造成问题。

## 磁盘IO分析

* `iostat -xz 1`查看磁盘IO情况。
* **r/s, w/s, rkB/s, wkB/s**等指标过大，可能会引起性能问题。
* **await**过大，可能是硬件设备遇到了瓶颈或者出现故障。一次IO操作一般超过20ms就说明磁盘压力过大。
* **avgqu-sz**大于1，可能是硬件设备已经饱和。
* **%util**越大表示磁盘越繁忙，100%表示已经饱和。
* 通过使用**strace**工具定位对文件IO的系统调用

IO性能差可能的原因：大量的随机读写，设备慢，文件太大。

## 网络IO分析

* `netstat -anpt`查看网络连接状况。当TIME\_WAIT或者CLOSE\_WAIT连接过多时，会影响应用的响应速度。前者需要优化内核参数，后者一般是代码Bug没有释放网络连接。
* 使用**tcpdump**来具体分析网络IO的数据。tcpdump出的文件直接打开是一堆二进制的数据，可以使用Wireshark查看具体的连接以及其中数据的内容。`tcpdump -i eth0 -w tmp.cap -tnn dst port 8080`
* `sar -n DEV`，查看吞吐率和吞吐数据包数，判断是否超过网卡限制。

## 在线代码分析

* **远程Debug**: Tomcat远程调试
* **在线Trace**：BTrace、HouseMD、Greys-atonomy、arthas

## 故障解决

* **代码Bug**：直接修复代码逻辑即可
* **性能问题**： 针对CPU、内存、IO，进行代码上的使用优化。
* **JVM配置**

## CPU使用优化

* **不要存在一直运行的线程**（无限循环），可以使用sleep休眠一段时间。这种情况普遍存在于一些pull方式消费数据的场景下，当一次pull没有拿到数据的时候建议sleep一下，再做下一次pull。
* 轮询的时候可以使用**wait/notify**机制代替循环。
* **避免正则表达式匹配、过多的计算**。例如，避免使用String的format、split、replace方法；避免使用正则去判断邮箱格式（有时候会造成死循环）；避免序列/反序列化。
* **使用线程池**，减少线程数以及线程的切换。
* 多线程对于锁的竞争可以考虑**减小锁的粒度**（使用ReetrantLock）、**拆分锁**（类似ConcurrentHashMap分bucket上锁）, 或者使用CAS、ThreadLocal、不可变对象等**无锁技术**。此外，多线程代码的编写最好使用JDK提供的并发包、Executors框架以及ForkJoin等，此外Disruptor和Actor在合适的场景也可以使用。
* 结合JVM和代码一起进行分析，**避免产生频繁的GC**，尤其是Full GC。

## 内存使用优化

* 使用**基本数据类型**而不是其包装类型能够节省内存。
* **尽量避免分配大对象**。大对象分配的代价以及初始化的代价很大，不同大小的大对象可能导致Java堆碎片，尤其是CMS；
* **避免改变数据结构大小**。如避免改变数组或array backed collections / containers的大小;对象构建（初始化）时最好显式批量定数组大小;改变大小导致不必要的对象分配，可能导致Java堆碎片。
* 避免保存重复的String对象，同时也需要小心String.subString()与String.intern()的使用, 中间过程会生成不少字符串。
* 尽量不要使用finalizer。
* **释放不必要的引用**：ThreadLocal使用完记得释放以防止内存泄漏，各种stream使用完也记得close。
* **使用对象池避免无节制创建对象，造成频繁GC**。但也不要随便使用对象池，除非像连接池、线程池这种初始化/创建资源消耗较大的场景。
* 缓存失效算法，可以考虑使用SoftReference、WeakReference保存缓存对象。
* 谨慎热部署/加载的使用，尤其是动态加载类等。
* 打印日志时**不要输出文件名、行号**，因为日志框架一般都是通过打印线程堆栈实现，生成大量String。此外，打印日志时，**先判断对应级别的日志是否打开再做操作**，否则也会生成大量String。

## IO使用优化

* 考虑使用异步写入代替同步写入，可以借鉴Redis的AOF机制。
* 利用预读取或者缓存，减少随机读。
* 尽量批量写入，减少IO次数和寻址。
* 使用数据库代替文件存储。
* 使用异步IO、多路复用IO/事件驱动IO代替同步阻塞IO。
* 使用协程提高网络IO性能: Quasar。

## JVM配置

* 合理设置各个代的大小。新生代尽量设置的大，但不能过大（会产生碎片），同样也要避免Survivor设置过大和过小。
* 选择合适的GC策略。需要根据不同的场景选择合适的GC策略。这里需要说的是，CMS并非全能的。除非特别需要再设置，毕竟CMS的新生代回收策略ParNew并非最快的，且会产生碎片。此外，G1直到JDK8的出现也并没有得到广泛应用，并不建议使用。
* 老年代优先使用Parallel GC（-XX:+UseParallel\[Old]GC），可以保证最大的吞吐量。由于CMS会产生碎片，确实有必要才改成CMS或G1。
* 注意内存墙（严重阻碍处理器性能发挥的内存瓶颈），一般讲单点应用堆内存设置为4G到5G即可，依靠可扩展性提高并发能力。
* 设置JVM的内存大小有一个经验法则：完成Full GC后，应该释放出70%的内存。
* 配置堆内存和永久代/元空间内存之和小于32GB，从而可以使用压缩指针节省对象指针的占用。
* 打开GC日志并读懂GC日志，以便于排查问题。GC日志文件可以使用GC Histogram（gchisto）生成图表和表格。

## 代码性能建议

* 算法、逻辑上是程序性能的首要，遇到性能问题，应该首先**优化程序的逻辑处理**。
* **优先考虑使用返回值而不是异常表示错误**。虽然现代JVM已经做了大量优化工作，但毕竟异常是有代价的，需要在合适的地方使用。一般用错误码返回值处理可能会发生的事情，用异常捕捉处理不期望发生的事情。如果使用异常并且比较关注性能，可以通过覆盖掉异常类的fillInStackTrace()方法为空方法，使其不拷贝栈信息。
* **查看自己的代码是否对内联是友好的**。内联友好指的方法的大小不超过35字节(默认的内联阈值，不建议修改）、非虚方法（虚方法指的是在运行期才能确定执行对象的方法，最新的JVM对非虚方法会通过CHA类层次分析来判断是否可以内联）。
