This post is the first of a two-part series of articles about how to tune Akka configurations
At some point, whether it is during your new actor-based system planning, or after you have a prototype working, you’ll likely find yourself digging into the Akka Docs to find the right combination of possibilities for routing, dispatcher, number of actors instances and so on… Depending on the complexity of your system and performance requirements, this could get tedious.
Let’s start with the Akka configuration, specifically the configuration of actor-instances, routing strategy and dispatchers and executors.
Below is the relevant section of the application.conf
I like to start by thinking about how many instances of an actor are suitable?
The size may depend on other configurations like routing strategy, dispatcher, threadpool size and more. Nevertheless, the nr-of-actor ‘strategy’ can already be decided at this point.
Let’s review our options and use cases:
It is possible to configure resizable routees (actor instances managed by a router).
Routees can be added or removed dynamically, based on performance. You can configure specifically how much to scale up and down in case of unusual behavior.
Scale / Back-Pressure DIY!
When one component is struggling to keep-up, the entire system must respond in a sensible way.
You may be somewhat familiar with Akka-Streams, widely known as a framework that manages your back-pressure. It’s possible to imitate the general behavior by yourself.
Let’s review some scenarios in which you may want to scale your routees:
The producer (in our use case, one of your actors), can produce faster than the received consumer (actor or any other source) can handle
In this case you may:
The consumer is faster than the producer
Here the consumer will block waiting for the next item.
“You press, you make a request, the Meeseeks fulfills the request, and then it stops existing”(Rick Sanchez)
Actor per request works in a very similar way. An instance is created for every request, process it and then it will be destroyed.
You can configure Spray/Akka-HTTP to work in actor-per-request mode or do it yourself. However, it is not part of the Akka configuration, so I won’t go into too much detail. In a nutshell:
Note: There is a context-switches overhead which could theoretically lead to memory issues.
Akka provides “strategies” for the Akka router to define the workload distribution among actors.
Let’s quickly review the the routing strategies:
*Can be solved by increasing the number of routees (which may cost in context-switches overhead)
**As a replacement for ‘smallest mailbox’. Latency differences could be high among connections to remote actors)
***The overhead depends on the task, whether it on the same machine or not
Java 7 introduced the Fork-Join executor.
As the name suggests, it forks a task into subtasks, each executed by a different thread and joins the results.
There are two main characters that are worth mentioning here.
According to Oracle docs:
A common case is to use Fork-Join executor for future tasks inside an actor. Here, the dispatcher’s configuration of the actor should be considered as well. For example, the more threads you have for the actor, the more ‘future’ tasks are performed, and you may want more threads for them.
The old Java 5 executor for asynchronous task execution can still fit in some cases and without the Fork-Join overhead.
While Fork-Join breaks the task for you, if you know how to break the task yourself, then your code should be already built as a minimal task, executed by a single thread, which fits a thread-pool-executor.
The thread-pool executor is used by the Akka Dispatcher and PinnedDispatcher.
Dispatcher allows you to define ‘min’, ‘max’ and increase ‘factor’ / ‘fixed’ size for your thread pool.
The key is to find the right balance for actor instances to work in parallel and use the threads as much as they are need so other actors and processes can work as well. It’s also true for the Fork-Join executor and needs to be quite accurate.
PinnedDispatcher dedicates a unique thread to each actor. This is usually not the pattern you want for the machine, given the limited resources. Hence, it makes sense for the actor to share a pool of threads. However, if your actor performs a preferred task, you won’t want its instances to share the pool.
Do not use it if you have more instances than the number of cores in the machine. It is also not recommended for Futures, because you’ll probably need more than 1 thread…
This executor tries its best to have your actor instance always schedule with the same thread, which should increase throughput.
This is recommended for a small number of actor instances, where you have much more instances than threads, it is just not possible.