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

chore(metastore): Update metastores after Flushing a dataobj #15883

Merged
merged 34 commits into from
Jan 28, 2025

Conversation

benclive
Copy link
Contributor

What this PR does / why we need it:

  • Adds support for building and updating a metastore component after pushing.
  • Relies on a fork of thanos-io/objstore which introduces a GetAndReplace method - this does the conditional writes and will fail if the underlying object changes before the write is processed.
  • I added a few more metrics so I can see how this performs over time. I'm currently using a 12 hour window per metastore file but I'm not sure if this is going to get too big and slow down the update process over time.

Which issue(s) this PR fixes:
Fixes #

Special notes for your reviewer:

Checklist

  • Reviewed the CONTRIBUTING.md guide (required)
  • Documentation added
  • Tests updated
  • Title matches the required conventional commits format, see here
    • Note that Promtail is considered to be feature complete, and future development for logs collection will be in Grafana Alloy. As such, feat PRs are unlikely to be accepted unless a case can be made for the feature actually being a bug fix to existing behavior.
  • Changes that require user attention or interaction to upgrade are documented in docs/sources/setup/upgrade/_index.md
  • If the change is deprecating or removing a configuration option, update the deprecated-config.yaml and deleted-config.yaml files respectively in the tools/deprecated-config-checker directory. Example PR

@benclive benclive requested a review from a team as a code owner January 22, 2025 16:47
@@ -181,6 +183,45 @@ func NewBuilder(cfg BuilderConfig, bucket objstore.Bucket, tenantID string) (*Bu
}, nil
}

// FromExisting updates this builder with content from an existing data object, replicating all the state like stream IDs and logs.
func (b *Builder) FromExisting(f io.ReadSeeker) error {
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'm not a massive fan of this method, and I think it could be made more efficient. I want to test it out before committing to any improvements here though.

@benclive benclive force-pushed the dataobj-comsumer-metastore branch from 772f4b1 to 1800951 Compare January 22, 2025 16:51
@rfratto rfratto force-pushed the dataobj-consumer branch 2 times, most recently from ad68a80 to 4961d28 Compare January 22, 2025 20:57
@rfratto rfratto force-pushed the dataobj-comsumer-metastore branch from 1800951 to be09cfb Compare January 22, 2025 21:07
Copy link
Member

@rfratto rfratto left a comment

Choose a reason for hiding this comment

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

🎉

I'm not sure about this approach long-term (data objects aren't very friendly to appends after they've already been written), but I don't want to block this moving forward.

I left a few small comments about package/API design. Not anything that needs to get addressed right this moment in the prototyping phase, though, but probably something that should be addressed prior to merging into main.

}
b.state = builderStateFlush
func (b *Builder) Flush(ctx context.Context) (string, error) {
_, err := b.FlushToBuffer()
Copy link
Member

Choose a reason for hiding this comment

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

By the way, this changes the behaviour of Flush where calling Flush immediately after a successful flush will cause it to re-write the same object.

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've moved Reset back into this method now I'm returning the FlushResult summary. Does that fix the issue?

Comment on lines 179 to 194
err = p.writeMetastores(backoff, dataobjPath)
if err != nil {
level.Error(p.logger).Log("msg", "failed to write metastores", "err", err)
return
}

// Reset builder after flushing & storing in metastore
p.builder.Reset()
Copy link
Member

@rfratto rfratto Jan 22, 2025

Choose a reason for hiding this comment

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

It seems like we made a few changes where logic gets moved to the processor:

  1. Resetting the state after a flush
  2. Digging into the state of the builder before writing the metastore index
  3. Splitting out flushing to object storage and flushing to a buffer

I think we can allow the metastore builder to have access to everything it needs without changing any of the above by returning some kind of report/summary on a successful call to Flush:

package dataobj 

// Flush flushes all buffered data to object storage. Calling Flush can result
// in a no-op if there is no buffered data to flush.
//
// If Flush builds an object but fails to upload it to object storage, the
// built object is cached and can be retried. [Builder.Reset] can be called to
// discard any pending data and allow new data to be appended.
//
// On a successful flush, a summary describing what was flushed is included. 
func (b *Builder) Flush() (Summary, error)  

// A Summary summarizes the data included in a flush from a [Builder]. 
type Summary struct {
  ObjectPath string   // Object storage path that was flushed to.
  Streams    []Stream // Streams included in the flush. 
}

// A Stream is an individual stream within a data object.
type Stream struct {
  // (Copy or subset of streams.Stream to avoid exposing internal API in an external package) 
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, this is a nice idea, thank you! This logic evolved from trying to figure out what I needed from the dataobj so I never stepped back to figure out a nicer way to organise it. I'll give this a go!

@@ -184,3 +202,78 @@ func (p *partitionProcessor) processRecord(record *kgo.Record) {
}
}
}

func (p *partitionProcessor) writeMetastores(backoff *backoff.Backoff, dataobjPath string) error {
Copy link
Member

Choose a reason for hiding this comment

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

Should we have some kind of dataobj/metastore package that's responsible for building and operating on metastores?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes.

@benclive benclive force-pushed the dataobj-comsumer-metastore branch from be09cfb to 1b4633a Compare January 24, 2025 10:49
}
b.state = builderStateFlush
func (b *Builder) Flush(ctx context.Context) (string, error) {
_, err := b.FlushToBuffer()
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've moved Reset back into this method now I'm returning the FlushResult summary. Does that fix the issue?

s.Rows = 0
}

var streamPool = sync.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.

this pool added about 10% more ops in the benchmark when re-using dataobjs between metastores. In reality we won't be reusing Streams very often (once per flush), so it might not be worth keeping this pool as the Stream objects will likely be deallocated between runs.
WDYT?

@@ -61,10 +78,36 @@ func New(metrics *Metrics, pageSize int) *Streams {
return &Streams{
metrics: metrics,
pageSize: pageSize,
lookup: make(map[uint64][]*Stream),
lookup: make(map[uint64][]*Stream, 1024),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This yielded a 10-20% speed up over the baseline. We're not likely to have this many Streams in a metastore but it would be worth it for the logs dataobj, I think.

@benclive benclive changed the title Dataobj comsumer metastore chore(metastore): Update metastores after Flushing a dataobj Jan 27, 2025
@benclive benclive merged commit 6a3c455 into dataobj-consumer Jan 28, 2025
43 of 59 checks passed
@benclive benclive deleted the dataobj-comsumer-metastore branch January 28, 2025 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants