In progress of updating README.
This commit is contained in:
parent
49b9c17dd4
commit
1348ee4527
2 changed files with 293 additions and 87 deletions
331
README.md
331
README.md
|
@ -1,24 +1,23 @@
|
||||||
Quorums
|
Quoracle
|
||||||
=======
|
========
|
||||||
|
|
||||||
## Installation
|
Quoracle is a library for constructing and analyzing [read-write quorum
|
||||||
TODO(mwhittaker): Make this package pip'able. For now, you have to clone and
|
systems](https://scholar.google.com/scholar?cluster=4847365665094368145). Run
|
||||||
install the dependencies yourself:
|
`pip install quoracle` and then follow along with the tutorial below to get
|
||||||
|
started.
|
||||||
|
|
||||||
```
|
## Quorum Systems
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tutorial
|
|
||||||
Given a set of nodes `X`, a _read-write quorum system_ is a pair `(R, W)` where
|
Given a set of nodes `X`, a _read-write quorum system_ is a pair `(R, W)` where
|
||||||
`R` is a set of subsets of `X` called _read quorums_ and `W` is a set of
|
|
||||||
subsets of `X` called _write quorums_. A read-write quorum system satisfies the
|
1. `R` is a set of subsets of `X` called _read quorums_,
|
||||||
property that every read quorum intersects every write quorum. This library
|
2. `W` is a set of subsets of `X` called _write quorums_, and
|
||||||
allows us to construct and analyze arbitrary read-write quorum systems. First,
|
3. every read quorum intersects every write quorum.
|
||||||
we import the library.
|
|
||||||
|
quoracle allows us to construct and analyze arbitrary read-write quorum
|
||||||
|
systems. First, we import the library.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from quorums import *
|
from quoracle import *
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, we specify the nodes in our quorum system. Our nodes can be strings,
|
Next, we specify the nodes in our quorum system. Our nodes can be strings,
|
||||||
|
@ -33,7 +32,7 @@ e = Node('e')
|
||||||
f = Node('f')
|
f = Node('f')
|
||||||
```
|
```
|
||||||
|
|
||||||
Here, we construct a two by three grid of nodes. Every row is read quorum, and
|
Now, we construct a two by three grid of nodes. Every row is read quorum, and
|
||||||
one element from every row is a write quorum. Note that when we construct a
|
one element from every row is a write quorum. Note that when we construct a
|
||||||
quorum system, we only have to specify the set of read quorums. The library
|
quorum system, we only have to specify the set of read quorums. The library
|
||||||
figures out the optimal set of write quorums automatically.
|
figures out the optimal set of write quorums automatically.
|
||||||
|
@ -42,32 +41,44 @@ figures out the optimal set of write quorums automatically.
|
||||||
grid = QuorumSystem(reads=a*b*c + d*e*f)
|
grid = QuorumSystem(reads=a*b*c + d*e*f)
|
||||||
```
|
```
|
||||||
|
|
||||||
This prints `{'a', 'b', 'c'}` and `{'d', 'e', 'f'}`.
|
This next code snippet prints out the read quorums `{'a', 'b', 'c'}` and `{'d',
|
||||||
|
'e', 'f'}`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
for r in grid.read_quorums():
|
for r in grid.read_quorums():
|
||||||
print(r)
|
print(r)
|
||||||
```
|
```
|
||||||
|
|
||||||
This prints `{'a', 'd'}`, `{'a', 'e'}`, `{'b', 'f'}`, `{'b', 'd'}`, ...
|
And this next code snippet prints out the write quorums `{'a', 'd'}`, `{'a',
|
||||||
|
'e'}`, `{'b', 'f'}`, `{'b', 'd'}`, ...
|
||||||
|
|
||||||
```python
|
```python
|
||||||
for w in grid.write_quorums():
|
for w in grid.write_quorums():
|
||||||
print(w)
|
print(w)
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, we could specify the write quorums...
|
Alternatively, we can construct a quorum system be specifying the write
|
||||||
|
quorums.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
QuorumSystem(writes=(a + b + c) * (d + e + f))
|
QuorumSystem(writes=(a + b + c) * (d + e + f))
|
||||||
```
|
```
|
||||||
|
|
||||||
or both the read and write quorums.
|
Or, we can specify both the read and write quorums.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
QuorumSystem(reads=a*b*c + d*e*f, writes=(a + b + c) * (d + e + f))
|
QuorumSystem(reads=a*b*c + d*e*f, writes=(a + b + c) * (d + e + f))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
But, remember that every read quorum must intersect every write quorum. If we
|
||||||
|
try to construct a quorum system with non-overlapping quorums, an exception
|
||||||
|
will be thrown.
|
||||||
|
|
||||||
|
```python
|
||||||
|
QuorumSystem(reads=a+b+c, writes=d+e+f)
|
||||||
|
# ValueError: Not all read quorums intersect all write quorums
|
||||||
|
```
|
||||||
|
|
||||||
We can check whether a given set is a read or write quorum. Note that any
|
We can check whether a given set is a read or write quorum. Note that any
|
||||||
superset of a quorum is also considered a quorum.
|
superset of a quorum is also considered a quorum.
|
||||||
|
|
||||||
|
@ -81,10 +92,19 @@ grid.is_write_quorum({'a', 'd', 'd'}) # True
|
||||||
grid.is_write_quorum({'a', 'b'}) # False
|
grid.is_write_quorum({'a', 'b'}) # False
|
||||||
```
|
```
|
||||||
|
|
||||||
The read resilience of our quorum system is the largest number `f` such that
|
## Resilience
|
||||||
|
The _read resilience_ of our quorum system is the largest number `f` such that
|
||||||
despite the failure of any `f` nodes, we still have at least one read quorum.
|
despite the failure of any `f` nodes, we still have at least one read quorum.
|
||||||
Write resilience is defined similarly, and resilience is the minimum of read
|
_Write resilience_ is defined similarly, and _resilience_ is the minimum of
|
||||||
and write resilience.
|
read and write resilience.
|
||||||
|
|
||||||
|
Here, we print out the read resilience, write resilience, and resilience of our
|
||||||
|
grid quorum system. We can fail any one node and still have a read quorum, but
|
||||||
|
if we fail one node from each row, we eliminate every read quorum, so the read
|
||||||
|
resilience is 1. Similarly, we can fail any two nodes and still have a write
|
||||||
|
quorum, but if we fail one node from every column, we eliminate every write
|
||||||
|
quorum, so our write resilience is 1. The resilience is the minimum of 1 and 2,
|
||||||
|
which is 1.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
grid.read_resilience() # 1
|
grid.read_resilience() # 1
|
||||||
|
@ -92,84 +112,226 @@ grid.write_resilience() # 2
|
||||||
grid.resilience() # 1
|
grid.resilience() # 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Strategies
|
||||||
A _strategy_ is a discrete probability distribution over the set of read and
|
A _strategy_ is a discrete probability distribution over the set of read and
|
||||||
write quorums. A strategy gives us a way to pick quorums at random. The load of
|
write quorums. A strategy gives us a way to pick quorums at random. We'll see
|
||||||
a node is the probability that the node is selected by the strategy, and the
|
how to construct optimal strategies in a second, but for now, we'll construct a
|
||||||
load of a strategy is the load of the most heavily loaded node. Using the
|
strategy by hand. To do so, we have to provide a probability distribution over
|
||||||
`strategy` method, we get a load-optimal strategy, i.e. the strategy with the
|
the read quorums and a probability distribution over the write quorums. Here,
|
||||||
lowest possible load.
|
we'll pick the top row twice as often as the bottom row, and we'll pick each
|
||||||
|
column uniformly at random. Note that when we specify a probability
|
||||||
|
distribution, we don't have to provide exact probabilities. We can simply pass
|
||||||
|
in weights, and the library will automatically normalize the weights into a
|
||||||
|
valid probability distribution.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# The read quorum strategy.
|
||||||
|
sigma_r = {
|
||||||
|
frozenset({'a', 'b', 'c'}): 2.,
|
||||||
|
frozenset({'d', 'e', 'f'}): 1.,
|
||||||
|
}
|
||||||
|
|
||||||
|
# The write quorum strategy.
|
||||||
|
sigma_w = {
|
||||||
|
frozenset({'a', 'd'}): 1.,
|
||||||
|
frozenset({'b', 'e'}): 1.,
|
||||||
|
frozenset({'c', 'f'}): 1.,
|
||||||
|
}
|
||||||
|
strategy = grid.make_strategy(sigma_r, sigma_w)
|
||||||
|
```
|
||||||
|
|
||||||
|
Once we have a strategy, we can use it to sample read and write quorums. Here,
|
||||||
|
we expect `get_read_quorum` to return the top row twice as often as the bottom
|
||||||
|
row, and we expect `get_write_quorum` to return every column uniformly at
|
||||||
|
random.
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(strategy.get_read_quorum())
|
||||||
|
print(strategy.get_read_quorum())
|
||||||
|
print(strategy.get_read_quorum())
|
||||||
|
print(strategy.get_read_quorum())
|
||||||
|
print(strategy.get_write_quorum())
|
||||||
|
print(strategy.get_write_quorum())
|
||||||
|
print(strategy.get_write_quorum())
|
||||||
|
print(strategy.get_write_quorum())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Load and Capacity
|
||||||
Typically in a distributed system, a read quorum of nodes is contacted to
|
Typically in a distributed system, a read quorum of nodes is contacted to
|
||||||
perform a read, and a write quorum of nodes is contacted to perform a write.
|
perform a read, and a write quorum of nodes is contacted to perform a write.
|
||||||
Though we get to pick a strategy, we don't get to pick the fraction of
|
Assume we have a workload with a _read fraction_ `fr` of reads and a _write
|
||||||
operations that are reads and the fraction of operations that are writes. This
|
fraction_ `fw = 1 - fr` of writes. Given a strategy, the _load of a node_ is
|
||||||
is determined by the workload. When constructing a strategy, we have to specify
|
the probability that the node is selected by the strategy. The _load of a
|
||||||
the workload. The returned strategy is optimal only against this workload.
|
strategy_ is the load of the most heavily loaded node. The _load of a quorum
|
||||||
Here, we construct a strategy assuming that 75% of all operations are reads.
|
system_ is the load of the optimal strategy, i.e. the strategy that achieves
|
||||||
|
the lowest load. The most heavily loaded node in a quorum system is a
|
||||||
|
throughput bottleneck, so the lower the load the better.
|
||||||
|
|
||||||
|
Let's calculate the load of our strategy assuming a 100% read workload (i.e. a
|
||||||
|
workload with a read fraction of 1).
|
||||||
|
|
||||||
|
- The load of `a` is 2/3 because the read quorum `{a, b, c}` is chosen 2/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `b` is 2/3 because the read quorum `{a, b, c}` is chosen 2/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `c` is 2/3 because the read quorum `{a, b, c}` is chosen 2/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `d` is 1/3 because the read quorum `{d, e, f}` is chosen 2/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `e` is 1/3 because the read quorum `{d, e, f}` is chosen 2/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `f` is 1/3 because the read quorum `{d, e, f}` is chosen 2/3 of
|
||||||
|
the time.
|
||||||
|
|
||||||
|
The largest node load is 2/3, so our strategy has a load of 2/3. Rather than
|
||||||
|
calculating load by hand, we can simply call the `load` function.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
strategy = grid.strategy(read_fraction=0.75)
|
print(strategy.load(read_fraction=1)) # 2/3
|
||||||
```
|
```
|
||||||
|
|
||||||
We can use the strategy to sample read and write quorums.
|
Now let's calculate the load of our strategy assuming a 100% write workload.
|
||||||
|
Again, we calculate the load on every node.
|
||||||
|
|
||||||
|
- The load of `a` is 1/3 because the write quorum `{a, d}` is chosen 1/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `b` is 1/3 because the write quorum `{b, e}` is chosen 1/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `c` is 1/3 because the write quorum `{c, f}` is chosen 1/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `d` is 1/3 because the write quorum `{a, d}` is chosen 1/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `e` is 1/3 because the write quorum `{b, e}` is chosen 1/3 of
|
||||||
|
the time.
|
||||||
|
- The load of `f` is 1/3 because the write quorum `{c, f}` is chosen 1/3 of
|
||||||
|
the time.
|
||||||
|
|
||||||
|
The largest node load is 1/3, so our strategy has a load of 1/3. Again, rather
|
||||||
|
than calculating load by hand, we can simply call the `load` function. Note
|
||||||
|
that we can pass in a `read_fraction` or `write_fraction` but not both.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
print(strategy.get_read_quorum())
|
print(strategy.load(write_fraction=1)) # 1/3
|
||||||
print(strategy.get_read_quorum())
|
|
||||||
print(strategy.get_read_quorum())
|
|
||||||
print(strategy.get_write_quorum())
|
|
||||||
print(strategy.get_write_quorum())
|
|
||||||
print(strategy.get_write_quorum())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
We can query the strategy's load.
|
Now let's calculate the load of our strategy on a 25% read and 75% write
|
||||||
|
workload.
|
||||||
|
|
||||||
|
- The load of `a` is `0.25 * 2/3 + 0.75 * 1/3 = 5/12` because 25% of the time
|
||||||
|
we perform a read and select the read quorum `{a, b, c}` with 2/3 probability
|
||||||
|
and 75% of the time, we perform a write and select the write quorum `{a, d}`
|
||||||
|
with 1/3 probability.
|
||||||
|
- The load of `b` is `0.25 * 2/3 + 0.75 * 1/3 = 5/12` because 25% of the time
|
||||||
|
we perform a read and select the read quorum `{a, b, c}` with 2/3 probability
|
||||||
|
and 75% of the time, we perform a write and select the write quorum `{b, e}`
|
||||||
|
with 1/3 probability.
|
||||||
|
- The load of `c` is `0.25 * 2/3 + 0.75 * 1/3 = 5/12` because 25% of the time
|
||||||
|
we perform a read and select the read quorum `{a, b, c}` with 2/3 probability
|
||||||
|
and 75% of the time, we perform a write and select the write quorum `{c, f}`
|
||||||
|
with 1/3 probability.
|
||||||
|
- The load of `d` is `0.25 * 1/3 + 0.75 * 1/3 = 1/3` because 25% of the time
|
||||||
|
we perform a read and select the read quorum `{d, e, f}` with 2/3 probability
|
||||||
|
and 75% of the time, we perform a write and select the write quorum `{a, d}`
|
||||||
|
with 1/3 probability.
|
||||||
|
- The load of `e` is `0.25 * 1/3 + 0.75 * 1/3 = 1/3` because 25% of the time
|
||||||
|
we perform a read and select the read quorum `{d, e, f}` with 2/3 probability
|
||||||
|
and 75% of the time, we perform a write and select the write quorum `{b, e}`
|
||||||
|
with 1/3 probability.
|
||||||
|
- The load of `f` is `0.25 * 1/3 + 0.75 * 1/3 = 1/3` because 25% of the time
|
||||||
|
we perform a read and select the read quorum `{d, e, f}` with 2/3 probability
|
||||||
|
and 75% of the time, we perform a write and select the write quorum `{c, f}`
|
||||||
|
with 1/3 probability.
|
||||||
|
|
||||||
|
The largest node load is 5/12, so our strategy has a load of 5/12. At this
|
||||||
|
point, you can see that calculating load by hand is extremely tedious. We could
|
||||||
|
have skipped all that work and called `load` instead!
|
||||||
|
|
||||||
```python
|
```python
|
||||||
strategy.load(read_fraction=0.75) # 0.458
|
print(strategy.load(read_fraction=0.25)) # 5/12
|
||||||
```
|
```
|
||||||
|
|
||||||
We can query the strategy's load on other workloads as well, though the
|
We can also compute the load on every node.
|
||||||
strategy may not be optimal.
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
strategy.load(read_fraction=0) # 0.333
|
print(strategy.node_load(a, read_fraction=0.25)) # 5/12
|
||||||
strategy.load(read_fraction=0.5) # 0.416
|
print(strategy.node_load(b, read_fraction=0.25)) # 5/12
|
||||||
strategy.load(read_fraction=1) # 0.5
|
print(strategy.node_load(c, read_fraction=0.25)) # 5/12
|
||||||
|
print(strategy.node_load(d, read_fraction=0.25)) # 1/3
|
||||||
|
print(strategy.node_load(e, read_fraction=0.25)) # 1/3
|
||||||
|
print(strategy.node_load(f, read_fraction=0.25)) # 1/3
|
||||||
```
|
```
|
||||||
|
|
||||||
This is a shorthand for
|
Our strategy has a load of 5/12 on a 25% read workload, but what about the
|
||||||
`grid.strategy(read_fraction=0.25).load(read_fraction=0.25)`.
|
quorum system? The quorum system does __not__ have a load of 5/12 because our
|
||||||
|
strategy is not optimal. We can call the `strategy` function to compute the
|
||||||
|
optimal strategy automatically.
|
||||||
|
|
||||||
|
```python
|
||||||
|
strategy = grid.strategy(read_fraction=0.25)
|
||||||
|
print(strategy)
|
||||||
|
# Strategy(reads={('a', 'b', 'c'): 0.5,
|
||||||
|
# ('d', 'e', 'f'): 0.5},
|
||||||
|
# writes={('a', 'f'): 0.33333333,
|
||||||
|
# ('b', 'e'): 0.33333333,
|
||||||
|
# ('c', 'd'): 0.33333333})
|
||||||
|
print(strategy.load(read_fraction=0.25)) # 3/8
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, we see that the optimal strategy picks all rows and all columns
|
||||||
|
uniformly. This strategy has a load of 3/8 on the 25% read workload. Since this
|
||||||
|
strategy is optimal, that means our quorum system also has a load of 3/8 on a
|
||||||
|
25% workload.
|
||||||
|
|
||||||
|
We can also query this strategy's load on other workloads as well. Note that
|
||||||
|
this strategy is optimal for a read fraction of 25%, but it may not be optimal
|
||||||
|
for other read fractions.
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(strategy.load(read_fraction=0)) # 1/3
|
||||||
|
print(strategy.load(read_fraction=0.5)) # 5/12
|
||||||
|
print(strategy.load(read_fraction=1)) # 1/2
|
||||||
|
```
|
||||||
|
|
||||||
|
We can also use a quorum system's `load` function. The code snippet below is a
|
||||||
|
shorthand for `grid.strategy(read_fraction=0.25).load(read_fraction=0.25)`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
grid.load(read_fraction=0.25) # 0.375
|
grid.load(read_fraction=0.25) # 0.375
|
||||||
```
|
```
|
||||||
|
|
||||||
In the real world, we don't often have a fixed workload. Workloads change
|
The capacity of strategy or quorum is simply the inverse of the load. Our
|
||||||
over time. Instead of specifying a fixed read fraction, we can provide a
|
quorum system has a load of 3/8 on a 25% read workload, so it has a capacity of
|
||||||
discrete probability distribution of read fractions. Here, we say that the
|
8/3.
|
||||||
read fraction is 10% half the time and 75% half the time. `strategy` will
|
|
||||||
return the strategy that minimizes the expected load according to this
|
```python
|
||||||
|
print(grid.capacity(read_fraction=0.25)) # 8/3
|
||||||
|
```
|
||||||
|
|
||||||
|
The _capacity_ of a quorum system is proportional to the maximum throughput
|
||||||
|
that it can achieve before a node becomes bottlenecked. Here, if every node
|
||||||
|
could process 100 commands per second, then our quorum system could process
|
||||||
|
800/3 commands per second.
|
||||||
|
|
||||||
|
## Workload Distributions
|
||||||
|
In the real world, we don't often have a workload with a fixed read fraction.
|
||||||
|
Workloads change over time. Instead of specifying a fixed read fraction, we can
|
||||||
|
provide a discrete probability distribution of read fractions. Here, we say
|
||||||
|
that the read fraction is 10% half the time and 75% half the time. `strategy`
|
||||||
|
will return the strategy that minimizes the expected load according to this
|
||||||
distribution.
|
distribution.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
distribution = {0.1: 0.5, 0.75: 0.5}
|
distribution = {0.1: 1, 0.75: 1}
|
||||||
strategy = grid.strategy(read_fraction=distribution)
|
strategy = grid.strategy(read_fraction=distribution)
|
||||||
strategy.load(read_fraction=distribution) # 0.404
|
strategy.load(read_fraction=distribution) # 0.404
|
||||||
```
|
```
|
||||||
|
|
||||||
We can also specify the write fraction instead of the read fraction, if we
|
## Heterogeneous Node
|
||||||
prefer.
|
|
||||||
|
|
||||||
```python
|
|
||||||
strategy = grid.strategy(write_fraction=0.75)
|
|
||||||
strategy.load(write_fraction=distribution) # 0.429
|
|
||||||
```
|
|
||||||
|
|
||||||
In the real world, not all nodes are equal. We often run distributed systems on
|
In the real world, not all nodes are equal. We often run distributed systems on
|
||||||
heterogenous hardware, so some nodes might be faster than others. To model
|
heterogeneous hardware, so some nodes might be faster than others. To model
|
||||||
this, we instatiate every node with its capacity. Here, nodes a, c, and e can
|
this, we instantiate every node with its capacity. Here, nodes `a`, `c`, and
|
||||||
process 1000 commands per second, while nodes b, d, and f can only process 500
|
`e` can process 1000 commands per second, while nodes `b`, `d`, and `f` can
|
||||||
requests per second.
|
only process 500 requests per second.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
a = Node('a', capacity=1000)
|
a = Node('a', capacity=1000)
|
||||||
|
@ -180,10 +342,10 @@ e = Node('e', capacity=1000)
|
||||||
f = Node('f', capacity=500)
|
f = Node('f', capacity=500)
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, load can be interpreted as the inverse of the peak throughput of the
|
Now, the definition of capacity becomes much simpler. The capacity of a quorum
|
||||||
quorum system. We can also call `capacity` to get this inverse directly.
|
system is simply the maximum throughput that it can achieve. The load can be
|
||||||
Here, our quorum system is capable of processing 1333 commands per second for
|
interpreted as the inverse of the capacity. Here, our quorum system is capable
|
||||||
a workload of 75% reads.
|
of processing 1333 commands per second for a workload of 75% reads.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
grid = QuorumSystem(reads=a*b*c + d*e*f)
|
grid = QuorumSystem(reads=a*b*c + d*e*f)
|
||||||
|
@ -215,17 +377,18 @@ grid.capacity(read_fraction=0.5) # 3913
|
||||||
grid.capacity(read_fraction=0) # 2000
|
grid.capacity(read_fraction=0) # 2000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# `f`-resilient Strategies
|
||||||
Another real world complication is the fact that machines sometimes fail and
|
Another real world complication is the fact that machines sometimes fail and
|
||||||
are sometimes slow. If we contact a quorum of nodes, some of them may fail, and
|
are sometimes slow. If we contact a quorum of nodes, some of them may fail, and
|
||||||
we'll get stuck waiting to hear back from them. Or, some of them may be
|
we'll get stuck waiting to hear back from them. Or, some of them may be
|
||||||
stragglers, and we'll wait longer than we'd like. We can address this problem
|
stragglers, and we'll wait longer than we'd like. We can address this problem
|
||||||
by contacting more than the bare minimum number of nodes.
|
by contacting more than the bare minimum number of nodes.
|
||||||
|
|
||||||
Formally, we say a read quorum (or write quorum) q is _f-resilient_ if despite
|
Formally, we say a read quorum (or write quorum) q is _`f`-resilient_ if
|
||||||
the failure of any f nodes, q still forms a read quorum (or write quorum). A
|
despite the failure of any `f` nodes, q still forms a read quorum (or write
|
||||||
strategy is f-resilient if it only selects f-resilient quorums. By default,
|
quorum). A strategy is `f`-resilient if it only selects `f`-resilient quorums.
|
||||||
`strategy` returns 0-resilient quorums. We can pass in the `f` argument to get
|
By default, `strategy` returns `0`-resilient quorums. We can pass in the `f`
|
||||||
more resilient strategies.
|
argument to get more resilient strategies.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
strategy = grid.strategy(read_fraction=0.5, f=1)
|
strategy = grid.strategy(read_fraction=0.5, f=1)
|
||||||
|
@ -238,6 +401,18 @@ strategy.get_read_quorum()
|
||||||
strategy.get_write_quorum()
|
strategy.get_write_quorum()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Latency
|
||||||
|
TODO(mwhittaker): Write.
|
||||||
|
|
||||||
|
## Network Load
|
||||||
|
TODO(mwhittaker): Write.
|
||||||
|
|
||||||
|
## Search
|
||||||
|
TODO(mwhittaker): Write.
|
||||||
|
|
||||||
|
## Case Study
|
||||||
|
TODO(mwhittaker): Update.
|
||||||
|
|
||||||
Putting everything together, we can use this library to pick quorum systems
|
Putting everything together, we can use this library to pick quorum systems
|
||||||
that are well suited to our workload. For example, say we're implementing a
|
that are well suited to our workload. For example, say we're implementing a
|
||||||
distributed file system and want to pick a 5 node quorum system with a
|
distributed file system and want to pick a 5 node quorum system with a
|
||||||
|
|
|
@ -19,6 +19,9 @@ QuorumSystem(writes=(a + b + c) * (d + e + f))
|
||||||
|
|
||||||
QuorumSystem(reads=a*b*c + d*e*f, writes=(a + b + c) * (d + e + f))
|
QuorumSystem(reads=a*b*c + d*e*f, writes=(a + b + c) * (d + e + f))
|
||||||
|
|
||||||
|
# QuorumSystem(reads=a+b+c, writes=d+e+f)
|
||||||
|
# ValueError: Not all read quorums intersect all write quorums
|
||||||
|
|
||||||
print(grid.is_read_quorum({'a', 'b', 'c'})) # True
|
print(grid.is_read_quorum({'a', 'b', 'c'})) # True
|
||||||
print(grid.is_read_quorum({'a', 'b', 'c', 'd'})) # True
|
print(grid.is_read_quorum({'a', 'b', 'c', 'd'})) # True
|
||||||
print(grid.is_read_quorum({'a', 'b', 'd'})) # False
|
print(grid.is_read_quorum({'a', 'b', 'd'})) # False
|
||||||
|
@ -31,30 +34,58 @@ print(grid.read_resilience()) # 1
|
||||||
print(grid.write_resilience()) # 2
|
print(grid.write_resilience()) # 2
|
||||||
print(grid.resilience()) # 1
|
print(grid.resilience()) # 1
|
||||||
|
|
||||||
strategy = grid.strategy(read_fraction=0.75)
|
# The read quorum strategy.
|
||||||
|
sigma_r = {
|
||||||
|
frozenset({'a', 'b', 'c'}): 2.,
|
||||||
|
frozenset({'d', 'e', 'f'}): 1.,
|
||||||
|
}
|
||||||
|
|
||||||
|
# The write quorum strategy.
|
||||||
|
sigma_w = {
|
||||||
|
frozenset({'a', 'd'}): 1.,
|
||||||
|
frozenset({'b', 'e'}): 1.,
|
||||||
|
frozenset({'c', 'f'}): 1.,
|
||||||
|
}
|
||||||
|
strategy = grid.make_strategy(sigma_r, sigma_w)
|
||||||
|
|
||||||
|
print(strategy.get_read_quorum())
|
||||||
print(strategy.get_read_quorum())
|
print(strategy.get_read_quorum())
|
||||||
print(strategy.get_read_quorum())
|
print(strategy.get_read_quorum())
|
||||||
print(strategy.get_read_quorum())
|
print(strategy.get_read_quorum())
|
||||||
print(strategy.get_write_quorum())
|
print(strategy.get_write_quorum())
|
||||||
print(strategy.get_write_quorum())
|
print(strategy.get_write_quorum())
|
||||||
print(strategy.get_write_quorum())
|
print(strategy.get_write_quorum())
|
||||||
|
print(strategy.get_write_quorum())
|
||||||
|
|
||||||
print(strategy.load(read_fraction=0.75)) # 0.458
|
print(strategy.load(read_fraction=1)) # 2/3
|
||||||
|
|
||||||
print(strategy.load(read_fraction=0)) # 0.333
|
print(strategy.load(write_fraction=1)) # 1/3
|
||||||
print(strategy.load(read_fraction=0.5)) # 0.416
|
|
||||||
print(strategy.load(read_fraction=1)) # 0.5
|
|
||||||
|
|
||||||
print(grid.load(read_fraction=0.25)) # 0.375
|
print(strategy.load(read_fraction=0.25)) # 5/12
|
||||||
|
|
||||||
|
print(strategy.node_load(a, read_fraction=0.25)) # 5/12
|
||||||
|
print(strategy.node_load(b, read_fraction=0.25)) # 5/12
|
||||||
|
print(strategy.node_load(c, read_fraction=0.25)) # 5/12
|
||||||
|
print(strategy.node_load(d, read_fraction=0.25)) # 1/3
|
||||||
|
print(strategy.node_load(e, read_fraction=0.25)) # 1/3
|
||||||
|
print(strategy.node_load(f, read_fraction=0.25)) # 1/3
|
||||||
|
|
||||||
|
strategy = grid.strategy(read_fraction=0.25)
|
||||||
|
print(strategy)
|
||||||
|
print(strategy.load(read_fraction=0.25)) # 3/8
|
||||||
|
|
||||||
|
print(strategy.load(read_fraction=0)) # 1/3
|
||||||
|
print(strategy.load(read_fraction=0.5)) # 5/12
|
||||||
|
print(strategy.load(read_fraction=1)) # 1/2
|
||||||
|
|
||||||
|
print(grid.load(read_fraction=0.25)) # 3/8
|
||||||
|
|
||||||
|
print(grid.capacity(read_fraction=0.25)) # 8/3
|
||||||
|
|
||||||
distribution = {0.1: 0.5, 0.75: 0.5}
|
distribution = {0.1: 0.5, 0.75: 0.5}
|
||||||
strategy = grid.strategy(read_fraction=distribution)
|
strategy = grid.strategy(read_fraction=distribution)
|
||||||
print(strategy.load(read_fraction=distribution)) # 0.404
|
print(strategy.load(read_fraction=distribution)) # 0.404
|
||||||
|
|
||||||
strategy = grid.strategy(write_fraction=0.75)
|
|
||||||
print(strategy.load(write_fraction=distribution)) # 0.429
|
|
||||||
|
|
||||||
a = Node('a', capacity=1000)
|
a = Node('a', capacity=1000)
|
||||||
b = Node('b', capacity=500)
|
b = Node('b', capacity=500)
|
||||||
c = Node('c', capacity=1000)
|
c = Node('c', capacity=1000)
|
||||||
|
|
Loading…
Reference in a new issue