AkkaPaint — simplicity and power of Akka
How to create shared drawing board with Akka & Cassandra
Once upon a time, there was complete chaos,
where you could draw and instantly share anything like a boss.
However, the days of Adobe Flash are long gone,
so a new idea was born:
make chaos great again!
And this is how from chaos arose AkkaPaint
The idea was pretty straightforward: create a drawing space which is:
- able to propagate changes to all users in real time,
You may ask, where is the challenge? The answer is: make the implementation really small and simple.
Drawing board representation — basics
A drawing board can be represented as a simple map between pixel coordinates and color representation of the pixels. It’s a really naive approach which doesn’t include any optimization, but it works surprisingly well. Furthermore, every change on the drawing board can be represented as an event which contains a pixel sequence and the new color applied to these pixels.
Drawing board as an actor
For me, Akka Toolkit seems to be a perfect fit for this problem. The painting board can be easily represented as an actor. The internal actor state can contain a map where pixels are keys and colors are values (
akkaPaintBoard: Map[Pixel, Color]). As we want to preserve the actor state between application restarts, we will use Persistent Actor here. Every change to the board will be saved as an event. Sounds great!
Scalable drawing board
The size of the drawing board can be enormous, which implies a lot of pixels to be processed and a lot of changes to be applied. Sadly, that’s too much for one actor. So, let’s split the whole board into small squares 100×100 pixels each. Such a square can be represented by one actor, and will hold the colors only of 10,000 pixels. Ideally, we want to scale the problem horizontally, as sometimes the whole board can’t fit into the memory of one machine, or we want to use the computing power of more machines. And here comes Akka Cluster Sharding! The idea is simple: actors (here called entities) form shards (a shard is simply a group of actors), and each of the shards can be located on a different machine. Furthermore, there is the concept of coordinator, which knows the location of each shard and entity. The shard ID and entity ID are extracted from incoming data using the two simple functions presented below (
extractEntityId) . The
shardingPixels method is responsible for splitting an incoming stream of pixel color changes into smaller packages addressed to exactly one entity.
This solution is illustrated here:
Creating a cluster and obtaining the
ShardRegion reference (
ShardRegion is a local actor representing the entrance to the cluster) can look like this:
We can send all messages to the
ShardRegion, which knows (thanks to the coordinator) how to route messages to the proper entity. Also, it is worth mentioning that if a new machine joins the cluster, some of the shards will be moved to that machine (look at
least-shard-allocation-strategy configuration). This process is called
resharding and is performed in a few steps:
1. The shard that will be moved to another machine is chosen by the coordinator.
2. The coordinator informs
ShardRegion to start buffering all messages that are incoming to this shard.
3. All of the actors inside the chosen shard are killed.
4. Shard and actors are started on a new machine (the state of the actor will be restored from the events that were previously persisted in a database).
5. All buffered data is sent to the newly restored shard.
The perceptive reader will surely notice here a potential inconvenience: with a lot of incoming messages during the resharding process, the buffer can overflow. Sadly, all you can do is to resize the buffer by setting the
akka.cluster.sharding.buffer-size configuration parameter.
The current state of the board persists even after changing the size of it in the code. Shards and entities will be dynamically created at runtime if the board is resized.
To keep all active users updated we can use WebSockets. In Play! framework, each WebSocket connection can be represented as an actor (yay, what a surprise :)). This
ClientConnectionActor can be registered in each entity, and each entity can send the updates completely asynchronously to the browser via
Total scalability of the AkkaPaint
The application consists of 3 main parts:
1. Play! web application (serves static data, parses json messages incoming via WebSocket, converts to json and pushes messages to the client browser)
2. Akka Cluster Sharding (updates internal actor state (saves events and snapshots), sends changes to all registered clients)
3. Cassandra database (saves events and snapshots streams, serves events and snapshots during cluster restart and resharding process)
Each of these parts can be easily scaled horizontally.
Try it on your own!
- Install and run Cassandra database
- Clone the project: akkapaint
- Type “sbt run”
- Go to the browser address: http://localhost:9000/demo and express yourself by drawing whatever you want to!
If you don’t want to download anything, you can try it online here: http://demo.akkapaint.org/. Maybe drop your country’s flag there?
Furthermore, you can lend your computer resources. If you want to join the cluster, find the
akkapaint-web.conf file and apply the “
if you want to join me" comments actions. Restart your application and voilà: in 10 seconds some of the shards should be moved to your machine (you should see some logging on your console).
We have walked through the general idea, and practical examples of some extraordinary Akka features (like persistent actors, clustering, sharding) which allowed us to build a multiuser, scalable AkkaPaint with the possibility of getting the updates in the real time. The current working implementation has only 288 LOC! Akkareally shines here. There are a lot more things I’ve implemented, such as:
- performance optimizations (e.g. messages serialized via Protocol Buffer)
- buffering messages (the updates to the browser are sent with a 1s tick)
- adjusting play configuration
- adjusting akka sharding configuration
- preparing some gatling tests
- painting images on AkkaPaint board, with the great help of akka-stream and rapture.io json library.
I am not able to describe everything in this blog post, as the text is already too long.
All those features can be found here: akkapaint. There are a lot of great ideas to be implemented (e.g. creating private boards, some compression and further performance optimizations, loading images through the browser, UI improvements…). All contributions are really welcome!