This appendix provides more detailed technical documentation about using a Zookeeper with a Spring State Machine.
Introducing a distributed state
on top of a single state machine
instance running on a single jvm
is a difficult and a complex topic.
Distributed State Machine
is introducing a few relatively complex
problems on top of a simple state machine due to its run-to-completion
model and generally because of its single thread execution model,
though orthogonal regions can be executed parallel. One other natural
problem is that a state machine transition execution is driven by triggers
which are either event
or timer
based.
Distributed Spring State Machine
is trying to solve problem of spanning
a generic State Machine
through a jvm boundary. Here we show that a generic
State Machine
concepts can be used in multiple jvm’s
and Spring
Application Contexts
.
We found that if Distributed State Machine
abstraction is carefully chosen
and backing distributed state repository guarantees CP
readiness, it is
possible to create a consistent state machine which is able to share
distributed state among other state machines in an ensemble.
Our results demonstrate that distributed state changes are consistent if backing
repository is CP
. We anticipate our distributed state machine to provide
a foundation to applications which need to work with a shared distributed
states. This model aims to provide a good methods for cloud applications
to have much easier ways to communicate with each others without having
a need to explicitly build these distributed state concepts.
Spring State Machine is not forced to use a single threaded execution model because once multiple regions are uses, regions can be executed parallel if necessary configuration is applied. This is an important topic because once user wants to have a parallel state machine execution it will make state changes faster for independent regions.
When state changes are no longer driven by a trigger in a local jvm or local state machine instance, transition logic needs to be controlled externally in an arbitrary persistent storage. This storage needs to have a ways to notify participating state machines when distributed state is changed.
CAP Theorem states that
"it is impossible for a distributed computer system to simultaneously
provide all three of the following guarantees, consistency
,
availability
and partition tolerance
". What this means is that
whatever is chosen for a backing persistence storage is it advisable
to be CP
. In this context CP
means consistency
and partition
tolerance
. Naturally Distributed Spring Statemachine
doesn’t care
about what is its CAP
level but in reality consistency
and
partition tolerance
are more important than availability
. This is
an exact reason why i.e. Zookeeper
is a CP
storage.
All tests presented in this article are accomplished by running custom
jepsen
tests in a following environment:
Zookeeper
instance constructing an ensemble with
all other nodes.
Zookeeper
node.
Zookeeper
instance. While connecting machine to multiple instances
is possible, it is not used here.
StateMachineEnsemble
using Zookeeper
ensemble.
jepsen
will use to send
events and check particular state machine statuses.
All jepsen tests for Spring Distributed Statemachine
are available from
Jepsen
Tests.
One design decision of a Distributed State Machine
was not to make
individual State Machine
instance aware of that it is part of a
distributed ensemble
. Because main functions and features of a
StateMachine
can be accessed via its interface, it makes sense to
wrap this instance using a DistributedStateMachine
, which simply
intercepts all state machine communication and collaborates with an
ensemble to orchestrate distributed state changes.
One other important concept is to be able to persist enough
information from a state machine order to reset a state machine state
from arbitrary state into a new deserialized state. This is naturally
needed when a new state machine instance is joining with an ensemble
and it needs to synchronize its own internal state with a distributed
state. Together with using concepts of distributed states and state
persisting it is possible to create a distributed state machine.
Currently only backing repository of a Distributed State Machine
is
implemented using a Zookeeper
.
As mentioned in Chapter 29, Using Distributed States distributed states are enabled by
wrapping an instance of a StateMachine
within a
DistributedStateMachine
. Specific StateMachineEnsemble
implementation is ZookeeperStateMachineEnsemble
providing
integration with a Zookeeper
.
We wanted to have a generic interface StateMachinePersist
which is
able to persist StateMachineContext
into an arbitrary storage and
ZookeeperStateMachinePersist
is implementing this interface for a
Zookeeper
.
While distributed state machine is using one set of serialized
contexts to update its own state, with zookeeper we’re having a
conceptual problem how these context changes can be listened. We’re
able to serialize context into a zookeeper znode
and eventually
listen when znode
data is modified. However Zookeeper
doesn’t
guarantee that you will get notification for every data change
because registered watcher
for a znode
is disabled once it fires
and user need to re-register that watcher
. During this short time
a znode
data can be changed thus resulting missing events. It is
actually very easy to miss these events by just changing data from a
multiple threads in a concurrent manner.
Order to overcome this issue we’re keeping individual context changes
in a multiple znodes
and we just use a simple integer counter to mark
which znode
is a current active one. This allows us to replay missed
events. We don’t want to create more and more znodes and then later
delete old ones, instead we’re using a simple concept of a circular
set of znodes. This allows to use predefined set of znodes where
a current can be determined with a simple integer counter. We already have
this counter by tracking main znode
data version which in
Zookeeper
is
an integer.
Size of a circular buffer is mandated to be a power of two not to get trouble when integer is going to overflow thus we don’t need to handle any specific cases.
Order to show how a various distributed actions against a state
machine work in a real life, we’re using a set of jepsen
tests to
simulate various conditions which may happen in a real distributed
cluster. These include a brain split
on a network level, parallel
events with a multiple distributed state machines
and changes in
an extended state variables
. Jepsen tests are based on a sample
Chapter 42, Web where this sample instance is run on
multiple hosts together with a Zookeeper
instance on every node
where state machine is run. Essentially every state machine sample
will connect to local Zookeeper
instance which allows use, via
jepsen
to simulate network conditions.
Plotted graphs below in this chapter contain states and events which directly maps to a state chart which can be found from Chapter 42, Web.
Sending an isolated single event into exactly one state machine in an ensemble is the most simplest testing scenario and demonstrates that a state change in one state machine is properly propagated into other state machines in an ensemble.
In this test we will demonstrate that a state change in one machine will eventually cause a consistent state change in other machines.
What’s happening in above chart:
S21
.
I
is sent to node n1
and all nodes report state change
from S21
to S22
.
C
is sent to node n2
and all nodes report state change
from S22
to S211
.
I
is sent to node n5
and all nodes report state change
from S211
to S212
.
K
is sent to node n3
and all nodes report state change
from S212
to S21
.
I
, C
, I
and K
one more time via random nodes.
Logical problem with multiple distributed state machines is that if a same event is sent into a multiple state machine exactly at a same time, only one of those events will cause a distributed state transitions. This is somewhat expected scenario because a first state machine, for this event, which is able to change a distributed state will control the distributed transition logic. Effectively all other machines receiving this same event will silently discard the event because distributed state is no longer in a state where particular event can be processed.
In this test we will demonstrate that a state change caused by a parallel events throughout an ensemble will eventually cause a consistent state change in all machines.
What’s happening in above chart:
Extended state machine variables are not guaranteed to be atomic at any given time but after a distributed state change, all state machines in an ensemble should have a synchronized extended state.
In this test we will demonstrate that a change in extended state variables in one distributed state machine will eventually be consistent in all distributed state machines.
What’s happening in above chart:
J
is send to node n5
with event variable testVariable
having value v1
. All nodes are then reporting having variable
testVariable
as value v1
.
J
is repeated from variable v2
to v8
doing same checks.
We need to always assume that sooner or later things in a cluster will
go bad whether it is just a crash of a Zookeeper
instance, a state
machine or a network problem like a brain split
. Brain split is a
situation where existing cluster members are isolated so that only
part of a hosts are able to see each others. Usual scenario is that a
brain split will create a minority and majority partitions of an
ensemble where hosts in a minority cannot participate in an ensemble
anymore until network status has been healed.
In below tests we will demonstrate that various types of brain-split’s in an ensemble will eventually cause fully synchronized state of all distributed state machines.
There are two scenarios having a one straight brain split in a
network where where Zookeeper
and Statemachine
instances are
split in half, assuming each Statemachine
will connect into a
local Zookeeper
instance:
Note | |
---|---|
In our current |
Note | |
---|---|
In below plots we have mapped a state machine error state into an
|
In this first test we show that when existing zookeeper leader was kept in majority, 3 out of 5 machines will continue as is.
What’s happening in above chart:
C
is sent to all machine leading a state change to
S211
.
n1/n2/n5
and n3/n4
. Nodes n3/n4
are left in minority and
nodes n1/n2/n5
construct a new healthy majority. Nodes in
majority will keep function without problems but nodes in minority
will get into error state.
n3/n4
will join
back into ensemble and synchronize its distributed status.
K1
is sent to all state machines to ensure that ensemble
is working properly. This state change will lead back to state
S21
.
In this second test we show that when existing zookeeper leader was kept in minority, all machines will error out:
What’s happening in above chart:
C
is sent to all machine leading a state change to
S211
.
Zookeeper
leader is kept in minority and all
instances are disconnected from ensemble.
K1
is sent to all state machines to ensure that ensemble
is working properly. This state change will lead back to state
S21
.
In this test we will demonstrate that killing existing state machine and then joining new instance back into an ensemble will keep the distributed state healthy and newly joined state machines will synchronize their states properly.
Note | |
---|---|
In this test, states are not checked between first |
What’s happening in above chart:
S21
into
S211
so that we can test proper state synchronize during join.
X
is marking when a specific node has been crashed and started.
S21
from S211
to make
sure that all state machines are still functioning properly.