欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > 一次caffeine引起的CPU飙升问题

一次caffeine引起的CPU飙升问题

2024/10/24 19:16:05 来源:https://blog.csdn.net/Ethan_199402/article/details/141168252  浏览:    关键词:一次caffeine引起的CPU飙升问题

背景

背景是上游服务接入了博主团队提供的sdk,已经长达3年,运行稳定无异常,随着最近冲业绩,流量越来越大,直至某一天,其中一个接入方(流量很大)告知CPU在慢慢上升且没有回落的迹象,dump文件能看到缓存的holder占用4个G,那不用说了,责无旁贷

打开他们的内存监控,G1的老年代占用大概是这个样子

在这里插入图片描述

可以看到,老年代每次gc后使用量都在上升,说明每次能gc的内存越来越少,而cpu也是蹭蹭往上追,直至崩掉

原因

查看dump,能看到我们的缓存对象cacheHoler占用高达4g,其中cacehKey的数量更是高达900w!

首先是惊呆了,这个缓存当初设置的maximumSize可只有2w啊,这900w什么鬼!

caffeine介绍

官方是这么说的,一句话,目前最牛逼的本地java缓存库

	Caffeine 是一个高性能Java 缓存库,提供接近最佳的命中率

我们先弄清楚caffeine的原理,是不是使用姿势有问题?

官方的一个小demo

LoadingCache<Key, Graph> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));

对比一下我们的适用方式

Caffeine.newBuilder().executor(executorService).refreshAfterWrite(12000, TimeUnit.SECONDS).expireAfterWrite(600, TimeUnit.SECONDS).maximumSize(20000).buildAsync(key -> {return callForDemo("demo");});

不同之处也就是我们用了异步的cache,定义了自己的线程池,指定了refreshAfterWrite参数为1200秒

好像姿势没什么问题?
这不咱也找到了官方的异步用法

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES)// Either: Build with a synchronous computation that is wrapped as asynchronous .buildAsync(key -> createExpensiveGraph(key));// Or: Build with a asynchronous computation that returns a future.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

思考每个参数的含义

  1. maximumSize:指定缓存可以包含的最大条目数。请注意,缓存可能会在超过此限制之前逐出某个条目,或者在逐出时暂时超过阈值。当缓存大小增长到接近最大值时,缓存会逐出不太可能再次使用的条目。例如,缓存可能会逐出某个条目,因为它最近未使用或非常经常使用。
  2. expireAfterAccess:指定在条目创建、最近替换其值或最后一次读取条目后经过固定持续时间后,应自动从缓存中删除每个条目
  3. refreshAfterWrite:指定在创建条目或最近替换其值后经过固定持续时间后,活动条目就有资格进行自动刷新。当出现对条目的第一个过时请求时,将执行自动刷新。触发刷新的请求将进行异步调用,并立即返回旧值。

一个重要信息:maximumSize的缓存不会立即驱逐
那为题是不是就出现在这里?

多线程模拟

那我们用1k个线程模拟一下从maximumSize为20的缓存实例获取缓存

        for (int i = 0; i < 1000; i++) {int finalI = i;new Thread(() -> {UserCharacter uc = new UserCharacter();uc.setUid(finalI +"");uc.setStationId(finalI +"111");configHolder.getAbResultCache(uc, "demo").getAbResults();}).start();}//Thread.sleep(2000);configHolder.getStats();
public void getStats() {System.out.println( abResultCache.synchronous().asMap());}

第一次调用,多线程获取缓存后立即查看缓存中的快照map数量为1000,明显超过maximumSize定义的20

第二次调用,添加代码Thread.sleep(5000);查看缓存中的快照map数量为20,正好是maximumSize定义的20

第三次调用,添加代码Thread.sleep(2000);查看缓存中的快照map数量为100-300不等,说明大于20的缓存条目正在被驱逐

结论

caffeine的缓存驱逐速度在高并发情况下跟不上缓存添加速度,造成内存gc不下来

且旧的缓存会被超过maximumSize的新缓存驱逐,所以20000个缓存其实根本没起到缓存的作用,很快就会被新缓存驱逐,10个线程一直被抢着来进行缓存的添加和驱逐,这也是为什么CPU快要被干爆了

那要怎么优化呢?

驱逐策略

caffeine提供了三类驱逐策略

基于size或者weigh

// Evict based on the number of entries in the cache
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder().maximumSize(10_000).build(key -> createExpensiveGraph(key));// Evict based on the number of vertices in the cache
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder().maximumWeight(10_000).weigher((Key key, Graph graph) -> graph.vertices().size()).build(key -> createExpensiveGraph(key));

maximumSize:指定缓存可以包含的最大条目数。请注意,缓存可能会在超过此限制之前逐出某个条目,或者在逐出时暂时超过阈值。当缓存大小增长到接近最大值时,缓存会逐出不太可能再次使用的条目。例如,缓存可能会逐出某个条目,因为它最近未使用或非常经常使用

weigher:如果不同的服务器空间具有不同的“权重”——例如,如果您的服务器值具有不同的内存占用——您可以指定一个权重函数Caffeine.weigher(Weigher)和一个最大的服务器权重Caffeine.maximumWeight(long)

基于时间

// Evict based on a fixed expiration policy
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));// Evict based on a varying expiration policy
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder().expireAfter(new Expiry<Key, Graph>() {public long expireAfterCreate(Key key, Graph graph, long currentTime) {// Use wall clock time, rather than nanotime, if from an external resourcelong seconds = graph.creationDate().plusHours(5).minus(System.currentTimeMillis(), MILLIS).toEpochSecond();return TimeUnit.SECONDS.toNanos(seconds);}public long expireAfterUpdate(Key key, Graph graph, long currentTime, long currentDuration) {return currentDuration;}public long expireAfterRead(Key key, Graph graph,long currentTime, long currentDuration) {return currentDuration;}}).build(key -> createExpensiveGraph(key));

咖啡因提供了快速定时驱动方法:

expireAfterAccess:(long, TimeUnit):指定在条目创建、最近替换其值或最后一次读取条目后经过固定持续时间后,应自动从缓存中删除每个条目。所有缓存读写操作(Cache.asMap().put(K, V)和Cache.asMap().get(Object))都会重置访问时间,但不会通过对 Cache#asMap 的集合视图的操作来重置访问时间。

expireAfterWrite:(long, TimeUnit):指定在创建条目或最近替换其值后经过固定持续时间后,活动条目就有资格进行自动刷新

expireAfter(Expiry):分别定义缓存创建、更新、读取多久后过期

Scheduler: Caffeine.scheduler(Scheduler)使用接口和方法指定调度线程,而不是依赖其他服务器活动来触发实例行维护。提供的调度器可能无法提供实时保证。该计划是尽最大努力的,并且不会对何时删除过期的条目做出任何硬性保证。

基于引用 weakKeys/weakValues/softValues

指定存储在缓存中的每个键或值都应包装在 {@link WeakReference} 中或{@link SoftReference} (默认情况下,使用强引用)。使用以上方法时,生成的缓存将使用标识 ({@code ==}) 比较来确定键的相等性

注意值value支持weakValues和softValues,而key只支持weakKeys

WeakReference:gc就会被回收
SoftReference:gc时如果没有足够的内存时会被回收,如何量化这个内存是否充足,点这里

以上驱逐策略官方建议优先采用maximumSize,除非你对WeakReference和SoftReference的适用相当熟悉并清楚由此产生的后果,不然不建议使用引用驱逐策略。

优化

回归本案例,高峰期我们的cacheHolder里面有900w个缓存实例,而maximumSize设置仅为20000,由于cacheKey是用户维度的,显然20000个key对一下c端服务来说太少了,但是调高maximumSize又会引起cacheHolderi自身占用过多内存,调高线程池的最大线程数又会对争抢正常业务的CPU资源

可能的优化方案有:

  1. 降低缓存kv的大小,比如缓存v的大小从1k降低到20byte
  2. 将缓存的过期时间从10分钟调整到1分钟,加速缓存淘汰速度
  3. 当前缓存获取属于IO密集型业务,可以适当调高线程池最大线程数,以便有更多线程资源被拿来进行缓存驱逐
  4. 使用专门的Scheduler,与put缓存的线程隔离,专门用来维护缓存的过期刷新等
  5. 碍于内存压力,考虑使用引用驱逐策略,在内存不足时优先GC缓存
  6. 如果以上方案都不适用,使用别的方案代替caffeine,比如本地内存、分布式缓存redis等等

参考:https://github.com/ben-manes/caffeine/wiki/Eviction

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com