How garbage collection in Erlang helps us to create scalable applications
Erlang is a functional programming language with dynamic typization. Its primary feature is working at a separate-process level. Personal computer CPUs have almost plateaued in terms of frequency and now develop mostly through increasing the number of cores. Apps written in Erlang allow you to fully utilize the advantages of multi-threaded CPUs and handle a large number of concurrent activities.
Use cases for Erlang include, for example, developing backend for fault-resistant web applications, messaging apps, task managers and server monitoring systems. Evrone engineers use Erlang in the company’s projects. One of Erlang’s biggest perks is its mechanism for garbage collection.
Scalability with Erlang
The cornerstone of the current implementation of code architecture is the scheduler, launching on each CPU core with its own process queue. Each process is allocated a certain limit of function calls that it has to do its work in, and after it runs out, the next process in the queue launches.
In Erlang virtual machine, the process is a completely isolated entity that has its own memory for a stack (process instructions) and a heap (data needed for the process). The process consists of two parts:
- A separate controller block containing general information on the process, references to memory spaces, stats, counters
- The memory space that the controller is referring to
When garbage collection is needed
If there isn’t enough space in memory to put new information, garbage collection is launched. It removes data that’s no longer necessary for the app to work, and then the app resumes. Erlang uses GC that separate objects in the heap into two categories: long-term and short-term. The first group contains data that survived at least one instance of garbage collection and, as such, is considered essential for the app to work. Therefore, the GC only returns to these each couple of cycles.
The minor Garbage Collector cycle picks out objects that nothing refers to anymore and that’s not needed for the process to function. It’s done in three steps:
- The GC finds the so-called Root Objects - objects that refer to data in the process’s memory (process dictionary, message queue, error data etc.). Then it picks out the data they refer to, and places them into the new memory area, leaving the rest to be deleted.
- Then the collector goes over the first batch of moved objects in the new memory area and only moves the data they refer to.
- The stack is moved to the new memory area, after which the garbage collection cycle is finished, and objects that nothing refers to anymore get removed. The data that survived the procedure is marked, so that in the next GC cycle it is transferred to the old heap area for the data that survived at least one cycle.
It is worth noting that the stack (execution instructions) and the heap (data for work) live in the process memory, they are located at the edges of the available memory segment and grow towards each other.
When there is no free space left, the scheduler interrupts the execution of the code and uses the garbage collector that allocates a new memory area and transfers only relevant objects to it. After this, copying may occur again if the size of the initially allocated memory is too large to reduce the size.
What to do with old objects
One of Erlang’s features is that the collector only undergoes the Major Collection procedure, returning to objects in the old heap, only every 65000 standard cycles. During major collection, objects that are still referred to get moved to a new area, while the irrelevant data is removed.
But in the meantime, the process memory is sure to grow, and earlier Erlang had a bug that could crash the process if it was executing for too long, either due to extensive garbage collection or NIF calls that the scheduler didn’t account for.
To solve these issues, a second scheduler type was added - the Dirty scheduler. It still runs on each CPU core, but all schedulers work within a single shared queue. Processes that are expected to run for a long time, past timeouts are put there. IF the system detects that there isn’t enough memory for the process to run and the next GC call might potentially take a long time, it’ll run in the dirty job queue later.
To provide the process the memory it needs now, since it still has an execution timeout, the Delay GC is implemented. The technology marks the current heap as “abandoned” and allocates a new memory area with enough free memory needed for the process, plus a little bit more.
If there still isn’t enough memory, another area is allocated, linked with the previous one and marked as “heap fragments”. It repeats the procedure until the process is satisfied.
How quickly does a process need more memory? In the current 21st version it looks like this:
- Numbers 0-22: 12 and 38 according to the Fibonacci sequence 23 or more: 20% increase.
- 23 or more: 20% increase A standard GC cycle takes the bigger of the following two numbers: 1/8th size of the old heap or 300% of required memory.
For long-term object GC required memory is still allocated, but if it’s 4 times more than the overall size of relevant objects, it gets reduced in half. Usually that’s done last, when the Erlang VM decides if an allocated memory area needs to be changed.
What Erlang is good for
Objects can be checked for relevancy without suspending the entire system, like it is often done in other popular programming languages like Ruby or Python. It can be achieved because processes in Erlang work as isolated parts of the system.
An Erlang VM is a rather mature software product, proven in soft real-time systems. At Evrone we use Erlang to create reliable, scalable applications in telecom, video streaming, mobile Internet, messaging systems and various web applications.