Skip to content
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

Combined ByteBufferPool #8171

Merged
merged 27 commits into from
Jul 4, 2022

Conversation

gregw
Copy link
Contributor

@gregw gregw commented Jun 16, 2022

WIP on the concept of a single ByteBufferPool that can act either as a normal pool or as a retainable pool

Signed-off-by: Greg Wilkins gregw@webtide.com

Signed-off-by: Greg Wilkins <gregw@webtide.com>
@gregw gregw requested a review from lorban June 16, 2022 00:02
@gregw gregw requested review from osbornjd and sbordet and removed request for osbornjd June 16, 2022 00:02
@gregw
Copy link
Contributor Author

gregw commented Jun 16, 2022

This is an alternative to #8169

Retained pool acquires from outer pool

Signed-off-by: Greg Wilkins <gregw@webtide.com>
updated modules and xml

Signed-off-by: Greg Wilkins <gregw@webtide.com>
don't make a private pool

Signed-off-by: Greg Wilkins <gregw@webtide.com>
improved dump

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Copy link
Contributor

@lorban lorban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT all the classes using the retainable pool still access it via RetainableByteBufferPool.findOrAdapt() except that we're not registering a retainable pool in the connector anymore.

I'm not sure what this brings compared to what we have now.

@@ -99,13 +99,13 @@ public MappedByteBufferPool(int factor, int maxQueueLength, Function<Integer, Bu
*/
public MappedByteBufferPool(int factor, int maxQueueLength, Function<Integer, Bucket> newBucket, long maxHeapMemory, long maxDirectMemory)
{
super(factor, maxQueueLength, maxHeapMemory, maxDirectMemory);
super(factor, 0, maxQueueLength, maxHeapMemory, maxDirectMemory, -1, -1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default heap and direct memory should be 0 to use the heuristic, not -1 for unlimited.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think all pools should have a retained pool by default.... or maybe that is OK since it will start empty and only fill if used???

Need to consider defaults... also some strangeness with -1 vs 0 for disabled vs heuristic. more review needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MappedByteBufferPool is primarily used by the client which benefits from a RBBP too, I don't see why we would disable the retainability by default.

That -1 vs 0 change you made breaks the pre-existing contract, perhaps you should revert to 0 for heuristic, -1 for unlimited and add -2 for non-retaining retainable pool?

@gregw
Copy link
Contributor Author

gregw commented Jun 17, 2022

@lorban said:

I'm not sure what this brings compared to what we have now.

All abstract byte buffer pools now contain a retainable pool (either a null one or real one). This means we need to pass and configure only a single pool.

I've kept findOrAdapt for now for backwards comparability for those who currently set a retainable pool on the server, but it could be simplified and then inlined/removed in 12.

Comment on lines 59 to 65
if (retainedHeapMemory < 0 && retainedDirectMemory < 0)
_retainableByteBufferPool = RetainableByteBufferPool.from(this);
else
_retainableByteBufferPool = newRetainableByteBufferPool(maxCapacity, maxBucketSize,
(retainedHeapMemory != 0) ? retainedHeapMemory : Runtime.getRuntime().maxMemory() / 4,
(retainedDirectMemory != 0) ? retainedDirectMemory : Runtime.getRuntime().maxMemory() / 4);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lorban this is the substantive change. Every AbstractByteBufferPool contains a RetainableByteBufferPool that can be configured by only two properties: retainedHeapMemory and retainedHeapMemory. This allows minimal changes to XML (see below) to support simple configuration of both types of pool.

@gregw
Copy link
Contributor Author

gregw commented Jun 20, 2022

@sbordet your thoughts?

release retained buffers when removed

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Move Bucket out of interface into abstract pool

Signed-off-by: Greg Wilkins <gregw@webtide.com>
{
return RetainableByteBufferPool.from(this);
}

public static class Lease
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbordet is Lease really the right name for this? It's kind of a ByteBufferCollection? Eitherway, can you add some javadoc for the class

@gregw gregw marked this pull request as ready for review June 20, 2022 01:46
* buffers and possibly a more efficient lookup mechanism based on the {@link org.eclipse.jetty.util.Pool} class.
* @return This pool wrapped as a RetainableByteBufferPool.
*/
default RetainableByteBufferPool asRetainableByteBufferPool()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the default implementation return a new instance each time while the AbstractByteBufferPool implementation always returns the same instance sounds like the lifecycle of the objects returned by this method is underdefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps. We can return a new instance here because it is a non allocating one - ie it is the null implementations. So would it be OK to say that if the pool returned does actually retain buffers then the pool returned must always be the same (or a wrapper of the same) retained pool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed default impl and javadoc now says must be the same instance always

_maxHeapMemory = (maxHeapMemory != 0) ? maxHeapMemory : Runtime.getRuntime().maxMemory() / 4;
_maxDirectMemory = (maxDirectMemory != 0) ? maxDirectMemory : Runtime.getRuntime().maxMemory() / 4;

if (retainedHeapMemory < 0 && retainedDirectMemory < 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the allocation of the _retainableByteBufferPool instance is done in the constructor, its release should be done in the clear method, shouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. The main byte buffer pool is keep hold of idle buffers only - ie ones that it has previously been acquired and released. The retainableByteBufferPool is kind of just like a client that has acquired but not yet released the buffer.

So clear should not release retained buffers any more than it should "take back" buffers allocated to other users. But perhaps it could trigger a shrinking of the retained pool to only buffers that are in use? Is t hat semantic already supported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lorban I think we need to review exactly what clear does on a retainable pool.... which means we need to review what remove does on a Pool. I don't think the semantic for that is correct, specially for removing active items. Also for multi items, remove might return false, but the item is actually closed so will eventually be removed?? this makes accounting on the return value of remove difficult. Needs review. So for now, let's not wire up clear.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clear contracts of both pools are slightly different, but not incompatible IMHO.

clear on BBP immediately stops holding onto its buffers.
clear on RBBP immediately stops holding onto the unused buffers it contains, and marks the ones still in use as to-be-removed-when-released.

If you clear the RBBP then clear the BBP you make RBBP give all its unused buffers back to the BBP then make BBP drop all its buffers. I think this is what we want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clear on RBBP immediately stops holding onto the unused buffers it contains, and marks the ones still in use as to-be-removed-when-released.

Yeah I think that is the intention, but it is not the implementation! So I think it needs review and javadocing before linking the clears together

*/
static RetainableByteBufferPool findOrAdapt(Container container, ByteBufferPool byteBufferPool)
{
RetainableByteBufferPool retainableByteBufferPool = container == null ? null : container.getBean(RetainableByteBufferPool.class);
if (retainableByteBufferPool == null)
return retainableByteBufferPool == null ? byteBufferPool.asRetainableByteBufferPool() : retainableByteBufferPool;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a RetainableByteBufferPool bean is added, this means there's going to be a useless ArrayRetainableByteBufferPool instance kept in the ByteBufferPool. This is going to be confusing in server dumps.

Plus, in the current implementation you return an object for whom the caller is sometimes responsible for its releasee and sometimes not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not adding it. I'm just supportin the use-case where a user has added their own with a specific configuration. If they also configure the main pool to have a retained buffers, then there is duplication.

Perhaps, since this is a major release, we don't support this backwards compatibility?

In which case, this method just become a call to asRetainableByteBufferPool and can be inlined. Maybe that is simplest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed and inlined

Updates from review.
removed findOrAdapt
removed default asRetainableByteBuffer

Signed-off-by: Greg Wilkins <gregw@webtide.com>
@gregw
Copy link
Contributor Author

gregw commented Jun 21, 2022

@lorban we need to think a bit harder about the default values. Currently we have lots of parameters that are defined as:

  • 0 use the value.

  • 0 use the default heuristic
  • -1 unlimited.

But I think we also want the ability to disable via these parameters. Ie how do you say you don't want to pool any direct memory or to not retain any heap memory.

Perhaps it would be better to have:

  • Long.MAX_VALUE for unlimited
  • 0 use the value

  • 0 for disabled (or essentially the 0 value)
  • -1 for the heuristic

We need to determine a single scheme and use it and document it in several places (all the javadoc in the constructors is currently different). Pity you can't inherit constructor javadoc!

thoughts?

@lorban
Copy link
Contributor

lorban commented Jun 21, 2022

@gregw the fact that we have two distinct pools in 10/11 is a design mistake we made. BBP/RBBP should have been better integrated from the start, like what you're suggesting.

But do we want to break backward compatibility with the 0 / -1 values? If not, we should keep 0 for heuristic, -1 for unlimited and maybe add -2 to disable the retaining feature? But do we want to disable the retaining feature, knowing that findOrAdapt() was introduced because of existing Connector constructors only accepting BBP?

This is something that should be totally cleaned up in 12. I don't mind the queue-based pool algorithm, but maybe 12 should only use the RBBP API everywhere?

@gregw
Copy link
Contributor Author

gregw commented Jun 21, 2022

@lorban I think this PR shows that having one BBP that contains a RBBP works pretty well and is the same to ThreadPool containing a ReservedThreadPool.

I think we do have some scope to clean up the -1, 0, values because they are mostly used internally... but perhaps best to leave to 12??? I'm on the fence.

I'm dubious we have the ability to remove the BBP/RBBP duality from jetty-12 entirely. I also think that this PR shows that it is not strictly necessary. The combined pool model can pretty much achieve most outcomes - ie we could configure to have a non zero RBBP, but a zero BBP if we wanted to. It's really just a matter of how much each API is used. I think we have more important things to work on rather than hunting down usages of BBP only to then find out days/weeks/months/years later that we have buffer leak as we were not handling all scenarios correctly.

@lorban
Copy link
Contributor

lorban commented Jun 21, 2022

@gregw I like the idea of the combined BBP / RBBP, it's a strong +1 but we definitely have to resolve what we want to do with the -1, 0, values as those are public, they're even in the bytebufferpool.mod file.

Regarding 12, we could keep the leak-resilient pool impl by default and still replace all BBP usages with RBBP.

@sbordet
Copy link
Contributor

sbordet commented Jun 21, 2022

I like the idea, but I think the implementations should give up using queues in favor of Pool, which we know being much faster.

jetty-server/src/main/config/etc/jetty-bytebufferpool.xml Outdated Show resolved Hide resolved
@@ -1,5 +1,6 @@
[description]
Configures the ByteBufferPool used by ServerConnectors.
Use module "bytebufferpool-logarithmic" for a more space efficient pool.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"more space efficient" is debatable. SSL requires buffers that are a few bytes over 16 KB large, meaning a log pool would waste nearly 50% of the memory it hands off in this scenario.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well more space efficient in the pool, but may be less space efficient in the buffer itself.

For the sizes we are commonly using, I'm actually thinking that just increasing the default factor to 4K is pretty good..... except for tiny buffers. Geting 1K when you only needed 6 bytes was bad enough, but 4K get's a bit ridiculous.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"less space efficient in the buffer" is indeed what my comment is about and your remark about tiny buffers is exactly my point, but it also applies to other scenarios.

When you consider that in a typical SSL scenario where the buffer pool is limited in size, configuring a logarithmic pool may very well be noticeably less efficient; i.e.: a pool configured to hold a maximum of 16 MB is only going to be able to hold at most ~500 SSL buffers when using a logarithmic divide while it could hold up to ~950 with a linear divide.

I'm just not sure if it's a real issue in practice, so I'm conservatively thinking that we should not change the default nor recommend alternatives until we have a clearer view.

jetty-server/src/main/config/modules/bytebufferpool.mod Outdated Show resolved Hide resolved
jetty-server/src/main/config/modules/bytebufferpool.mod Outdated Show resolved Hide resolved
@lorban
Copy link
Contributor

lorban commented Jun 28, 2022

I've had a look at AsyncCompletionTest.testAsyncIOWrite.[66] and it seems that a bug/poor feature in LogarithmicArrayByteBufferPool triggers a pre-existing bug in HttpOutput.write(int b). If the logic after if (!flush) is executed more than once, then the test fails.

This happens when LogarithmicArrayByteBufferPool returns a 128 bytes buffer to acquireBuffer()'s 99 bytes buffer request, and since that test tries to write over 300 bytes the buffer runs out, multiple flushes are needed and something goes wrong along the way. This does not happen with ArrayByteBufferPool as the returned buffer has a 8192 bytes capacity which is enough to reduce the flushes to a single one.

So LogarithmicArrayByteBufferPool (and its retainable counterpart) should be modified so that they don't create tiny buckets by default (buckets of 1, 2, 4, 8, 16, 32, 64... bytes are created by default) then the HttpOutput.write(int b) method that writes one single byte at a time should be investigated to figure out what's wrong with the flushes.

@gregw
Copy link
Contributor Author

gregw commented Jun 28, 2022

@lorban isn't that a bug in the unit test? Small buffets might not be desirable, but it is bigger than asked for, so the test should not assume it can write 330 bytes without blocking

Avoid double bean add.

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Updates from review

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Fixed JPMS issue

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Fixed async IO write test

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Fixed BufferedResponseHandlerTest

Signed-off-by: Greg Wilkins <gregw@webtide.com>
@gregw
Copy link
Contributor Author

gregw commented Jun 29, 2022

hmmm so we appear to have a few tests that are relying on either getting extra space in a buffer or not getting extra space in a buffer.... so changing the buffer pool algorithm and/or defaults has broken a few tests. Not good. Fixing as I find them.

Fixed more tests

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Fixed FastCGI that was expecting a larger buffer than requested

Signed-off-by: Greg Wilkins <gregw@webtide.com>
minor cleanups

Signed-off-by: Greg Wilkins <gregw@webtide.com>
@lorban
Copy link
Contributor

lorban commented Jun 29, 2022

I've been thinking that we could add some statistics collection to the pools that would record the monitored buffer sizes requested vs the ones served for each bucket. This would help figuring out how to tune the bucket sizes.

@gregw
Copy link
Contributor Author

gregw commented Jun 29, 2022

@lorban So my thinking on sizing/algorithm goes likes this:

  1. 1024 factor with max 64K linear is not great:
    • 64 buckets per sub-pool for 256 buckets in total... lot's of scope for buffers to get lost/left in seldom used buckets.
    • the 2 retained sub-pools are always fully populated, so that's 128 buckets, most of which will very rarely get used.
    • we do ask for small buffers (eg 16bytes for a HTTP/1.1 chunk), so 1024 is a lot of wasted space for that.
  2. 4096 factor with max 64K linear is better and worse:
    • 16 buckets per sub-pool for 64 in total is more manageable.
    • 32 retained buckets is better
    • but 4096 for a small buffer is much worse
  3. log buckets with min 0, max 64K gives buckets 1,2,4,8,16,32,64,128,256,512,1K,2K,4K,8K,16K,32K,64K
    • that's 17 buckets per sub-pool for 68 in total - OK
    • 34 retained buckets are always created, which sucks a little
    • small buffers are well handled.... perhaps too well
    • mid size buffers can be wasteful, as in the SSL example of 16K+1 getting a 32K buffer
  4. log buckets with min 16, max 64K gives buckets 16,32,64,128,256,512,1K,2K,4K,8K,16K,32K,64K
    • that's 13 buckets per sub-pool for 52 in total - OK
    • 26 retained buckets are always created, which sucks a little less
    • small buffers are handled OK .... perhaps too well
    • mid size buffers can be wasteful, as in the SSL example of 16K+1 getting a 32K buffer
  5. We could consider custom buckets that are non-linear and non-log. For example: 16, 1K, 8K, 16K, 17K, 32K, 64K
    • 7 buckets per sub-pool, 28 in total is good
    • only 14 retained buckets always created
    • small buffers are handled OK
    • mid size buffers can be optimised by hand
    • bucket lookup might be a little more expensive, but could be optimized with a lookup table.

Without stats, I think options 2, 3 or 4 are all OK, with a slight preference for 4. We'd need stats to say if 5 is worth it or if yet another strategy would be better.

@lorban
Copy link
Contributor

lorban commented Jun 29, 2022

@gregw with the default memory limit being HeapSize / 4 I agree that options 4 is reasonable even if SSL would be a bit wasteful.

Option 5 is IIRC how kernel slab allocation is usually done as most sizes are known at compilation time. If we had stats, we could go that route but without such guidance, I'd prefer to rely on either linear or log.

gregw added 2 commits July 1, 2022 08:28
Updates from review

Signed-off-by: Greg Wilkins <gregw@webtide.com>
Updates from review

Signed-off-by: Greg Wilkins <gregw@webtide.com>
@gregw gregw requested a review from lorban June 30, 2022 22:39
Updates from review

Signed-off-by: Greg Wilkins <gregw@webtide.com>
@gregw gregw requested a review from lorban July 2, 2022 05:12
@gregw gregw merged commit 2b817f0 into jetty-10.0.x Jul 4, 2022
@gregw gregw deleted the jetty-10.0.x-one-buffer-pool-to-rule-them-all branch July 4, 2022 00:38
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