Skip to content

Optimizing blockchain reactor.#1805

Merged
ebuchman merged 4 commits intodevelopfrom
jae/optimize_blockchain
Jul 24, 2018
Merged

Optimizing blockchain reactor.#1805
ebuchman merged 4 commits intodevelopfrom
jae/optimize_blockchain

Conversation

@jaekwon
Copy link
Contributor

@jaekwon jaekwon commented Jun 23, 2018

With this change and cosmos/iavl#65 and tmlibs reverse iterators, the bottleneck I think is the speed of signature validation. Before optimizing that it might make sense to consider a different kind of blockchain syncing strategy, and before that, straight up state syncing.

We should implement state syncing soon after launch. :)

We're still limited to around 100 blocks/sec on the testnet, but state syncing won't be too difficult to implement given RangeProof.

@jaekwon jaekwon requested review from ebuchman, melekes and xla as code owners June 23, 2018 05:01
Copy link
Contributor

@melekes melekes left a comment

Choose a reason for hiding this comment

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

🥇 🏎 🍰

continue FOR_LOOP
} else {
// Try again quickly next loop.
didProcessCh <- struct{}{}
Copy link
Contributor

Choose a reason for hiding this comment

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

This won't lead to deadlock, right? otherwise, optional write (with default) would be here

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it shouldn't since we read an element off the channel to start this block and nothing else writes to the channel except a mutually exclusive block of code. so this should be fine, but it's not immediately obvious

state/store.go Outdated
// Responses are indexed by height so they can also be loaded later to produce Merkle proofs.
func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) {
db.SetSync(calcABCIResponsesKey(height), abciResponses.Bytes())
db.Set(calcABCIResponsesKey(height), abciResponses.Bytes())
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to flush these to disk incase we crash before we get to call SaveState

Copy link
Contributor

Choose a reason for hiding this comment

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

Though I would have thought the persistence tests would have caught this if it was wrong. Hmph.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, we should flush these, cuz otherwise we'll commit to the app and possibly crash, losing the responses.

I think it's difficult to catch this because to catch it, it isn't sufficient to fail after saveABCIResponse(), but we need to also call updateState+blockExec.Commit and then fail (exit) before leveldb finalizes...

I think we can simulate this by implementing a special wrapper around DB that delays .Set() for many seconds until maybe the next *Sync call. I'll make an issue of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ebuchman
Copy link
Contributor

@ValarDragon has indicated that we can cut signature verification time in half by using batch verification.

@milosevic recently wrote a spec for this reactor here: https://github.com/tendermint/tendermint/blob/develop/docs/spec/reactors/block_sync/reactor.md

Also note some previously identified issues in #766

@zmanian
Copy link
Contributor

zmanian commented Jun 24, 2018

I have a somewhat fundamental question here.

Is it optimal that we need information from 2 blocks to verify signatures?

It seems like we could include all the information needed to verify signatures in each block, do signature verification in pool.AddBlock and only checked for consistency of data between block n, block n+1 in the reactor.

This would allow for massive parallelism on signature verification and immediately punish peers who send us bad sigs.

@zmanian zmanian added the C:sync Component: Fast Sync, State Sync label Jun 24, 2018
@ebuchman
Copy link
Contributor

It seems like we could include all the information needed to verify signatures in each block,

This would come at the cost of slowing down consensus and would be a non-trivial change. Currently we consider having seen a commit for block H when we see the +2/3 precommits for the block H, but the commit only becomes canonical and included in the blockchain when its included as the LastCommit of the next block H+1

@zmanian
Copy link
Contributor

zmanian commented Jun 25, 2018

How about if we check in the blockpool when we recieve block n if n-1 is in the pool and if it is we do a signature verification then on n-1 and then have a flag on the block is sig verification passed and then the blockchain reactor just does the remaining verification work.

@milosevic
Copy link
Contributor

@jaekwon You might want to take a look at this issue: #1734. The requester choice strategy seems like not optimal. This might lead to bad utilisation of peer resources during fast sync protocol. Furthermore, there are some design choices in blockchain reactor that might also potentially negatively affect performance:

  1. During processing of bcBlockRequestMessage that is executed in the p2p receive routine, we are reading block from disc and block during that time p2p receive routine.
  2. We are locking global mutex when handling of several requests, creating potentially contention on this lock and potentially also blocking p2p receive routine.

Before optimising blockchain reactor, maybe it make sense to first profile its current performance so we understand bottlenecks so we are able to measure impact of some potential modifications.
Hopefully, the spec https://github.com/tendermint/tendermint/blob/develop/docs/spec/reactors/block_sync/reactor.md can be useful in that case as high level design document.

continue FOR_LOOP
} else {
// Try again quickly next loop.
didProcessCh <- struct{}{}
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it shouldn't since we read an element off the channel to start this block and nothing else writes to the channel except a mutually exclusive block of code. so this should be fine, but it's not immediately obvious


// TimeFormat is used for generating the sigs
const TimeFormat = "2006-01-02T15:04:05.000Z"
const TimeFormat = time.RFC3339Nano
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there some reason for this ? I believe this makes the PR a breaking change, while it would otherwise not be

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consistency. We should use Nano everywhere.


const capacity = 1000 // must be bigger than peers count
requestsCh := make(chan BlockRequest, capacity)
requestsCh := make(chan BlockRequest, maxTotalRequesters)
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 a benefit of this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's just some reasonable constant that is already defined. vs having a magic 1000 in the middle of a function declaration.

@milosevic
Copy link
Contributor

The suggested change brings several issues, but I am not sure that we properly address them with this PR.

  1. Verifying committed blocks is time consuming task and as @jaekwon pointed out in this PR, the logic for verifying blocks, their execution and saving to disk should probably be moved into a separate routine. Checking blocks can maybe be triggered upon new block reception, instead of being time based. There is probably benefit (as mention in this PR) in batching writes to disk and executing blocks against app.
  2. Sending requests for block is in the current version is potentially blocked by other blocks in poolRoutine() function. It is not clear to me what would be the drawback of sending requests directly from Requester task, and also ensuring that request is sent. In the current version, we are using trySend and if buffer is full, request will not be sent.

As there are also several other concerns related to blockchain reactor, I would suggest to follow the new process and first come up with the ADR with a suggested modification, and then follow up with the implementation. I am fine with merging this PR, it makes more sense then the previous version, but we will probably follow up with ADR in one of the next iterations. Maybe it would make sense to clean up this PR before merge as not all suggested changes are related to blockchain reactor optimisation.

const (
requestIntervalMS = 100
maxTotalRequesters = 1000
requestIntervalMS = 2
Copy link
Contributor

Choose a reason for hiding this comment

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

This will effectively mean no sleep in requestRoutine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's OK, it just needs to prevent a hot loop.

Revert to SetSync for saveABCIResponses() as per Ethan's feedback
@codecov-io
Copy link

codecov-io commented Jul 20, 2018

Codecov Report

Merging #1805 into develop will decrease coverage by 0.15%.
The diff coverage is 11.62%.

@@             Coverage Diff             @@
##           develop    #1805      +/-   ##
===========================================
- Coverage     61.5%   61.34%   -0.16%     
===========================================
  Files          197      197              
  Lines        15670    15684      +14     
===========================================
- Hits          9638     9622      -16     
- Misses        5215     5247      +32     
+ Partials       817      815       -2
Impacted Files Coverage Δ
blockchain/pool.go 66.43% <0%> (-3.15%) ⬇️
state/store.go 64.17% <100%> (ø) ⬆️
blockchain/reactor.go 40.43% <8.1%> (-0.58%) ⬇️
p2p/pex/errors.go 25% <0%> (-25%) ⬇️
rpc/grpc/api.go 81.81% <0%> (-7.08%) ⬇️
p2p/pex/pex_reactor.go 72.81% <0%> (-0.68%) ⬇️
p2p/pex/addrbook.go 69.36% <0%> (-0.5%) ⬇️
p2p/peer.go 60.22% <0%> (ø) ⬆️
rpc/core/net.go 0% <0%> (ø) ⬆️
... and 2 more

@jaekwon
Copy link
Contributor Author

jaekwon commented Jul 20, 2018

@zmanian How about if we check in the blockpool when we recieve block n if n-1 is in the pool and if it is we do a signature verification then on n-1 and then have a flag on the block is sig verification passed and then the blockchain reactor just does the remaining verification work.

That seems like a great idea. Given the interest of time, lets work on this (or maybe other designs) after launch.

@milosevic During processing of bcBlockRequestMessage that is executed in the p2p receive routine, we are reading block from disc and block during that time p2p receive routine.

Great point, that probably makes a significant difference, though I don't know how much.

@milosevic We are locking global mutex when handling of several requests, creating potentially contention on this lock and potentially also blocking p2p receive routine.

There's definitely lock contention, though it isn't clear how bad it is. For this and the above point, I agree we should profile and get a clear picture of current performance and document the procedure, as it will help us understand how to optimize code elsewhere as well.

I think for now we can merge this PR, and make another issue to address the great ideas in the comments here.

@jaekwon
Copy link
Contributor Author

jaekwon commented Jul 20, 2018

Also, so I don't forget, another idea for optimization is to use the LastBlockHash to verify a chain of blocks without having to verify signatures for every block. This idea shouldn't be implemented lightly, as it might have consequences for PoS security, possibly DDoS attack vectors (depending on how it's implemented), peer selection strategy (it might make more sense to request a contiguous range from a single peer), etc.

@ebuchman ebuchman merged commit b92860b into develop Jul 24, 2018
@ebuchman ebuchman deleted the jae/optimize_blockchain branch July 24, 2018 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C:sync Component: Fast Sync, State Sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants