记录一次线上 OOM

记录一次线上 OOM 事故

日期: 2020-11-02 18:30

**描述:**最开始是 APP 首页加载报 网络超时,后来管理后端也出现了网络超时的情况。

个人排查过程:

  1. 查看线上应用 pod 运行状态,发现存在大量服务消费者处于 Crashback 状态;
  2. 考虑基础服务(baseservice)是否可用,发现基础服务日志正常输出,表名有正常的业务逻辑处理;
  3. 查看 JVM 监控发现一直在 YoungGC 和 FullGC,这样导致没有时间处理业务逻辑,并发现 JVM 线程达到了 2.2K 个,且查看 Pod 错误信息为 OOM;
  4. 立即对基础服务(baseservice)做了重启,重启后两分钟,APP 又无法使用,查看 Pod 状态为 OOM;
  5. 突然想起下午收到的蚂蚁雄兵的推广短信,然后结合日志,发现批量发送短信中使用了多线程;
  6. 查看 JVM 线程发现确实是这一部分代码的问题,负责人要求暂时关闭批量发送短信服务后回复正常;
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

禁止使用 Executors 创建线程池

  线程池不允许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式可更明确线程池的运行规则,规避资源耗尽的风险。

说明: Executors 返回的线程池对象的弊端如下:

  1. FixedThreadPool 和 SingleThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致 OOM (在高并发 或 线程处理耗时高是特别容易发生 OOM)。
  2. 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)好,为什么这么说?

个人见解:

  1. Error 错误往往都是致命的错误,使我们无法从代码上进行人为解决的;
  2. 在分布式应用中如果没有配置合理的调用超时时间,Error 错误可能会导致依赖该服务的应用(消费者)堆积大量请求,也可能会导致消费者发生 Error 致命错误(特别是基础服务发生 Error 错误时)。此次线上事故不就是如此吗?
  3. Exception 不会直接导致应用宕机,能保证服务中其它接口正常提供服务;
  4. Exception 属于运行时异常,发生异常时,可认为介入进行错误的业务逻辑处理。
  5. … …

参考文献:
阿里巴巴《Java开发手册(嵩山版)》

### 关于线OOM (Out Of Memory) 案例分析及解决方案 #### 线上环境中的OOM问题概述 在线上环境中,当应用程序遭遇 Out of Memory (OOM) 错误时,通常会调用 `Thread::ThrowOutOfMemoryError` 函数并传递描述错误详情的消息参数 msg[^1]。这类异常不仅影响用户体验还可能导致服务中断。 #### 实际案例解析 假设某 Android 应用程序频繁出现崩溃现象,在日志中发现大量由系统抛出的 OOM 异常记录。进一步调查表明该应用存在不合理加载图片资源的情况——即一次性尝试加载过多高分辨率图像至内存中而未做适当优化处理。这使得虚拟机无法分配足够的连续空间来满足请求从而触发了 OOM 错误。 针对上述情况采取如下措施: - **减少单次加载量**:限制每次仅读取一定数量的小尺寸缩略图而非原始大小; - **启用缓存机制**:对于已加载过的图片实施 LRU 缓存策略以便重复访问时不需重新获取; - **异步操作**:采用后台线程完成耗时较长的任务如网络请求或磁盘IO动作防止阻塞主线程造成响应延迟甚至卡死状况的发生。 经过以上改进之后有效地缓解了因图片加载不当所引发的一系列性能瓶颈问题显著降低了 OOM 发生概率提升了整体稳定性表现。 #### 工具辅助诊断流程 面对较大规模的应用程序,手动排查可能存在效率低下且难以全面覆盖所有潜在风险点的问题。此时可以借助专业的调试工具来进行更深入细致地剖析工作。例如 JVisualVM 是一款功能强大的 Java 应用性能监控平台能够帮助开发者快速定位到具体哪一部分代码消耗了大量的堆内存量进而指导后续修复方向的选择不过需要注意的是如果待检测的数据集非常庞大则建议预先调整好 JVM 的启动参数以确保有足够的可用 RAM 来支持整个分析过程顺利开展[^2]。 另外还可以考虑使用其他专门用于 heap dump 文件解析的专业软件比如 Eclipse MAT 或 Visual VM 自身集成的功能模块等它们各自具备独特的优势可以根据实际需求灵活选用最合适的选项。 #### 内存泄漏预防指南 为了避免未来再次遇到类似的挑战可以从以下几个方面着手加强防护力度: - 定期审查现有架构设计是否存在不必要的对象持有关系特别是静态成员变量以及监听器注册注销逻辑是否严谨无遗漏之处; - 对第三方库保持警惕谨慎引入未经充分测试验证的新依赖项以免埋下隐患; - 培养良好的编程习惯遵循最佳实践编写易于维护扩展性强的高质量源码。 ```java // 示例代码展示如何安全释放Bitmap资源 public void recycleBitmap(Bitmap bitmap){ if(bitmap != null && !bitmap.isRecycled()){ bitmap.recycle(); System.gc(); // 提示垃圾回收器尽快清理不再使用的对象 } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值