Skip to content

Commit bd8a1aa

Browse files
committed
Small optimizations in badger write-path key creation to reduce memory
pressure. Also, add some benchmarks for easier profiling to improve performance in the future. Signed-off-by: Michael Burman <yak@iki.fi>
1 parent fb55050 commit bd8a1aa

File tree

5 files changed

+252
-57
lines changed

5 files changed

+252
-57
lines changed

plugin/storage/badger/spanstore/cache_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ func TestExpiredItems(t *testing.T) {
6666

6767
func TestOldReads(t *testing.T) {
6868
runWithBadger(t, func(store *badger.DB, t *testing.T) {
69-
s1Key := createIndexKey(serviceNameIndexKey, []byte("service1"), time.Now(), model.TraceID{High: 0, Low: 0})
70-
s1o1Key := createIndexKey(operationNameIndexKey, []byte("service1operation1"), time.Now(), model.TraceID{High: 0, Low: 0})
69+
timeNow := model.TimeAsEpochMicroseconds(time.Now())
70+
s1Key := createIndexKey(serviceNameIndexKey, []byte("service1"), timeNow, model.TraceID{High: 0, Low: 0})
71+
s1o1Key := createIndexKey(operationNameIndexKey, []byte("service1operation1"), timeNow, model.TraceID{High: 0, Low: 0})
7172

7273
tid := time.Now().Add(1 * time.Minute)
7374

plugin/storage/badger/spanstore/read_write_test.go

+185
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import (
1919
"fmt"
2020
"io"
2121
"io/ioutil"
22+
"log"
23+
"os"
24+
"runtime/pprof"
2225
"testing"
2326
"time"
2427

@@ -251,6 +254,8 @@ func TestIndexSeeks(t *testing.T) {
251254
trs, err = sr.FindTraces(context.Background(), params)
252255
assert.NoError(t, err)
253256
assert.Equal(t, 6, len(trs))
257+
assert.Equal(t, uint64(56), trs[0].Spans[0].TraceID.Low)
258+
assert.Equal(t, uint64(51), trs[5].Spans[0].TraceID.Low)
254259
})
255260
}
256261

@@ -442,3 +447,183 @@ func runFactoryTest(tb testing.TB, test func(tb testing.TB, sw spanstore.Writer,
442447
}()
443448
test(tb, sw, sr)
444449
}
450+
451+
// Benchmarks intended for profiling
452+
453+
func writeSpans(sw spanstore.Writer, tags []model.KeyValue, services, operations []string, traces, spans int, high uint64, tid time.Time) {
454+
for i := 0; i < traces; i++ {
455+
for j := 0; j < spans; j++ {
456+
s := model.Span{
457+
TraceID: model.TraceID{
458+
Low: uint64(i),
459+
High: high,
460+
},
461+
SpanID: model.SpanID(j),
462+
OperationName: operations[j],
463+
Process: &model.Process{
464+
ServiceName: services[j],
465+
},
466+
Tags: tags,
467+
StartTime: tid.Add(time.Duration(time.Millisecond)),
468+
Duration: time.Duration(time.Millisecond * time.Duration(i+j)),
469+
}
470+
_ = sw.WriteSpan(&s)
471+
}
472+
}
473+
}
474+
475+
func BenchmarkWrites(b *testing.B) {
476+
runFactoryTest(b, func(tb testing.TB, sw spanstore.Writer, sr spanstore.Reader) {
477+
tid := time.Now()
478+
traces := 1000
479+
spans := 32
480+
tagsCount := 64
481+
tags, services, operations := makeWriteSupports(tagsCount, spans)
482+
483+
f, err := os.Create("writes.out")
484+
if err != nil {
485+
log.Fatal("could not create CPU profile: ", err)
486+
}
487+
if err := pprof.StartCPUProfile(f); err != nil {
488+
log.Fatal("could not start CPU profile: ", err)
489+
}
490+
defer pprof.StopCPUProfile()
491+
492+
b.ResetTimer()
493+
for a := 0; a < b.N; a++ {
494+
writeSpans(sw, tags, services, operations, traces, spans, uint64(0), tid)
495+
}
496+
b.StopTimer()
497+
})
498+
}
499+
500+
func makeWriteSupports(tagsCount, spans int) ([]model.KeyValue, []string, []string) {
501+
tags := make([]model.KeyValue, tagsCount)
502+
for i := 0; i < tagsCount; i++ {
503+
tags[i] = model.KeyValue{
504+
Key: fmt.Sprintf("a%d", i),
505+
VStr: fmt.Sprintf("b%d", i),
506+
}
507+
}
508+
operations := make([]string, spans)
509+
for j := 0; j < spans; j++ {
510+
operations[j] = fmt.Sprintf("operation-%d", j)
511+
}
512+
services := make([]string, spans)
513+
for i := 0; i < spans; i++ {
514+
services[i] = fmt.Sprintf("service-%d", i)
515+
}
516+
517+
return tags, services, operations
518+
}
519+
520+
func makeReadBenchmark(b *testing.B, tid time.Time, params *spanstore.TraceQueryParameters, outputFile string) {
521+
runLargeFactoryTest(b, func(tb testing.TB, sw spanstore.Writer, sr spanstore.Reader) {
522+
tid := time.Now()
523+
524+
// Total amount of traces is traces * tracesTimes
525+
traces := 1000
526+
tracesTimes := 1
527+
528+
// Total amount of spans written is traces * tracesTimes * spans
529+
spans := 32
530+
531+
// Default is 160k
532+
533+
tagsCount := 64
534+
tags, services, operations := makeWriteSupports(tagsCount, spans)
535+
536+
for h := 0; h < tracesTimes; h++ {
537+
writeSpans(sw, tags, services, operations, traces, spans, uint64(h), tid)
538+
}
539+
540+
f, err := os.Create(outputFile)
541+
if err != nil {
542+
log.Fatal("could not create CPU profile: ", err)
543+
}
544+
if err := pprof.StartCPUProfile(f); err != nil {
545+
log.Fatal("could not start CPU profile: ", err)
546+
}
547+
defer pprof.StopCPUProfile()
548+
549+
b.ResetTimer()
550+
for a := 0; a < b.N; a++ {
551+
sr.FindTraces(context.Background(), params)
552+
}
553+
b.StopTimer()
554+
})
555+
556+
}
557+
558+
func BenchmarkServiceTagsRangeQueryLimitIndexFetch(b *testing.B) {
559+
tid := time.Now()
560+
params := &spanstore.TraceQueryParameters{
561+
StartTimeMin: tid,
562+
StartTimeMax: tid.Add(time.Duration(time.Millisecond * 2000)),
563+
ServiceName: "service-1",
564+
Tags: map[string]string{
565+
"a8": "b8",
566+
},
567+
}
568+
569+
params.DurationMin = time.Duration(1 * time.Millisecond) // durationQuery takes 53% of total execution time..
570+
params.NumTraces = 50
571+
572+
makeReadBenchmark(b, tid, params, "scanrangeandindexlimit.out")
573+
}
574+
575+
func BenchmarkServiceIndexLimitFetch(b *testing.B) {
576+
tid := time.Now()
577+
params := &spanstore.TraceQueryParameters{
578+
StartTimeMin: tid,
579+
StartTimeMax: tid.Add(time.Duration(time.Millisecond * 2000)),
580+
ServiceName: "service-1",
581+
}
582+
583+
params.NumTraces = 50
584+
585+
makeReadBenchmark(b, tid, params, "serviceindexlimit.out")
586+
}
587+
588+
// Opens a badger db and runs a a test on it.
589+
func runLargeFactoryTest(tb testing.TB, test func(tb testing.TB, sw spanstore.Writer, sr spanstore.Reader)) {
590+
f := badger.NewFactory()
591+
opts := badger.NewOptions("badger")
592+
v, command := config.Viperize(opts.AddFlags)
593+
594+
dir := "/mnt/ssd/badger/testRun"
595+
err := os.MkdirAll(dir, 0700)
596+
defer os.RemoveAll(dir)
597+
assert.NoError(tb, err)
598+
keyParam := fmt.Sprintf("--badger.directory-key=%s", dir)
599+
valueParam := fmt.Sprintf("--badger.directory-value=%s", dir)
600+
601+
command.ParseFlags([]string{
602+
"--badger.ephemeral=false",
603+
"--badger.consistency=false", // Consistency is false as default to reduce effect of disk speed
604+
keyParam,
605+
valueParam,
606+
})
607+
608+
f.InitFromViper(v)
609+
610+
err = f.Initialize(metrics.NullFactory, zap.NewNop())
611+
assert.NoError(tb, err)
612+
613+
sw, err := f.CreateSpanWriter()
614+
assert.NoError(tb, err)
615+
616+
sr, err := f.CreateSpanReader()
617+
assert.NoError(tb, err)
618+
619+
defer func() {
620+
if closer, ok := sw.(io.Closer); ok {
621+
err := closer.Close()
622+
assert.NoError(tb, err)
623+
} else {
624+
tb.FailNow()
625+
}
626+
627+
}()
628+
test(tb, sw, sr)
629+
}

plugin/storage/badger/spanstore/reader.go

+24-24
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,14 @@ func (r *TraceReader) scanTimeRange(startTime time.Time, endTime time.Time) ([]*
220220
}
221221

222222
func createPrimaryKeySeekPrefix(traceID model.TraceID) []byte {
223-
buf := new(bytes.Buffer)
224-
buf.WriteByte(spanKeyPrefix)
225-
binary.Write(buf, binary.BigEndian, traceID.High)
226-
binary.Write(buf, binary.BigEndian, traceID.Low)
227-
return buf.Bytes()
223+
key := make([]byte, 1+sizeOfTraceID)
224+
key[0] = spanKeyPrefix
225+
pos := 1
226+
binary.BigEndian.PutUint64(key[pos:], traceID.High)
227+
pos += 8
228+
binary.BigEndian.PutUint64(key[pos:], traceID.Low)
229+
230+
return key
228231
}
229232

230233
// GetServices fetches the sorted service list that have not expired
@@ -378,7 +381,6 @@ func mergeJoinIds(left, right [][]byte) [][]byte {
378381
// sortMergeIds does a sort-merge join operation to the list of TraceIDs to remove duplicates
379382
func sortMergeIds(query *spanstore.TraceQueryParameters, ids [][][]byte) []model.TraceID {
380383
// Key only scan is a lot faster in the badger - use sort-merge join algorithm instead of hash join since we have the keys in sorted order already
381-
382384
var merged [][]byte
383385

384386
if len(ids) > 1 {
@@ -501,19 +503,20 @@ func (r *TraceReader) scanIndexKeys(indexKeyValue []byte, startTimeMin time.Time
501503
defer it.Close()
502504

503505
// Create starting point for sorted index scan
504-
startIndex := make([]byte, 0, len(indexKeyValue)+len(startStampBytes))
505-
startIndex = append(startIndex, indexKeyValue...)
506-
startIndex = append(startIndex, startStampBytes...)
506+
startIndex := make([]byte, len(indexKeyValue)+len(startStampBytes))
507+
copy(startIndex, indexKeyValue)
508+
copy(startIndex[len(indexKeyValue):], startStampBytes)
507509

508-
for it.Seek(startIndex); scanFunction(it, indexKeyValue, model.TimeAsEpochMicroseconds(startTimeMax)); it.Next() {
510+
timeMax := model.TimeAsEpochMicroseconds(startTimeMax)
511+
for it.Seek(startIndex); scanFunction(it, indexKeyValue, timeMax); it.Next() {
509512
item := it.Item()
510513

511514
// ScanFunction is a prefix scanning (since we could have for example service1 & service12)
512515
// Now we need to match only the exact key if we want to add it
513516
timestampStartIndex := len(it.Item().Key()) - (sizeOfTraceID + 8) // timestamp is stored with 8 bytes
514517
if bytes.Equal(indexKeyValue, it.Item().Key()[:timestampStartIndex]) {
515-
key := []byte{}
516-
key = append(key, item.Key()...) // badger reuses underlying slices so we have to copy the key
518+
key := make([]byte, len(item.Key()))
519+
copy(key, item.Key())
517520
indexResults = append(indexResults, key)
518521
}
519522
}
@@ -548,9 +551,9 @@ func (r *TraceReader) scanRangeIndex(indexStartValue []byte, indexEndValue []byt
548551
defer it.Close()
549552

550553
// Create starting point for sorted index scan
551-
startIndex := make([]byte, 0, len(indexStartValue)+len(startStampBytes))
552-
startIndex = append(startIndex, indexStartValue...)
553-
startIndex = append(startIndex, startStampBytes...)
554+
startIndex := make([]byte, len(indexStartValue)+len(startStampBytes))
555+
copy(startIndex, indexStartValue)
556+
copy(startIndex[len(indexStartValue):], startStampBytes)
554557

555558
timeIndexEnd := model.TimeAsEpochMicroseconds(startTimeMax)
556559

@@ -562,9 +565,8 @@ func (r *TraceReader) scanRangeIndex(indexStartValue []byte, indexEndValue []byt
562565
timestampStartIndex := len(it.Item().Key()) - (sizeOfTraceID + 8) // timestamp is stored with 8 bytes
563566
timestamp := binary.BigEndian.Uint64(it.Item().Key()[timestampStartIndex : timestampStartIndex+8])
564567
if timestamp <= timeIndexEnd {
565-
key := []byte{}
566-
key = item.KeyCopy(key)
567-
key = append(key, item.Key()...) // badger reuses underlying slices so we have to copy the key
568+
key := make([]byte, len(item.Key()))
569+
copy(key, item.Key())
568570
indexResults = append(indexResults, key)
569571
}
570572
}
@@ -584,10 +586,8 @@ func scanRangeFunction(it *badger.Iterator, indexEndValue []byte) bool {
584586

585587
// traceIDToComparableBytes transforms model.TraceID to BigEndian sorted []byte
586588
func traceIDToComparableBytes(traceID *model.TraceID) []byte {
587-
buf := new(bytes.Buffer)
588-
589-
binary.Write(buf, binary.BigEndian, traceID.High)
590-
binary.Write(buf, binary.BigEndian, traceID.Low)
591-
592-
return buf.Bytes()
589+
traceIDBytes := make([]byte, sizeOfTraceID)
590+
binary.BigEndian.PutUint64(traceIDBytes, traceID.High)
591+
binary.BigEndian.PutUint64(traceIDBytes[8:], traceID.Low)
592+
return traceIDBytes
593593
}

plugin/storage/badger/spanstore/rw_internal_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ func TestEncodingTypes(t *testing.T) {
6969
err := sw.WriteSpan(&testSpan)
7070
assert.NoError(t, err)
7171

72-
key, _, _ := createTraceKV(&testSpan, protoEncoding)
72+
startTime := model.TimeAsEpochMicroseconds(testSpan.StartTime)
73+
74+
key, _, _ := createTraceKV(&testSpan, protoEncoding, startTime)
7375
e := &badger.Entry{
7476
Key: key,
7577
ExpiresAt: uint64(time.Now().Add(1 * time.Hour).Unix()),

0 commit comments

Comments
 (0)