实战-商品中心-l2cache高并发场景下出现OOM的分析和优化方案

本文分析了一次电商平台在双十二压测期间商品中心出现OOM的问题。问题源于商品中心的缓存框架l2cache,在高并发场景下,同一key的大量refresh消息导致ForkJoinPool的无界队列堆积,进而引发OOM。优化方案包括:1) 控制同一key的refresh消息发送,如加分布式锁限制频率;2) 在消费消息侧过滤重复的refresh操作,使用AtomicInteger过滤并发刷新任务。这两个方案旨在解决队列堆积和重复消息问题,避免OOM。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

l2cache 源码地址

概要:

为应对双十二活动,现对电商平台核心主链路进行压测,在压测过程中商品中心出现OOM。

压测环境的基本情况(只列出部分数据):
1、商品中心:8核16G,120个pod
2、redis配置:192G读写分离版(32节点),单片带宽:192M
3、DB配置:PolarDB顶配(88核 710G,1主6从)
4、商品中心集成 l2cache 缓存框架(一级缓存:caffeine,二级缓存:redis)

问题

商品中心在压测时出现OOM,具体错误日志如下:

在这里插入图片描述

分析

1、通过上面的日志可得知,具体的错误点为:

com.github.benmanes.caffeine.cache.LocalLoadingCache.java:166

Exception thrown during refresh

2、分析caffeine源码 LocalLoadingCache

interface LocalLoadingCache<K, V> extends LocalManualCache<K, V>, LoadingCache<K, V> {
  // 省略无关代码 ...

  // refresh 方法的作用:异步加载key的新值。加载新值时,get(key)将继续返回以前的值(如果有),除非它被逐出
  default void refresh(K key) {
    requireNonNull(key);

    long[] writeTime = new long[1];
    long startTime = cache().statsTicker().read();
    V oldValue = cache().getIfPresentQuietly(key, writeTime);
    CompletableFuture<V> refreshFuture = (oldValue == null)
        ? cacheLoader().asyncLoad(key, cache().executor())
        : cacheLoader().asyncReload(key, oldValue, cache().executor());
    refreshFuture.whenComplete((newValue, error) -> {
      long loadTime = cache().statsTicker().read() - startTime;
      if (error != null) {
        // 关键的错误日志在这里
        logger.log(Level.WARNING, "Exception thrown during refresh", error);
        cache().statsCounter().recordLoadFailure(loadTime);
        return;
      }
  	// 省略无关代码 ...
  }
  // 省略无关代码 ...
}

通过上面的源码可知,关键的方法在 LocalLoadingCache.refresh(),所以继续排查l2cache里面有调用该方法的代码。

3、排查l2cache里面调用LocalLoadingCache.refresh()的代码

通过分析,发现有两处代码有调用,如下:

com.coy.l2cache.cache.CaffeineCache#refreshAll
com.coy.l2cache.cache.CaffeineCache#refresh

4、继续分析,上面两个方法在哪里被调用,如下:

# 该方法没有被调用的地方,所以可以忽略
com.coy.l2cache.cache.CaffeineCache#refreshAll
public class CaffeineCache extends AbstractAdaptingCache implements Level1Cache {
    // 该方法被CacheMessageListener.onMessage方法调用
    @Override
    public void refresh(Object key) {
        if (isLoadingCache()) {
            logger.debug("[CaffeineCache] refresh cache, cacheName={}, key={}", this.getCacheName(), key);
            ((LoadingCache) caffeineCache).refresh(key);
        }
    }
}

5、CacheMessageListene缓存监听器,作用是在集群环境下通过监听方式来保证缓存的一致性。

public class CacheMessageListener implements MessageListener {

    @Override
    public void onMessage(CacheMessage message) {
        try {
            if (this.cacheInstanceId.equalsIgnoreCase(message.getInstanceId())) {
                logger.debug("[CacheMessageListener][SyncCache] instanceId is same no need to deal, message={}", message.toString());
                return;
            }
            logger.info("[CacheMessageListener][SyncCache] instanceId={}, cacheName={}, cacheType={}, optType={}, key={}",
                    message.getInstanceId(), message.getCacheName(), message.getCacheType(), message.getOptType(), message.getKey());

            Level1Cache level1Cache = getLevel1Cache(message);
            if (null == level1Cache) {
                return;
            }
            // 关键在这里
            if (CacheConsts.CACHE_REFRESH.equals(message.getOptType())) {
                level1Cache.refresh(message.getKey());
            } else {
                level1Cache.clearLocalCache(message.getKey());
            }
        } catch (Exception e) {
            logger.error("[CacheMessageListener][SyncCache] error", e);
        }
    }

}

通过上面的代码分析,已经定位到具体的代码,也可以基本判断该OOM问题是该场景下出现的。

6、回过头结合 CacheMessageListener 的日志继续分析

1)所有缓存维度的 refresh 消息日志数量

通过下图可以发现 2020-12-02 17:00 ~ 2020-12-03 23:00 缓存监听器的refresh消息操作日志,有13,916,848,020条(139亿)。这个数量已经非常恐怖啦,说明此处有问题。

在这里插入图片描述

2)单个缓存维度的 refresh 消息日志数量

通过下图可以发现 2020-12-02 17:00 ~ 2020-12-03 23:00 缓存监听器的refresh消息操作日志,cacheName=actDiscountCache 的refresh消息操作日志有12,820,529,131条(128亿)。
这么大的refresh消息量,说明这个缓存维度有问题。

在这里插入图片描述

3)单个key维度的 refresh 消息日志数量

* and CacheMessageListener and refresh and actDiscountCache and 56781
key=56781的refresh操作日志有6,358,694,099条(63亿)

在这里插入图片描述

4)单个key维度的 put 日志数量

* and RedissonRBucketCache and put and actDiscountCache and 56781
key=56781的put NullValue 到redis的操作日志有67,466,579条(0.67亿)

在这里插入图片描述

那么问题来了,为什么会有这么多refresh日志和put日志呢?

1)为什么同一个key有大量的put日志?

压测环境有120个pod,在高并发的情况下,获取同一个key的并发请求被打到这120个pod上,当redis中没有数据时,会从DB加载数据并put到redis中,然后发送一个refresh消息(短时间内可以理解为是重复的refresh消息)。以上就解释了同一个key为什么会有这么多的put日志。

理论上来讲,这种并发加载同一个key的情况,是需要对同一个key来加分布式锁处理的,否则请求会打到下游DB上,导致DB的压力增加,但是由于目前DB是阿里云PolarDB顶配,所以结合实际情况,在不影响用户体验且DB能支撑的情况下不加分布式锁。

2)为什么同一个key有大量的refresh日志?

压测环境有120个pod,每一个put日志会发送一个缓存刷新消息,这个缓存刷新消息会被其他119个pod消费,并执行refresh操作,结合上面的日志分析,可初步算出refresh日志数量=put日志数量(0.67) * 120=8,095,989,480(80亿+)。

从上面的分析可知,并发场景下同一个key存在大量的重复refresh消息。

注:这个值不是那么精准,是一个理论值,实际可能会更多。因为消息处理过程中如果redis缓存过期,那么还是会继续加载继续发送。

总结

通过上面的分析可得知问题关键点如下

1、高并发情况下,在加载同一个key的缓存数据时,会触发发送refresh消息,而每个refresh消息会被其他120个pod消费,所以每个pod都会受到大量重复的refresh消息。

2、在收到大量重复refresh消息时,每个消息都会往caffeine的默认线程池 ForkJoinPool 的无界队列中新增一个任务,而caffeine的refresh()方法是异步执行的,所以在 消费速度 跟不上 生产速度 的情况下,会导致队列中堆积大量消息,最终导致OOM,这个也就是这次OOM的根本原因所在。

优化

那么针对上面总结的问题该怎么处理呢?目前有两个初步的方案 :

1、发送消息侧的优化:

从源头控制发送refresh消息的量,限制集群环境下单位时间内同一个key只能发送一次refresh消息。具体优化如下:

1)发送消息优化为异步

2)对同一个key加分布式锁,保证500ms内同一个key只会发送一次refresh消息。

2、消费消息侧的优化:

从消费侧单pod维度过滤掉重复的refresh消息,去掉不必要的refresh操作。具体优化如下:

1)基于 ValueLoaderWarpper 定义一个类型为 AtomicIntegerwaitRefreshNum 字段(默认值为0),该字段用于过滤并发执行同一个key的refresh操作,保证同一个key只有一个refresh操作在执行。

2)消费消息后,通过key获取对应的 ValueLoaderWarpper

3)执行 waitRefreshNum.getAndIncrement(),并发的原子操作。

4)如果返回值>0,则表示重复消息,直接返回。也就是说当前key存在正在执行的refresh任务,或者队列中存在等待执行的refresh任务。此处是关键,高并发情况下可过滤掉大量重复消息。

5)如果返回值=0,则执行LoadingCache.refresh()。也就是说当前key无正在执行的refresh任务,且队列中无等待执行的refresh任务。

6) ValueLoaderWarpper加载数据完毕后,将waitRefreshNum 设置为0,保证该key下次可继续执行refresh。

至此,通过上面两个维度的优化方案来解决 ForkJoinPool 的队列堆积大量refresh任务导致OOM的问题。

具体代码见:l2cache 二级缓存框架

<think>好的,我现在需要帮助用户解决在Windows下设置JVM参数-Xms8g-Xmx8g后仍然出现OOM的问题。首先,我需要回顾用户之前提供的引用内容,特别是引用3引用4,因为它们讨论了OOM堆内存配置的关系。 用户的问题是在配置了8G的堆内存后仍然遇到OOM,这可能涉及几个方面。首先,OOM可能不仅仅是因为堆内存不足,还可能存在内存泄漏或者堆外内存的使用问题。根据引用3,堆内存设置过大反而可能减少其他内存区域的空间,比如虚拟机栈,从而影响线程数量,但这里用户的问题是关于OOM,所以更可能是堆内存本身的问题。 接下来,我需要考虑可能的根本原因。根据引用3,如果堆内存设置过大,可能导致其他区域(如方法区、本地方法栈)的内存不足,但这里用户设置的是8G,假设物理内存足够的话,这可能不是主因。但引用3也提到,高并发情况下,如果堆内存设置过大,可能反而导致线程数量减少,不过OOM主要与堆有关,所以需要重点检查堆的使用情况。 引用4提到可以通过生成HeapDump来分析OOM原因,用户可能在配置参数时已经添加了HeapDump参数,但可能需要进一步分析dump文件。此外,用户可能需要检查是否有内存泄漏,比如未释放的对象或缓存过度使用。 另外,需要考虑堆外内存的使用,比如直接内存(Direct Memory)或JNI调用,这些不受-Xmx参数限制。例如,使用NIO的ByteBuffer会占用直接内存,而直接内存的大小由-XX:MaxDirectMemorySize控制,如果未设置,可能与堆内存设置相关,但默认情况下可能足够。 还有可能的是,应用程序本身需要的内存超过了8G,比如加载大量数据或处理大文件,这时候即使堆设置为8G也可能不够,需要调整参数或优化代码。 解决方案方面,首先建议用户生成并分析HeapDump,使用工具如Eclipse MAT或VisualVM来查看内存中的对象。其次,检查是否有内存泄漏,优化代码,减少不必要的对象持有。然后,调整其他内存参数,比如元空间(Metaspace)的大小,使用-XX:MaxMetaspaceSize,因为Metaspace默认不受-Xmx限制。此外,检查堆外内存的使用,适当调整直接内存大小。 还需要考虑JVM的垃圾回收机制,如果存在频繁的GC导致应用暂停,可能需要调整垃圾回收器或相关参数,比如使用G1GC来提高效率。同时,确保物理内存足够,避免系统级别的内存不足。 总结起来,用户的问题可能涉及堆内存不足、内存泄漏、堆外内存使用、元空间不足等多个方面,需要系统性地排查调整参数,结合工具分析具体情况。</think>### 在 Windows 下设置 `-Xms8g -Xmx8g` 仍出现 OOM 的原因及解决方案 --- #### 一、**根本原因分析** 即使设置了 `-Xms8g -Xmx8g`,仍出现 OOM 的常见原因如下: 1. **堆内存实际需求超出 8GB** - **对象驻留**:应用程序可能创建了大量长期存活的对象(如缓存、静态集合),导致堆内存逐渐耗尽[^3] - **内存泄漏**:未正确释放资源(如未关闭数据库连接、未清理集合引用),导致对象无法被垃圾回收(GC)[^4] - **大文件/数据加载**:一次性加载超大文件(如 CSV、JSON)到内存中,直接超出堆容量限制 2. **堆外内存溢出** - **直接内存(Direct Memory)**:使用 `ByteBuffer.allocateDirect()` 或 NIO 操作时,分配的直接内存不受 `-Xmx` 限制,需通过 `-XX:MaxDirectMemorySize` 单独设置[^3] - **本地方法调用(JNI)**:通过 JNI 调用的本地库可能分配大量非堆内存 3. **元空间(Metaspace)或方法区溢出** - **动态类加载**:框架(如 Spring、Hibernate)动态生成类可能撑满元空间 - **默认限制**:元空间默认无上限(依赖系统内存),需通过 `-XX:MaxMetaspaceSize` 限制 4. **线程栈溢出(StackOverflowError)** -OOM 不同,但需注意 `-Xss` 参数设置不当可能导致线程数受限,间接影响堆内存分配 --- #### 二、**诊断步骤** 1. **生成并分析 Heap Dump** - 启动时添加参数: ```cmd -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\dump ``` - 使用工具(如 **Eclipse MAT** 或 **VisualVM**)分析 Dump 文件,定位内存占用最高的对象[^4] 2. **监控内存使用** - 使用 `jconsole` 或 `jstat -gc <pid>` 实时观察堆内存分布: ```text S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 8192.0 8192.0 0.0 0.0 65536.0 65536.0 81920.0 81920.0 4864.0 4608.0 512.0 448.0 1 0.015 2 0.030 0.045 ``` - 关注 `OU`(老年代使用量) `EU`(年轻代使用量)是否持续增长 3. **检查堆外内存** - 通过 `Native Memory Tracking` 追踪: ```cmd -XX:NativeMemoryTracking=detail ``` - 使用 `jcmd <pid> VM.native_memory detail` 查看非堆内存分配 --- #### 三、**解决方案** 1. **优化堆内存使用** - **限制缓存大小**:使用 WeakHashMap 或 Guava Cache 的软引用策略 - **分批次处理数据**:避免一次性加载大文件,改为流式处理 - **修复内存泄漏**:通过 Dump 文件定位代码中的无效对象引用 2. **调整 JVM 参数** - **增加元空间限制**: ```cmd -XX:MaxMetaspaceSize=512m ``` - **限制直接内存**: ```cmd -XX:MaxDirectMemorySize=2g ``` - **优化 GC 策略**(如启用 G1 垃圾回收器):
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白云coy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值