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
rt: reduce no-op wakeups in the multi-threaded scheduler #4383
Conversation
9bc41b1
to
175d8b8
Compare
175d8b8
to
49192be
Compare
@@ -620,8 +620,7 @@ impl Core { | |||
// If a task is in the lifo slot, then we must unpark regardless of | |||
// being notified | |||
if self.lifo_slot.is_some() { | |||
worker.shared.idle.unpark_worker_by_id(worker.index); | |||
self.is_searching = true; | |||
self.is_searching = !worker.shared.idle.unpark_worker_by_id(worker.index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the key bit. When the worker wakes, it only enters the "searching" state if it was unparked by another thread. This distinguishes unparks from other threads to signal to the worker that it should steal work vs. the I/O driver unparked the thread because events arrived.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it might be worth having a comment in the code explaining that? not a hard blocker though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me --- the actual change is pretty simple and makes sense based on the explanation in the PR description.
It might be nice to add a bit to the comments in this code explaining this behavior, but that's not a hard blocker.
I'd like to test this out in Linkerd today or tomorrow, I'll post benchmark results if I get a chance to do that!
@@ -620,8 +620,7 @@ impl Core { | |||
// If a task is in the lifo slot, then we must unpark regardless of | |||
// being notified | |||
if self.lifo_slot.is_some() { | |||
worker.shared.idle.unpark_worker_by_id(worker.index); | |||
self.is_searching = true; | |||
self.is_searching = !worker.shared.idle.unpark_worker_by_id(worker.index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it might be worth having a comment in the code explaining that? not a hard blocker though.
No change in observed vector throughput, per these results. That said, we don't track CPU use in our experimental setup and the observation duration in a PR is constrained to 200 seconds to make turn-around time feasible. |
@blt thanks for checking. It would be interesting to know the CPU load as that would let us know if it falls within the scope of this change. If you are saturating all workers in your tests, then there would be no visible improvement as it only applies when only a few workers are actually kept busy. |
Unfortunately we don't capture that kind of saturation information yet, though we do have an issue for it: vectordotdev/vector#10456. That said, we're intending to fully saturate vector so it's reasonable to believe there's little idle time. |
Co-authored-by: Eliza Weisman <eliza@buoyant.io>
…io-rs#4383)" This reverts commit 4eed411.
…io-rs#4383)" This reverts commit 4eed411.
…io-rs#4383)" This reverts commit 4eed411.
…io-rs#4383)" This reverts commit 4eed411.
…io-rs#4383)" This reverts commit 4eed411.
This PR reduces the number of times worker threads wake up without having work to do in the multi-threaded scheduler. Unnecessary wake-ups are expensive and slow down the scheduler. I have observed this change reduce no-op wakes by up to 50%.
The multi-threaded scheduler is work-stealing. When a worker has tasks to process, and other workers are idle (parked), these idle workers must be unparked so that they can steal work from the busy worker. However, unparking threads is expensive, so there is an optimization that avoids unparking a worker if there already exists workers in a "searching" state (the worker is unparked and looking for work). This works pretty well, but transitioning from 1 "searching" worker to 0 searching workers introduces a race condition where a thread unpark can be lost:
Because this should be a rare condition, Tokio solves this by always unparking a new worker when the current worker:
When the newly unparked worker wakes, if the race condition described above happened, "thread 2"'s work will be found. Otherwise, it will just go back to sleep.
Now we come to the issue at hand. A bug incorrectly set a worker to "searching" when the I/O driver unparked the thread. In a situation where the scheduler was only partially under load and is able to operate with 1 active worker, the I/O driver would unpark the thread when new I/O events are received, incorrectly transition it to "searching", find new work generated by inbound I/O events, incorrectly transition itself from the last searcher -> no searchers, and unpark a new thread. This new thread would wake, find no work and go back to sleep.
Note that, when the scheduler is fully saturated, this change will make no impact as most workers are always unparked and the optimization to avoid unparking threads described at the top applies.
Benchmarks
Hyper
This benches Hyper's "hello" server using
wrk -t1 -c400 -d10s http://127.0.0.1:3000/
master
this PR
mini-redis
This benches mini-redis using
redis-benchmark -c 5 -t get,set
master
this PR