Skip to content

[ETCM-103] Restartable state sync #730

New issue

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

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

Already on GitHub? # to your account

Merged
merged 10 commits into from
Oct 16, 2020

Conversation

KonradStaniec
Copy link
Contributor

@KonradStaniec KonradStaniec commented Oct 9, 2020

Description

Makes it possible to restart state sync if the pivot block goes stale during it.

Proposed Solution

The way it works is:

  1. After switching to state sync, we leave the main fast sync loop running, but only thing we are doing there is checking how many peers have possible pivot block larger than our current by some margin.
  2. When the number of peers with this better possible block is larger than min peers to pick pivot block we: stop state sync, pick new pivot, sync up blockchain date up to new pivot, restart state sync from new pivot

Possible improvements

The best possible improvement would be to have concurrent blockchain and state download, then this whole dance with updating pivot would be unnecessary, only thing the state sync would need to do would be to track best synced block and restart when best synced block is larger by some margin than current state sync pivot. This would require a little rehaul of the way we are handling available peers in upper layers of Mantis to avoid simultaneous concurrent request for state and blockchain data.

Bonus

SyncControllerSpec test have beed refactored to use autopilot.

Testing

I was able to sync to mainnet 4 times now with this setup. (for now without node restarts during state sync, as proper restarting requires one more ticket https://jira.iohk.io/browse/ETCM-213 i.e refiling bloom filter after node restart. Without it it should theroticly be possible to do but can be painfully slow due to large number of false positives from bloom filter which not coresspond to database content)

@KonradStaniec KonradStaniec requested a review from mmrozek October 9, 2020 14:23
@KonradStaniec KonradStaniec requested a review from kapke October 14, 2020 08:55
@KonradStaniec KonradStaniec marked this pull request as ready for review October 14, 2020 08:55
# Current Size of ETC state trie is aroud 150M Nodes, so 200M is set to have some reserve
# If the number of elements inserted into bloom filter would be significally higher that expected, then number
# of false positives would rise which would degrade performance of state sync
state-sync-bloomFilter-size = 200000000
Copy link
Contributor

Choose a reason for hiding this comment

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

state-sync-bloom-filter-size? to be consistent


# Max number of mpt nodes held in memory in state sync, before saving them into database
# 100k is around 60mb (each key-value pair has around 600bytes)
state-sync-persistBatch-size = 100000
Copy link
Contributor

Choose a reason for hiding this comment

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

state-sync-persist-batch-size

# If new pivot block received from network will be less than fast sync current pivot block, the re-try to chose new
# pivot will be scheduler after this time. Avarage block time in etc/eth is around 15s so after this time, most of
# network peers should have new best block
pivot-block-reSchedule-interval = 15.seconds
Copy link
Contributor

Choose a reason for hiding this comment

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

pivot-block-reschedule-interval

scheduler.scheduleOnce(syncConfig.pivotBlockReScheduleInterval, self, UpdatePivotBlock(updateReason))
}

def waitingForPivotBlockUpdate(updateReason: PivotBlockUpdateReason): Receive = handleCommonMessages orElse {
case PivotBlockSelector.Result(pivotBlockHeader) =>
log.info(s"New pivot block with number ${pivotBlockHeader.number} received")
if (pivotBlockHeader.number >= syncState.pivotBlock.number) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It will be more readable if you use pattern matching instead of nested ifs

reScheduleAskForNewPivot(updateReason)
} else {
updatePivotSyncState(updateReason, pivotBlockHeader)
syncState = syncState.copy(updatingPivotBlock = false)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think syncState = syncState.copy(updatingPivotBlock = false) should be done in updatePivotSyncState method

val (nodes, newState) = state.getNodesToPersist
nodes.foreach { case (hash, (data, reqType)) =>
reqType match {
case _: CodeRequest =>
blockchain.storeEvmCode(hash, data).commit()
bloomFilter.put(hash)
Copy link
Contributor

Choose a reason for hiding this comment

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

Very minor: You could call bloomFilter.put(hash) before the pattern matching

// restart. This can be done by exposing RockDb iterator to traverse whole mptnode storage.
// Another possibility is that there is some light way alternative in rocksdb to check key existence
state.memBatch.contains(req.nodeHash) || isInDatabase(req)
if (state.memBatch.contains(req.nodeHash)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Simpler: state.memBatch.contains(req.nodeHash) || (bloomFilter.mightContain(req.nodeHash) && isInDatabase(req))

}

case PersistSyncState => persistSyncState()

case UpdatePivotBlock(state) => updatePivotBlock(state)
}

private def updatePivotBlock(state: FinalBlockProcessingResult): Unit = {
private def updatePivotBlock(state: PivotBlockUpdateReason): Unit = {
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: state or reason then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reason - forgot to change

@@ -267,6 +275,17 @@ class FastSync(
)
syncState =
syncState.updatePivotBlock(pivotBlockHeader, syncConfig.fastSyncBlockValidationX, updateFailures = true)

case NodeRestart =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't it be named SyncRestart? If fast-sync actor gets restarted due to some failure catched by supervisor, it's going to be restarted with clean state the same way is if it was restarted whole node.

That also makes me think - shouldn't SyncController watch for Fast-Sync restarts and start it once such happens?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So i agree that SyncRestart is more compelling name.

Question about supervision is more nuanced, in general I am not sure we handle it well across all codebase, but for this particual case we are fine as: FastySync is child of SyncController and default strategy for uncatched Exception in child is just restart it. It will probably mean some of the request in flight will be later ignored and some of the peers gets unnecessry blacklis. Those missed requests may triger some weird error condition. In my view this whole class was not designed with restarts in mind, but rather with handing all excpetion by itself.

(info.maxBlockNumber - syncConfig.pivotBlockOffset) - state.pivotBlock.number >= syncConfig.maxPivotBlockAge
}

private def getPeerWithTooFreshNewBlock(
Copy link
Contributor

Choose a reason for hiding this comment

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

is it really too fresh or more fresh enough to update to?

@@ -784,8 +860,15 @@ object FastSync {

case object ImportedPivotBlock extends HeaderProcessingResult

sealed abstract class FinalBlockProcessingResult
sealed abstract class PivotBlockUpdateReason {
def nodeRestart: Boolean = this match {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: isNodeRestart?

Copy link
Contributor

@kapke kapke left a comment

Choose a reason for hiding this comment

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

The code looks good!
I'd like to see how it works though. How much time should I expect to wait on mainnet before state sync starts?

@KonradStaniec
Copy link
Contributor Author

So currently sync time looks like on average:

  • 8-10h for blockchain
  • 6-10h for state sync

And state sync starts only after blockchain is downloaded. There is also still one issue with blockchain which makes blockchain state to be stuck, and restart with some config tweak is needed to resume it, if it happen to you let me know. (we already have ticket to track it, and I suspect what is the issue here)

State Sync has higher variability in sync times as it is more parallel and it depends on number of peers , which for now is totally random due to our random walk nature of current disovery. On my machine i had finished state sync in 6h when i got 9-10 peers, and 10h when i got 3-4peers.

Copy link
Contributor

@mmrozek mmrozek left a comment

Choose a reason for hiding this comment

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

LGTM!

@KonradStaniec KonradStaniec merged commit 6e3c185 into develop Oct 16, 2020
@KonradStaniec KonradStaniec deleted the etcm-103/restartable-state-sync branch October 16, 2020 12:25
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants