
The book "Distributed Systems. Design Patterns

Brendan Burns, a reputable specialist in cloud technologies and Kubernetes, sets out in this small work the absolute minimum necessary for the correct design of distributed systems. This book describes the ageless patterns of designing distributed systems. It will help you not only create such systems from scratch, but also effectively convert existing ones.
Excerpt. Decorator Pattern. Convert a request or response
FaaS is ideal when you need simple functions that process input data and then transfer them to other services. This kind of pattern can be used to extend or decorate HTTP requests sent or received by another service. This pattern is schematically shown in Fig. 8.1.
By the way, in programming languages there are several analogies to this pattern. In particular, Python has function decorators that are functionally similar to request or response decorators. Since decorating transformations do not store state and often are added ex-facto as the service develops, they are ideally suited for implementation as FaaS. In addition, the FaaS lightness means that you can experiment with different decorators until you find one that integrates more closely with the service.

Adding default values to the input parameters of HTTP RESTful API requests demonstrates the benefits of the Decorator pattern. Many API requests have fields that need to be filled out with reasonable values if they were not specified by the caller. For example, you want the field to default to true. This is difficult to achieve with classic JSON, because the default empty field is null, which is usually interpreted as false. To solve this problem, you can add the logic of substituting default values either in front of the API server or in the application code (for example, if (field == null) field = true). However, both of these approaches are not optimal, since the default substitution mechanism is conceptually independent of request processing.
Considering what was said earlier in the section on single-node patterns, you might be wondering why we did not design the default substitution service in the form of an adapter container. This approach makes sense, but it also means that the scaling of the default lookup service and the scaling of the API service itself become dependent on each other. Substituting default values is a computationally easy operation, and for it, most likely, you will not need many instances of the service.
In the examples in this chapter, we will use the kubeless FaaS framework (https://github.com/kubeless/kubeless). Kubeless is deployed on top of Kubernetes container orchestrator service. If you have already prepared the Kubernetes cluster, then proceed with the installation of Kubeless, which can be downloaded from the corresponding site (https://github.com/kubeless/kubeless/releases). Once you have the kubeless executable, you can install it in the cluster with the kubeless install command.
Kubeless is installed as a third-party Kubernetes API add-on. This means that after installation it can be used as part of the kubectl command-line tool. For example, the functions deployed in the cluster can be seen by running the kubectl get functions command. There are currently no functions deployed in your cluster.
Workshop Substitution of default values before request processing
You can demonstrate the usefulness of the Decorator pattern in FaaS using the example of substituting default values in a RESTful call for parameters whose values have not been set by the user. With FaaS, this is quite simple. The default lookup function is written in Python:
# Простая функция-обработчик, подставляющая значения
# по умолчанию
def handler(context):
# Получаем входное значение
obj = context.json
# Если поле "name" отсутствует, инициализировать его
# случайной строкой
if obj.get("name", None) is None:
obj["name"] = random_name()
# Если отсутствует поле 'color', установить его
# значение в 'blue'
if obj.get("color", None) is None:
obj["color"] = "blue"
# Выполнить API-вызов с учетом значений параметров
# по умолчанию
# и вернуть результат
return call_my_api(obj)
Save this function to a file called defaults.py. Remember to replace the call_my_api call with the API you want. This default substitution function can be registered as a kubeless function with the following command:
kubeless function deploy add-defaults \
--runtime python27 \
--handler defaults.handler \
--from-file defaults.py \
--trigger-http
To test it, you can use the kubeless tool:
kubeless function call add-defaults --data '{"name": "foo"}'
The Decorator pattern shows how easy it is to adapt and extend existing APIs with additional features like validating or substituting default values.
Event handling
Most systems are query-oriented - they process continuous flows of user and API requests. Despite this, there are quite a few event-oriented systems. The difference between the request and the event, it seems to me, lies in the concept of the session. Requests are parts of a larger interaction process (session). In the general case, each user request is part of the process of interacting with a web application or the API as a whole. I see events as more “one-time”, asynchronous in nature. Events are important and should be handled accordingly, but they are torn out of the main context of interaction and the answer to them comes only after some time. An example of an event is a user's subscription to a certain service, which will cause the sending of a greeting letter; uploading a file to a shared folder, which will lead to sending notifications to all users of this folder; or even preparing the computer for a reboot, which will notify the operator or automated system that appropriate action is required.
Since these events are largely independent and have no internal state, and their frequency is very variable, they are ideally suited for working in event-oriented FaaS architectures. They are often deployed next to the “battle” application server to provide additional capabilities or for background processing of data in response to emerging events. In addition, since new types of processed events are constantly added to the service, the simplicity of the function deployment makes them suitable for implementing event handlers. And since each event is conceptually independent of the others, the forced weakening of relationships within a system built on the basis of functions allows us to reduce its conceptual complexity, allowing the developer to focus on the steps necessary to process only one specific type of event.
A specific example of integrating an event-oriented component into an existing service is the implementation of two-factor authentication. In this case, the event will be the user's login to the system. A service can generate an event for this action and pass it to a handler function. The handler will send an authentication code in the form of a text message based on the transmitted code and user contact information.
Workshop Implementing Two-Factor Authentication
Two-factor authentication indicates that for the user to enter the system, he needs something that he knows (for example, a password), and something that he has (for example, a phone number). Two-factor authentication is much better than just a password, because an attacker will have to steal both your password and your phone number to gain access.
When planning the implementation of two-factor authentication, you need to process a request for generating a random code, register it with the login service and send a message to the user. You can add code that implements this functionality directly into the login service itself. This complicates the system, makes it more monolithic. Sending a message should be done simultaneously with the code generating the login web page, which may introduce a certain delay. This delay degrades the quality of user interaction with the system.
It would be better to create a FaaS service that would generate a random number asynchronously, register it with the login service and send it to the user's phone. Thus, the login server can simply execute an asynchronous request to the FaaS service, which in parallel will perform the relatively slow task of registering and sending the code.
To see how this works, consider the following code:
def two_factor(context):
# Сгенерировать случайный шестизначный код
code = random.randint(1 00000, 9 99999)
# Зарегистрировать код в службе входа в систему
user = context.json["user"]
register_code_with_login_service(user, code)
# Для отправки сообщения воспользуемся библиотекой Twillio
account = "my-account-sid"
token = "my-token"
client = twilio.rest.Client(account, token)
user_number = context.json["phoneNumber"]
msg = "Здравствуйте, {}, ваш код аутентификации:
{}.".format(user, code)
message = client.api.account.messages.create(to=user_number,
from_="+1 20652 51212",
body=msg)
return {"status": "ok"}
Then register FaaS in kubeless:
kubeless function deploy add-two-factor \
--runtime python27 \
--handler two_factor.two_factor \
--from-file two_factor.py \
--trigger-http
An instance of this function can be asynchronously generated from client-side JavaScript code after the user enters the correct password. The web interface can immediately display the page for entering the code, and the user, as soon as he receives the code, can inform him of the login service in which this code is already registered.
So, the FaaS approach has greatly facilitated the development of a simple, asynchronous, event-oriented service that is initiated when a user logs on to the system.
Event Conveyors
There are a number of applications that, in fact, are easier to consider as a pipeline of loosely coupled events. Event pipelines often resemble the good old flowcharts. They can be represented as a directed graph of synchronization of related events. Within the framework of the Event Pipeline pattern, nodes correspond to functions, and the arcs connecting them correspond to HTTP requests or other kind of network calls.
Between the elements of the container, as a rule, there is no common state, but there may be a common context or other reference point, on the basis of which the search in the storage will be performed.
What is the difference between such a pipeline and microservice architecture? There are two important differences. The first and most important difference between service functions and constantly running services is that event pipelines are essentially event driven. Microservice architecture, on the contrary, implies a set of constantly working services. In addition, event pipelines can be asynchronous and bind a variety of events. It’s hard to imagine how Jira’s application approval can be integrated into a microservice application. At the same time, it is easy to imagine how it integrates into the event pipeline.
As an example, consider a pipeline in which the source event is the loading of code into a version control system. This event causes a rebuild of code. The assembly may take several minutes, after which an event is generated that triggers the testing function of the assembled application. Depending on the success of the assembly, the test function takes different actions. If the assembly was successful, an application is created, which must be approved by the person for the new version of the application to go into operation. Closing the application serves as a signal for putting the new version into operation. If the assembly failed, Jira makes a request for the detected error and the pipeline exits.
Workshop Implementing a pipeline for registering a new user
Consider the task of implementing a sequence of actions for registering a new user. When creating a new account, a whole series of actions are always performed, for example, sending a welcome email. There are also a number of actions that may not be performed every time, for example, subscribing to an e-mail newsletter about new versions of a product (also known as spam).
One approach involves creating a monolithic service for creating new accounts. With this approach, one development team is responsible for the entire service, which is also deployed as a whole. This makes it difficult to conduct experiments and make changes to the process of user interaction with the application.
Consider the implementation of user login as an event pipeline of several FaaS services. With this separation, the user creation function has no idea what happens during user login. She has two lists:
- a list of necessary actions (for example, sending a welcome email);
- a list of optional actions (for example, subscribing to a newsletter).
Each of these actions is also implemented as FaaS, and the list of actions is nothing more than a list of HTTP callback functions. Therefore, the user creation function has the following form:
def create_user(context):
# Безусловный вызов всех необходимых обработчиков
for key, value in required.items():
call_function(value.webhook, context.json)
# Необязательные обработчики выполняются
# при соблюдении определенных условий
for key, value in optional.items():
if context.json.get(key, None) is not None:
call_function(value.webhook, context.json)
Each of the handlers can now also be implemented according to the FaaS principle:
def email_user(context):
# Получить имя пользователя
user = context.json['username']
msg = 'Здравствуйте, {}, спасибо, что воспользовались нашим
замечательным сервисом!".format(user)
send_email(msg, contex.json['email])
def subscribe_user(context):
# Получить имя пользователя
email = context.json['email']
subscribe_user(email)
Decomposed in this way, the FaaS service becomes much simpler, contains fewer lines of code and focuses on the implementation of one specific function. The microservice approach simplifies code writing, but can lead to difficulties in deploying and managing three different microservices. Here the FaaS approach proves itself in all its glory, because as a result of its use it becomes very simple to manage small pieces of code. Visualization of the process of creating a user in the form of an event pipeline also allows us to understand in general terms what exactly happens when a user logs in, simply by tracking the change in context from function to function within the pipeline.
»More details on the book can be found on the publisher’s website
» Table of Contents
»Excerpt
For Khabrozhiteley a 20% discount on the coupon - Design patterns
Upon the payment of the paper version of the book, an electronic version of the book is sent by e-mail.