Sentinel icon indicating copy to clipboard operation
Sentinel copied to clipboard

优化CtSph#lookProcessChain 创建拦截链方法(减少第一次创建拦截链持有锁占用时间,提升rt,减少gc压力)

Open LilMosey opened this issue 8 months ago • 7 comments

原逻辑:

Image

chainMap 是一个 volatile 修饰的 Map。首次获取 ProcessorSlotChain 时,如果为空,旧逻辑会进入同步块,new 一个新的 map,并通过 putAll 拷贝原有数据后再新增,最终整体替换 chainMap,利用 volatile 替换引用实现可见性。

优化后:

Image 新逻辑为在同步块中直接put。(synchronized本身保证可见性)

好处:

1: 提高rt: 避免在高并发下频繁进行 putAll 拷贝,特别是在 map 较大(如含有数百个资源)的情况下,putAll花费的时间越多,优化可以减少锁占用时间,从而提高rt。

2: 减少GC压力: 直接put无需频繁创建大量短生命周期的临时 map,降低内存占用和 GC 负担。

理由: 看了整个代码后,发现真正使用chainMap的地方也只有lookProcessChain这一个方法,其余的方法比如resetChainMap、getChainMap是default修饰的,仅仅用于测试调用,只能在当前包调用,所以这里我没有加synchronized。而entrySize方法是一个public修饰的方法,为了保证其可见性,我使用了synchronized关键字。

synchronized关键字本身通过内存屏障保证了可见性,因此chainMap不需要加volatile关键字修饰。

类似的实现比如java中的Collections.synchronizedMap()。 Image

后续思考:

Image

当资源数限制(如 6000)放开后,或者可以允许超一点点去追求极致的性能的话,代码还可以进行如下优化:

chainMap使用ConcurrentHashMap,不需要显示进行加锁,直接采用ConcurrentHashMap#computeIfAbsent方法,这样锁的粒度更小,性能更好。

Image

LilMosey avatar Mar 28 '25 09:03 LilMosey

我理解这边主要讨论了 chainMap 使用 HashMap 还是 ConcurrentHashMap 的问题。首先我们明确下使用的场景,chainMap 的使用场景主要分成两个阶段,一个是初始化阶段,一个是运行阶段。在初始化阶段中,相较于性能,我们会更关注准确性,包括对于数量的控制等;在运行阶段,则更关注性能。 回到这个问题,我用 Benchmark 跑了一下性能测试,在较高强度下,只考虑 get 操作,HashMap 相较于 ConcurrentHashMap 还是表现出了一定的优势。考虑到前面提到的使用场景,虽然初始化阶段的性能表现会略差于 ConcurrentHashMap,当前可能还是保持使用 HashMap 以获取更高运行阶段的性能更合适。 在确定了这个结论后,再看下关于更新 map 的方式,由于使用的是 HashMap,并发的读写操作存在风险,所以采用了现在这种方式。 个人的一些想法,欢迎继续讨论。

LearningGp avatar Apr 18 '25 02:04 LearningGp

我理解这边主要讨论了 chainMap 使用 HashMap 还是 ConcurrentHashMap 的问题。首先我们明确下使用的场景,chainMap 的使用场景主要分成两个阶段,一个是初始化阶段,一个是运行阶段。在初始化阶段中,相较于性能,我们会更关注准确性,包括对于数量的控制等;在运行阶段,则更关注性能。 回到这个问题,我用 Benchmark 跑了一下性能测试,在较高强度下,只考虑 get 操作,HashMap 相较于 ConcurrentHashMap 还是表现出了一定的优势。考虑到前面提到的使用场景,虽然初始化阶段的性能表现会略差于 ConcurrentHashMap,当前可能还是保持使用 HashMap 以获取更高运行阶段的性能更合适。 在确定了这个结论后,再看下关于更新 map 的方式,由于使用的是 HashMap,并发的读写操作存在风险,所以采用了现在这种方式。 个人的一些想法,欢迎继续讨论。

我的表达可能不清晰,我的意思是在 lookProcessChain 方法的初始化过程中,即使chainMap使用hashMap,也不需要使用 copyOnWrite 来频繁拷贝 map,因为 synchronized 已经保证了线程安全和可见性,直接通过 put 操作即可。此外,我也意识到关于 HashMap 和 ConcurrentHashMap 的使用,特别是在 get 操作上的性能差异,感谢你的提醒,之前确实没考虑到这一层。

LilMosey avatar Apr 18 '25 03:04 LilMosey

lookProcessChain 方法中的第一个 get 操作不在 synchronized 的范围内哈,还是会存在并发 get 和 put 的情况

LearningGp avatar Apr 18 '25 03:04 LearningGp

lookProcessChain 方法中的第一个 get 操作不在 synchronized 的范围内哈,还是会存在并发 get 和 put 的情况

是的,第一个get操作不在synchronized范围内,但是他做了一层double check。后面synchronized代码块中没有必要对chainMap进行copyOnWrite的操作。

LilMosey avatar Apr 18 '25 03:04 LilMosey

lookProcessChain 方法中的第一个 get 操作不在 synchronized 的范围内哈,还是会存在并发 get 和 put 的情况

是的,第一个get操作不在synchronized范围内,但是他做了一层double check。后面synchronized代码块中没有必要对chainMap进行copyOnWrite的操作。

有道理,即便有 resize 导致的脏读也是读到 null ,在有 double check 的情况下后面的 copyOnWrite 操作确实没有必要

LearningGp avatar Apr 18 '25 03:04 LearningGp

lookProcessChain 方法中的第一个 get 操作不在 synchronized 的范围内哈,还是会存在并发 get 和 put 的情况

是的,第一个get操作不在synchronized范围内,但是他做了一层double check。后面synchronized代码块中没有必要对chainMap进行copyOnWrite的操作。

有道理,即便有 resize 导致的脏读也是读到 null ,在有 double check 的情况下后面的 copyOnWrite 操作确实没有必要

大佬请教一下两个问题:

  1. 我提交了一个 PR#3514,但被工作流阻塞了,本地执行测试用例时是正常通过的,不清楚具体原因。
  2. 关于开发分支的问题,官网推荐使用 master 作为开发分支,但实际大多数 PR 却是以 1.8 分支为开发分支,请问这里的分支策略是怎样的?

LilMosey avatar Apr 18 '25 06:04 LilMosey

lookProcessChain 方法中的第一个 get 操作不在 synchronized 的范围内哈,还是会存在并发 get 和 put 的情况

是的,第一个get操作不在synchronized范围内,但是他做了一层double check。后面synchronized代码块中没有必要对chainMap进行copyOnWrite的操作。

有道理,即便有 resize 导致的脏读也是读到 null ,在有 double check 的情况下后面的 copyOnWrite 操作确实没有必要

之前使用 CopyOnWrite 是为了保证 resetChainMap、getChainMap 和 entrySize 方法操作 map 时的可见性。考虑到 resetChainMap 和 getChainMap 是 default 方法,仅在包内调用,我的pr中对entrySize 通过 synchronized 保证可见性,因此去掉了 CopyOnWrite。

LilMosey avatar Apr 22 '25 08:04 LilMosey