
The world is constantly changing. Progress creates new technological challenges. In accordance with the changing demands, the architecture of information systems must evolve. Today, we will discuss event-driven architecture, concurrency, parallelism, and asynchrony, as well as how to coexist peacefully with all of this in Erlang.
Introduction
Depending on the size of the designed system and its requirements, we developers choose a way to exchange information within the system. In most cases, a broker-based approach, for example using RabbitMQ or Kafka, is a practical option for organizing service interactions. However, sometimes the event stream, SLA, and level of system control are such that off-the-shelf messaging doesn’t suit our needs. Of course, we can complicate the system a bit by taking on the responsibility for the transport layer and cluster formation, for example using ZeroMQ or nanomsg. But if the system has sufficient bandwidth and capabilities of a standard Erlang cluster, the question of adding an additional entity requires thorough examination and economic justification.
The topic of reactive distributed applications is quite broad. To fit the format of this article, today we will focus only on homogeneous environments built on Erlang/Elixir. The Erlang/OTP ecosystem allows for the implementation of a reactive architecture with minimal labor costs. However, we will need a messaging layer.
Theoretical Basis
The design process begins with defining goals and constraints. The primary goal does not lie in development for the sake of development. We need to create a secure and scalable tool that can support and, most importantly, develop modern applications at various levels: starting from single-server applications that serve a small audience, which can later evolve into clusters of 50-60 nodes, to federations of clusters. Thus, the main goal is to maximize profits by reducing the cost of development and ownership of the resulting system.
We will outline 4 main requirements for the final system:
- Cevent-driven orientation.
The system is always ready to pass through a stream of events and take the necessary actions; - MScalability.
Individual blocks can scale both vertically and horizontally. The entire system should have the capability for infinite horizontal growth; - Ofault tolerance.
All levels and services must have the ability to automatically recover from failures; - Guaranteed response time.
Time is valuable, and users should not have to wait too long.
Remember the old story about 'The little engine that could'? To ensure that the designed system successfully transitions from prototype stage to being progressive, its foundation must meet minimal requirements. CAN.
The messaging as an infrastructural tool and basis for all services now includes another aspect: ease of use for programmers.
Event-driven focus
For the application to grow from a single instance server to a cluster, its architecture must ensure loose coupling. This requirement is met by an asynchronous model. In this model, the sender and receiver take care of the information payload of the message and do not worry about the transmission and routing within the system.
Scalability
Scalability and efficiency of the system go hand in hand. Application components must be able to utilize all available resources. The more efficiently we can utilize our capabilities and the more optimized our processing methods are, the less money we spend on equipment.
Within a single machine, Erlang creates a highly concurrent environment. The balance between concurrency and parallelism can be set by the choice of the number of operating system threads available for the Erlang VM and the number of schedulers utilizing these threads.
Erlang processes do not share state and operate in a non-blocking mode. This ensures relatively low latency and higher throughput compared to traditional applications built on blocking synchronization. The Erlang scheduler takes care of fair distribution of CPU and IO, and the absence of locks allows the application to respond even under peak loads or failures.
At the cluster level, there is also a problem with resource utilization. It is important that all machines in the cluster are evenly loaded and that the network is not overloaded. Imagine a situation where user traffic lands on incoming load balancers (haproxy, nginx, etc.), which distribute requests for processing as evenly as possible among a set of available backends. Within the application infrastructure, the service that implements the required interface is just the last mile, and it will need to request a number of other services in order to respond to the initial request. Internal requests also require routing and balancing.
To effectively manage data streams, messaging must provide developers with an interface for managing routing and load distribution. This allows developers to solve both standard tasks and rare occurrences using microservice patterns (aggregator, proxy, chain, branch, etc.).
From a business perspective, scalability is one of the tools for managing risks. The key is to meet customer demands while optimizing equipment usage:
- By increasing hardware capacity due to advancements, it will not sit idle due to software inadequacies. Erlang scales vertically very well and will always be able to utilize all CPU cores and available memory.
- In cloud environments, we can manage the amount of hardware based on current or predicted loads and ensure SLA adherence.
Fault tolerance
Let’s consider two axioms: 'Failures are unacceptable' and 'Failures will always occur.' For businesses, software failure means financial loss, and worse yet, loss of reputation. By balancing possible losses with the cost of developing fault-tolerant software, a compromise can often be found.
In the short term, an architecture that incorporates fault tolerance saves money on purchasing ready-made clustering solutions. They are expensive and also contain flaws.
In the long term, a fault-tolerant architecture pays back multiple times on the investments made at all stages of development.
Messaging within the codebase is still in development, allowing for detailed interaction between components within the system. This simplifies the task of responding to and managing failures, as all responsible components handle failures, and the final system knows how to return to its operational state automatically after a failure by design.
Responsiveness
Regardless of failures, the application must respond to requests and meet SLAs. The reality is that people do not want to wait, and businesses must adapt. Increasingly, high responsiveness is expected from applications.
Responsive applications operate in a mode close to real-time. The Erlang VM functions in soft real-time mode. For certain areas, such as stock trading, healthcare, and industrial equipment management, hard real-time mode is crucial.
Responsive systems enhance UX and are beneficial for business.
Preliminary Summary
In planning this article, I wanted to share my experience of creating a message broker and building complex systems based on it. However, the theoretical and motivational part turned out to be quite extensive.
In the second part of the article, I will discuss the nuances of implementing exchange points, message exchange patterns, and their applications.
In the third part, we will cover general questions about service organization, routing, and load balancing. We will discuss the practical aspects of scalability and resilience of systems.
End of the first part.
Photo .
Source: habr.com
