8000 优化CtSph#lookProcessChain 创建拦截链方法(减少第一次创建拦截链持有锁占用时间,提升rt,减少gc压力) · Issue #3513 · alibaba/Sentinel · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
LilMosey opened this issue Mar 28, 2025 · 7 comments

Comments

@LilMosey
Copy link
LilMosey commented Mar 28, 2025

原逻辑:

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 added a commit to LilMosey/Sentinel that referenced this issue Mar 28, 2025
LilMosey added a commit to LilMosey/Sentinel that referenced this issue Mar 29, 2025
@LilMosey LilMosey changed the title 优化CtSph#lookProcessChain 创建拦截链方法(不太确定是否安全,希望各位大佬一起讨论一下) 优化CtSph#lookProcessChain 创建拦截链方法(减少第一次创建拦截链持有锁占用时间,提升rt,减少gc压力) Apr 9, 2025
LilMosey added a commit to LilMosey/Sentinel that referenced this issue Apr 11, 2025
@LearningGp
Copy link
Collaborator
LearningGp commented Apr 18, 2025

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

@LilMosey
Copy link
Author
LilMosey commented Apr 18, 2025

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

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

@LilMosey LilMosey reopened this Apr 18, 2025
@LearningGp
Copy link
Collaborator

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

@LilMosey
Copy link
Author

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

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

@LearningGp
Copy link
Collaborator
LearningGp commented Apr 18, 2025
AB35

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

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

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

@LilMosey
Copy link
Author

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
Copy link
Author

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。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
0