-
Notifications
You must be signed in to change notification settings - Fork 38.5k
DefaultSubscriptionRegistry: Reduced thread contention #25298
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
Conversation
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.
Thanks for this PR!
I really like the fact that you added benchmarks to discuss the performance improvements, thank you!
...jmh/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryBenchmark.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
@Benchmark | ||
public void registerSubscription(ServerState serverState, Requests request) { |
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.
Benchmark methods should either return a value or use Blackhole
to consume values produced by the benchmark method. Without that, we're at risk that the JVM performs dead code elimination. See https://github.com/spring-projects/spring-framework/wiki/Micro-Benchmarks for more information.
The register/unregister subscriptions return void
, but using Blackhole
on findSubscriptionsInternal
should do the trick.
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.
thanks for this finding, I will add the blackhole
...jmh/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryBenchmark.java
Outdated
Show resolved
Hide resolved
...ing/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java
Outdated
Show resolved
Hide resolved
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.
I've had a detailed first look and scheduled it for 5.3 M2. The only comment I have at this time is to skip the use of Stream
to reduce Object allocation along with the comments about the benchmark from @bclozel.
Let me know if you plan to update those, or if not we can also take it from here.
Thanks for all this!
|
||
this.destinations = IntStream.range(0, this.numberOfDestinations) | ||
.mapToObj(i -> "/some/destination/" + i) | ||
.toArray(String[]::new); |
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.
There probably should be some pattern destinations.
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.
I will add a benchmark for pattern destinations. Actually, there two places where I can put a pattern.
- new subscription can be a pattern destination
- other already registered subscriptions can be a pattern
I will add a flag at both places.
Hey! It's very good implementation (especially its performance), but I have run into a deadlock:)
I think it could be fixed removing first |
ah, my apologize. I've found the same this morning. |
Thank you all for the review. I have fixed all findings mentioned above. Please have a look. |
if (!result.isEmpty()) { | ||
this.updateCache.put(destination, result.deepCopy()); | ||
this.accessCache.put(destination, result); | ||
private final Queue<String> cacheEvictionPolicy = new LinkedBlockingQueue<>(); |
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.
I did some perf testing of "LinkedBlockingQueue vs ConcurrentLinkedQueue" and I have no clear winner.
I think ConcurrentLinkedQueue.size() would cause lots of CPU cache miss while traversing through the whole linked list. That is something I don't know hot to test on local machine without some bigger test preparation. That is why I left LinkedBlockingQueue there, but I will change it to ConcurrentLinkedQueue if you like it more.
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.
finally, I used ConcurrentLinkedQueue and AtomicInteger to maintain the size.
42c3689
to
a6e2f96
Compare
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.
I think there's only one remaining task being discussed with Rossen: pattern destinations in the benchmark.
Other than that, it looks good! Thanks very much!
* DestinationCache is now synchronized on multiple 'destination' locks (previously a single shared lock) * DestinationCache keeps destinations without any subscriptions (previously such destinations were recomputed over and over) * SessionSubscriptionRegistry is now a 'sessionId -> subscriptionId -> (destination,selector)' map for faster lookups (previously 'sessionId -> destination -> set of (subscriptionId,selector)') closes spring-projectsgh-24395
I dedicated my Laptop i5-8250U @ 1.6Ghz, ram @ 2.4Ghz to perf tests for two days .Because it took ~6hours to run one set of JMH tests, I would like to share them with you. I ran the following:
I had to fix setCacheLimit(0) on the old implementation by swapping these two lines: results-oldFixed-4-threads.txt results-newImpl-single-thread.txt |
Subscription previousValue = this.subscriptionRegistry.addSubscription(sessionId, subscriptionId, subscription); | ||
if (previousValue == null) { | ||
this.destinationCache.updateAfterNewSubscription(destination, isAntPattern, sessionId, subscriptionId); | ||
} |
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.
Subscription id's are supposed to be unique within a session/connection. That means previousValue should always be null. Do you know of a case where it wouldn't be?
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.
I had "better to be safe than sorry" on my mind. Actually, you are right and it complicated the code. I removed it. Thanks for this finding.
One thing that would still be useful for the benchmark is a setup category that has both finding and registering/unrestering. Within that category, patterns vs no patterns, where patterns should probably exceed the cache size, since they allow variation in destinations and a greater number of destinations, e.g. with path variables |
@rstoyanchev Any (static/pattern) subscription registration updates only destinations that are currently cached. In other words subscription registration does not add any new cache entries and could not trigger a cache overflow. The cache overflow can happen then server sends (findSubscriptionsInternal() method) to more destinations than is the cache size limit. This case is already covered by a benchmark, please see a cacheSizeLimt=0 column in a table above. I am happy to implement more benchmarks. Could you please describe the benchmark setup you proposed in more details so I can get on the same page? |
Sorry, I meant to say where destinations (not patterns) exceed the cache size. I was merely pointing out the current setups demonstrate well individual aspects but it would be useful to have a combined setup (both find and subscribe/unsubscribe) as it happens at runtime. My second thought was that when patterns are used, it's easy to imagine the number of destinations exceeding the cache size and that would also be a good match for such a combined setup. Does that make sense? By the way I'm already reviewing and polishing locally so please don't submit any further changes. I was just thinking out loud but it can be done separately later. |
@rstoyanchev I am sorry for closing and reopening this issue. I unintentionally deleted (and then resurrected) a remote branch in my github repo and it most likely closed this PR. Please force push your changes if needed.
I really like your idea of a more realistic performance test. We can do a setup where e.g.:
E.g. our case on a single server in production is:
I can imagine that someone is not using static destinations, but is using patterns only, or is registered to much more destinations.
I think, this kind of benchmark is already there in a form of cache size = 0. In such case any find operation does not find an cached entry and perform recalculation, store and evict. |
Renaming, trimming of method parameters, minor refactoring of helper methods, comments, etc. Completely functionally neutral. See gh-25298
@trim09 your changes are now in master. Thanks again for the very detailed contribution! I did a little polishing, completely functionally neutral. That aside I also reviewed it with @jhoeller and we spotted one issue. Since the One more point that I wanted to raise with you before making changes. In the previous implementations, removal iterated over the cache looking for a match by
No worries. For the more realistic test, yes we can work on that next and it can have a couple of sub-categories with static destinations like your scenario and also with patterns. Here is one example with stock quotes by ticker which would then broadcasts. |
@rstoyanchev
Great catch! I agree, it was in a wrong order. 👍
👍
It looks great! Thanks.
Cool! I really enjoyed my first contribution here and I am considering a next performance improvement. |
I closed this review as it has been already merged in master. |
I've updated the removal logic so we should be all good here.
Thanks again, much appreciated! |
(previously a single shared lock)
(previously such destinations were recomputed over and over)
'sessionId -> subscriptionId -> (destination,selector)' map
for faster lookups
(previously 'sessionId -> destination -> set of (subscriptionId,selector)')
closes gh-24395