optimizing badger cache, won a 10-15% improvement in most benchmarks

This commit is contained in:
2025-11-16 15:07:36 +00:00
parent 9bb3a7e057
commit 95bcf85ad7
72 changed files with 8158 additions and 4048 deletions

View File

@@ -16,15 +16,20 @@ import (
"next.orly.dev/pkg/utils/units"
)
// D implements the Database interface using Badger as the storage backend
type D struct {
ctx context.Context
cancel context.CancelFunc
dataDir string
Logger *logger
*badger.DB
seq *badger.Sequence
seq *badger.Sequence
ready chan struct{} // Closed when database is ready to serve requests
}
// Ensure D implements Database interface at compile time
var _ Database = (*D)(nil)
func New(
ctx context.Context, cancel context.CancelFunc, dataDir, logLevel string,
) (
@@ -37,6 +42,7 @@ func New(
Logger: NewLogger(lol.GetLogLevel(logLevel), dataDir),
DB: nil,
seq: nil,
ready: make(chan struct{}),
}
// Ensure the data directory exists
@@ -54,8 +60,8 @@ func New(
opts := badger.DefaultOptions(d.dataDir)
// Configure caches based on environment to better match workload.
// Defaults aim for higher hit ratios under read-heavy workloads while remaining safe.
var blockCacheMB = 512 // default 512 MB
var indexCacheMB = 256 // default 256 MB
var blockCacheMB = 1024 // default 512 MB
var indexCacheMB = 512 // default 256 MB
if v := os.Getenv("ORLY_DB_BLOCK_CACHE_MB"); v != "" {
if n, perr := strconv.Atoi(v); perr == nil && n > 0 {
blockCacheMB = n
@@ -69,15 +75,42 @@ func New(
opts.BlockCacheSize = int64(blockCacheMB * units.Mb)
opts.IndexCacheSize = int64(indexCacheMB * units.Mb)
opts.BlockSize = 4 * units.Kb // 4 KB block size
// Prevent huge allocations during table building and memtable flush.
// Badger's TableBuilder buffer is sized by BaseTableSize; ensure it's small.
opts.BaseTableSize = 64 * units.Mb // 64 MB per table (default ~2MB, increased for fewer files but safe)
opts.MemTableSize = 64 * units.Mb // 64 MB memtable to match table size
// Keep value log files to a moderate size as well
opts.ValueLogFileSize = 256 * units.Mb // 256 MB value log files
// Reduce table sizes to lower cost-per-key in cache
// Smaller tables mean lower cache cost metric per entry
opts.BaseTableSize = 8 * units.Mb // 8 MB per table (reduced from 64 MB to lower cache cost)
opts.MemTableSize = 16 * units.Mb // 16 MB memtable (reduced from 64 MB)
// Keep value log files to a moderate size
opts.ValueLogFileSize = 128 * units.Mb // 128 MB value log files (reduced from 256 MB)
// CRITICAL: Keep small inline events in LSM tree, not value log
// VLogPercentile 0.99 means 99% of values stay in LSM (our optimized inline events!)
// This dramatically improves read performance for small events
opts.VLogPercentile = 0.99
// Optimize LSM tree structure
opts.BaseLevelSize = 64 * units.Mb // Increased from default 10 MB for fewer levels
opts.LevelSizeMultiplier = 10 // Default, good balance
opts.CompactL0OnClose = true
opts.LmaxCompaction = true
opts.Compression = options.None
// Enable compression to reduce cache cost
opts.Compression = options.ZSTD
opts.ZSTDCompressionLevel = 1 // Fast compression (500+ MB/s)
// Disable conflict detection for write-heavy relay workloads
// Nostr events are immutable, no need for transaction conflict checks
opts.DetectConflicts = false
// Performance tuning for high-throughput workloads
opts.NumCompactors = 8 // Increase from default 4 for faster compaction
opts.NumLevelZeroTables = 8 // Increase from default 5 to allow more L0 tables before compaction
opts.NumLevelZeroTablesStall = 16 // Increase from default 15 to reduce write stalls
opts.NumMemtables = 8 // Increase from default 5 to buffer more writes
opts.MaxLevels = 7 // Default is 7, keep it
opts.Logger = d.Logger
if d.DB, err = badger.Open(opts); chk.E(err) {
return
@@ -88,6 +121,10 @@ func New(
// run code that updates indexes when new indexes have been added and bumps
// the version so they aren't run again.
d.RunMigrations()
// Start warmup goroutine to signal when database is ready
go d.warmup()
// start up the expiration tag processing and shut down and clean up the
// database after the context is canceled.
go func() {
@@ -108,6 +145,29 @@ func New(
// Path returns the path where the database files are stored.
func (d *D) Path() string { return d.dataDir }
// Ready returns a channel that closes when the database is ready to serve requests.
// This allows callers to wait for database warmup to complete.
func (d *D) Ready() <-chan struct{} {
return d.ready
}
// warmup performs database warmup operations and closes the ready channel when complete.
// Warmup criteria:
// - Wait at least 2 seconds for initial compactions to settle
// - Ensure cache hit ratio is reasonable (if we have metrics available)
func (d *D) warmup() {
defer close(d.ready)
// Give the database time to settle after opening
// This allows:
// - Initial compactions to complete
// - Memory allocations to stabilize
// - Cache to start warming up
time.Sleep(2 * time.Second)
d.Logger.Infof("database warmup complete, ready to serve requests")
}
func (d *D) Wipe() (err error) {
err = errors.New("not implemented")
return

39
pkg/database/factory.go Normal file
View File

@@ -0,0 +1,39 @@
package database
import (
"context"
"fmt"
"strings"
)
// NewDatabase creates a database instance based on the specified type.
// Supported types: "badger", "dgraph"
func NewDatabase(
ctx context.Context,
cancel context.CancelFunc,
dbType string,
dataDir string,
logLevel string,
) (Database, error) {
switch strings.ToLower(dbType) {
case "badger", "":
// Use the existing badger implementation
return New(ctx, cancel, dataDir, logLevel)
case "dgraph":
// Use the new dgraph implementation
// Import dynamically to avoid import cycles
return newDgraphDatabase(ctx, cancel, dataDir, logLevel)
default:
return nil, fmt.Errorf("unsupported database type: %s (supported: badger, dgraph)", dbType)
}
}
// newDgraphDatabase creates a dgraph database instance
// This is defined here to avoid import cycles
var newDgraphDatabase func(context.Context, context.CancelFunc, string, string) (Database, error)
// RegisterDgraphFactory registers the dgraph database factory
// This is called from the dgraph package's init() function
func RegisterDgraphFactory(factory func(context.Context, context.CancelFunc, string, string) (Database, error)) {
newDgraphDatabase = factory
}

102
pkg/database/interface.go Normal file
View File

@@ -0,0 +1,102 @@
package database
import (
"context"
"io"
"time"
"next.orly.dev/pkg/database/indexes/types"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/filter"
"next.orly.dev/pkg/encoders/tag"
"next.orly.dev/pkg/interfaces/store"
)
// Database defines the interface that all database implementations must satisfy.
// This allows switching between different storage backends (badger, dgraph, etc.)
type Database interface {
// Core lifecycle methods
Path() string
Init(path string) error
Sync() error
Close() error
Wipe() error
SetLogLevel(level string)
Ready() <-chan struct{} // Returns a channel that closes when database is ready to serve requests
// Event storage and retrieval
SaveEvent(c context.Context, ev *event.E) (exists bool, err error)
GetSerialsFromFilter(f *filter.F) (serials types.Uint40s, err error)
WouldReplaceEvent(ev *event.E) (bool, types.Uint40s, error)
QueryEvents(c context.Context, f *filter.F) (evs event.S, err error)
QueryAllVersions(c context.Context, f *filter.F) (evs event.S, err error)
QueryEventsWithOptions(c context.Context, f *filter.F, includeDeleteEvents bool, showAllVersions bool) (evs event.S, err error)
QueryDeleteEventsByTargetId(c context.Context, targetEventId []byte) (evs event.S, err error)
QueryForSerials(c context.Context, f *filter.F) (serials types.Uint40s, err error)
QueryForIds(c context.Context, f *filter.F) (idPkTs []*store.IdPkTs, err error)
CountEvents(c context.Context, f *filter.F) (count int, approximate bool, err error)
FetchEventBySerial(ser *types.Uint40) (ev *event.E, err error)
FetchEventsBySerials(serials []*types.Uint40) (events map[uint64]*event.E, err error)
GetSerialById(id []byte) (ser *types.Uint40, err error)
GetSerialsByIds(ids *tag.T) (serials map[string]*types.Uint40, err error)
GetSerialsByIdsWithFilter(ids *tag.T, fn func(ev *event.E, ser *types.Uint40) bool) (serials map[string]*types.Uint40, err error)
GetSerialsByRange(idx Range) (serials types.Uint40s, err error)
GetFullIdPubkeyBySerial(ser *types.Uint40) (fidpk *store.IdPkTs, err error)
GetFullIdPubkeyBySerials(sers []*types.Uint40) (fidpks []*store.IdPkTs, err error)
// Event deletion
DeleteEvent(c context.Context, eid []byte) error
DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) error
DeleteExpired()
ProcessDelete(ev *event.E, admins [][]byte) error
CheckForDeleted(ev *event.E, admins [][]byte) error
// Import/Export
Import(rr io.Reader)
Export(c context.Context, w io.Writer, pubkeys ...[]byte)
ImportEventsFromReader(ctx context.Context, rr io.Reader) error
ImportEventsFromStrings(ctx context.Context, eventJSONs []string, policyManager interface{ CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error) }) error
// Relay identity
GetRelayIdentitySecret() (skb []byte, err error)
SetRelayIdentitySecret(skb []byte) error
GetOrCreateRelayIdentitySecret() (skb []byte, err error)
// Markers (metadata key-value storage)
SetMarker(key string, value []byte) error
GetMarker(key string) (value []byte, err error)
HasMarker(key string) bool
DeleteMarker(key string) error
// Subscriptions (payment-based access control)
GetSubscription(pubkey []byte) (*Subscription, error)
IsSubscriptionActive(pubkey []byte) (bool, error)
ExtendSubscription(pubkey []byte, days int) error
RecordPayment(pubkey []byte, amount int64, invoice, preimage string) error
GetPaymentHistory(pubkey []byte) ([]Payment, error)
ExtendBlossomSubscription(pubkey []byte, tier string, storageMB int64, daysExtended int) error
GetBlossomStorageQuota(pubkey []byte) (quotaMB int64, err error)
IsFirstTimeUser(pubkey []byte) (bool, error)
// NIP-43 Invite-based ACL
AddNIP43Member(pubkey []byte, inviteCode string) error
RemoveNIP43Member(pubkey []byte) error
IsNIP43Member(pubkey []byte) (isMember bool, err error)
GetNIP43Membership(pubkey []byte) (*NIP43Membership, error)
GetAllNIP43Members() ([][]byte, error)
StoreInviteCode(code string, expiresAt time.Time) error
ValidateInviteCode(code string) (valid bool, err error)
DeleteInviteCode(code string) error
PublishNIP43MembershipEvent(kind int, pubkey []byte) error
// Migrations (version tracking for schema updates)
RunMigrations()
// Utility methods
EventIdsBySerial(start uint64, count int) (evs []uint64, err error)
}