Skip to content

Reduce futex usage in ParkingLot#2907

Merged
wwbmmm merged 2 commits intoapache:masterfrom
JimChengLin:master
Mar 6, 2025
Merged

Reduce futex usage in ParkingLot#2907
wwbmmm merged 2 commits intoapache:masterfrom
JimChengLin:master

Conversation

@JimChengLin
Copy link
Copy Markdown
Contributor

@JimChengLin JimChengLin commented Mar 4, 2025

What problem does this PR solve?

Issue Number: #1727

Problem Summary: high cpu usage because of futex in parking lot

What is changed and the side effects?

Changed:
optimize parking lot

Side effects:

  • Performance effects(性能影响):
    minor improvement for most cases. significant for some cases.
  • Breaking backward compatibility(向后兼容性):
    No

Check List:

  • Please make sure your changes are compilable(请确保你的更改可以通过编译).
  • When providing us with a new feature, it is best to add related tests(如果你向我们增加一个新的功能, 请添加相关测试).
  • Please follow Contributor Covenant Code of Conduct.(请遵循贡献者准则).

@JimChengLin
Copy link
Copy Markdown
Contributor Author

@lorinlee PTAL

@wwbmmm
Copy link
Copy Markdown
Contributor

wwbmmm commented Mar 4, 2025

LGTM
CI失效的问题可以merge master解决

@JimChengLin
Copy link
Copy Markdown
Contributor Author

LGTM CI失效的问题可以merge master解决

Merged

@yanglimingcn
Copy link
Copy Markdown
Contributor

LGTM

@wwbmmm wwbmmm mentioned this pull request Mar 6, 2025
@wwbmmm wwbmmm merged commit 77431a3 into apache:master Mar 6, 2025
20 checks passed
@chenBright
Copy link
Copy Markdown
Contributor

chenBright commented Jul 13, 2025

使用 #2907#2916 后,futex 自旋锁竞争变更激烈了,CPU开销涨了17%左右。

image
image image
image

我们的场景是worker数多(200),worker usage比较小,rpc qps 10W+,会主动起一些bthread。

原因估计是刚好有worker完成futex_wait后,_waiter_num并不能可靠地获取到当前有waiter,提前返回继续去signal下一个ParkingLot,使得_pending_signal递增更频繁,导致futex_wait_private失败的概率变大,futex_wait_private的次数也变多了。

我觉得_waiter_num和_pending_signal的组合操作需要原子的,才能解决这个问题。

@JimChengLin CC

@JimChengLin
Copy link
Copy Markdown
Contributor Author

使用 #2907#2916 后,futex 自旋锁竞争变更激烈了,CPU开销涨了17%左右。

image image image image 我们的场景是worker数多(200),worker usage比较小,rpc qps 10W+,会主动起一些bthread。

原因估计是_waiter_num并不能可靠地获取到当前有waiter,提起返回继续去signal下一个ParkingLot,使得_pending_signal递增更频繁,导致futex_wait_private失败的概率变大,futex_wait_private的次数也变多了。

我觉得_waiter_num和_pending_signal的组合操作需要原子的,才能解决这个问题。

@JimChengLin CC

我理解这个MR 只会比之前 futex wake 更少,而不会更多。是说 waiter num 错误的返回没有 waiter,导致 parking lot 产生热点了吗?_pending_signal 修改次数没有变化吧。从你描述的场景来看,worker num 比较多,导致几乎永远可以有空闲 worker 来 futex 唤醒,所以有可能的问题是激烈竞争下,最优的策略是第一个 parking lot 就唤醒,而不要尝试后面的。在这个 MR 之前,一个线程可能就访问一个 parking lot,在这个 MR 可能需要访问陌生的 parking lot?但我很怀疑把 waiter num 和 signal 合并后,会有什么收益,这样对条件的竞争不是更严重吗?

@JimChengLin
Copy link
Copy Markdown
Contributor Author

把 parking lot 默认 shard 数目调高试试呢?这样对单个 parking lot 竞争更小,waiter num 和 signal 的不同步概率就更低。

@chenBright
Copy link
Copy Markdown
Contributor

在这个 MR 之前,一个线程可能就访问一个 parking lot,在这个 MR 可能需要访问陌生的 parking lot?

是的,一个ParkingLot已经futex_wait,挂起了。但是此时signal里有可能没有同步到_waiter_num大于0,所以直接返回,去访问其他ParkingLot了。这个PR之前是直接futex_wake就成功了。我理解是这个差异可能会导致futex竞争变大了。暂时没想到其他场景了。

但我很怀疑把 waiter num 和 signal 合并后,会有什么收益,这样对条件的竞争不是更严重吗?

应该没啥性能收益吧,单纯考虑正确性而已,使用signal/wait都用CAS或者fetch_add保证可见性,有个问题就是wait也加入竞争了。

但是目前不知道要咋实现。

把 parking lot 默认 shard 数目调高试试呢?这样对单个 parking lot 竞争更小,waiter num 和 signal 的不同步概率就更低。

试过了,调高ParkingLot数目不会降低signal/wait的qps,都相比这个PR之前高。

@JimChengLin
Copy link
Copy Markdown
Contributor Author

在这个 MR 之前,一个线程可能就访问一个 parking lot,在这个 MR 可能需要访问陌生的 parking lot?

是的,一个ParkingLot已经futex_wait,挂起了。但是此时signal里有可能没有同步到_waiter_num大于0,所以直接返回,去访问其他ParkingLot了。这个PR之前是直接futex_wake就成功了。我理解是这个差异可能会导致futex竞争变大了。暂时没想到其他场景了。

但我很怀疑把 waiter num 和 signal 合并后,会有什么收益,这样对条件的竞争不是更严重吗?

应该没啥性能收益吧,单纯考虑正确性而已,使用signal/wait都用CAS或者fetch_add保证可见性,有个问题就是wait也加入竞争了。

但是目前不知道要咋实现。

把 parking lot 默认 shard 数目调高试试呢?这样对单个 parking lot 竞争更小,waiter num 和 signal 的不同步概率就更低。

试过了,调高ParkingLot数目不会降低signal/wait的qps,都相比这个PR之前高。

那这个可能是 corner case,这种永远能都唤醒 + waiter num 偶尔是 0 的 case,这个 MR 可能确实是负优化。绝大多数情况,特别是永远都唤醒不了的 case,这个 MR 优化是比较大的。

@chenBright
Copy link
Copy Markdown
Contributor

那这个可能是 corner case,这种永远能都唤醒 + waiter num 偶尔是 0 的 case,这个 MR 可能确实是负优化。绝大多数情况,特别是永远都唤醒不了的 case,这个 MR 优化是比较大的。

@JimChengLin 我觉得这里还是有问题:当前只有一个worker wait的同时,non-worker起了一个任务,选中了该worker,signal时没同步到waiter num,也会出现信号丢失,task 永远不会被消。类似#2916 (comment)

@JimChengLin
Copy link
Copy Markdown
Contributor Author

@JimChengLin 我觉得这里还是有问题:当前只有一个worker wait的同时,non-worker起了一个任务,选中了该worker,signal时没同步到waiter num,也会出现信号丢失,task 永远不会被消。类似#2916 (comment)

你说的 case #2916 是不是解决了呀。就是 waiter num = 0,只表示当前肯定没有,但是return 的瞬间,如果有新 worker 进来 wait 就 signal lost 了。解决方案是 signal 的时候,无论 waiter num 都变更 butex flag,这样 worker 就有一个double check pattern。

@chenBright
Copy link
Copy Markdown
Contributor

@JimChengLin 我觉得这里还是有问题:当前只有一个worker wait的同时,non-worker起了一个任务,选中了该worker,signal时没同步到waiter num,也会出现信号丢失,task 永远不会被消。类似#2916 (comment)

你说的 case #2916 是不是解决了呀。就是 waiter num = 0,只表示当前肯定没有,但是return 的瞬间,如果有新 worker 进来 wait 就 signal lost 了。解决方案是 signal 的时候,无论 waiter num 都变更 butex flag,这样 worker 就有一个double check pattern。

描述有问题,应该是ParkingLot的所有worker先wait了,non-worker signal时都没同步到waiter num,即使变更 futex,worker也不会被唤醒了。

@JimChengLin
Copy link
Copy Markdown
Contributor Author

JimChengLin commented Jul 21, 2025

描述有问题,应该是ParkingLot的所有worker先wait了,non-worker signal时都没同步到waiter num,即使变更 futex,worker也不会被唤醒了。

wait 之前肯定 waiter num + 1,所以这种情况也不存在

@chenBright
Copy link
Copy Markdown
Contributor

我认为会有这个问题:

注意,memory fence不等于可见性,即使线程2恰好在线程1在把ready设置为true后读取了ready也不意味着它能看到true,因为同步cache是有延时的。memory fence保证的是可见性的顺序:“假如我看到了a的最新值,那么我一定也得看到b的最新值”。

线程2在线程1执行waiter num + 1后,并不一定能看到waiter num大于0。

@JimChengLin
Copy link
Copy Markdown
Contributor Author

JimChengLin commented Jul 22, 2025

注意,memory fence不等于可见性,即使线程2恰好在线程1在把ready设置为true后读取了ready也不意味着它能看到true,因为同步cache是有延时的。memory fence保证的是可见性的顺序:“假如我看到了a的最新值,那么我一定也得看到b的最新值”。

正如引用的文章所说,可见性和 mem order 不是完全相等的两个东西,这里的 mem order 是没有问题的。atomic add 和 futex wait 的局部可见顺序是确定的。

@chenBright
Copy link
Copy Markdown
Contributor

但也没法保证另一个线程signal中atomic load能看到waiter num大于0吧?

@JimChengLin
Copy link
Copy Markdown
Contributor Author

但也没法保证另一个线程signal中atomic load能看到waiter num大于0吧?

waiter 走 butex wait 有 double check 呀,wait 就挂不上去,因为 signal 会改 flag

@chenBright
Copy link
Copy Markdown
Contributor

chenBright commented Jul 22, 2025

  1. T1 waiter_num + 1
  2. T1 futex_wait
  3. T2 waiter_num load等于0

这样的顺序呢?

@JimChengLin
Copy link
Copy Markdown
Contributor Author

  1. T1 waiter_num + 1
  2. T1 futex_wait
  3. T2 waiter_num load等于0

如果没有 memory order 限制,是会发生你说的问题。但是 memory order 保证了,你先改了 signal flag 然后又看到 waiter num = 0,那么 T1 一定能在 wait 的时候意识到 signal flag 已经被修改了。多线程就不要思考全局单一顺序了,思考局部顺序就行。

@chenBright
Copy link
Copy Markdown
Contributor

chenBright commented Jul 22, 2025

我说的问题是先waiter_num + 1 & futex_wait,再改了 signal flag,好像没有memory order来保证这时候waiter_num load大于0吧?

@JimChengLin
Copy link
Copy Markdown
Contributor Author

我说的问题是先waiter_num + 1 & futex_wait,再改了 signal flag,好像没有memory order来保证这时候waiter_num load大于0吧?

多核是同步网络系统,不能按异步网络的分布式系统(Raft Paxos 考虑的情况)来看。waiter num + 1 然后 futex wait,这个 futex wait 有 release 语义。如果别的观测者能观测到 waiter num = 0,那么观测者视角来看,futex wait 就一定没执行,那执行到的时候一定会发现 signal flag 变更了,wait 挂不上去。

@chenBright
Copy link
Copy Markdown
Contributor

即使线程2恰好在线程1在把ready设置为true后读取了ready也不意味着它能看到true,因为同步cache是有延时的。

futex wait 有 release 语义保证别的观察者futex_wake看到futex wait,就能看到wait_num + 1吧。别的观测者能观测到 waiter num = 0,不会执行futex_wake,还会有这个保证吗?套用文章的说法就是,即使线程2恰好在线程1在把wait_num + 1后读取了wait_num也不意味着它能看到大于0,因为同步cache是有延时的。既然有延时的话,也不能保证futex wait后能看到wait_num大于0。

如果别的观测者能观测到 waiter num = 0,那么观测者视角来看,futex wait 就一定没执行。

如你所说,这种情况下,在这个PR之前,futex_wake也是返回0,然后会去signal其他ParkingLot。其实跟这个PR后是一样的。这样的话,应该怎么解释这个PR后,futex 自旋锁竞争变更多了很多呢?

@JimChengLin
Copy link
Copy Markdown
Contributor Author

@lorinlee 能来 clearify 一下吗?

@chenBright
Copy link
Copy Markdown
Contributor

@wwbmmm 有空看看

@wwbmmm
Copy link
Copy Markdown
Contributor

wwbmmm commented Aug 12, 2025

@wwbmmm 有空看看

简单的解决方案,可不可以加一个flag来控制这个优化?对于signal较频繁、worker数较少的场景,可以开启flag,减少futex_wake次数。对于signal不频繁、worker数较多的场景可以关闭flag,避免负优化

@chenBright @JimChengLin

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

Successfully merging this pull request may close these issues.

4 participants