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

Track Postgres buffer cache usage #633

Merged
merged 3 commits into from
Nov 19, 2024
Merged

Track Postgres buffer cache usage #633

merged 3 commits into from
Nov 19, 2024

Conversation

seanlinsley
Copy link
Member

@seanlinsley seanlinsley commented Nov 13, 2024

This PR adds Postgres buffer cache tracking on a per-table/index basis.

Since querying pg_buffercache is slow and grows slower with larger buffer cache sizes, by default we will not collect buffer cache stats for a database with more than 200 GB of shared_buffers. This can be configured with the max_buffer_cache_monitoring_gb setting, which accepts an integer number of gigabytes to use as the threshold.

@seanlinsley seanlinsley requested a review from a team November 13, 2024 23:36
select {
case <-ctx.Done():
case ts.BufferCache = <-bufferCacheReady:
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Since querying pg_buffercache can be slow (~12 seconds for 200 GB of shared buffers), the query is performed in a separate goroutine so full snapshot collection can proceed without blocking on the buffer cache data, until it's needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean that there can be num_of_servers extra connections happening concurrently? Could this be a problem with the pganalyze user connection limit?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this will run an additional database connection concurrently when generating a full snapshot, so we could run into the connection limit. But I don't think that's likely to happen because by default we have a limit of 10 connections per server.

Copy link
Contributor

Choose a reason for hiding this comment

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

But I don't think that's likely to happen because by default we have a limit of 10 connections per server.

Ah true, it's per server, not per collector - yeah in that case, it's at most one additional connection per server, so it's gonna be okay 👍

I had another question around here: you say "until it's needed", and it's true - if the full snapshot finishes running (outputting buffer cache) before this ts.BufferCache = <-bufferCacheReady, it'll output the data of 10 mins ago, correct? IOW, full snapshot with a timely buffer cache info can be obtained only when this goroutine finishes before the "outputting buffer cache" part is done.
I think it's better than making a full snapshot runs longer (so I'm not opposed to it), but just wanted to double check if my understanding is correct.

Copy link
Member Author

Choose a reason for hiding this comment

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

if the full snapshot finishes running (outputting buffer cache) before this ts.BufferCache = <-bufferCacheReady, it'll output the data of 10 mins ago, correct?

No, the code is blocking on the message from that channel. It will wait for the new buffer cache stats to be available. For our production server that has 198 GB of shared buffers, querying pg_buffercache takes 12 seconds. I think that's short enough to not be an issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we log the collection duration at Verbose level? Users could check pg_stat_statements instead, but it might be convenient to have this here for debugging.

FROM pg_buffercache
GROUP BY 1, 2`

func GetBufferCache(ctx context.Context, server *state.Server, globalCollectionOpts state.CollectionOpts, logger *util.Logger, postgresVersion state.PostgresVersion, channel chan state.BufferCache) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Currently this doesn't perform a Postgres version check or include a way to disable the functionality. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

You could technically disable by setting MaxBufferCacheMonitoringGB to zero. Re: version check, what's in your mind?

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe Lukas had mentioned that we won't want to run this for some older Postgres versions, though I don't know the details.

Copy link
Contributor

Choose a reason for hiding this comment

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

Checking the release notes for all the X.0 major versions we support (I don't think a performance improvement like this would be released in a minor version), I don't see anything about pg_buffercache except in 16, with the summary function. Going through the Postgres commit history, I see

commit 6e654546fb61f62cc982d0c8f62241b3b30e7ef8
Author: Heikki Linnakangas <[email protected]>
Date:   Thu Sep 29 13:16:30 2016 +0300

    Don't bother to lock bufmgr partitions in pg_buffercache.
    
    That makes the view a lot less disruptive to use on a production system.
    Without the locks, you don't get a consistent snapshot across all buffers,
    but that's OK. It wasn't a very useful guarantee in practice.
    
    Ivan Kartyshov, reviewed by Tomas Vondra and Robert Haas.
    
    Discusssion: <[email protected]>

which I think might be what Lukas was talking about (I recall locking being mentioned). Running git tag --contains 6e654546fb61f62cc982d0c8f62241b3b30e7ef8 confirms this, showing all versions back to 10.0 including this. Going back one more version in the release notes and checking the release notes for 10 backs that up, noting the reduced locking requirement. So I don't think we need a version check, since we no longer support any versions that would have performance problems.

I agree with Keiko that setting the max to zero is probably sufficient to disable the feature.

output/transform/merge_partition_sizes.go Outdated Show resolved Hide resolved
untrackedBytes += bytes
continue
}
s.DatabaseStatictics[databaseIdx].UntrackedCacheBytes = untrackedBytes
Copy link
Member Author

@seanlinsley seanlinsley Nov 13, 2024

Choose a reason for hiding this comment

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

This allows us to track buffer cache usage from untracked databases and tables.

Note: input/postgres/relations.go calls bufferCache[dataFilenode] = 0 to zero out buffer cache entries, so that the code here only tracks buffer usage not associated with a known table/index.

@@ -212,6 +212,10 @@ type ServerConfig struct {
// once the server is promoted
SkipIfReplica bool `ini:"skip_if_replica"`

// The maximum shared_buffers size in gigabytes that the collector will monitor
// pg_buffercache. Defaults to 200 GB.
MaxBufferCacheMonitoringGB int `ini:"max_buffer_cache_monitoring_gb"`
Copy link
Member Author

Choose a reason for hiding this comment

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

Thoughts on the naming of this setting? It's difficult to find a name that's descriptive without being too long.

Copy link
Contributor

Choose a reason for hiding this comment

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

It reads okay to me

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, seems okay.

@@ -212,6 +212,10 @@ type ServerConfig struct {
// once the server is promoted
SkipIfReplica bool `ini:"skip_if_replica"`

// The maximum shared_buffers size in gigabytes that the collector will monitor
// pg_buffercache. Defaults to 200 GB.
MaxBufferCacheMonitoringGB int `ini:"max_buffer_cache_monitoring_gb"`
Copy link
Contributor

Choose a reason for hiding this comment

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

It reads okay to me

FROM pg_buffercache
GROUP BY 1, 2`

func GetBufferCache(ctx context.Context, server *state.Server, globalCollectionOpts state.CollectionOpts, logger *util.Logger, postgresVersion state.PostgresVersion, channel chan state.BufferCache) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You could technically disable by setting MaxBufferCacheMonitoringGB to zero. Re: version check, what's in your mind?

select {
case <-ctx.Done():
case ts.BufferCache = <-bufferCacheReady:
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean that there can be num_of_servers extra connections happening concurrently? Could this be a problem with the pganalyze user connection limit?

input/postgres/relation_stats.go Outdated Show resolved Hide resolved
row.CachedDataBytes = bufferCache[dataFilenode]
row.CachedToastBytes = bufferCache[toastFilenode]
bufferCache[dataFilenode] = 0
bufferCache[toastFilenode] = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not understanding the logic here well for why you're setting zero here (and below). Can you please explain?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe that comment should be here instead? And a code comment rather than just a PR comment? I like the approach, but I, too, was confused on first reading this.

output/transform/postgres_relations.go Outdated Show resolved Hide resolved
@@ -212,6 +212,10 @@ type ServerConfig struct {
// once the server is promoted
SkipIfReplica bool `ini:"skip_if_replica"`

// The maximum shared_buffers size in gigabytes that the collector will monitor
// pg_buffercache. Defaults to 200 GB.
MaxBufferCacheMonitoringGB int `ini:"max_buffer_cache_monitoring_gb"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, seems okay.

select {
case <-ctx.Done():
case ts.BufferCache = <-bufferCacheReady:
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we log the collection duration at Verbose level? Users could check pg_stat_statements instead, but it might be convenient to have this here for debugging.

sizeGB := 0
db.QueryRowContext(ctx, QueryMarkerSQL+bufferCacheSizeSQL).Scan(&sizeGB)
if sizeGB > server.Config.MaxBufferCacheMonitoringGB {
logger.PrintWarning("GetBufferCache: skipping collection. To enable, set max_buffer_cache_monitoring_gb to a value over %d", sizeGB)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want this printed for every full snapshot? I think there are legitimate cases where you want the extension to be present (for manual checks), but don't want the performance hit. Maybe we should only log this for a test run?

FROM pg_buffercache
GROUP BY 1, 2`

func GetBufferCache(ctx context.Context, server *state.Server, globalCollectionOpts state.CollectionOpts, logger *util.Logger, postgresVersion state.PostgresVersion, channel chan state.BufferCache) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Checking the release notes for all the X.0 major versions we support (I don't think a performance improvement like this would be released in a minor version), I don't see anything about pg_buffercache except in 16, with the summary function. Going through the Postgres commit history, I see

commit 6e654546fb61f62cc982d0c8f62241b3b30e7ef8
Author: Heikki Linnakangas <[email protected]>
Date:   Thu Sep 29 13:16:30 2016 +0300

    Don't bother to lock bufmgr partitions in pg_buffercache.
    
    That makes the view a lot less disruptive to use on a production system.
    Without the locks, you don't get a consistent snapshot across all buffers,
    but that's OK. It wasn't a very useful guarantee in practice.
    
    Ivan Kartyshov, reviewed by Tomas Vondra and Robert Haas.
    
    Discusssion: <[email protected]>

which I think might be what Lukas was talking about (I recall locking being mentioned). Running git tag --contains 6e654546fb61f62cc982d0c8f62241b3b30e7ef8 confirms this, showing all versions back to 10.0 including this. Going back one more version in the release notes and checking the release notes for 10 backs that up, noting the reduced locking requirement. So I don't think we need a version check, since we no longer support any versions that would have performance problems.

I agree with Keiko that setting the max to zero is probably sufficient to disable the feature.

// See also https://www.postgresql.org/docs/current/pgbuffercache.html
const bufferCacheSQL string = `
SELECT reldatabase, relfilenode, count(*) * current_setting('block_size')::int
FROM pg_buffercache
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be installed in a schema other than public, or a schema that's not in the search path.

Copy link
Contributor

Choose a reason for hiding this comment

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

Separately, here and in other SQL in this PR, we should schema-qualify references to system tables and functions with pg_catalog, like we do elsewhere.

Comment on lines 39 to 41
COALESCE(toast.relname, '') AS toast_table,
coalesce(pg_relation_filenode(c.oid), 0) AS data_filenode,
coalesce(pg_relation_filenode(c.reltoastrelid), 0) AS toast_filenode
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
COALESCE(toast.relname, '') AS toast_table,
coalesce(pg_relation_filenode(c.oid), 0) AS data_filenode,
coalesce(pg_relation_filenode(c.reltoastrelid), 0) AS toast_filenode
COALESCE(toast.relname, '') AS toast_table,
COALESCE(pg_relation_filenode(c.oid), 0) AS data_filenode,
COALESCE(pg_relation_filenode(c.reltoastrelid), 0) AS toast_filenode

Or the other way around (assuming you change other occurrences here). I don't especially care, but the inconsistency hurts readability.

row.CachedDataBytes = bufferCache[dataFilenode]
row.CachedToastBytes = bufferCache[toastFilenode]
bufferCache[dataFilenode] = 0
bufferCache[toastFilenode] = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe that comment should be here instead? And a code comment rather than just a PR comment? I like the approach, but I, too, was confused on first reading this.

GROUP BY 1, 2`
const bufferCacheExtensionSQL string = `
SELECT COALESCE((
SELECT nspname
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably be calling quote_literal(nspname) here, to make sure we support extensions that are installed in schemas that require quoting (e.g., "this schema has spaces"), but I noticed that our handling of the pg_stat_statements schema doesn't do this either, and I don't think we've heard complaints, so we can leave this and fix both in a follow-up patch instead.

Copy link
Contributor

@msakrejda msakrejda left a comment

Choose a reason for hiding this comment

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

Great, looks good to me.

Copy link
Contributor

@keiko713 keiko713 left a comment

Choose a reason for hiding this comment

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

Looks like Maciek dropped a few more feedback but otherwise, looks good to me too!
(also nice reviews, Maciek. +1 on all points you made)

@seanlinsley seanlinsley merged commit 867e346 into main Nov 19, 2024
3 checks passed
@seanlinsley seanlinsley deleted the 4388-buffer-cache branch November 19, 2024 18:56
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.

3 participants