对于线上遇到的 “疑难杂症”,需要通过理性的思维去分析问题、排查问题、定位问题、解决问题,同时,如果解决掉所遇到的问题或瓶颈后,也可以在能力范围之内尝试最优解以及适当考虑拓展性。
JVM线上环境常见故障与排查思路
JVM在线上环境往往会出现一下问题:
JVM内存泄漏
JVM内存溢出
业务线程死锁
应用程序一场宕机
县城堵塞/响应速度变慢
CPU利用率飙升或100%
线上排查及其解决问题的思路
分析问题:根据理论知识 + 经验分析问题,判断问题可能出现的位置或可能引发问题的原因,将目标缩小到一定范围。
排查问题:基于上一步结果,从引发问题的“可疑性”角度出发,从高到低依次进行排查,进一步排除一些选项,将目标范围进一步缩小。
定位问题:通过相关监控数据的辅助,以更细粒度的手段,将引发问题的原因定位到精确位置。
解决问题:判断问题引发的愿意及其位置后,采取相关措施对其进行修改。
常识最优解:将原有的问题解决后,在能力范围内,且环境允许的情况下,应该适当考虑问题的最优解(可以从性能、拓展性、并发等角度出发)。
作为 “新时代的程序构建者”,那当然得学会合理使用工具来帮助我们快速解决问题:
摘取或复制问题的关键片段
粘贴搜索
观察返回结果中,选择标题与描述与自己问题较匹配的资料进入。
多看几个后,根据其解决方案尝试解决问题。
成功解决后皆大欢喜,尝试无果后 “找人 / 问群”。
“外力” 无法解决问题时自己动手,根据之前的步骤依次排查解决。
线上问题排查的方向
应用程序本身导致的问题
程序内部频发触发GC,造成系统出现长时间停顿,导致客户端堆积大量请求;
JVM参数配置不合理,导致线上运行失控,如堆内存、各内存区域太小等;
Java程序代码存在缺陷,导致线上运行出现Bug,如死锁 / 内存泄漏、溢出等;
程序内部资源使用不合理,导致出现问题,如线程 / DB连接 / 网络连接 / 堆外内存等。
上下游内部系统导致的问题
上游服务出现并发情况,导致当前程序请求量急剧增加,从而引发问题拖垮系统;
下游服务出现问题,导致当前程序堆积大量请求拖垮系统,如Redis宕机 / DB阻塞等;
程序锁部署的机器本身导致的问题
服务器机房网络出现问题,导致网络出现阻塞、当前程序假死等故障;
服务器中因其他程序原因、硬件问题、环境因素等原因导致系统不可用;
服务器因遭到入侵导致Java程序收到影响。
第三方RPC远程调用导致的问题
作为被调用者提供给第三方调用,第三方流量突增,导致当前程序负载过重出现问题;
作为调用者调用第三方,但第三方出现问题,引发雪崩问题而造成当前程序崩溃。
JVM线上事故问题合集
线上排查之前
在排查问题时,诱发问题的原因也有可能来自上下游系统。因此,当出现问题时,首先得定位出现问题的节点,然后针对节点进行排错。但无论是那个节点(java应用、DB、上下游等),出现问题的原因无非就几个方向:代码、CPU、磁盘、内存以及网络问题,所以遇到线上问题时,合理采用OS与JVM提供的工具(df、free、top、jstack、jmap、ps等),将这些方面依次排查一遍即可。
不过需要额外注意:JVM 提供的大部分工具在使用时会影响性能,所以如果你的程序是以单机的模式部署,那最好在排查问题之前做好流量迁移(改 DNS、Nginx 配置等)。如果你的程序是以集群模式部署,那么可以将其中一台机器隔离出来,用于保留现场,也为了更方便的调试问题。
同时,如果线上的机器已经无法提供正常服务,那么在排查问题之前首先要做到的是 “及时止损”,可以采用版本回滚、服务降级、重启应用等手段让当前节点恢复正常服务。
JVM内存溢出(OOM)
什么是内存溢出:一个木桶只能装40L水,多出来的水会从桶顶溢出。
在Java内存空间中,会有多个区域会发生OOM问题,如堆空间、元空间、栈空间等。通常,线上环境产生内存溢出的原因大致有三类:
JVM分配的内存太小,不足以支撑程序的正常执行时数据增长;
编写的Java程序内部存在问题、有Bug,导致GC回收速率跟不上分配速率;
堆空间、栈分配不足导致堆 / 栈溢出,一般不会出现这种情况;
自己的代码或引入的第三方以来存在内存溢出问题,导致可用内存不足。
上述2、4问题皆是由编写的Java程序代码不严谨导致的OOM,由于Java内存中产生大量垃圾对象,导致新对象没有空闲内存分配,从而产生的溢出。
在排查OOM问题时,核心是:哪里OOM了?为什么OOM了?怎么避免出现OOM?
同时,在排查过程中,应到要建立数据的分析之上,也就是指Dump数据。
获取Dump文件的方式有两种:
启动时设置-XX:HeapDumpPath,事先指定OOM出现时,自动到处Dump文件。
重启并在程序运行一段时间后,通过工具导出。
java线上OOM排查
// JVM启动参数:-Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=/usr/local/java/java_code/java_log/Heap_OOM.hprof
public class OOM {
// 测试内存溢出的对象类
public static class OomObject{}
public static void main(String[] args){
List<OomObject> OOMlist = new ArrayList<>();
// 死循环:反复往集合中添加对象实例
for(;;){
OOMlist.add(new OomObject());
}
}
}
root@localhost ~]# java -Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/java/jav
[1] 9999
等待一段时间后,可以看到在/usr/local/java/java_code/java_log/目录下,已经自动导出了堆Dump文件,接下来我们只需要把这个Dump文件直接往Eclipse MAT(Memory Analyzer Tool)工具里面一丢,然后它就能自动帮你把 OOM 的原因分析出来,然后根据它分析的结果改善对应的代码即可。
线上OOM问题排查思路:
首先获取Dump文件,最好是上线部署时配置了,这样可以保留第一现场,但如若未配置对应参数,可以调小堆空间,然后重启程序的时候重新配置参数,争取做到 “现场” 重现。
如果无法通过配置参数获得程序 OOM 自然导出的Dump文件,那则可以等待程序在线上运行一段时间,并协调测试人员对各接口进行压测,而后主动式的通过jmap等工具导出堆的Dump文件(这种方式没有程序自动导出的Dump文件效果好)。
将Dump文件传输到本地,然后通过相关的Dump分析工具分析,如 JDK 自带的jvisualvm,或第三方的MAT工具等。
根据分析结果尝试定位问题,先定位问题发生的区域,如:确定是堆外内存还是堆内空间溢出,如果是堆内,是哪个数据区发生了溢出。确定了溢出的区域之后,再分析导致溢出的原因(后面会列出一下常见的 OOM 原因)。
根据定位到的区域以及原因,做出对应的解决措施,如:优化代码、优化 SQL 等。
线上内存溢出问题小结
内粗溢出问题绝对是线上问题的常客,但通常情况下,OOM大多数因为代码问题导致的,在程序中容易引发OOM的情况:
一次性从外部将提及庞大的数据载入内存,如DB读表、读本地文件等;
程序中使用容器(Map/List/Set等)未及时清理,内存紧张而GC无法回收;
程序逻辑中存在死循环或大量循环,或单个循环中产生大量重复的对象实例;
程序中引入的第三方依赖中存在Bug问题;
程序中存在内存泄漏问题,一直在蚕食可用内存,GC无法回收导致内存溢出;
第三方以来加载大量类库,元空间无法载入所有类元数据。
不过 Java 程序中,堆空间、元空间、栈空间等区域都可能出现 OOM 问题,其中元空间的溢出大部分原因是由于分配空间不够导致的,当然,也不排除会存在 “例外的类库” 导致 OOM。真正意义上的栈空间 OOM 在线上几乎很难遇见,所以实际线上环境中,堆空间 OOM是最常见的,大部分需要排查 OOM 问题的时候,几乎都是堆空间发生了溢出。
JVM内存泄漏
举例:一个木桶只能装40L水,但此刻我往里面丢块2KG的金砖,那该水桶在之后的过程中,最多只能装38L的水。此时这种情况换到程序的内存中,就被称为内存泄漏。
在发生内存溢出时,有可能是因为内存泄漏诱发的,但内存泄漏绝对不可能因为 OOM 引发。
出现内存泄漏主要情况:
堆内泄漏:由于代码不合理导致内存中出现泄漏,如垃圾对象与静态对象保持着引用、未正确关闭外部连接等。
堆外泄漏:申请bugger流后未释放内存、直接内存中的数据未手动清理等。
JVM内存泄漏排查
// JVM启动参数:-Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=/usr/local/java/java_code/java_log/Heap_MemoryLeak.hprof
// 如果不做限制,想要观测到内存泄漏导致OOM问题需要很长时间。
public class MemoryLeak {
// 长生命周期对象,静态类型的root节点
static List<Object> ROOT = new ArrayList<>();
public static void main(String[] args) {
// 不断创建新的对象,使用后不手动将其从容器中移除
for (int i = 0;i <= 999999999;i++) {
Object obj = new Object();
ROOT.add(obj);
obj = i;
}
}
}
root@localhost ~]# java -Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/java/jav
[1] 78849
Exception in thread "main" OutOfMemoryError: Java heap space
at Arrays.copyOf(Arrays.java:3210)
at Arrays.copyOf(Arrays.java:3181)
at ArrayList.grow(ArrayList.java:261)
at ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at ArrayList.ensureCapacityInternal(ArrayList.java:227)
at ArrayList.add(ArrayList.java:458)
at MemoryLeak.main(MemoryLeak.java:14)