Files
zapdb/badger/cmd/write_bench.go

515 lines
13 KiB
Go

/*
* Copyright 2019 Dgraph Labs, Inc. and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"math"
"math/rand"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/spf13/cobra"
"github.com/dgraph-io/badger/v3"
"github.com/dgraph-io/badger/v3/options"
"github.com/dgraph-io/badger/v3/pb"
"github.com/dgraph-io/badger/v3/y"
"github.com/dgraph-io/ristretto/z"
)
var writeBenchCmd = &cobra.Command{
Use: "write",
Short: "Writes random data to Badger to benchmark write speed.",
Long: `
This command writes random data to Badger to benchmark write speed. Useful for testing and
performance analysis.
`,
RunE: writeBench,
}
var (
wo = struct {
keySz int
valSz int
numKeys float64
syncWrites bool
force bool
sorted bool
showLogs bool
valueThreshold int64
numVersions int
vlogMaxEntries uint32
loadBloomsOnOpen bool
detectConflicts bool
zstdComp bool
showDir bool
ttlDuration string
encryptionKey string
showKeysCount bool
blockCacheSize int64
indexCacheSize int64
dropAllPeriod string
dropPrefixPeriod string
gcPeriod string
gcDiscardRatio float64
}{}
sizeWritten uint64
gcSuccess uint64
sstCount uint32
vlogCount uint32
files []string
entriesWritten uint64
)
const (
mil float64 = 1e6
)
func init() {
benchCmd.AddCommand(writeBenchCmd)
writeBenchCmd.Flags().IntVarP(&wo.keySz, "key-size", "k", 32, "Size of key")
writeBenchCmd.Flags().IntVar(&wo.valSz, "val-size", 128, "Size of value")
writeBenchCmd.Flags().Float64VarP(&wo.numKeys, "keys-mil", "m", 10.0,
"Number of keys to add in millions")
writeBenchCmd.Flags().BoolVar(&wo.syncWrites, "sync", false,
"If true, sync writes to disk.")
writeBenchCmd.Flags().BoolVarP(&wo.force, "force-compact", "f", true,
"Force compact level 0 on close.")
writeBenchCmd.Flags().BoolVarP(&wo.sorted, "sorted", "s", false, "Write keys in sorted order.")
writeBenchCmd.Flags().BoolVarP(&wo.showLogs, "verbose", "v", false, "Show Badger logs.")
writeBenchCmd.Flags().Int64VarP(&wo.valueThreshold, "value-th", "t", 1<<10, "Value threshold")
writeBenchCmd.Flags().IntVarP(&wo.numVersions, "num-version", "n", 1, "Number of versions to keep")
writeBenchCmd.Flags().Int64Var(&wo.blockCacheSize, "block-cache-mb", 256,
"Size of block cache in MB")
writeBenchCmd.Flags().Int64Var(&wo.indexCacheSize, "index-cache-mb", 0,
"Size of index cache in MB.")
writeBenchCmd.Flags().Uint32Var(&wo.vlogMaxEntries, "vlog-maxe", 1000000, "Value log Max Entries")
writeBenchCmd.Flags().StringVarP(&wo.encryptionKey, "encryption-key", "e", "",
"If it is true, badger will encrypt all the data stored on the disk.")
writeBenchCmd.Flags().BoolVar(&wo.loadBloomsOnOpen, "load-blooms", true,
"Load Bloom filter on DB open.")
writeBenchCmd.Flags().BoolVar(&wo.detectConflicts, "conficts", false,
"If true, it badger will detect the conflicts")
writeBenchCmd.Flags().BoolVar(&wo.zstdComp, "zstd", false,
"If true, badger will use ZSTD mode. Otherwise, use default.")
writeBenchCmd.Flags().BoolVar(&wo.showDir, "show-dir", false,
"If true, the report will include the directory contents")
writeBenchCmd.Flags().StringVar(&wo.dropAllPeriod, "dropall", "0s",
"If set, run dropAll periodically over given duration.")
writeBenchCmd.Flags().StringVar(&wo.dropPrefixPeriod, "drop-prefix", "0s",
"If set, drop random prefixes periodically over given duration.")
writeBenchCmd.Flags().StringVar(&wo.ttlDuration, "entry-ttl", "0s",
"TTL duration in seconds for the entries, 0 means without TTL")
writeBenchCmd.Flags().StringVarP(&wo.gcPeriod, "gc-every", "g", "0s", "GC Period.")
writeBenchCmd.Flags().Float64VarP(&wo.gcDiscardRatio, "gc-ratio", "r", 0.5, "GC discard ratio.")
writeBenchCmd.Flags().BoolVar(&wo.showKeysCount, "show-keys", false,
"If true, the report will include the keys statistics")
}
func writeRandom(db *badger.DB, num uint64) error {
value := make([]byte, wo.valSz)
y.Check2(rand.Read(value))
es := uint64(wo.keySz + wo.valSz) // entry size is keySz + valSz
batch := db.NewManagedWriteBatch()
ttlPeriod, errParse := time.ParseDuration(wo.ttlDuration)
y.Check(errParse)
for i := uint64(1); i <= num; i++ {
key := make([]byte, wo.keySz)
y.Check2(rand.Read(key))
vsz := rand.Intn(wo.valSz) + 1
e := badger.NewEntry(key, value[:vsz])
if ttlPeriod != 0 {
e.WithTTL(ttlPeriod)
}
err := batch.SetEntryAt(e, 1)
for err == badger.ErrBlockedWrites {
time.Sleep(time.Second)
batch = db.NewManagedWriteBatch()
err = batch.SetEntryAt(e, 1)
}
if err != nil {
panic(err)
}
atomic.AddUint64(&entriesWritten, 1)
atomic.AddUint64(&sizeWritten, es)
}
return batch.Flush()
}
func readTest(db *badger.DB, dur time.Duration) {
now := time.Now()
keys, err := getSampleKeys(db)
if err != nil {
panic(err)
}
fmt.Println("*********************************************************")
fmt.Printf("Total Sampled Keys: %d, read in time: %s\n", len(keys), time.Since(now))
fmt.Println("*********************************************************")
if len(keys) == 0 {
fmt.Println("DB is empty, hence returning")
return
}
c := z.NewCloser(0)
readStartTime := time.Now()
for i := 0; i < numGoroutines; i++ {
c.AddRunning(1)
go readKeys(db, c, keys)
}
// also start printing stats
c.AddRunning(1)
go printReadStats(c, readStartTime)
<-time.After(dur)
c.SignalAndWait()
}
func writeSorted(db *badger.DB, num uint64) error {
value := make([]byte, wo.valSz)
y.Check2(rand.Read(value))
es := 8 + wo.valSz // key size is 8 bytes and value size is valSz
writer := db.NewStreamWriter()
if err := writer.Prepare(); err != nil {
return err
}
wg := &sync.WaitGroup{}
writeCh := make(chan *z.Buffer, 3)
writeRange := func(start, end uint64, streamId uint32) {
// end is not included.
defer wg.Done()
kvBuf := z.NewBuffer(5<<20, "Benchmark.WriteSorted")
var sz int
for i := start; i < end; i++ {
key := make([]byte, 8)
binary.BigEndian.PutUint64(key, i)
kv := &pb.KV{
Key: key,
Value: value,
Version: 1,
StreamId: streamId,
}
badger.KVToBuffer(kv, kvBuf)
sz += es
atomic.AddUint64(&entriesWritten, 1)
atomic.AddUint64(&sizeWritten, uint64(es))
if sz >= 4<<20 { // 4 MB
writeCh <- kvBuf
kvBuf = z.NewBuffer(1<<20, "Benchmark.WriteSorted")
sz = 0
}
}
writeCh <- kvBuf
}
// Let's create some streams.
width := num / 4
streamID := uint32(0)
for start := uint64(0); start < num; start += width {
end := start + width
if end > num {
end = num
}
streamID++
wg.Add(1)
go writeRange(start, end, streamID)
}
go func() {
wg.Wait()
close(writeCh)
}()
log.Printf("Max StreamId used: %d. Width: %d\n", streamID, width)
for kvs := range writeCh {
if err := writer.Write(kvs); err != nil {
panic(err)
}
y.Check(kvs.Release())
}
log.Println("DONE streaming. Flushing...")
return writer.Flush()
}
func writeBench(cmd *cobra.Command, args []string) error {
opt := badger.DefaultOptions(sstDir).
WithValueDir(vlogDir).
WithSyncWrites(wo.syncWrites).
WithCompactL0OnClose(wo.force).
WithValueThreshold(wo.valueThreshold).
WithNumVersionsToKeep(wo.numVersions).
WithBlockCacheSize(wo.blockCacheSize << 20).
WithIndexCacheSize(wo.indexCacheSize << 20).
WithValueLogMaxEntries(wo.vlogMaxEntries).
WithEncryptionKey([]byte(wo.encryptionKey)).
WithDetectConflicts(wo.detectConflicts).
WithLoggingLevel(badger.INFO)
if wo.zstdComp {
opt = opt.WithCompression(options.ZSTD)
}
if !wo.showLogs {
opt = opt.WithLogger(nil)
}
fmt.Printf("Opening badger with options = %+v\n", opt)
db, err := badger.OpenManaged(opt)
if err != nil {
return err
}
defer func() {
start := time.Now()
err := db.Close()
log.Printf("DB.Close. Error: %v. Time taken to close: %s", err, time.Since(start))
}()
fmt.Println("*********************************************************")
fmt.Println("Starting to benchmark Writes")
fmt.Println("*********************************************************")
startTime = time.Now()
num := uint64(wo.numKeys * mil)
c := z.NewCloser(4)
go reportStats(c, db)
go dropAll(c, db)
go dropPrefix(c, db)
go runGC(c, db)
if wo.sorted {
err = writeSorted(db, num)
} else {
err = writeRandom(db, num)
}
c.SignalAndWait()
fmt.Printf(db.LevelsToString())
return err
}
func showKeysStats(db *badger.DB) {
var (
internalKeyCount uint32
invalidKeyCount uint32
validKeyCount uint32
)
txn := db.NewTransactionAt(math.MaxUint64, false)
defer txn.Discard()
iopt := badger.DefaultIteratorOptions
iopt.AllVersions = true
iopt.InternalAccess = true
it := txn.NewIterator(iopt)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
i := it.Item()
if bytes.HasPrefix(i.Key(), []byte("!badger!")) {
internalKeyCount++
}
if i.IsDeletedOrExpired() {
invalidKeyCount++
} else {
validKeyCount++
}
}
fmt.Printf("Valid Keys: %d Invalid Keys: %d Internal Keys: %d\n",
validKeyCount, invalidKeyCount, internalKeyCount)
}
func reportStats(c *z.Closer, db *badger.DB) {
defer c.Done()
t := time.NewTicker(time.Second)
defer t.Stop()
var count int
for {
select {
case <-c.HasBeenClosed():
return
case <-t.C:
count++
if wo.showKeysCount {
showKeysStats(db)
}
// fetch directory contents
if wo.showDir {
err := filepath.Walk(sstDir, func(path string, info os.FileInfo, err error) error {
fileSize := humanize.IBytes(uint64(info.Size()))
files = append(files, "[Content] "+path+" "+fileSize)
if filepath.Ext(path) == ".vlog" {
vlogCount++
}
if filepath.Ext(path) == ".sst" {
sstCount++
}
return nil
})
if err != nil {
log.Printf("Error while fetching directory. %v.", err)
} else {
fmt.Printf("[Content] Number of files:%d\n", len(files))
for _, file := range files {
fmt.Println(file)
}
fmt.Printf("SST Count: %d vlog Count: %d\n", sstCount, vlogCount)
}
}
dur := time.Since(startTime)
sz := atomic.LoadUint64(&sizeWritten)
entries := atomic.LoadUint64(&entriesWritten)
bytesRate := sz / uint64(dur.Seconds())
entriesRate := entries / uint64(dur.Seconds())
fmt.Printf("[WRITE] Time elapsed: %s, bytes written: %s, speed: %s/sec, "+
"entries written: %d, speed: %d/sec, jemalloc: %s\n",
y.FixedDuration(time.Since(startTime)),
humanize.IBytes(sz), humanize.IBytes(bytesRate), entries, entriesRate,
humanize.IBytes(uint64(z.NumAllocBytes())))
if count%10 == 0 {
fmt.Printf(db.LevelsToString())
}
}
}
}
func runGC(c *z.Closer, db *badger.DB) {
defer c.Done()
period, err := time.ParseDuration(wo.gcPeriod)
y.Check(err)
if period == 0 {
return
}
t := time.NewTicker(period)
defer t.Stop()
for {
select {
case <-c.HasBeenClosed():
return
case <-t.C:
if err := db.RunValueLogGC(wo.gcDiscardRatio); err == nil {
atomic.AddUint64(&gcSuccess, 1)
} else {
log.Printf("[GC] Failed due to following err %v", err)
}
}
}
}
func dropAll(c *z.Closer, db *badger.DB) {
defer c.Done()
dropPeriod, err := time.ParseDuration(wo.dropAllPeriod)
y.Check(err)
if dropPeriod == 0 {
return
}
t := time.NewTicker(dropPeriod)
defer t.Stop()
for {
select {
case <-c.HasBeenClosed():
return
case <-t.C:
fmt.Println("[DropAll] Started")
err := db.DropAll()
for err == badger.ErrBlockedWrites {
err = db.DropAll()
time.Sleep(time.Millisecond * 300)
}
if err != nil {
fmt.Println("[DropAll] Failed")
} else {
fmt.Println("[DropAll] Successful")
}
}
}
}
func dropPrefix(c *z.Closer, db *badger.DB) {
defer c.Done()
dropPeriod, err := time.ParseDuration(wo.dropPrefixPeriod)
y.Check(err)
if dropPeriod == 0 {
return
}
t := time.NewTicker(dropPeriod)
defer t.Stop()
for {
select {
case <-c.HasBeenClosed():
return
case <-t.C:
fmt.Println("[DropPrefix] Started")
prefix := make([]byte, 1+int(float64(wo.keySz)*0.1))
y.Check2(rand.Read(prefix))
err = db.DropPrefix(prefix)
if err != nil {
panic(err)
} else {
fmt.Println("[DropPrefix] Successful")
}
}
}
}
func printReadStats(c *z.Closer, startTime time.Time) {
defer c.Done()
t := time.NewTicker(time.Second)
defer t.Stop()
for {
select {
case <-c.HasBeenClosed():
return
case <-t.C:
dur := time.Since(startTime)
sz := atomic.LoadUint64(&sizeRead)
entries := atomic.LoadUint64(&entriesRead)
bytesRate := sz / uint64(dur.Seconds())
entriesRate := entries / uint64(dur.Seconds())
fmt.Printf("[READ] Time elapsed: %s, bytes read: %s, speed: %s/sec, "+
"entries read: %d, speed: %d/sec\n", y.FixedDuration(time.Since(startTime)),
humanize.IBytes(sz), humanize.IBytes(bytesRate), entries, entriesRate)
}
}
}