-
Notifications
You must be signed in to change notification settings - Fork 454
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
[dbnode] Concurrent time series indexing within a single batch #2146
Conversation
) | ||
|
||
var ( | ||
errDocNotFound = errors.New("doc not found") | ||
) | ||
|
||
const ( | ||
// Slightly buffer the work to avoid blocking main thread. | ||
indexQueueSize = 2 << 7 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, yeah perhaps we even increase this to 1024?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Also I think I've seen the actual number usually present as a comment, i.e. indexQueueSize = 2 << 7 // 128
NoFinalizeKey: true, | ||
}) | ||
} | ||
b.Unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We actually should be ok to avoid locking here yeah if we're sharding by name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I thought that the go runtime would complain about concurrent map access even if we were accessing diff keys. Lemme try w/o.
job.batchErr.AddWithLock(index.BatchError{Err: err, Idx: job.idx}) | ||
} | ||
if newField { | ||
b.Lock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guess can't avoid this lock however since unique fields are shared. We could always create a uniqueFields per worker though and iterate over a list of lists possible in the NewOrderedBytesSliceIter
iterator (would need a special implementation of sort.Interface to handle the fact it's a slice of slices).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added sharding for both unique fields and fields map. Removed all locks w/in the index worker.
e78beca
to
619d427
Compare
Codecov Report
@@ Coverage Diff @@
## master #2146 +/- ##
========================================
+ Coverage 71.4% 72.2% +0.8%
========================================
Files 1018 1019 +1
Lines 88346 88430 +84
========================================
+ Hits 63085 63890 +805
+ Misses 20940 20245 -695
+ Partials 4321 4295 -26
Continue to review full report at Codecov.
|
bbbc8c6
to
f61d08c
Compare
src/m3ninx/index/batch.go
Outdated
e.Lock() | ||
defer e.Unlock() | ||
if err.Err == nil { | ||
return | ||
} | ||
e.errs = append(e.errs, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can probably be a little bit more efficient as:
if err.Err == nil {
return
}
e.Lock()
e.errs = append(e.errs, err)
e.Unlock()
Also, seems to only be used in one place; might be more straightforward to do the locking there rather than adding a mutex on the struct? Take it or leave it though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just moved the locking code around.
@@ -78,6 +78,7 @@ var ( | |||
|
|||
func TestBuilderFields(t *testing.T) { | |||
builder, err := NewBuilderFromDocuments(testOptions) | |||
defer builder.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a test, instead of deferring close here, maybe add require.NoError(t, builder.Close())
at the end of the test?
@@ -105,6 +106,7 @@ func TestBuilderFields(t *testing.T) { | |||
|
|||
func TestBuilderTerms(t *testing.T) { | |||
builder, err := NewBuilderFromDocuments(testOptions) | |||
defer builder.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a test, instead of deferring close here, maybe add require.NoError(t, builder.Close())
at the end of the test?
data []*fieldsMap | ||
} | ||
|
||
func newShardedFieldsMap( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: may be a good idea to add sanity checks for shardInitialCapacity
and numShards
t, found := fieldMap.Get(k) | ||
if found { | ||
return t, found | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe more go-ish as
if t, found := fieldMap.Get(k); found {
return t, true
}
s.data[shard].SetUnsafe(k, v, opts) | ||
} | ||
|
||
// ResetTerms keeps fields around but resets the terms set for each one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: mismatch between comment and func name
@@ -25,29 +25,32 @@ import ( | |||
"sort" | |||
|
|||
"github.com/m3db/m3/src/m3ninx/index/segment" | |||
|
|||
"github.com/twotwotwo/sorts" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is BSD3, pretty sure that has Apache2 compat, but just looking to doublecheck? (Also, think it may need to be added to our glide file?)
edit: we're good
@@ -86,6 +90,51 @@ func (b *OrderedBytesSliceIter) Close() error { | |||
return nil | |||
} | |||
|
|||
type sortableSliceOfSliceOfByteSlices struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: think we typically postpend this with the direction (Asc
or Desc
)
func NewBuilderFromDocuments(opts Options) (segment.DocumentsBuilder, error) { | ||
return &builder{ | ||
func NewBuilderFromDocuments(opts Options) (segment.CloseableDocumentsBuilder, error) { | ||
concurrency := runtime.NumCPU() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this come from Options
and default to runtime.NumCPU()
?
if shardInitialCapcity > 0 { | ||
shardInitialCapcity /= concurrency | ||
} | ||
shardUniqueFields := make([][]byte, 0, shardInitialCapcity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be more performant if you allocate the entire [][]byte
as a full block, then index into it; i.e.
shardUniqueFields := make([][]byte, 0, concurrency * shardInitialCapcity)
for i := 0; i < concurrency; i++ {
//...
b.uniqueFields = append(b.uniqueFields, shardUniqueFields[i*concurrency:(i+1)*concurrency])
// ...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't be able to grow a shard in this case. Or it would be difficult to do so.
} | ||
shardUniqueFields := make([][]byte, 0, shardInitialCapcity) | ||
b.uniqueFields = append(b.uniqueFields, shardUniqueFields) | ||
b.fields = newShardedFieldsMap(concurrency, shardInitialCapcity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be possible to bulk-allocate these similarly to b.uniqueFields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as before. Growing a shard would be very painful.
job.batchErr.AddWithLock(index.BatchError{Err: err, Idx: job.idx}) | ||
} | ||
if newField { | ||
b.uniqueFields[job.shard] = append(b.uniqueFields[job.shard], job.field.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this happen if the post failed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good call, moving around the logic here.
if newField { | ||
b.uniqueFields[job.shard] = append(b.uniqueFields[job.shard], job.field.Name) | ||
} | ||
b.wg.Done() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be misreading this, but it looks like there's a disconnect between the wg.Add and the wg.Done, where a single wait group may be used by multiple inserts and it's not clear that they complete on time? Maybe rather than adding a wait group on builder
, may be better to explicitly init one and pass it into index
and add it to the indexJob
?
569a4fb
to
86cfdd5
Compare
src/dbnode/storage/index.go
Outdated
@@ -748,6 +748,7 @@ func (i *nsIndex) Flush( | |||
|
|||
builderOpts := i.opts.IndexOptions().SegmentBuilderOptions() | |||
builder, err := builder.NewBuilderFromDocuments(builderOpts) | |||
defer builder.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to move this after the err != nil { return err }
since if err != nil
then builder
will be nil and calling builder.Close()
will cause a nil ptr panic.
src/dbnode/storage/index/block.go
Outdated
}) | ||
// Free segment builder resources. | ||
if b.compact.segmentBuilder != nil { | ||
b.compact.segmentBuilder.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to check the error returned from segment builder too here and emit an invariant error if cannot close.
|
||
"github.com/m3db/m3/src/m3ninx/doc" | ||
"github.com/m3db/m3/src/m3ninx/index" | ||
"github.com/m3db/m3/src/m3ninx/index/segment" | ||
"github.com/m3db/m3/src/m3ninx/postings" | ||
"github.com/m3db/m3/src/m3ninx/util" | ||
"go.uber.org/atomic" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This should be grouped next to other third party imports such as "github.com/cespare/xxhash"
|
||
func (s *shardedFieldsMap) Get(k []byte) (*terms, bool) { | ||
for _, fieldMap := range s.data { | ||
if t, found := fieldMap.Get(k); found { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, can we perhaps just hash the k
to work out which map to find?
Otherwise we have to do 32 map accesses (on 32 core machine) vs checking just 1 map. This is done for every single field during a compaction to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or just make the Get(k)
take a shard too, i.e. Get(shard int, k []byte)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I already have a ShardedGet
method. The (b *builder) Terms(field []byte)
call doesn't already have shard
pre-computed so I added this regular Get
. But you are right it looks like it's cheaper to compute the hash vs multiple map accesses.
Here are some benchmark results:
notbdu @ Bos-MacBook-Pro.local m3 (bdu/concurrent-index) $ go test -v -bench . ./bench_test.go
goos: darwin
goarch: amd64
BenchmarkMapAccess-8 20000000 83.9 ns/op
BenchmarkHashing-8 200000000 6.21 ns/op
6f350ea
to
77ffa29
Compare
@@ -751,6 +751,7 @@ func (i *nsIndex) Flush( | |||
if err != nil { | |||
return err | |||
} | |||
defer builder.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch.
go b.indexWorker(indexQueue) | ||
|
||
// Give each shard a fraction of the configured initial capacity. | ||
shardInitialCapcity := opts.InitialCapacity() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shardInitialCapcity
should be shardInitialCapacity
?
@@ -236,7 +298,9 @@ func (b *builder) Fields() (segment.FieldsIterator, error) { | |||
} | |||
|
|||
func (b *builder) Terms(field []byte) (segment.TermsIterator, error) { | |||
terms, ok := b.fields.Get(field) | |||
// NB(bodu): The # of indexQueues and field map shards are equal. | |||
shard := int(xxhash.Sum64(field) % uint64(len(b.indexQueues))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this a method on the builder to reuse this code? I see it replicated on this line and line 230 i.e.
func (b *builder) shardForField(field []byte) {
return int(xxhash.Sum64(field) % uint64(len(b.indexQueues)))
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM other than remaining comments
Co-Authored-By: Rob Skillington <[email protected]>
What this PR does / why we need it:
Improves indexing perf.
Special notes for your reviewer:
Does this PR introduce a user-facing and/or backwards incompatible change?:
Does this PR require updating code package or user-facing documentation?: