-
Notifications
You must be signed in to change notification settings - Fork 837
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
Idle pool connections use excessive memory #1110
Comments
The original idea was to prevent multiple large queries in a row from having to each allocate new buffers. But I see how this strategy could cause large amounts of memory to be pinned. Currently, |
Right. I think there's at least two problematic scenarios:
I think that thinning the connection struct on release would be a good start. Less sure about the second point. Pooling the query builders with If the slices always had a fixed size (kind of like "pages"), it would be different, but that would be more complicated, since the query builder would have to stitch together multiple slices. |
I think this should be resolved in a457da8.
Regarding |
That works for me, thanks! I didn't think you'd be so receptive to give up slice reuse. This change will do an unnecessary allocation if the connection is never used again afterwards, and I'd prefer idle connections to not take up any heap at all. But it's pretty small, and connections are typically pooled and reused, so not a big deal unless you have hundreds of pooled connections. |
Well, it only gives up reuse for queries that have more than 64 arguments or more than 256 bytes of argument data. I believe that the vast majority of queries should use less than that. For the rarer large queries ... well, the Go GC is a lot better than it used to be. So I am optimistic that overall this change will be a win. If it does prove a problem for certain workloads we can revisit a |
Thanks. I've confirmed locally that this patch fixes the issue. |
We are currently investing excessive memory usage in our application. Some profiling leads us to
pgx.Conn
, which seems to hold onto some memory even after the connection is released back to the pool.Here is a repo with a reproducible case.
Some debugging shows that
extendedQueryBuilder
keeps some slices around that rarely get resized:We have a process that pumps ~500MB of data through a single connection. Then the connection is released, but the above logic would mean that it would subsequently take several hundred
Query()
(which callsReset()
) calls for the slices to be reallocated.From what I can tell, it is actually
*pgx.Conn
(not*pgconn.Conn
) that is pooled, so when a connection is released to the pool, it's stored with the query builder as-is, together with things like the map of prepared statements. I don't see a mechanism whereby the pool "thins" a connection once released.We're not seeing just one connection holding onto memory, either. Even with just one concurrent connection, I bet it's not the same
*pgx.Conn
that ends up being acquired every time. Therefore, we'll have several idle connections in the pool that are all containing these big slices.Testing appears to confirm this. The test case above inserts a bunch of data, and the Go heap grows to around 5GB on my my machine, which does not get reclaimed until the pool is closed. I also tried modifying
extendedQueryBuilder
to clear its slices onReset()
, and that also works.If the above is correct, I suggest two things:
extendedQueryBuilder
should be much more conservative about how much memory it keeps around. Even when using a single connection, that connection might be used for many different things: For example, we have some gRPC endpoints that stream data in and/or out, and those streams might live for hours at a time, using just a single connection.Thoughts?
Addendum: In my test case, the slices actually stick around even after all idle connections are destroyed. Only closing the pool appears to reclaims the memory. Not sure why. Edit: This is why.
The text was updated successfully, but these errors were encountered: