|
|
@ -1,158 +0,0 @@ |
|
|
|
# Design goals |
|
|
|
|
|
|
|
The design goals for Tendermint (and the SDK and related libraries) are: |
|
|
|
|
|
|
|
* Simplicity and Legibility |
|
|
|
* Parallel performance, namely ability to utilize multicore architecture |
|
|
|
* Ability to evolve the codebase bug-free |
|
|
|
* Debuggability |
|
|
|
* Complete correctness that considers all edge cases, esp in concurrency |
|
|
|
* Future-proof modular architecture, message protocol, APIs, and encapsulation |
|
|
|
|
|
|
|
|
|
|
|
## Justification |
|
|
|
|
|
|
|
Legibility is key to maintaining bug-free software as it evolves toward more |
|
|
|
optimizations, more ease of debugging, and additional features. |
|
|
|
|
|
|
|
It is too easy to introduce bugs over time by replacing lines of code with |
|
|
|
those that may panic, which means ideally locks are unlocked by defer |
|
|
|
statements. |
|
|
|
|
|
|
|
For example, |
|
|
|
|
|
|
|
```go |
|
|
|
func (obj *MyObj) something() { |
|
|
|
mtx.Lock() |
|
|
|
obj.something = other |
|
|
|
mtx.Unlock() |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
It is too easy to refactor the codebase in the future to replace `other` with |
|
|
|
`other.String()` for example, and this may introduce a bug that causes a |
|
|
|
deadlock. So as much as reasonably possible, we need to be using defer |
|
|
|
statements, even though it introduces additional overhead. |
|
|
|
|
|
|
|
If it is necessary to optimize the unlocking of mutex locks, the solution is |
|
|
|
more modularity via smaller functions, so that defer'd unlocks are scoped |
|
|
|
within a smaller function. |
|
|
|
|
|
|
|
Similarly, idiomatic for-loops should always be preferred over those that use |
|
|
|
custom counters, because it is too easy to evolve the body of a for-loop to |
|
|
|
become more complicated over time, and it becomes more and more difficult to |
|
|
|
assess the correctness of such a for-loop by visual inspection. |
|
|
|
|
|
|
|
|
|
|
|
## On performance |
|
|
|
|
|
|
|
It doesn't matter whether there are alternative implementations that are 2x or |
|
|
|
3x more performant, when the software doesn't work, deadlocks, or if bugs |
|
|
|
cannot be debugged. By taking advantage of multicore concurrency, the |
|
|
|
Tendermint implementation will at least be an order of magnitude within the |
|
|
|
range of what is theoretically possible. The design philosophy of Tendermint, |
|
|
|
and the choice of Go as implementation language, is designed to make Tendermint |
|
|
|
implementation the standard specification for concurrent BFT software. |
|
|
|
|
|
|
|
By focusing on the message protocols (e.g. ABCI, p2p messages), and |
|
|
|
encapsulation e.g. IAVL module, (relatively) independent reactors, we are both |
|
|
|
implementing a standard implementation to be used as the specification for |
|
|
|
future implementations in more optimizable languages like Rust, Java, and C++; |
|
|
|
as well as creating sufficiently performant software. Tendermint Core will |
|
|
|
never be as fast as future implementations of the Tendermint Spec, because Go |
|
|
|
isn't designed to be as fast as possible. The advantage of using Go is that we |
|
|
|
can develop the whole stack of modular components **faster** than in other |
|
|
|
languages. |
|
|
|
|
|
|
|
Furthermore, the real bottleneck is in the application layer, and it isn't |
|
|
|
necessary to support more than a sufficiently decentralized set of validators |
|
|
|
(e.g. 100 ~ 300 validators is sufficient, with delegated bonded PoS). |
|
|
|
|
|
|
|
Instead of optimizing Tendermint performance down to the metal, lets focus on |
|
|
|
optimizing on other matters, namely ability to push feature complete software |
|
|
|
that works well enough, can be debugged and maintained, and can serve as a spec |
|
|
|
for future implementations. |
|
|
|
|
|
|
|
|
|
|
|
## On encapsulation |
|
|
|
|
|
|
|
In order to create maintainable, forward-optimizable software, it is critical |
|
|
|
to develop well-encapsulated objects that have well understood properties, and |
|
|
|
to re-use these easy-to-use-correctly components as building blocks for further |
|
|
|
encapsulated meta-objects. |
|
|
|
|
|
|
|
For example, mutexes are cheap enough for Tendermint's design goals when there |
|
|
|
isn't goroutine contention, so it is encouraged to create concurrency safe |
|
|
|
structures with struct-level mutexes. If they are used in the context of |
|
|
|
non-concurrent logic, then the performance is good enough. If they are used in |
|
|
|
the context of concurrent logic, then it will still perform correctly. |
|
|
|
|
|
|
|
Examples of this design principle can be seen in the types.ValidatorSet struct, |
|
|
|
and the rand.Rand struct. It's one single struct declaration that can be used |
|
|
|
in both concurrent and non-concurrent logic, and due to its well encapsulation, |
|
|
|
it's easy to get the usage of the mutex right. |
|
|
|
|
|
|
|
### example: rand.Rand |
|
|
|
|
|
|
|
`The default Source is safe for concurrent use by multiple goroutines, but |
|
|
|
Sources created by NewSource are not`. The reason why the default |
|
|
|
package-level source is safe for concurrent use is because it is protected (see |
|
|
|
`lockedSource` in <https://golang.org/src/math/rand/rand.go>). |
|
|
|
|
|
|
|
But we shouldn't rely on the global source, we should be creating our own |
|
|
|
Rand/Source instances and using them, especially for determinism in testing. |
|
|
|
So it is reasonable to have rand.Rand be protected by a mutex. Whether we want |
|
|
|
our own implementation of Rand is another question, but the answer there is |
|
|
|
also in the affirmative. Sometimes you want to know where Rand is being used |
|
|
|
in your code, so it becomes a simple matter of dropping in a log statement to |
|
|
|
inject inspectability into Rand usage. Also, it is nice to be able to extend |
|
|
|
the functionality of Rand with custom methods. For these reasons, and for the |
|
|
|
reasons which is outlined in this design philosophy document, we should |
|
|
|
continue to use the rand.Rand object, with mutex protection. |
|
|
|
|
|
|
|
Another key aspect of good encapsulation is the choice of exposed vs unexposed |
|
|
|
methods. It should be clear to the reader of the code, which methods are |
|
|
|
intended to be used in what context, and what safe usage is. Part of this is |
|
|
|
solved by hiding methods via unexported methods. Another part of this is |
|
|
|
naming conventions on the methods (e.g. underscores) with good documentation, |
|
|
|
and code organization. If there are too many exposed methods and it isn't |
|
|
|
clear what methods have what side effects, then there is something wrong about |
|
|
|
the design of abstractions that should be revisited. |
|
|
|
|
|
|
|
|
|
|
|
## On concurrency |
|
|
|
|
|
|
|
In order for Tendermint to remain relevant in the years to come, it is vital |
|
|
|
for Tendermint to take advantage of multicore architectures. Due to the nature |
|
|
|
of the problem, namely consensus across a concurrent p2p gossip network, and to |
|
|
|
handle RPC requests for a large number of consuming subscribers, it is |
|
|
|
unavoidable for Tendermint development to require expertise in concurrency |
|
|
|
design, especially when it comes to the reactor design, and also for RPC |
|
|
|
request handling. |
|
|
|
|
|
|
|
|
|
|
|
# Guidelines |
|
|
|
|
|
|
|
Here are some guidelines for designing for (sufficient) performance and concurrency: |
|
|
|
|
|
|
|
* Mutex locks are cheap enough when there isn't contention. |
|
|
|
* Do not optimize code without analytical or observed proof that it is in a hot path. |
|
|
|
* Don't over-use channels when mutex locks w/ encapsulation are sufficient. |
|
|
|
* The need to drain channels are often a hint of unconsidered edge cases. |
|
|
|
* The creation of O(N) one-off goroutines is generally technical debt that |
|
|
|
needs to get addressed sooner than later. Avoid creating too many |
|
|
|
goroutines as a patch around incomplete concurrency design, or at least be |
|
|
|
aware of the debt and do not invest in the debt. On the other hand, Tendermint |
|
|
|
is designed to have a limited number of peers (e.g. 10 or 20), so the creation |
|
|
|
of O(C) goroutines per O(P) peers is still O(C\*P=constant). |
|
|
|
* Use defer statements to unlock as much as possible. If you want to unlock sooner, |
|
|
|
try to create more modular functions that do make use of defer statements. |
|
|
|
|
|
|
|
# Mantras |
|
|
|
|
|
|
|
* Premature optimization kills |
|
|
|
* Readability is paramount |
|
|
|
* Beautiful is better than fast. |
|
|
|
* In the face of ambiguity, refuse the temptation to guess. |
|
|
|
* In the face of bugs, refuse the temptation to cover the bug. |
|
|
|
* There should be one-- and preferably only one --obvious way to do it. |