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

Maintain min_pool_size for pools with forced user (even if no client conns) #947

Merged

Conversation

fschmager
Copy link
Contributor

@fschmager fschmager commented Sep 15, 2023

Continuation/Restart of PR #437 (shoutout to @barqshasbite) to address issue #426.

The default behaviour of PGBouncer requires that clients be connected to a pool before it is initialized. There is no way to toggle this behaviour to ensure that a pool is initialized immediately by PGBouncer.

This means that min_pool_size will only be respected if clients are connected. If all clients disconnect, then the pool can drop below the min_pool_size setting to 0 (server_lifetime will eventually close all connections). This can cause performance problems when a client finally reconnects since all of the pool connections have to be recreated.

This PR changes things so that PgBouncer (creates and) maintains pools that have user connections and, this is new, for those pools that have force users.

This is changing behavior as min_pool_size will now be enforced even if there are no client connection.

src/janitor.c Outdated
Comment on lines 684 to 700
* Creating new pools to enable `min_pool_size` enforcement even if there are no active clients
* (min_pool_size_requires_clients=0).
*
* If clients never connect there won't be a pool to maintain the min_pool_size on, which means we have to
* proactively create a pool, so that it can be maintained.
*
* We are doing this only for forced users to reduce the risk of creating connections in unexpected ways, where
* there are many users. Are all database/user combos even known ahead of time?
*/
statlist_for_each_safe(item, &database_list, tmp) {
db = container_of(item, PgDatabase, head);
if (database_min_pool_size_without_clients(db) &&
database_min_pool_size(db) > 0 &&
db->forced_user != NULL) {
get_pool(db, db->forced_user);
}
}
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 believe this is the crucial part. Creating pools to maintain the min_pool_size. Pools needs a database and a user. If we don't have a user we can't create a pool.
Are all user/database combinations known or are there ways to configure users externally? Do we want that? Is doing this just for forced users sufficient and clear (unsurprising) enough?

For my use case, enabling this for forced users is sufficient.

Copy link
Member

Choose a reason for hiding this comment

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

I believe this is the crucial part. Creating pools to maintain the min_pool_size. Pools needs a database and a user. If we don't have a user we can't create a pool.

I agree.

Are all user/database combinations known or are there ways to configure users externally?

Using auth_query, users can be dynamically loaded. So no, not all combinations are known.

Do we want that? Is doing this just for forced users sufficient and clear (unsurprising) enough?

I think preferably we'd have a way to specify what pools (i.e. database+user combinations) should always be maintained. Something like this (suggests for better config name welcome):

mydb = min_pool_size=3 ensure_pools_exist_for=postgres,jelte,fschmager

We could even have that option default to the forced user if the forced user is specified. Which I think would make sense as a default. So this would already automatically create the pool (and thus maintain the min_pool_size):

mydb = min_pool_size=4 user=fschmager

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 was being conservative to maintain back-compat. The "maintain pools for forced users"-feature should be an opt-in as not to change existing behavior.

A blunt global setting to opt in might just do the trick (name up for debate):

create_pools_for_forced_users = 1

I don't know enough about how the community is using pgbounce. I just know my own use case, and for me "global opt-in + forced users == create & maintain pool" is exactly what I need. Feels like the most straightforward and least surprising path.

If we want to make it more complex:

  • Do we need an opt-out on a per-pool basis?
  • Do we need a way to maintain pools for non-forced users?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this is our increment. I add the global opt-in to maintain min_pool_size for dbs with forced users. If we see that folks want pool-specific opt-out or opt-in for non-forced users we can add that later depending on demand.

Copy link
Member

@JelteF JelteF Sep 15, 2023

Choose a reason for hiding this comment

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

I was being conservative to maintain back-compat.

Yeah I understood that, but I don't think it's worth the extra config in this case (we already have way too many of them). Automatically maintaining a pool for forced users makes a lot of sense to me. As long as you don't set min_pool_size the behaviour will be the same. And if you do set min_pool_size I think the only reasonable thing is to actually maintain that min_pool_size from startup.

I think it would be useful to specify what pools to create for non-forced users, but I don't think it's strictly necessary for this PR.

There's one thing that I think should be considered though: What happens when PgBouncer starts a little earlier than postgres and thus the janitor cannot initiate the connections. But then once postgres is started a client connects. There might be some PgBouncer timeouts in play that we should ignore in that case, to actually retry opening a connection.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if you do set min_pool_size I think the only reasonable thing is to actually maintain that min_pool_size from startup

I agree, this was exactly my assumption. I think we can improve the situation with documentation of min_pool_size. Adding the caveat that this only works for forced users, especially on the global min_pool_size end. The term "forced users" might not be widely know and might need a bit of word smithing.

I'd be more than happy to drop the setting flags.

There might be some PgBouncer timeouts in play that we should ignore in that case, to actually retry opening a connection.

Let me think out loud to explain what I understand.

no "forced pool" (for want of a better word) scenario:
PgBouncer starts up, client connects, but postgres isn't available yet. Connection timeout might kick in, error, no pool created, no pool needs to be maintained.

"forced pool" scenario:
PgBouncer starts up and tries to create backend connection for "forced pools". It will do that every 1/3sec. If postgres isn't available.
The operation fails and will get retried 1/3sec later.
Is what you are concerned about the connection-creation timeout being higher than 1/3sec and connection-creation attempts will pile up? Or is pool maintenance single threaded and waiting for a connection will just delay the next run of pool-maintenance? Or is there a problem around clients trying to get a backend connection while the maintenance is trying to create such a connection?

While we work through that to see if anything needs to change around timetouts, I'm going ahead with simplifying the code by removing the additional settings and adjusting the docs.

src/server.c Outdated Show resolved Hide resolved
test/test.ini Outdated Show resolved Hide resolved
@fschmager fschmager marked this pull request as ready for review September 15, 2023 12:44
@fschmager fschmager changed the title Add min_pool_size_requires_clients setting Maintain min_pool_size for pools with forced user (even if no client conns) Sep 15, 2023
@fschmager
Copy link
Contributor Author

fschmager commented Sep 28, 2023

@JelteF I'm struggling with the tests. The Linux (Debian/Ubuntu) CFLAGS:-O0 -g ENABLE_VALGRIND:yes check keeps failing. What is that check doing differently that might explain the failed tests?

What's the release cadence? I'd love for this to get out soon. Don't want to miss the boat.

@JelteF
Copy link
Member

JelteF commented Sep 28, 2023

Generally it slows tests down more, because it's running with valgrind (and without optimizations).

@JelteF
Copy link
Member

JelteF commented Sep 28, 2023

Release cadance is irregular, but roughly every 2-4 months.

@JelteF
Copy link
Member

JelteF commented Sep 28, 2023

I think it's because with the config used in the min_pool_size test we have many forced user connections. By setting min_pool_size globally, it starts opening connections for all of those. I think it would be best to update the test, and not set min_pool_size globally at all, but only on a specific database. Just like you're doing for your new test. i.e. replace the old min_pool_size test with a min_pool_size_without_forced_user test.

@@ -37,9 +38,32 @@ async def test_pool_size(pg, bouncer):

@pytest.mark.asyncio
async def test_min_pool_size(pg, bouncer):
bouncer.admin("set min_pool_size = 3")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test has been changed to not set the global min_pool_size anymore. Caused too many connection creations in a number of pools overloading the github test runner.
Rather this test check various combinations with individual pools.

@fschmager
Copy link
Contributor Author

I hadn't thought of the existing test setting min_pool_size globally. That was the ticket! I removed the global setting of min_pool_size to get the tests to run. We have pretty good test coverage of min_pool_size on a per-pool basis, but no test for global min_pool_size.

Copy link
Member

@JelteF JelteF left a comment

Choose a reason for hiding this comment

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

Looks good to me. I left some final suggestions on comment/docs improvements.

doc/config.md Outdated Show resolved Hide resolved
doc/config.md Outdated Show resolved Hide resolved
src/janitor.c Outdated Show resolved Hide resolved
test/test_limits.py Outdated Show resolved Hide resolved
test/test_limits.py Outdated Show resolved Hide resolved
test/test_limits.py Outdated Show resolved Hide resolved
@JelteF JelteF merged commit dd7a06f into pgbouncer:master Oct 3, 2023
7 checks passed
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.

None yet

3 participants