Slowness due to blocked threads in Channel.getChannel call#887
Slowness due to blocked threads in Channel.getChannel call#887mwiede merged 6 commits intomwiede:masterfrom
Conversation
…hread environment
|
Summary of changes made:
The implementation now provides better performance and thread safety for concurrent channel operations while maintaining backward compatibility with the OpenSSH workaround. |
norrisjeremy
left a comment
There was a problem hiding this comment.
The strategy that is being proposed here could potentially result in memory leaks due to stranded Channels.
For example, the old strategy, while likely slow as you have pointed out, there was no chance for a Channel to not be added to the pool, even if they happened to somehow share the same id value.
However, the strategy proposed here, if two Channel's ever ended up sharing the same id value, the later created one would overwrite it's position in the pool & thus result in a memory leak (since the earlier created Channel would no longer be present in the pool).
| nextIndex = (currentIndex + 1) & Integer.MAX_VALUE; | ||
| } while (!index.compareAndSet(currentIndex, nextIndex)); | ||
| id = currentIndex; | ||
| pool.put(id, this); |
There was a problem hiding this comment.
To illustrate, what I mean, suppose at time X, the id value for a particular Channel A was computed as a value of 5.
Then suppose at some later time Y, the id value for a different Channel B happens to be computed as a value of 5 (since the AtomicInteger index value cycled through enough values to rollover and begin to be recycled).
The pool.put(id, this) call here that puts Channel B into into the pool with an id key value of 5 overwrites the previous entry with a key value of 5 that points at the earlier Channel A.
And thus we have a memory leak, since Channel A is no longer being maintained in the pool.
|
Thank you very much for the problem you identified:
Here is solution 1: Here is solution 2: @norrisjeremy I prefer solution 1, what about you ? |
|
Hi @DavidTavoularis, I'm not a fan of either solution honestly, because it artificially restricts the reuse of a specific id values. Thanks, |
|
Hi @norrisjeremy thank you for suggesting to use ReadWriteLock. I just updated the pull request with these new changes:
Benefits:
Key operations:
This solution provides thread safety without the artificial restrictions of collision detection, while improving performance for concurrent read operations. |
|
Hi @DavidTavoularis, See #888 for my current draft proposal. Thanks, |
FYI, I still need to think through the memory usage ramifications of changing the |
|
@norrisjeremy I like your approach, here are my comments, regarding your pull-request:
Best Regards |
|
@norrisjeremy Let's estimate the memory usage for 10000 Channels. Original Vector (or ArrayList) approach = O(N):
Map<Integer, Set> approach = O(1):
Another approach would be to have 2 maps (a regular one, and one only for collisions) = O(1):
80KB vs 890KB vs 320KB I agree that it requires more thinking. |
Hi @DavidTavoularis, I've implemented 1, 2 & 4 as you suggested. |
|
@norrisjeremy Thanks, I agree with you for item 3 |
Yes, that is what I'm worried about and thus want to think further on. |
| private static final Lock readLock = poolLock.readLock(); | ||
| private static final Lock writeLock = poolLock.writeLock(); |
There was a problem hiding this comment.
I would omit these two properties and simply derive the read & write locks in the methods that need to use them like I do in #888.
|
@norrisjeremy Local ephemeral port range (typically ~28,000-65,000 ports) If yes, then the maximum amount of simultaneous Channels is about 50000, but I don't believe that anyone would use more than 10000 simultaneous Channels, a more typical usage would be 1000 simultaneous Channels for high-performance systems (this is my use-case). |
The id values are only locally significant to a particular session. |
|
I believe that there is usually a limit of 10 channels per SSH session (cf MaxSessions 10 in /etc/ssh/sshd_config) |
| channels[count++] = c; | ||
| channels.add(c); | ||
| } | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
I'm not sure why this inner try block that catches exceptions & ignores them exists.
I wonder if we should just drop it?
There was a problem hiding this comment.
agreed, I dropped it and pushed in PR
|
Main Achievement: Replaced slow synchronized blocks with ReadWriteLock for better thread concurrency. Key Changes:
Benefits: Multiple threads can now lookup channels concurrently instead of being serialized. Write operations still require exclusive access for data consistency. Same memory usage (~80KB for 10K channels) with significantly better scalability for read-heavy SSH workloads. Compatibility: All APIs unchanged, maintains OpenSSH compatibility and existing behavior. |
| private static Vector<Channel> pool = new Vector<>(); | ||
| private static final ReadWriteLock poolLock = new ReentrantReadWriteLock(); | ||
| private static int index = 0; | ||
| private static List<Channel> pool = new ArrayList<>(); |
There was a problem hiding this comment.
I think pool could be marked final?
|
norrisjeremy
left a comment
There was a problem hiding this comment.
I think going with this more conservative approach is safer than the alternate approach I explored in #888, so I think we should go with this.
agreed |
|
Hi @DavidTavoularis, So I have started exploring a different approach to fixing this issue, see #889. Thanks, |
|
@mwiede I think what we should do is merge this PR (#887), and then I will rebase my PR (#889) on top of the changes in this, and then I will take #889 out of draft status and you can then merge it. |



No description provided.