More on this book
Kindle Notes & Highlights
Read between
May 18 - August 12, 2024
Memoization is the practice of caching the result of a function invocation. In npm, you can find many packages to implement asynchronous memoization with little effort; one of the most complete packages is memoizee (https://npmjs.org/package/memoizee).
Deferring the steps of an algorithm is not the only option we have for running CPU-bound tasks; another pattern for preventing the event loop from blocking is using child processes. We already know that Node.js gives its best when running I/O intensive applications such as web servers, which allows us to optimize resource utilization, thanks to its asynchronous architecture. So, the best way we have to maintain the responsiveness of an application is to not run expensive CPU-bound tasks in the context of the main application and use instead separate processes. This has three main advantages:
...more
It is worth mentioning that threads can be a possible alternative to processes when running CPU-bound tasks. Currently, there are a few npm packages that expose an API for working with threads to userland modules; one of the most popular is webworker-threads (https://npmjs.org/package/webworker-threads). However, even if threads are more lightweight, full-fledged processes can offer more flexibility and a better level of isolation in the case of problems such as freezing or crashing.
The characteristics of Node.js were perfect for the implementation of distributed systems, made of nodes orchestrating their operations through the network. Node.js was born to be distributed. Unlike other web platforms, the word scalability enters the vocabulary of a Node.js developer very early in the life of an application, mainly because of its single-threaded nature, incapable of exploiting all the resources of a machine, but often there are more profound reasons. As we will see in this chapter, scaling an application does not only mean increasing its capacity, enabling it to handle more
...more
Assuming we are using commodity hardware, the capacity that a single thread can support is limited no matter how powerful a server can be, therefore, if we want to use Node.js for high-load applications, the only way is to scale it across multiple processes and machines.
There are many ways to achieve this, and the book The Art of Scalability by Martin L. Abbott and Michael T. Fisher, proposes an ingenious model to represent them, called the scale cube. This model describes scalability in terms of the following three dimensions: X Axis: Cloning Y Axis: Decomposing by service/functionality Z Axis: Splitting by data partition
The most intuitive evolution of a monolithic, unscaled application is moving right along the X axis, which is simple, most of the time inexpensive (in terms of development cost), and highly effective. The principle behind this technique is elementary—that is, cloning the same application n times and letting each instance handle 1/nth of the workload. Scaling along the Y axis means decomposing the application based on its functionalities, services, or use cases. In this instance, decomposing means creating different, standalone applications, each with its own codebase, sometimes with its own
...more
As we will see, microservices is a term that at the moment is most commonly associated with a fine-grained Y axis scaling. The last scaling dimension is the Z axis, where the application is split in such a way that each instance is responsible for only a portion of the whole data. This is a technique mainly used in databases and also takes the name of horizontal partitioning or sharding.
The round robin algorithm distributes the load evenly across the available servers on a rotational basis. The first request is forwarded to the first server, the second to the next server in the list, and so on. When the end of the list is reached, the iteration starts again from the beginning. This is one of the simplest and most used load-balancing algorithms; however, it's not the only one. More sophisticated algorithms allow assigning priorities, selecting the least loaded server or the one with the fastest response time.
As we already mentioned, scaling an application also brings other advantages, in particular, the ability to maintain a certain level of service even in the presence of malfunctions or crashes. This property is also known as resiliency and it contributes towards the availability of a system.
The other alternative we have to support stateful communications is having the load balancer routing all the requests associated with a session always to the same instance of the application. This technique is also called sticky load balancing.
The cluster module is not the only option we have to scale a Node.js web application. In fact, more traditional techniques are often preferred because they offer more control and power in highly available production environments. The alternative to using cluster is to start multiple standalone instances of the same application running on different ports or machines, and then use a reverse proxy (or gateway) to provide access to those instances, distributing the traffic across them.
To provide a single access point to our application, we can then use a reverse proxy, a special device or service placed between the clients and the instances of our application, which takes any request and forwards it to a destination server, returning the result to the client as if it was itself the origin. In this scenario, the reverse proxy is also used as a load balancer, distributing the requests among the instances of the application.
Pattern: use a reverse proxy to balance the load of an application across multiple instances running on different ports or machines.
For example, consider the use case when a product is being purchased; the Checkout module has to update the availability of the Product object, and if those two modules are in the same application, it's too easy for a developer to just obtain a reference to a Product object and update its availability directly. Maintaining a low coupling between internal modules is very hard in monolithic application, partly because the boundaries between them are not always clear or properly enforced.
A high coupling is often one of the main obstacles to the growth of an application and prevents its scalability in terms of complexity. In fact, an intricate dependency graph means that every part of the system is a liability; it has to be maintained for the entire life of the product, and any change should be carefully evaluated because every component is like a wooden block in a Jenga tower, moving or removing one of them can cause the entire tower to collapse. This often results in the building of conventions and development processes to cope with the increasing complexity of the project.
In reality, there is no strict rule on how small or big a service should be, it's not the size that matters in the design of a Microservice architecture; instead, it's a combination of different factors, mainly loose coupling, high cohesion, and integration complexity.
each fundamental component of the e-commerce application is now a self-sustaining and independent entity, living in its own context, with its own database. In practice, they are all independent applications exposing a set of related services (high cohesion).
The data ownership of a service is an important characteristic of the Microservice architecture. This is why the database also has to be split to maintain the proper level of isolation and independence. If a unique shared database is used, it would become much easier for the services to work together; however, this would also introduce a coupling between the services (based on data), nullifying some of the advantages of having different applications.
The first pattern we are going to show makes use of an API proxy, a server that proxies the communications between a client and a set of remote API. In the Microservice architecture, its main purpose is to provide a single access point for multiple API endpoints, but it can also offer load balancing, caching, authentication, and traffic limiting, all features that prove out to be very useful to implement a solid API solution.
API orchestration The pattern we are going to describe next is probably the most natural and explicit way to integrate and compose a set of services, and it's called API orchestration. Daniel Jacobson, VP of engineering for the Netflix API, in one of his blog posts (http://thenextweb.com/dd/2013/12/17/future-api-design-orchestration-layer), defines API Orchestration as follows: An API Orchestration Layer (OL) is an abstraction layer that takes generically-modeled data elements and/or features and prepares them in a more specific way for a targeted developer or application. The generically
...more
The whole process happens without any explicit intervention from external entities such as an Orchestrator. The responsibility for spreading the knowledge and keeping information in sync is distributed across the services themselves. There is no God service that has to know how to move the gears of the entire system, each service is in charge of its own part of the integration.
when dealing with distributed architectures, the term messaging system is used to describe a specific class of solutions, patterns, and architectures that are meant to facilitate the exchange of information over the network.
In general, we can identify three types of messages, depending on their purpose: Command Message Event Message Document Message
The Command Message is already familiar to us; it's essentially a serialized Command Object as we described it in Chapter 4, Design Patterns. The purpose of this type of message is to trigger the execution of an action or a task on the receiver.
An Event Message is used to notify another component that something has occurred. It usually contains the type of the event and sometimes also some details such as the context, the subject or actor involved.
The Document Message is primarily meant to transfer data between components and machines. The main characteristic that differentiates a Document from a Command (which might also contain data) is that the message does not contain any information that tells the receiver what to do with the data. On the other side, the main difference from an Event message is mainly the absence of an association with a particular occurrence, with something that happened.
Another important advantage of asynchronous communications is that the messages can be stored and then delivered as soon as possible or at a later time. This might be useful when the receiver is too busy to handle new messages or when we want to guarantee the delivery. In messaging systems, this is made possible using a message queue, a component that mediates the communication between the sender and the receiver, storing any message before it gets delivered to its destination,
The broker eliminates these complexities from the equation: each node can be totally independent and can communicate with an undefined number of peers without directly knowing their details. A broker can also act as a bridge between the different communication protocols, for example, the popular RabbitMQ broker (http://www.rabbitmq.com) supports Advanced Message Queuing Protocol (AMQP), Message Queue Telemetry Transport (MQTT), and Simple/Streaming Text Orientated Messaging Protocol (STOMP), enabling multiple applications supporting different messaging protocols to interact.
there might be different reasons to avoid a broker: Removing a single point of failure A broker has to be scaled, while in a peer-to-peer architecture we only need to scale the single nodes Exchanging messages without intermediaries can greatly reduce the latency of the transmission
An important abstraction in a messaging system is the message queue (MQ). With a message queue, the sender and the receiver(s) of the message don't necessarily need to be active and connected at the same time to establish a communication, because the queuing system takes care of storing the messages until the destination is able to receive them. This behavior is opposed to the set and forget paradigm, where a subscriber can receive messages only during the time it is connected to the messaging system.
The durable subscriber is probably the most important pattern enabled by a message queue,
The Redis publish/subscribe commands implement a set and forget mechanism (QoS0). However, Redis can still be used to implement a durable subscriber using a combination of other commands (without relying directly on its publish/subscribe implementation).
It is nice to see how the Microservice approach allows our system to survive even without one of its components—the history service. There would be a temporary reduction of functionality (no chat history available) but people would still be able to exchange chat messages in real time. Awesome!
One important difference with the HTTP load balancers we have seen in the previous chapter, is that, here, the consumers have a more active role. In fact, as we will see later, most of the time it's not the producer that connects to the consumers, but they are the consumers themselves that connect to the task producer or the task queue in order to receive new jobs. This is a great advantage in a scalable system as it allows us to seamlessly increase the number of workers without modifying the producer or adopting a service registry.