core, ethdb, trie: experiment with batch-replay-based uncaching

This commit is contained in:
Martin Holst Swende 2019-03-20 21:06:47 +01:00 committed by Péter Szilágyi
parent ed644a983d
commit f94deb6a3f
No known key found for this signature in database
GPG key ID: E9AE538CEDF8293D
6 changed files with 117 additions and 42 deletions

View file

@ -124,6 +124,10 @@ type tableBatch struct {
prefix string prefix string
} }
func (b *tableBatch) Replay(replay ethdb.DbEventLogger) error {
panic("implement me")
}
// Put inserts the given value into the batch for later committing. // Put inserts the given value into the batch for later committing.
func (b *tableBatch) Put(key, value []byte) error { func (b *tableBatch) Put(key, value []byte) error {
return b.batch.Put(append([]byte(b.prefix), key...), value) return b.batch.Put(append([]byte(b.prefix), key...), value)

View file

@ -34,6 +34,9 @@ type Batch interface {
// Reset resets the batch for reuse // Reset resets the batch for reuse
Reset() Reset()
// Replay replays the batch into another batch
Replay(logger DbEventLogger) error
} }
// Batcher wraps the NewBatch method of a backing data store. // Batcher wraps the NewBatch method of a backing data store.

View file

@ -40,6 +40,13 @@ type Deleter interface {
Delete(key []byte) error Delete(key []byte) error
} }
// DbEventLogger wraps Put and Delete to serve as a recipient
// for batch replays
type DbEventLogger interface {
Writer
Deleter
}
// Stater wraps the Stat method of a backing data store. // Stater wraps the Stat method of a backing data store.
type Stater interface { type Stater interface {
// Stat returns a particular internal stat of the database. // Stat returns a particular internal stat of the database.

View file

@ -172,6 +172,23 @@ func (db *Database) NewBatch() ethdb.Batch {
} }
} }
type dbWrapper struct {
wrapped ethdb.DbEventLogger
}
func (dbw *dbWrapper) Put(key, value []byte) {
dbw.wrapped.Put(key, value)
}
func (dbw *dbWrapper) Delete(key []byte) {
dbw.wrapped.Delete(key)
}
// Replay replays batch contents.
func (b *batch) Replay(r ethdb.DbEventLogger) error {
return b.b.Replay(&dbWrapper{r})
}
// NewIterator creates a binary-alphabetical iterator over the entire keyspace // NewIterator creates a binary-alphabetical iterator over the entire keyspace
// contained within the leveldb database. // contained within the leveldb database.
func (db *Database) NewIterator() ethdb.Iterator { func (db *Database) NewIterator() ethdb.Iterator {

View file

@ -200,6 +200,17 @@ type batch struct {
size int size int
} }
func (b *batch) Replay(replay ethdb.DbEventLogger) error {
for _, keyvalue := range b.writes {
if keyvalue.delete {
replay.Delete(keyvalue.key)
continue
}
replay.Put(keyvalue.key, keyvalue.value)
}
return nil
}
// Put inserts the given value into the batch for later committing. // Put inserts the given value into the batch for later committing.
func (b *batch) Put(key, value []byte) error { func (b *batch) Put(key, value []byte) error {
b.writes = append(b.writes, keyvalue{common.CopyBytes(key), common.CopyBytes(value), false}) b.writes = append(b.writes, keyvalue{common.CopyBytes(key), common.CopyBytes(value), false})

View file

@ -82,6 +82,7 @@ type Database struct {
preimagesSize common.StorageSize // Storage size of the preimages cache preimagesSize common.StorageSize // Storage size of the preimages cache
lock sync.RWMutex lock sync.RWMutex
batchLogger *BatchEventLogger
} }
// rawNode is a simple binary blob used to differentiate between collapsed trie // rawNode is a simple binary blob used to differentiate between collapsed trie
@ -293,12 +294,14 @@ func NewDatabaseWithCache(diskdb ethdb.KeyValueStore, cache int) *Database {
Hasher: trienodeHasher{}, Hasher: trienodeHasher{},
}) })
} }
return &Database{ db := &Database{
diskdb: diskdb, diskdb: diskdb,
cleans: cleans, cleans: cleans,
dirties: map[common.Hash]*cachedNode{{}: {}}, dirties: map[common.Hash]*cachedNode{{}: {}},
preimages: make(map[common.Hash][]byte), preimages: make(map[common.Hash][]byte),
} }
db.batchLogger = newBatchEventLogger(db)
return db
} }
// DiskDB retrieves the persistent storage backing the trie database. // DiskDB retrieves the persistent storage backing the trie database.
@ -660,6 +663,61 @@ func (db *Database) Cap(limit common.StorageSize) error {
return nil return nil
} }
type BatchEventLogger struct {
db *Database
}
func newBatchEventLogger(db *Database) *BatchEventLogger {
return &BatchEventLogger{db}
}
// Put reacts to batch writes, and implements uncache:
// is the post-processing step of a commit operation where the already
// persisted trie is removed from the cache. The reason behind the two-phase
// commit is to ensure consistent data availability while moving from memory
// to disk.
func (p *BatchEventLogger) Put(key []byte, value []byte) error {
// key is hash
// value is rlp
//log.Info("proxybatch", "key", fmt.Sprintf("0x%x", key))
hash := common.BytesToHash(key)
db := p.db
rlp := value
// If the node does not exist, we're done on this path
node, ok := db.dirties[hash]
if !ok {
return nil
}
// Node still exists, remove it from the flush-list
switch hash {
case db.oldest:
db.oldest = node.flushNext
db.dirties[node.flushNext].flushPrev = common.Hash{}
case db.newest:
db.newest = node.flushPrev
db.dirties[node.flushPrev].flushNext = common.Hash{}
default:
db.dirties[node.flushPrev].flushNext = node.flushNext
db.dirties[node.flushNext].flushPrev = node.flushPrev
}
// Uncache the node's subtries and remove the node itself too
//for _, child := range node.childs() {
// db.uncache(child)
//}
delete(db.dirties, hash)
db.dirtiesSize -= common.StorageSize(common.HashLength + int(node.size))
// Move the flushed node into the clean cache to prevent insta-reloads
if db.cleans != nil {
db.cleans.Set(string(hash[:]), rlp)
}
return nil
}
func (p *BatchEventLogger) Delete(key []byte) error {
panic("Not implemented")
}
// Commit iterates over all the children of a particular node, writes them out // Commit iterates over all the children of a particular node, writes them out
// to disk, forcefully tearing down all references in both directions. // to disk, forcefully tearing down all references in both directions.
// //
@ -673,7 +731,6 @@ func (db *Database) Commit(node common.Hash, report bool) error {
start := time.Now() start := time.Now()
batch := db.diskdb.NewBatch() batch := db.diskdb.NewBatch()
// Move all of the accumulated preimages into a write batch // Move all of the accumulated preimages into a write batch
for hash, preimage := range db.preimages { for hash, preimage := range db.preimages {
if err := batch.Put(db.secureKey(hash[:]), preimage); err != nil { if err := batch.Put(db.secureKey(hash[:]), preimage); err != nil {
@ -689,6 +746,11 @@ func (db *Database) Commit(node common.Hash, report bool) error {
batch.Reset() batch.Reset()
} }
} }
if err := batch.Write(); err != nil {
db.lock.RUnlock()
return err
}
batch.Reset()
// Move the trie itself into the batch, flushing if enough data is accumulated // Move the trie itself into the batch, flushing if enough data is accumulated
nodes, storage := len(db.dirties), db.dirtiesSize nodes, storage := len(db.dirties), db.dirtiesSize
if err := db.commit(node, batch); err != nil { if err := db.commit(node, batch); err != nil {
@ -703,16 +765,15 @@ func (db *Database) Commit(node common.Hash, report bool) error {
return err return err
} }
db.lock.RUnlock() db.lock.RUnlock()
// Write successful, clear out the flushed data // Write successful, clear out the flushed data
db.lock.Lock() db.lock.Lock()
defer db.lock.Unlock() defer db.lock.Unlock()
batch.Replay(db.batchLogger)
batch.Reset()
db.preimages = make(map[common.Hash][]byte) db.preimages = make(map[common.Hash][]byte)
db.preimagesSize = 0 db.preimagesSize = 0
db.uncache(node)
memcacheCommitTimeTimer.Update(time.Since(start)) memcacheCommitTimeTimer.Update(time.Since(start))
memcacheCommitSizeMeter.Mark(int64(storage - db.dirtiesSize)) memcacheCommitSizeMeter.Mark(int64(storage - db.dirtiesSize))
memcacheCommitNodesMeter.Mark(int64(nodes - len(db.dirties))) memcacheCommitNodesMeter.Mark(int64(nodes - len(db.dirties)))
@ -751,46 +812,18 @@ func (db *Database) commit(hash common.Hash, batch ethdb.Batch) error {
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
return err return err
} }
db.lock.RUnlock()
{
db.lock.Lock()
batch.Replay(db.batchLogger)
batch.Reset() batch.Reset()
db.lock.Unlock()
}
db.lock.RLock()
} }
return nil return nil
} }
// uncache is the post-processing step of a commit operation where the already
// persisted trie is removed from the cache. The reason behind the two-phase
// commit is to ensure consistent data availability while moving from memory
// to disk.
func (db *Database) uncache(hash common.Hash) {
// If the node does not exist, we're done on this path
node, ok := db.dirties[hash]
if !ok {
return
}
// Node still exists, remove it from the flush-list
switch hash {
case db.oldest:
db.oldest = node.flushNext
db.dirties[node.flushNext].flushPrev = common.Hash{}
case db.newest:
db.newest = node.flushPrev
db.dirties[node.flushPrev].flushNext = common.Hash{}
default:
db.dirties[node.flushPrev].flushNext = node.flushNext
db.dirties[node.flushNext].flushPrev = node.flushPrev
}
// Uncache the node's subtries and remove the node itself too
for _, child := range node.childs() {
db.uncache(child)
}
delete(db.dirties, hash)
db.dirtiesSize -= common.StorageSize(common.HashLength + int(node.size))
// Move the flushed node into the clean cache to prevent insta-reloads
if db.cleans != nil {
db.cleans.Set(string(hash[:]), node.rlp())
}
}
// Size returns the current storage size of the memory cache in front of the // Size returns the current storage size of the memory cache in front of the
// persistent database layer. // persistent database layer.
func (db *Database) Size() (common.StorageSize, common.StorageSize) { func (db *Database) Size() (common.StorageSize, common.StorageSize) {