Skip to content

Resolve Parquet shard count via bucket index to optimize storage calls#7648

Open
SungJin1212 wants to merge 5 commits into
cortexproject:masterfrom
SungJin1212:parquet-shard-count-from-bucket-index
Open

Resolve Parquet shard count via bucket index to optimize storage calls#7648
SungJin1212 wants to merge 5 commits into
cortexproject:masterfrom
SungJin1212:parquet-shard-count-from-bucket-index

Conversation

@SungJin1212

Copy link
Copy Markdown
Member

What this PR does:
This PR updates the Parquet shard resolution logic to utilize the bucket index, reducing the number of object storage calls.

Which issue(s) this PR fixes:
Fixes #

Checklist

  • Tests updated
  • Documentation added
  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]
  • docs/configuration/v1-guarantees.md updated if this PR introduces experimental flags

shardCounts := make(map[string]int, len(blockIDs))

if p.bucketIndexEnabled {
idx, err := bucketindex.ReadIndex(ctx, p.indexBucket, p.userID, p.limits, p.logger)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be cached with some TTL instead? Or we would rather have a separate goroutine to sync bucket index periodically rather than resolving it at query time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we use bucketindex.Loader it has built-in caching

@SungJin1212 SungJin1212 force-pushed the parquet-shard-count-from-bucket-index branch 2 times, most recently from 6656f19 to 2f7ecc9 Compare June 29, 2026 02:21
@SungJin1212

Copy link
Copy Markdown
Member Author

@yeya24
I adapted bucketindex.Loader to the parquet store gateway.

  • In InitialSync, I start the indexLoader.
  • Its metrics are tagged with component="store-gateway" so they don't collide with the querier's loader in single-binary mode.

@yeya24

yeya24 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

I wonder how this bucket index integration works with the Store gateway hybrid mode mentioned in #7140.

Do we imagine the hybrid mode to be as part of TSDB bucket store, or Parquet bucket store or a new federated store.

@SungJin1212

SungJin1212 commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

@yeya24
I haven't thought about hybrid mode deeply yet.. but my preference would be a new federated store that holds both the TSDB and parquet bucket stores and splits blocks using the bucket index.

@SungJin1212

Copy link
Copy Markdown
Member Author

@yeya24
How about extending the Parquet bucket store into hybrid mode, since the current Parquet SG has a query hole?

@SungJin1212 SungJin1212 requested a review from yeya24 July 1, 2026 01:47
…e calls

Signed-off-by: SungJin1212 <tjdwls1201@gmail.com>
Signed-off-by: SungJin1212 <tjdwls1201@gmail.com>
Signed-off-by: SungJin1212 <tjdwls1201@gmail.com>
@SungJin1212 SungJin1212 force-pushed the parquet-shard-count-from-bucket-index branch from 2f7ecc9 to 9bddc94 Compare July 7, 2026 02:12
}
shardCounts[b.ID.String()] = numShards
}
return shardCounts, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we try to cache the shard counts result? Because every time we get the whole bucket index and iterate all its blocks to populate shard map. Even though the bucket index is cached, re-populating the map seems wasted. Especially if we only try to query few block IDs among the bucket index

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agree, I addressed this by memoizing the shard count map instead of rebuilding it on every request.


// Read converter marks and expand to per-shard (blockID, shardID) lists.
// TODO(Sungjin1212): Read the shard count from the bucket index instead of reading the converter mark for each block.
shardCounts, err := p.resolveShardCounts(ctx, blockIDs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall I think this is fine... But for most of the usecases we only have 1 shard for the parquet file.

Signed-off-by: SungJin1212 <tjdwls1201@gmail.com>
@SungJin1212 SungJin1212 force-pushed the parquet-shard-count-from-bucket-index branch from 4986a1a to f6dbed1 Compare July 8, 2026 07:34

@friedrichg friedrichg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

}
numShards := b.Parquet.Shards
if numShards <= 0 {
numShards = 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Imagine having 2 shards in a block, but the bucket index doesn't have it yet. That means we get 1 shard only.

I think, for cases where query_store_after is configured as zero, the default, this would cause partial queries in bucket index blocks that don't have shards in them. But then again who is using query_store_after with zero?

I think we should definitely change the default for query_store_after to be bigger than 0 and discourage very low query store after in general. Not good performance.

Then again, what about backfilling up historical blocks to object storage. Maybe we should fall back to the following instead of using 1

func (p *parquetBucketStore) shardCountFromConverterMark(ctx context.Context, blockID string) (int, error) {
	uid, err := ulid.Parse(blockID)
	if err != nil {
		return 0, errors.Wrapf(err, "failed to parse block ID %s", blockID)
	}
	marker, err := cortex_parquet.ReadConverterMark(ctx, uid, p.bucket, p.logger)
	if err != nil {
		return 0, errors.Wrapf(err, "failed to read converter mark for block %s", blockID)
	}
	return marker.Shards, nil
}

We can definitely do this in a follow up PR

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Then again, what about backfilling up historical blocks to object storage. Maybe we should fall back to the following instead of using 1

Agree, we need to read the block's converter mark directly on a map miss.

I think we should definitely change the default for query_store_after to be bigger than 0 and discourage very low query store after in general. Not good performance.

Agree on discouraging very low values for performance, but query_store_after is coupled with the query_ingesters_within. So bumping the query_store_after default would also require bumping query_ingesters_within, which is a broader change affecting all users. I'd keep that as a separate discussion.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 8, 2026
…x is stale

Signed-off-by: SungJin1212 <tjdwls1201@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/store-gateway lgtm This PR has been approved by a maintainer size/L type/performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants