Skip to content

Commit b54eaf3

Browse files
committed
perf: tune badger defaults and add db bench
1 parent 42228e2 commit b54eaf3

File tree

5 files changed

+361
-4
lines changed

5 files changed

+361
-4
lines changed

pkg/store/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The `store` package provides a persistent storage solution for Evolve, designed
66

77
The storage system consists of a key-value store interface that allows for the persistence of blockchain data. It leverages the IPFS Datastore interface (`go-datastore`) with a Badger database implementation by default.
88

9+
Badger options are tuned for the ev-node write pattern (append-heavy with periodic overwrites) via `store.BadgerOptions()`. Use `tools/db-bench` to validate performance against Badger defaults.
10+
911
## Core Components
1012

1113
### Storage Interface

pkg/store/badger_options.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package store
2+
3+
import (
4+
"runtime"
5+
6+
badger4 "github.com/ipfs/go-ds-badger4"
7+
)
8+
9+
// BadgerOptions returns ev-node tuned Badger options for the node workload.
10+
// These defaults favor write throughput for append-heavy usage.
11+
func BadgerOptions() *badger4.Options {
12+
opts := badger4.DefaultOptions
13+
14+
// Disable conflict detection to reduce write overhead; ev-node does not rely
15+
// on Badger's multi-writer conflict checks for correctness.
16+
opts.Options = opts.Options.WithDetectConflicts(false)
17+
// Allow more L0 tables before compaction kicks in to smooth bursty ingest.
18+
opts.Options = opts.Options.WithNumLevelZeroTables(10)
19+
// Stall threshold is raised to avoid write throttling under heavy load.
20+
opts.Options = opts.Options.WithNumLevelZeroTablesStall(20)
21+
// Scale compaction workers to available CPUs without over-saturating.
22+
opts.Options = opts.Options.WithNumCompactors(compactorCount())
23+
24+
return &opts
25+
}
26+
27+
func compactorCount() int {
28+
// Badger defaults to 4; keep a modest range to avoid compaction thrash.
29+
count := runtime.NumCPU()
30+
if count < 4 {
31+
return 4
32+
}
33+
if count > 8 {
34+
return 8
35+
}
36+
return count
37+
}

pkg/store/kv.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const EvPrefix = "0"
1818
// NewDefaultKVStore creates instance of default key-value store.
1919
func NewDefaultKVStore(rootDir, dbPath, dbName string) (ds.Batching, error) {
2020
path := filepath.Join(rootify(rootDir, dbPath), dbName)
21-
return badger4.NewDatastore(path, nil)
21+
return badger4.NewDatastore(path, BadgerOptions())
2222
}
2323

2424
// NewPrefixKVStore creates a new key-value store with a prefix applied to all keys.
@@ -56,8 +56,7 @@ func rootify(rootDir, dbPath string) string {
5656

5757
// NewTestInMemoryKVStore builds KVStore that works in-memory (without accessing disk).
5858
func NewTestInMemoryKVStore() (ds.Batching, error) {
59-
inMemoryOptions := &badger4.Options{
60-
Options: badger4.DefaultOptions.WithInMemory(true),
61-
}
59+
inMemoryOptions := BadgerOptions()
60+
inMemoryOptions.Options = inMemoryOptions.Options.WithInMemory(true)
6261
return badger4.NewDatastore("", inMemoryOptions)
6362
}

tools/db-bench/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# db-bench
2+
3+
Local BadgerDB benchmark for ev-node write patterns (append + overwrite).
4+
5+
## Usage
6+
7+
Run the tuned defaults:
8+
9+
```sh
10+
go run ./tools/db-bench -bytes 536870912 -value-size 4096 -batch-size 1000 -overwrite-ratio 0.1
11+
```
12+
13+
Compare against Badger defaults:
14+
15+
```sh
16+
go run ./tools/db-bench -profile all -bytes 1073741824 -value-size 4096 -batch-size 1000 -overwrite-ratio 0.1
17+
```
18+
19+
Notes:
20+
21+
- `-bytes` is the total data volume; the tool rounds down to full `-value-size` writes.
22+
- `-profile all` runs `evnode` and `default` in separate subdirectories under a temp base dir.

tools/db-bench/main.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"io/fs"
8+
"math"
9+
"math/rand"
10+
"os"
11+
"path/filepath"
12+
"strconv"
13+
"time"
14+
15+
ds "github.com/ipfs/go-datastore"
16+
badger4 "github.com/ipfs/go-ds-badger4"
17+
18+
"github.com/evstack/ev-node/pkg/store"
19+
)
20+
21+
type config struct {
22+
baseDir string
23+
reset bool
24+
keepTemp bool
25+
totalBytes int64
26+
valueSize int
27+
batchSize int
28+
overwriteRatio float64
29+
profile string
30+
}
31+
32+
type profile struct {
33+
name string
34+
open func(path string) (ds.Batching, error)
35+
}
36+
37+
type result struct {
38+
profile string
39+
writes int
40+
bytes int64
41+
duration time.Duration
42+
mbPerSec float64
43+
writesPerS float64
44+
dbSize int64
45+
}
46+
47+
func main() {
48+
cfg := parseFlags()
49+
50+
profiles := []profile{
51+
{name: "evnode", open: openEvnode},
52+
{name: "default", open: openDefault},
53+
}
54+
55+
baseDir, cleanup := ensureBaseDir(cfg.baseDir, cfg.keepTemp)
56+
defer cleanup()
57+
58+
selected := selectProfiles(profiles, cfg.profile)
59+
if len(selected) == 0 {
60+
fmt.Fprintf(os.Stderr, "Unknown profile %q (valid: evnode, default, all)\n", cfg.profile)
61+
os.Exit(1)
62+
}
63+
64+
for _, p := range selected {
65+
profileDir := filepath.Join(baseDir, p.name)
66+
if cfg.reset {
67+
_ = os.RemoveAll(profileDir)
68+
}
69+
if err := os.MkdirAll(profileDir, 0o755); err != nil {
70+
fmt.Fprintf(os.Stderr, "Failed to create db dir %s: %v\n", profileDir, err)
71+
os.Exit(1)
72+
}
73+
74+
res, err := runProfile(p, profileDir, cfg)
75+
if err != nil {
76+
fmt.Fprintf(os.Stderr, "Profile %s failed: %v\n", p.name, err)
77+
os.Exit(1)
78+
}
79+
printResult(res)
80+
}
81+
}
82+
83+
func parseFlags() config {
84+
cfg := config{}
85+
flag.StringVar(&cfg.baseDir, "dir", "", "DB base directory (default: temp dir)")
86+
flag.BoolVar(&cfg.reset, "reset", false, "remove existing DB directory before running")
87+
flag.BoolVar(&cfg.keepTemp, "keep", false, "keep temp directory (only used when -dir is empty)")
88+
flag.Int64Var(&cfg.totalBytes, "bytes", 512<<20, "total bytes to write")
89+
flag.IntVar(&cfg.valueSize, "value-size", 1024, "value size in bytes")
90+
flag.IntVar(&cfg.batchSize, "batch-size", 1000, "writes per batch commit")
91+
flag.Float64Var(&cfg.overwriteRatio, "overwrite-ratio", 0.1, "fraction of writes that overwrite existing keys (0..1)")
92+
flag.StringVar(&cfg.profile, "profile", "evnode", "profile to run: evnode, default, all")
93+
flag.Parse()
94+
95+
if cfg.totalBytes <= 0 {
96+
exitError("bytes must be > 0")
97+
}
98+
if cfg.valueSize <= 0 {
99+
exitError("value-size must be > 0")
100+
}
101+
if cfg.batchSize <= 0 {
102+
exitError("batch-size must be > 0")
103+
}
104+
if cfg.overwriteRatio < 0 || cfg.overwriteRatio > 1 {
105+
exitError("overwrite-ratio must be between 0 and 1")
106+
}
107+
108+
return cfg
109+
}
110+
111+
func runProfile(p profile, dir string, cfg config) (result, error) {
112+
totalWrites := int(cfg.totalBytes / int64(cfg.valueSize))
113+
if totalWrites == 0 {
114+
return result{}, fmt.Errorf("total bytes (%d) smaller than value size (%d)", cfg.totalBytes, cfg.valueSize)
115+
}
116+
actualBytes := int64(totalWrites) * int64(cfg.valueSize)
117+
118+
rng := rand.New(rand.NewSource(1)) // Deterministic data for comparable runs.
119+
value := make([]byte, cfg.valueSize)
120+
if _, err := rng.Read(value); err != nil {
121+
return result{}, fmt.Errorf("failed to seed value bytes: %w", err)
122+
}
123+
124+
overwriteEvery := 0
125+
if cfg.overwriteRatio > 0 {
126+
overwriteEvery = int(math.Round(1.0 / cfg.overwriteRatio))
127+
if overwriteEvery < 1 {
128+
overwriteEvery = 1
129+
}
130+
}
131+
132+
kv, err := p.open(dir)
133+
if err != nil {
134+
return result{}, fmt.Errorf("failed to open db: %w", err)
135+
}
136+
137+
ctx := context.Background()
138+
start := time.Now()
139+
140+
batch, err := kv.Batch(ctx)
141+
if err != nil {
142+
_ = kv.Close()
143+
return result{}, fmt.Errorf("failed to create batch: %w", err)
144+
}
145+
146+
pending := 0
147+
keysWritten := 0
148+
for i := 0; i < totalWrites; i++ {
149+
keyIndex := keysWritten
150+
if overwriteEvery > 0 && i%overwriteEvery == 0 && keysWritten > 0 {
151+
keyIndex = i % keysWritten
152+
} else {
153+
keysWritten++
154+
}
155+
156+
if err := batch.Put(ctx, keyForIndex(keyIndex), value); err != nil {
157+
_ = kv.Close()
158+
return result{}, fmt.Errorf("batch put failed: %w", err)
159+
}
160+
161+
pending++
162+
if pending == cfg.batchSize {
163+
if err := batch.Commit(ctx); err != nil {
164+
_ = kv.Close()
165+
return result{}, fmt.Errorf("batch commit failed: %w", err)
166+
}
167+
batch, err = kv.Batch(ctx)
168+
if err != nil {
169+
_ = kv.Close()
170+
return result{}, fmt.Errorf("failed to create batch: %w", err)
171+
}
172+
pending = 0
173+
}
174+
}
175+
176+
if pending > 0 {
177+
if err := batch.Commit(ctx); err != nil {
178+
_ = kv.Close()
179+
return result{}, fmt.Errorf("final batch commit failed: %w", err)
180+
}
181+
}
182+
183+
if err := kv.Sync(ctx, ds.NewKey("/")); err != nil {
184+
_ = kv.Close()
185+
return result{}, fmt.Errorf("sync failed: %w", err)
186+
}
187+
188+
if err := kv.Close(); err != nil {
189+
return result{}, fmt.Errorf("close failed: %w", err)
190+
}
191+
192+
duration := time.Since(start)
193+
mbPerSec := (float64(actualBytes) / (1024 * 1024)) / duration.Seconds()
194+
writesPerSec := float64(totalWrites) / duration.Seconds()
195+
dbSize, err := dirSize(dir)
196+
if err != nil {
197+
return result{}, fmt.Errorf("failed to compute db size: %w", err)
198+
}
199+
200+
return result{
201+
profile: p.name,
202+
writes: totalWrites,
203+
bytes: actualBytes,
204+
duration: duration,
205+
mbPerSec: mbPerSec,
206+
writesPerS: writesPerSec,
207+
dbSize: dbSize,
208+
}, nil
209+
}
210+
211+
func openEvnode(path string) (ds.Batching, error) {
212+
return badger4.NewDatastore(path, store.BadgerOptions())
213+
}
214+
215+
func openDefault(path string) (ds.Batching, error) {
216+
return badger4.NewDatastore(path, nil)
217+
}
218+
219+
func keyForIndex(i int) ds.Key {
220+
return ds.NewKey("k/" + strconv.Itoa(i))
221+
}
222+
223+
func dirSize(root string) (int64, error) {
224+
var size int64
225+
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
226+
if err != nil {
227+
return err
228+
}
229+
if d.Type().IsRegular() {
230+
info, err := d.Info()
231+
if err != nil {
232+
return err
233+
}
234+
size += info.Size()
235+
}
236+
return nil
237+
})
238+
return size, err
239+
}
240+
241+
func printResult(res result) {
242+
fmt.Printf("\nProfile: %s\n", res.profile)
243+
fmt.Printf("Writes: %d\n", res.writes)
244+
fmt.Printf("Data: %s\n", humanBytes(res.bytes))
245+
fmt.Printf("Duration: %s\n", res.duration)
246+
fmt.Printf("Throughput: %.2f MB/s\n", res.mbPerSec)
247+
fmt.Printf("Writes/sec: %.2f\n", res.writesPerS)
248+
fmt.Printf("DB size: %s\n", humanBytes(res.dbSize))
249+
}
250+
251+
func selectProfiles(profiles []profile, name string) []profile {
252+
if name == "all" {
253+
return profiles
254+
}
255+
for _, p := range profiles {
256+
if p.name == name {
257+
return []profile{p}
258+
}
259+
}
260+
return nil
261+
}
262+
263+
func ensureBaseDir(dir string, keep bool) (string, func()) {
264+
if dir != "" {
265+
return dir, func() {}
266+
}
267+
268+
tempDir, err := os.MkdirTemp("", "evnode-db-bench-*")
269+
if err != nil {
270+
exitError(fmt.Sprintf("failed to create temp dir: %v", err))
271+
}
272+
273+
if keep {
274+
fmt.Printf("Using temp dir: %s\n", tempDir)
275+
return tempDir, func() {}
276+
}
277+
278+
return tempDir, func() { _ = os.RemoveAll(tempDir) }
279+
}
280+
281+
func humanBytes(size int64) string {
282+
const unit = 1024
283+
if size < unit {
284+
return fmt.Sprintf("%d B", size)
285+
}
286+
div, exp := int64(unit), 0
287+
for n := size / unit; n >= unit; n /= unit {
288+
div *= unit
289+
exp++
290+
}
291+
return fmt.Sprintf("%.1f %ciB", float64(size)/float64(div), "KMGTPE"[exp])
292+
}
293+
294+
func exitError(msg string) {
295+
fmt.Fprintln(os.Stderr, msg)
296+
os.Exit(1)
297+
}

0 commit comments

Comments
 (0)