-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Reduce contention in timer driver #6504
Comments
I think sharding could go a long way with relatively little code. On On second thoughts, driving each shard adds more complexity... |
Hi. I would like to provide a possible solution here. I think for most web applications(or most applications), perhaps canceling timeouts before they are completed is the main source of performance issues. (1) Background In production environments, servers that handle network data requests often set timeouts for connections or requests. See https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/ However, multi-thread concurrent timeout operations may be affected by lock contention performance. And it because the reason some web server use it own timeout crate instead the timeout in tokio. See https://github.com/cloudflare/pingora/tree/main/pingora-timeout (2) Solution We can assign a The details here can be further discussed and considered, and this is only a preliminary idea :) Yes, we want to make the timer thread-local for every worker threadl, for reducing the lock contention. Since (3) Performance improvement
use std::time::Duration;
use tokio::time::timeout;
#[tokio::main]
async fn main() {
let fut = async { 1 }; // a vecry quick async task, but might timeout
let _to = timeout(Duration::from_secs(1), fut);
} |
This is not true. You can spawn a separate thread for the tokio runtime and register/poll the timeout on a different thread by using Handle::enter() on that thread. This is how https://github.com/smol-rs/async-compat works |
Yes, your view is correct. More precisely, |
I think we can achieve our goal this way:
The advantage of doing so is that there is no need to introduce additional complexity of sharding. Always using Instant for sharding is another solution I came up with, but due to the loss of some thread-local, the cost of lock contention might be higher. I prefer the first option, but the second option is also acceptable. |
Here's what I'm thinking. We shard the timer into several shards. When timers are created from within the runtime, they register with whichever timer shard is associated with the current runtime thread. Timers from outside the runtime pick a timer shard randomly. When a timer is cancelled before completion, it must lock the shard it was registered with. That may or may not be the one on the current thread, depending on whether it has been moved across threads. It could be worth asking someone with a running production system to instrument their timers to see how often that happens. Anyway, when we poll the IO/timer driver, it's generally done on just one thread. To start with, I don't think we should try to change that. The driver can just lock all shards one after the other, get the minimum timer from each one, and use that for its sleep duration. I'd like to avoid locking the timer driver mutex in drop of None of this should happen in one PR. I think these changes can be done incrementally. |
I would like to solove this issue by some PRs incrementally. It might start with reducing the unnecessary lock contention most timeout are cancelled before they complete (often the case for timeouts). |
That's great! Specifically, what change do you intend to start with in the first PR? |
Hi. I think the most pressing issue currently is that even if timeout has never been registered, locks will still be used in cancel. This will be my first change. |
I agree. That would also be my first change. Go for it. :) |
See tokio-rs#6504 Below are relevant benchmark results for various GOMAXPROCS values on m1/: Below are relevant benchmark results of the current version: single_thread_timeout time: [21.869 ns 21.987 ns 22.135 ns] change: [-3.4429% -2.0709% -0.8759%] (p = 0.00 < 0.05) Change within noise threshold. Found 7 outliers among 100 measurements (7.00%) 3 (3.00%) high mild 4 (4.00%) high severe multi_thread_timeout-8 time: [4.4835 ns 4.6138 ns 4.7614 ns] change: [-4.3554% +0.1643% +4.5114%] (p = 0.95 > 0.05) No change in performance detected. Found 9 outliers among 100 measurements (9.00%) 8 (8.00%) high mild 1 (1.00%) high severe Below are relevant benchmark results of this version: single_thread_timeout time: [40.227 ns 40.416 ns 40.691 ns] change: [+81.321% +82.817% +84.121%] (p = 0.00 < 0.05) Performance has regressed. Found 14 outliers among 100 measurements (14.00%) 3 (3.00%) high mild 11 (11.00%) high severe multi_thread_timeout-8 time: [183.16 ns 186.02 ns 188.21 ns] change: [+3765.0% +3880.4% +3987.4%] (p = 0.00 < 0.05) Performance has regressed. Found 10 outliers among 100 measurements (10.00%) 4 (4.00%) low severe 6 (6.00%) low mild
See tokio-rs#6504 Below are relevant benchmark results of the current version on m1 mac: single_thread_timeout time: [21.869 ns 21.987 ns 22.135 ns] change: [-3.4429% -2.0709% -0.8759%] (p = 0.00 < 0.05) Change within noise threshold. Found 7 outliers among 100 measurements (7.00%) 3 (3.00%) high mild 4 (4.00%) high severe multi_thread_timeout-8 time: [4.4835 ns 4.6138 ns 4.7614 ns] change: [-4.3554% +0.1643% +4.5114%] (p = 0.95 > 0.05) No change in performance detected. Found 9 outliers among 100 measurements (9.00%) 8 (8.00%) high mild 1 (1.00%) high severe Below are relevant benchmark results of this version on m1 mac: single_thread_timeout time: [40.227 ns 40.416 ns 40.691 ns] change: [+81.321% +82.817% +84.121%] (p = 0.00 < 0.05) Performance has regressed. Found 14 outliers among 100 measurements (14.00%) 3 (3.00%) high mild 11 (11.00%) high severe multi_thread_timeout-8 time: [183.16 ns 186.02 ns 188.21 ns] change: [+3765.0% +3880.4% +3987.4%] (p = 0.00 < 0.05) Performance has regressed. Found 10 outliers among 100 measurements (10.00%) 4 (4.00%) low severe 6 (6.00%) low mild
See tokio-rs#6504 Below are relevant benchmark results of this PR on m1 mac: single_thread_timeout time: [21.869 ns 21.987 ns 22.135 ns] change: [-3.4429% -2.0709% -0.8759%] (p = 0.00 < 0.05) Change within noise threshold. Found 7 outliers among 100 measurements (7.00%) 3 (3.00%) high mild 4 (4.00%) high severe multi_thread_timeout-8 time: [4.4835 ns 4.6138 ns 4.7614 ns] change: [-4.3554% +0.1643% +4.5114%] (p = 0.95 > 0.05) No change in performance detected. Found 9 outliers among 100 measurements (9.00%) 8 (8.00%) high mild 1 (1.00%) high severe Below are relevant benchmark results of current version on m1 mac: single_thread_timeout time: [40.227 ns 40.416 ns 40.691 ns] change: [+81.321% +82.817% +84.121%] (p = 0.00 < 0.05) Performance has regressed. Found 14 outliers among 100 measurements (14.00%) 3 (3.00%) high mild 11 (11.00%) high severe multi_thread_timeout-8 time: [183.16 ns 186.02 ns 188.21 ns] change: [+3765.0% +3880.4% +3987.4%] (p = 0.00 < 0.05) Performance has regressed. Found 10 outliers among 100 measurements (10.00%) 4 (4.00%) low severe 6 (6.00%) low mild
I tried removing the lock call on drop, but it wasn't super trivial due to the happens-before nature. Loom was not happy. The not-yet-polled state can be hardcoded. The deregistered state could perhaps be set when sleep returns Ready, I'm not sure if that will solve the loom problems though since that also only reads the state deregistered atomic. |
This strategy sounds good to me. |
I have often heard reports about performance issues due to large amounts of timers. Specifically, this is due to contention on the mutex in the timer driver.
Some ideas:
Sleep
instance is dropped before it is polled, its destructor will still lock the mutex to check whether it needs to be removed from the driver. This seems unnecessary, and will be a common case for timeouts.tokio::time::sleep
), or the case where most timers are cancelled before they complete (often the case for timeouts). Can we do anything to differentiate this in the driver?Before spending a lot of time on a large change, please check with us beforehand by posting here or in Discord so that we can discuss whether your implementation idea is what we want.
PRs related to this issue:
The text was updated successfully, but these errors were encountered: