记录一次线上 OOM 事故
日期: 2020-11-02 18:30
**描述:**最开始是 APP 首页加载报 网络超时,后来管理后端也出现了网络超时的情况。
个人排查过程:
- 查看线上应用 pod 运行状态,发现存在大量服务消费者处于 Crashback 状态;
- 考虑基础服务(baseservice)是否可用,发现基础服务日志正常输出,表名有正常的业务逻辑处理;
- 查看 JVM 监控发现一直在 YoungGC 和 FullGC,这样导致没有时间处理业务逻辑,并发现 JVM 线程达到了 2.2K 个,且查看 Pod 错误信息为 OOM;
- 立即对基础服务(baseservice)做了重启,重启后两分钟,APP 又无法使用,查看 Pod 状态为 OOM;
- 突然想起下午收到的蚂蚁雄兵的推广短信,然后结合日志,发现批量发送短信中使用了多线程;
- 查看 JVM 线程发现确实是这一部分代码的问题,负责人要求暂时关闭批量发送短信服务后回复正常;
禁止使用 Executors 创建线程池
线程池不允许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式可更明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致 OOM (在高并发 或 线程处理耗时高是特别容易发生 OOM)。
- CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM (在高并发 或 线程处理耗时高是特别容易发生 OOM)。
Executors 存在什么问题?
为什么不允许使用 Executors ?
我们先来一个简单的例子,模拟一下使用 Executors 导致 OOM 的情况。
public class ExecutorsDemo {
private static ExecutorService executor = Executors.newFixedThreadPool(15);
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(new SubThread());
}
}
}
class SubThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
}
}
通过指定 JVM 参数:-Xmx8m -Xms8m 运行以上代码,会抛出 OOM:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:9)
以上代码指出,ExecutorsDemo.java 的第 9 行,就是代码中的 executor.execute(new SubThread());
Executors 为什么存在缺陷 ?
通过上面的例子,我们知道了 Executors 创建的线程池存在 OOM 的风险,那么到底是什么原因导致的呢?我们需要深入 Executors 的源码来分析一下。
其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实已经说了,真正的导致 OOM 的其实是 LinkedBlockingQueue.offer 方法。
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:9)
Executors#newFixedThreadPool 方法底层实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以发现底层是通过 LinkedBlockingQueue 实现的,Java 中 BlockingQueue 主要有两种实现,分别是 ArrayBlockingQueue 和 LinckedBlockingQueue。
-
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,必须设置容量。
-
LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE。
这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE。也就是说,如果我们不设置 LinkedBlockingQueue 的容量的话,其默认容量将会是 Integer.MAX_VALUE。
而 newFixedThreadPool 中创建 LinkedBlockingQueue 时,并未指定容量。此时,LinkedBlockingQueue 就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下,在高并发或线程处理比较耗时时就很有可能因为任务过多而导致内存溢出问题。
newFixedThreadPool 和 newSingleThreadExecutor 两个工厂方法上,并不是说 newCachedThreadPool 和 newScheduledThreadPool 这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致 OOM。
创建线程池的正确姿势
避免使用 Executors 创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。
private static ExecutorService ec = new ThreadPoolExecutor(
10,
10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出 java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。
但是异常(Exception)总比发生错误(Error)要好。对吗 !?
除了自己定义 ThreadPoolExecutor 外。还有其他方法。这个时候第一时间就应该想到开源类库,如 apache 和 guava 和 hutool等。
个人推荐 guava 提供的 ThreadFactoryBuilder 来创建线程池。
/**
* 创建线程池的正确姿势
* 当线程池中的线程 maximumPoolSize + capacity 之和时报 java.util.concurrent.RejectedExecutionException 但不至于 OOM error
* @author yangdejun
* @date 2020/09/03
**/
public class ThreadPoolExecutorDemo {
// 正确姿势 1
private static ExecutorService es = new ThreadPoolExecutor(
30,
200,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue(100));
/**
* 正确姿势 2 guava
* 通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源
*/
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
private static ExecutorService pool = new ThreadPoolExecutor(
30,
200,
10L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(200),
namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
for (int i = 0; i < 300; i++) {
try {
es.execute(new SubThread());
} catch (Exception e) {
e.printStackTrace();
}
}
for (int i = 0; i < Integer.MAX_VALUE; i++) {
pool.execute(new SubThread());
}
}
}
通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源。
思考: 发生异常(Exception)要比发生错误(Error)好,为什么这么说?
个人见解:
- Error 错误往往都是致命的错误,使我们无法从代码上进行人为解决的;
- 在分布式应用中如果没有配置合理的调用超时时间,Error 错误可能会导致依赖该服务的应用(消费者)堆积大量请求,也可能会导致消费者发生 Error 致命错误(特别是基础服务发生 Error 错误时)。此次线上事故不就是如此吗?
- Exception 不会直接导致应用宕机,能保证服务中其它接口正常提供服务;
- Exception 属于运行时异常,发生异常时,可认为介入进行错误的业务逻辑处理。
- … …
参考文献:
阿里巴巴《Java开发手册(嵩山版)》