Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storing intermediary IAVL versions in memory and not to disk #150

Closed
wants to merge 4 commits into from

Conversation

mattkanwisher
Copy link
Contributor

Storing intermediary IAVL versions in memory and not to disk

Motivation: Both Cosmos and Loom Network save an IAVL version per block, then go back and delete these versions. So you have constant churn on the IAVL and underlying Leveldb database. When realistically what you want is to only store every X Blocks.

At Berlin Tendermint Conference, Zaki and I surmised a plan where new versions are in memory, while still pointing back to nodes on disk to prevent needing to load entire IAVL into main memory. Loom IAVL tree is around 256gb so this is not feasible otherwise.

Usage

OLD Code would be like

hash, version, err := s.tree.SaveVersion()

New Caller code would look like

	oldVersion := s.Version()
  	var version int64
 	var hash []byte
 	//Every X versions we should persist to disk
 	if s.flushInterval == 0 || ((oldVersion+1)%s.flushInterval == 0) {
 		if s.flushInterval != 0 {
 			log.Error(fmt.Sprintf("Flushing mem to disk at version %d\n", oldVersion+1))
 			hash, version, err = s.tree.FlushMemVersionDisk()
 		} else {
 			hash, version, err = s.tree.SaveVersion()
 		}
 	} else {
 		hash, version, err = s.tree.SaveVersionMem()
 	}

FlushMemVersionDisk:
Flushes the current memory version to disk

SaveVersionMem:
Saves the current tree to memory instead of disk and gives you back an apphash

This is an opt in feature, you have to call new apis to get it.
We also have a PR that demonstrates its usage https://github.com/loomnetwork/loomchain/pull/1232/files

We are now commiting every 1000 blocks, so we store 1000x less. Also we have signficant improves in IO at least double from not having to Prune old versions of the IAVL Tree

Storing intermidiary IAVL versions in memory and not to disk

Motivation: Both Cosmos and Loom Network save an IAVL version per block, then go back and delete these versions. So you have constant churn on the IAVL and underlying Leveldb database. When realistically what you want is to only store every X Blocks.

At Berlin Tendermint Conference, Zaki and I surmised a plan where new versions are in memory, while still pointing back to nodes on disk to prevent needing to load entire IAVL into main memory. Loom IAVL tree is around 256gb so this is not feasible otherwise.


Usage

OLD Code would be like

```go
hash, version, err := s.tree.SaveVersion()
```

New Caller code would look like

```go
	oldVersion := s.Version()
  	var version int64
 	var hash []byte
 	//Every X versions we should persist to disk
 	if s.flushInterval == 0 || ((oldVersion+1)%s.flushInterval == 0) {
 		if s.flushInterval != 0 {
 			log.Error(fmt.Sprintf("Flushing mem to disk at version %d\n", oldVersion+1))
 			hash, version, err = s.tree.FlushMemVersionDisk()
 		} else {
 			hash, version, err = s.tree.SaveVersion()
 		}
 	} else {
 		hash, version, err = s.tree.SaveVersionMem()
 	}
```

FlushMemVersionDisk:
Flushes the current memory version to disk

SaveVersionMem:
Saves the current tree to memory instead of disk and gives you back an apphash

This is an opt in feature, you have to call new apis to get it. 
We also have a PR that demonstrates its usage https://github.com/loomnetwork/loomchain/pull/1232/files

We are now commiting every 1000 blocks, so we store 1000x less. Also we have signficant improves in IO at least double from not having to Prune old versions of the IAVL Tree
@jackzampolin
Copy link
Member

Thank you for the PR @mattkanwisher!! Getting some folks to review this!

@@ -165,11 +185,16 @@ func (ndb *nodeDB) DeleteVersion(version int64, checkLatestVersion bool) {
// Saves orphaned nodes to disk under a special prefix.
// version: the new version being saved.
// orphans: the orphan nodes created since version-1
func (ndb *nodeDB) SaveOrphans(version int64, orphans map[string]int64) {
func (ndb *nodeDB) SaveOrphans(version int64, orphans map[string]int64, flushToDisk bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of parameter flushToDisk bool? It seems useless in SaveOrphans

@yutianwu
Copy link

yutianwu commented Jul 3, 2019

When we restart the node, un-persisted changes in memory will be lost and that means the latest version of state may not persisted. will it work when the node restart and find out could not load the root node of the latest version. should we replay the blocks to get the versions not persisted when restart.

It seems in loomchain there is just one app store, so it may works for tendermint will relay all blocks automatically if the height of state and block height do not match. but it may does not work in cosmos for it have multi app store.

@ultraeric
Copy link

ultraeric commented Jul 3, 2019

Concerned with the parametrization of the new functions, in particular tree.SaveVersion and tree.SaveVersionMem and the complexity that is imposed on the caller. Also could use some better documentation. Two suggestions for changes:

  1. Wrap some of the SaveVersionMem functionality into SaveVersion and have options be parametrized in the tree constructor. In particular, parametrize the save frequency into the tree constructor. This ideally will reduce migration complexity for anybody that is interested in using these new optimizations. Thus, calling SaveVersion will by default use mem-based caching so long as the tree was initialized with it enabled.
  2. Document at the top of mutable_tree.go and nodedb.go the current orphaning mechanism, as well as this new orphaning mechanism and how to use it.


// DeleteVersionFull deletes a tree version from disk or memory based on the flag. The version can then no
// longer be accessed.
func (tree *MutableTree) DeleteVersionFull(version int64, memDeleteAlso bool) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be refactored into two functions DeleteVersionMem and DeleteVersionDisk? Then DeleteVersion can just call both functions.


// FlushMemDisk saves a new tree to disk and removes all the versions in memory
func (tree *MutableTree) FlushMemVersionDisk() ([]byte, int64, error) {
x, y, err := tree.saveVersion(true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have more descriptive vars

@yutianwu
Copy link

yutianwu commented Jul 8, 2019

on second thought, we can also flush mem version asynchronously. Assume that we have high throughput, though we save state every x blocks, but when it comes to that block, the blocking time may increased. So if we can flush mem version to disk asynchronously, we will have the same performance for each block.

@jackzampolin
Copy link
Member

@yutianwu yeah doing this in a goroutine is def best practice

func (tree *MutableTree) FlushMemVersionDisk() ([]byte, int64, error) {
x, y, err := tree.saveVersion(true)
tree.ndb.dbMem = dbm.NewMemDB()
tree.memVersions = map[int64]bool{}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, if we do not delete all memVersions and keep recent n version would be better, for it would fail when query recent version of state like querystore, if height is not exactly the latest one, it would fail for the version is just deleted.

@tac0turtle
Copy link
Member

Closing this in favor of #158

@tac0turtle tac0turtle closed this Aug 9, 2019
@AdityaSripal
Copy link
Member

@mattkanwisher how often are you flushing versions to disk with loom?

Have you compared the results of various pruning strategies. Are you benchmarking your application with various pruning strategies or are you benchmarking by just testing IAVL in isolation?

Would be very useful to hear your approach to benchmarking this and the results you got. thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants