Messaging Systems
Modern cloud native architecture that emphasizes resiliency through scalable, loosely coupled components relies on asynchronous messaging between components. Messaging-based architectures allow microservices to scale and become location transparent because systems are not connecting to each other directly; instead, they are communicating through messages brokered by a messaging system. This enables load management, elasticity, and flow control by shaping and monitoring the message queues in the system. It also enables developers to manage failures as messages. The asynchronous and nonblocking communication in message-driven architectures consumes resources only while active, delivering optimized resource usage and cost optimizations. Today several leading CNCF projects exist in the messaging space, including NATS.io and Cloud Events, providing a wide array of features, programming models, and performance characteristics.
In message-driven architectures, a message usually represents an object and its state or a change in its state. For instance, in an ecommerce application, a message that is sent from a cart service to an order service can identify an item by its ID, the quantity to be purchased, payment information, and the user who is purchasing it. The orders system can process the messages as they arrive, checking inventory, processing the payment, and sending more messages to other systems, such as a fulfillment system for the orders placed. Here, the systems are not directly communicating, nor do they know the other systems that are receiving the messages. This allows for flexible architectures—if one service were to be replaced, the other services would not even be aware of this change. This allows systems to scale independently and maintain well-defined failure boundaries, avoiding cascading failures. If a service fails, the messages intended for that failed receiver simply wait for the system to return to an operational state and pick up from the next message that is to be processed. Developers thus can work with well-defined contracts expressed as message formats, and rollout changes to parts of the system can be done with greater velocity and agility, yet without the need for highly coordinated release workflows.
Several messaging approaches offer different semantics, including message queuing and publish subscribe. These models have three key components: message producers, the messaging system, and message consumers. Message producers are applications that generate messages. Message consumers are applications that consume and process messages. They are connected by the messaging system, which provides features such as message storage, message order, at-least-once delivery, and at-most-once delivery. Note that the messaging system decouples the producer and consumer. Each component knows only about the messaging system, not about each other. This isolation of each component allows them to be independently deployed, scaled, and patched.
In message queuing, messages sent by the message producer are stored in a buffer until they are dequeued or consumed by another component. The message is processed only by the message consumer that dequeues it from the queuing system.
In the publish subscribe (Pub/Sub) model, shown in Figure 3-11, the producers (also known as publishers) publish messages onto a topic, and the message can be processed by all consumers (also called subscribers) who have subscribed to the topic. Note that in the Pub/Sub model, the same message is processed by many subscribers, unlike in a queue.
FIGURE 3-11 The Pub/Sub Model Connects Distributed Systems Using Messages That Are Published and Consumed on a Topic
Among these three components, the messaging system is usually the most complex. It handles message persistence, ordering, delivery guarantees, and more. The producer and the consumer interact with the messaging system using an API that the messaging system provides. The APIs provided by the messaging system aim to make the producer and consumer code as simple as possible. For most platforms, the producer API simply lets an application publish a message, and the consumer API lets an application read these messages. In practice, when there is a multitude of producers and consumers, the messaging system provides the heavy lifting of ensuring the throughput, infrastructure resources, and delivery semantics. This makes the task of running and maintaining open-source messaging systems such as Kafka and NATS appreciably complex and nontrivial. As a solution to this problem, most cloud vendors provide a fully managed messaging system that either is built on top of the open-source tools or has API compatibility with open-source tools.