© Kasun Indrasiri and Prabath Siriwardena 2018
Kasun Indrasiri and Prabath SiriwardenaMicroservices for the Enterprisehttps://doi.org/10.1007/978-1-4842-3858-5_3

3. Inter-Service Communication

Kasun Indrasiri1  and Prabath Siriwardena1
(1)
San Jose, CA, USA
 

In the microservices architecture, services are autonomous and communicate over the network to cater a business use case. A collection of such services forms a system and the consumers often interact with those systems. Therefore, a microservices-based application can be considered a distributed system running multiple services on different network locations. A given service runs on its own process. So microservices interact using inter-process or inter-service communication styles.

In this chapter we discuss the microservice communication styles and the standard protocols that are used for microservice communication. The chapter compares and contrasts those protocols and styles. However, we will defer the discussion on integrating microservices, resilient inter-service communication, and service discovery. Those topics are discussed in detail in Chapter 6, “Microservices Governance” and Chapter 7, “Integrating Microservices”.

Fundamentals of Microservices Communication

As discussed in the first chapter, services are business capability-oriented and the interactions between these services form a system or a product, which is related to a particular set of business use cases. Hence inter-service communication is a key factor to the success of the microservices architecture.

A microservices-based application consists of a suite of independent services that communicate with each other through messaging. Messaging is not a new concept in distributed systems. In monolithic applications, business functionalities of different processors/components are invoked using function calls or language-level method calls. In Service Oriented Architecture (SOA), this was shifted toward more loosely coupled web service level messaging, which is primarily based on SOAP, on top of different protocols such as HTTP, message queuing, etc. Almost all of the service interactions are implemented at the centralized Enterprise Service Bus (ESB) layer.

In the context of microservices, there is no such restriction as with SOA/Web Services to use a specific communication pattern and message format. Rather, the microservices architecture favors selecting the appropriate service collaboration mechanism and message format that are used to exchange information, based on the use case.

Microservice communications styles are predominantly about how services send or receive data from one service to the other. The most common type of communication styles used in microservices are synchronous and asynchronous.

Synchronous Communication

In the synchronous communication style, the client sends a request and waits for a response from the service. Both parties have to keep the connection open until the client receives the response. The execution logic of the client cannot proceed without the response. Whereas in asynchronous communication, the client can send a message and be completely done with it without waiting for a response.

Note that synchronous and blocking communication are two different things. There are some textbooks and resources that interpret the synchronous communication as a pure blocking scenario in which the client thread basically blocks until it gets the response. That is not correct. We can use a non-blocking IO implementation, which registers a callback function once the service responds back, and the client thread can be returned without blocking on a particular response. Hence, the synchronous communication style can be built on top of a non-blocking asynchronous implementation.

REST

Representational State Transfer (REST) is an architectural style that builds distributed systems based on hypermedia. The REST model uses a navigational scheme to represent objects and services over a network. These are known as resources. A client can access the resource using the unique URI and a representation of the resource is returned. REST doesn’t depend on any of the implementation protocols, but the most common implementation is the HTTP application protocol. While accessing RESTful resources with the HTTP protocol, the URL of the resource serves as the resource identifier and GET, PUT, DELETE, POST, and HEAD are the standard HTTP operations to be performed on that resource. The REST architecture style is inherently based on synchronous messaging.

To understand how we can use the REST architectural style in service development, let’s consider the order-management scenario of the online retail application. We can model the order-management scenario as a RESTful service called Order Processing. As shown in Figure 3-1, you can define the resource order for this service.
../images/461146_1_En_3_Chapter/461146_1_En_3_Fig1_HTML.jpg
Figure 3-1

Different operations that you can execute on the Order Processing RESTful service

To place a new order, you can use the HTTP POST message with the content of the order, which is sent to the URL (http://xyz.retail.com/order). The response from the service contains an HTTP 201 Created message with the location header pointing to the newly created resource (http://xyz.retail.com/order/123456). Now you can retrieve the order details from that URL by sending an HTTP GET request. Similarly, you can update the order and delete the order using the appropriate HTTP methods.

Since REST is a style, when it comes to the realization of RESTful services, we need to make sure that our RESTful services are fully aligned with the core principles of REST. In fact, most of the RESTful services out there violate the core REST style concepts. To design a proper RESTful service, Leonard Richardson has defined1 a maturity model for REST-based services.

Richardson Maturity Model

There are four levels in the Richardson Maturity Model:
  • Level 0 – Swamp of PoX : A service at this level is in fact not considered RESTful at all. For example, suppose that there is a SOAP web service exposed over HTTP to implement online retail capabilities. This service has one URL (http://xyz.retail.com/legacy/RetailService) and, based on the content of the request, it decides the operation (order processing, customer management, product search, etc.) that it needs to carry out. A single HTTP method (in most cases, POST) is used and no HTTP constructs or concepts are used for the logic of the service. Everything is based on the content of the message. The best example of a Level 0 service is any SOAP web service.

  • Level 1 – Resource URIs : A service is considered to be at this level when it has individual URIs for each resource, but the message still contains operation details. For example, the retail application can have resources for /orders, /products, /customers, etc., but the CRUD (Create, Read, Update, Delete) operations for a given resource are still done through the content of the message. No HTTP methods or response status codes are used.

  • Level 2 – HTTP Verbs : Instead of using the message content to determine the operation, we can use an HTTP verb instead. Hence, an HTTP POST message that is sent to the /order context can add a new order (order details are given in the message content but now we don’t have operation details). Also, the proper HTTP status codes should be supported. For example, the response of an invalid request should be on status code 500. The example that we illustrated in Figure 3-1 facilitates all these capabilities. A RESTful service, which is at Level 2 or higher, is considered to be a proper REST API.

  • Level 3 – Hypermedia Controls : At level 3, the service responses have links that control the application state for the client. Often this concept is known as HATEOAS (Hypertext as The Engine of Application State). The hypermedia controls tell us what we can do next, and the URI of the resource we need to manipulate to do it. Rather than us having to know where to post our next request, the hypermedia in the response tell us how to do it.

Owing to the fact that REST is primarily implemented on top of HTTP (which is widely used, less complicated, and firewall friendly), REST is the most common form of microservice communication style adopted by microservices implementations. Let’s consider some best practices when using RESTful microservices.
  • RESTful HTTP-based microservices (REST is independent of the implementation protocol, but there are no non-HTTP RESTful services used in practice) are good for external facing microservices (or APIs) because it’s easier to expose them through existing infrastructures, such as firewalls, reverse proxies, load balancers, etc.

  • The resource scope should be fine-grained so that we can directly map it to an operation-related business capability (e.g., add order).

  • Use the Richardson maturity models and other RESTful service best practices for designing services.

  • Use versioning strategies when applicable. (Versioning is typically implemented at the API Gateway level when you decide to expose your service as APIs. We discuss this in detail in Chapter 7.)

As a lot of microservices frameworks use REST as their de-facto styles, you may be tempted to use it for all your microservices implementations. But you should use REST only if it’s the most appropriate style for your use cases. (Be sure to consider the other suitable styles, which are discussed in this chapter.)

Try it out

You can try a RESTful web services sample in Chapter 4, “Developing Services”.

gRPC

Remote Procedure Calls (RPC) was a popular inter-process communication technology for building client-server applications in distributed systems. It was quite popular before the advent of web services and RESTful services. The key objective of RPC is to make the process of executing code on a remote machine as simple and straightforward as calling a local function. Most conventional RPC implementations, such as CORBA, have drawbacks, such as the complexity involved in remote calls and using TCP as the transport protocol. Most conventional RPC technologies have lost their popularity. So you may wonder why we are talking about another RPC technology, gRPC.

gRPC2 (gRPC Remote Procedure Calls) was originally developed by Google as an internal project called Stubby, which focused on high performance inter-service communication technology. Later it was open sourced as gRPC. There are alternatives such as Thrift, which is really fast. But Thrift-based communication required a lot of work from the developer side, as it exposed the low-level network details to the users (it exposes raw sockets), which made life harder for the developers. Also, being based on TCP is also a major limitation, as it’s not suitable for modern web APIs and mobile devices.

gRPC enables the communication between applications built on top of heterogeneous technologies. It is based on the idea of defining a service and specifying the methods that can be called remotely with their parameters and return types. gRPC tries to overcome most of the limitations of the traditional RPC implementations.

By default, gRPC uses protocol buffers3, Google’s mature open source mechanism for serializing structured data (can be used with other data formats such as JSON). Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data. gRPC uses protocol buffers, such as the Interface Definition Language (IDL), to describe both the service interface and the structure of the payload messages. Once a service is defined using the protocol buffer IDL, the service consumers can create a server skeleton and the client can create the stub to invoke the service in multiple programming languages.

Using HTTP2 as the transport protocol is a key reason for the success and wide adaptation of gRPC. So, it’s quite handy to understand the advantages that HTTP2 contains.

A Glimpse of HTTP2

Despite its wide adaptation, HTTP 1.1 has several limitations that hinder modern web scale computing. The main limitations of HTTP 1.1 are:
  • Head of line blocking: Each connection can handle one request at a time. If the current request is blocked, then the next request will wait. Therefore, we have to maintain multiple connections between the client and server to support real use cases. HTTP1.1 defines a pipeline to overcome this, but it’s not widely adopted.

  • HTTP 1.1 protocol overhead: In HTTP 1.1, many headers are repeated across multiple requests. For example, headers such as User-Agent and Cookie are sent over and over, which is a waste of bandwidth. HTTP 1.1 defines the GZIP format to compress the payload, but that doesn’t apply to the headers.

HTTP2 came up with solutions for most of these limitations. Most importantly, HTTP2 extends the HTTP capabilities, which makes it fully backward compatible with existing applications.

All communication between a client and server is performed over a single TCP connection that can carry any number of bidirectional flows of bytes. HTTP2 defines the concept of a stream, which is a bidirectional flow of bytes within an established connection, which may carry one or more messages. Frame is the smallest unit of communication in HTTP2, and each frame contains a frame header, which at a minimum identifies the stream to which the frame belongs. A message is a complete sequence of frames that map to a logical HTTP message, such as a request or response, which consists of one or more frames. So, based on this approach, the request and response can be fully multiplexed, by allowing the client and server to break down an HTTP message into independent frames, interleave them, and then reassemble them on the other end.

HTTP2 avoids header repetition and introduces header compression to optimize the use of bandwidth. It also introduces a new feature of sending server push messages without using the request-response style messages. HTTP2 is also a binary protocol, which boosts its performance. It also supports message priorities out of the box.

Inter-Service Communication with gRPC

By now you should have a good understanding of the advantages of HTTP2 and how it helps gRPC to perform better. Let’s dive into a complete example that’s implemented using gRPC. As illustrated in Figure 3-2, suppose that, in our online retail application example, there is a Returns service calling a Product Management service to update the product inventory with returned items. The Returns service is implemented using Java and the Product Management service is implemented using the Go language. The Product Management service is using gRPC and it exposes its contract via the ProductMgt.proto file.
../images/461146_1_En_3_Chapter/461146_1_En_3_Fig2_HTML.jpg
Figure 3-2

gRPC communication

So, the Product Management service developer will use the ProductMgt.proto file to generate the server-side skeleton in the Go language. Basically, the developer uses this in his project and complies it, so that it generates the service and client stubs. To implement the service, we can use the generated service stub and implement the required business logic of that service.

The consumer, which is the Returns service, can use the same ProductMgt.proto file to generate the client-side stub (in Java) and invoke the service. The Product Management service, which has an operation to add products, will have a ProductMgt.proto definition similar to the following.
// ProductMgt.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "kasun.mfe.ecommerce";
option java_outer_classname = "EcommerceProto";
option objc_class_prefix = "HLW";
package ecommerce;
service ProductMgt {
  rpc AddProduct (ProductRequest) returns (ProductResponse) {}
}
message ProductRequest {
  string productID = 1;
  string name = 2;
  string description = 3;
}
message ProductResponse {
  string productID = 1;
  string status = 2;
}

Under the hood, when a client invokes the service, the client-side gRCP library uses the Proto Buf and marshals the remote function call, which is then sent over HTTP2. At the server side, the request is un-marshaled and the respective function invocation is executed using Proto Buf. The response follows a similar execution flow from the server to the client.

Try it out

You can try a gRPC service sample in Chapter 4, “Developing Services”.

gRPC allows server streaming RPCs where the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages.
rpc productUpdate(ProdUpdateReq) returns (stream ProdUpdateStatues){
}
Similarly, you can use client-streaming RPCs where the client writes a sequence of messages and sends them over to the server as a stream. Once the client has finished writing the messages, it waits for the server to read them and returns its response.
rpc productUpdate(stream productUpdates) returns (ProdUpdateStatus) {
}
Bidirectional streaming RPCs are where both sides send a sequence of messages using a read-write stream. The two streams operate independently, so clients and servers can read and write in whatever order they like. For example, the server could wait to receive all the client messages before writing its responses, or it could alternately read a message and then write a message, or follow some other combination of reads and writes.
rpc productUpdate(stream productUpdates) returns (stream ProdUpdateStatuses){
}

Authentication mechanisms supported in gRPC are SSL/TLS and token-based authentications with Google.

Error Handling with gRPC

You can also implement various error-handling techniques with gRPC. If an error occurs, gRPC returns one of its error status codes and an optional string error message that provides further details of the error.

REST and gRPC are the most commonly used synchronous messaging protocols in microservices implementations. However, there are several other synchronous messaging technologies that are occasionally used for microservice implementations.

GraphQL

The RESTful services are built on the concept of resources, which are manipulated via HTTP methods. When the service you develop fits the resource-based architecture of REST, it works flawlessly. But as soon as it deviates from the RESTful style, the service fails to deliver the expected outcome.

Also, in some scenarios, the client needs data from multiple resources at the same time, which results in invocation of multiple resources via multiple service calls (or always sending large responses with redundant data).

GraphQL4 addresses such concerns in a conventional REST-based service by providing a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

The client can send a GraphQL query to the API and get exactly what the client needs, and the client has full control over the data it gets. The typical REST APIs require loading from multiple URLs, but GraphQL-based services get all the data your app needs in a single request.

For example, you may send the following query to a GraphQL-based service:
{
  hero {
    name
  }
}
And retrieve the result, which is shown here.
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

The query has exactly the same shape as the result and the server knows exactly what fields the client requests.

The GraphQL server exposes a schema describing the API. This schema is made up of type definitions. Each type has one or more fields, which each take zero or more arguments and return a specific type. An example GraphQL schema is one for a book and an author of a book. Here we have defined the types for Book and Author, along with the operation name. In our previous example, we used a shorthand syntax where we omit both the query keyword and the query name, but in production apps , it’s useful to include these to make our code less ambiguous. So, in this example we have query as the operation type and latestBooks as the operation name:
type Book {
    isbn: ID
    title: String
    text: String
    category: String
    author: Author
}
type Author {
    id: ID
    name: String
    thumbnail: String
    books: [Book]
}
# The Root Query for the application
type Query {
    latestBooks(count: Int, offset: Int): [Book]!
}
# The Root Mutation for the application
type Mutation {
    addBook(id: String!, title: String!, text: String!, category: String, author: Author!) : Book!
}

The operation type is query, mutation, or subscription and it describes what type of operation you’re intending to do. The operation type is required unless you’re using the query shorthand syntax, in which case you can’t supply a name or variable definition for your operation.

Every GraphQL service has a query type and may or may not have a mutation type. These types are the same as a regular object type, but they are special because they define the entry point of every GraphQL query. Similar to a query, you can define fields on the mutation type, and those are available as the root mutation fields you can call in your query. As a convention any operations that cause writes should be sent explicitly via a mutation.

Try it out

You can try a GraphQL service sample in Chapter 4, “Developing Services”.

WebSockets

WebSockets5 protocol can simply be introduced as TCP over the web. It can be considered a transport protocol, which is fully duplex and asynchronous. So, you can overlay any messaging protocol on top of WebSockets.

The WebSockets protocol uses a single TCP connection for traffic in both directions and uses HTTP as the initial handshaking protocol, so that it can work with the existing infrastructure. It works on top of TCP for the initial handshake, and then it acts like raw TCP sockets. Figure 3-3 shows the interactions of a client and server that use WebSockets.
../images/461146_1_En_3_Chapter/461146_1_En_3_Fig3_HTML.jpg
Figure 3-3

WebSockets communication

There are quite a few similarities with HTTP2 and WebSockets , such as using a single connection, bi-directional messaging, binary messages, etc. But the difference here is that WebSockets allows you to build your own messaging protocol on top of WebSockets (for instance, you can build MQTT or AMQP over WebSockets). The WebSockets architecture consists of a socket that is opened between the client and the server for full-duplex (bidirectional) communication. So, if your microservices require having such full-duplex communication and the ability to route the traffic via the web, then WebSockets is a great choice.

Thrift

Thrift6 allows you to define data types and service interfaces in a simple definition file. Similar to gRPC, using the interface definition, the compiler generates code to be used to easily build RPC clients and servers that communicate seamlessly across programming languages. Thrift uses TCP as the transport protocol and it is known to have very high-performance messaging but it lacks certain interoperability requirements such as firewalls and load balancers and ease of use when it comes to developing services.

Asynchronous Communication

In most of the early implementations of the microservices architecture, synchronous communication is being embraced as the de-facto inter-service communication style. However, asynchronous communication between microservices is getting increasingly popular as it makes services more autonomous.

In asynchronous communication, the client does not wait for a response in a timely manner. The client may not receive a response at all or the response will be received asynchronously via a different channel.

The asynchronous messaging between microservices is implemented with the use of a lightweight and dumb message broker. There is no business logic in the broker and it is a centralized entity with high-availability. There are two main types of asynchronous messaging styles—single receiver and multiple receivers.

Single Receiver

In single receiver mode, a given message is reliably delivered from a producer to exactly one consumer through a message broker (see Figure 3-4). Since this an asynchronous messaging style, the producer doesn’t wait for a response from the consumer nor during the time of producing the message, and the consumer may or may not be available. This is useful when sending asynchronous message-based commands from one microservice to the other. Given that microservices can be implemented using different technologies, we must implement reliable message delivery between the producer and consumer microservices in a technology-agnostic manner.
../images/461146_1_En_3_Chapter/461146_1_En_3_Fig4_HTML.jpg
Figure 3-4

Single receiver based asynchronous communication with AMQP

The Advanced Message Queuing Protocol (AMQP) protocol is the most commonly used standard with single receiver-based communication.

AMQP

AMQP7 is a messaging protocol that deals with publishers and consumers. The publishers produce the messages and the consumers pick them up and process them. It’s the job of the message broker to ensure that the messages from a publisher go to the right consumers. AMQP ensures reliability of message delivery, rapid and ensured delivery of messages, and message acknowledgements.

Let’s take an example scenario from our online retail application use case. Suppose that the Checkout microservice places an order as an asynchronous command to the Order Processing microservice.

We can use an AMQP message broker (such as RabbitMQ8 or ActiveMQ9) as the dumb messaging infrastructure and the Checkout microservice can produce a message to the specific queue in the broker. On an independent channel, the Order Processing microservice can subscribe to the queue as a consumer, and it receives messages in asynchronous fashion. For this use case, we can identify the following key components.
  • Message: Content of data transferred, which includes the payload and message attributes.

  • AMQP Message Broker A central application that implements the AMQP protocol, which accepts connections from producers for message queuing and from consumers for consuming messages from queues.

  • Producer: An application that puts messages in a queue.

  • Consumer: An application that receives messages from queues.

The producers may specify various message attributes, which can be useful to the broker or which only intend to be processed by the consuming microservice.

Since networks are unreliable, the communication between the broker and microservices may fail to process messages. AMQP defines a concept called message acknowledgements , which is useful in implementing reliable delivery of the messages. When a message is delivered to a consumer, the consumer notifies the broker, either automatically or when the application code decides to do so. In message acknowledgements mode, the broker will only completely remove a message from a queue when it receives a notification for that message (or group of messages). Also, since we are using a queue, we can ensure the ordered delivery and processing of the messages. There is a set of reliability mechanisms introduced by AMQP.

The failures can occur between the network interactions of broker, publisher, and consumer or the runtime of broker and client applications can fail.

Acknowledgements allow the consumer to indicate to the server that it has received the message successfully. Similarly, the broker can use acknowledgements to inform the producer that it has successfully received the message (e.g., confirms in RabbitMQ). Therefore, an acknowledgement signals the receipt of a message and a transfer of ownership, whereby the receiver assumes full responsibility for it.

AMQP 0-9-1 offers a heartbeat feature to ensure that the application layer promptly finds out about disrupted connections and completely unresponsive peers. Heartbeats also work with network equipment, which may terminate idle TCP connections.
  • Broker failures: In order to handle broker failures, the AMQP standard defines a concept of durability for exchanges, queues, and persistent messages, requiring that a durable object or persistent message survive a restart. Enable clustering for the broker that you are using.

  • Producer failures: When producing messages to the broker, the producers should retransmit any messages for which an acknowledgement has not been received from the broker. There is a possibility of message duplication here, because the broker might have sent a confirmation that never reached the producer (due to network failures, etc.). Therefore, consumer applications will need to perform de-duplication or handle incoming messages in an idempotent manner (i.e. internal state doesn’t change even if we process the same message multiple times).

In the event of network failure (or a runtime crashing), messages can be duplicated, and consumers must be prepared to handle them. If possible, the simplest way to handle this is to ensure that your consumers handle messages in an idempotent way rather than explicitly deal with de-duplication.

In failure conditions, when a message cannot be routed, messages may be returned to publishers, dropped, or, if the broker implements an extension, placed into a so-called “dead letter queue”. Publishers can decide how to handle such situations by publishing messages using certain parameters.

There are several open source message broker solutions out there and RabbitMQ could possibly be the most popular and widely used one. Apache ActiveMQ, ActiveMQ Artemis, and Apache Qpid are also popular.

There are multiple versions of the AMQP specification and v0-9-1 is the most commonly used one. The latest version is 1.0, which is yet to be fully adopted across the industry. From the microservices architecture perspective, we don't have to learn the details of the AMQP protocol; having a foundation level knowledge of the capabilities and messaging patterns it offers will be sufficient. AMQP also defines other message exchanging styles than single receiver-based exchanging, such as fan-out, topic, and header-based exchanging.

Important

AMQP message brokers are usually developed as monolithic runtimes and are not intended to be used only in the microservices architecture. Hence, most brokers come with various features that allow you to put business logic inside the broker (such as routing). So, we have to be extra cautious when using these brokers and we must only use them as dumb brokers. All the asynchronous messaging smarts should lie on our service logic only.

We discuss a development use case similar to the one described here in Chapter 4.

Multiple Receivers

When the asynchronous messages produced by one producer should go to more than one consumer, the publisher-subscriber or multiple receiver style communication will be useful.

As an example use case, suppose that there is a Product Management service , which produces the information on the price updates of the products. That information has to be propagated to multiple microservices, such as Shopping Cart, Fraud Detection, Subscriptions, etc. As shown in Figure 3-5, we can use an event bus as the communication infrastructure and the Product Management service can publish the price update events to a particular topic. The services that are interested in receiving price update events should subscribe to the same topic. The interested services will receive events on subscribed topics only if they are available at the time of the event broadcasting by the event bus. If the underlying event bus implementation supports it, the subscriber may have the option of subscribing as a durable subscriber, in which it will receive all the events despite being offline at the time of the event broadcasting.
../images/461146_1_En_3_Chapter/461146_1_En_3_Fig5_HTML.jpg
Figure 3-5

Multiple receivers (pub-sub) based asynchronous communication

There are multiple messaging protocols that support pub-sub messaging. Most of the AMQP-based brokers support pub-sub, but Kafka (which has its own messaging protocol) is the most widely used message broker for multiple receiver/pub-sub type messaging between microservices. Let’s take a closer look at Kafka and revisit this implementation with Kafka.

Kafka

Apache Kafka10 is a distributed publish/subscribe messaging system. It is often described as a distributed commit log , as the data within Kafka is stored durably, in order, and can be read deterministically. Also, data is distributed within the Kafka system for failover and scalability.

Let’s take a closer look at how we can implement a publisher-subscriber scenario using Kafka. As illustrated in Figure 3-6, we can use Kafka as the distributed pub-sub messaging system to build multiple microservice asynchronous messaging scenarios.

The unit of data used in Kafka is known as a message (an array of bytes). Unlike other messaging protocols, the data contained within it does not have a specific format or meaning to Kafka. Messages can also contain metadata, which can be helpful in publishing or consuming messages.
../images/461146_1_En_3_Chapter/461146_1_En_3_Fig6_HTML.jpg
Figure 3-6

Multiple receivers (pub-sub) based asynchronous communication with Kafka

These messages in Kafka are categorized into topics . A given topic can optionally be split into multiple partitions . A partition is a logical and physical componentization of a given topic, in which the producer can decide to which partition it wants to write. This is typically done using the message metadata known as key. Kafka generates a hash of the key and maps it to a specific partition (i.e., all messages produced with a given key reside on the same partition). Partitions are the primary mechanism in Kafka for parallelizing consumption and scaling a topic beyond the throughput limits of a single node. Each partition can be hosted in different nodes.

Each message is written to a partition with the offset number. The offset is an incrementally numbered position that starts at 0 at the beginning of the partition.

The producers write the messages to a specific topic. By default, the producer does not care what partition a specific message is written to and will distribute messages over all partitions of a topic evenly. In some cases, the producer will direct messages to specific partitions using the message metadata, key.

The consumers subscribe to one or more topics and read the messages in the order in which they were produced. The consumer keeps track of which messages it has already consumed by keeping track of the offset of messages. Each message in a given partition has a unique offset. By storing the offset of the last consumed message for each partition (in Zookeeper or in Kafka), a consumer can stop and restart without losing its place.

Consumers work as part of a consumer group, where one or more consumers work together to consume a topic. The group ensure that one consumer group member only consumes message from each partition. However, a given consumer group member can consume from multiple partitions. Obliviously, the concept of a consumer group is useful when we have to horizontally scale the consumption of a topic with a high message volume.

Now let’s try to take these concepts further with the scenario shown in Figure 3-6. In the following multiple-receiver messaging scenario, which is based on Kafka, there are two topics created in Kafka: price_update and order_update.

There are two Product Management producers (microservices from different departments) publishing product price update information (the producer can explicitly write to a given partition or the load can be distributed among the partitions evenly). For the price_update topic, there are three different consumers (from three consumer groups). They will independently read messages from Kafka and will maintain their own offset. For instance, suppose that there is an outage of Subscription services so that it was not able to receive the price updates during certain time. When the Subscription service starts again, it can check the currently persisted offset and pull all the events it received when it was offline.

The order-processing scenario with the order_update topic is slightly different. In that scenario, we can assume that we have a high load coming through the order update channels and we need to scale the consumer side to process them more efficiently. Hence, we use a consumer group with two consumers, so that they can parallel-process the order update events from multiple partitions.

A single Kafka server is known as a broker , and it serves message publishing and consuming. Multiple Kafka brokers can operate as a single cluster. From the multiple brokers in a cluster, there is a single broker, which functions as the cluster controller and takes care of the cluster administrative operations such as assigning partitions to brokers and detecting brokers failures. A partition is assigned to multiple brokers and there is a leader broker for each partition. Therefore, Kafka messaging has high availability because of the replication of the partitions across multiple brokers and the ability to appoint a new leader if the existing partition leader is not available (Kafka uses Zookeeper for most of the coordination tasks).

With clustering, Kafka is becoming even more powerful, so that you can massively scale your asynchronous messaging capabilities.

Tip

Can we do exactly-once delivery with Kafka? There is an interesting article11 on how we can implement exactly-once delivery with Kafka. Most of the concepts start from the key Kafka principles that we discussed in this chapter, and you can easily build the same level of guaranteed delivery with your microservices.

We’ve only discussed a selected subset of capabilities of Kafka that are useful in building the multiple receiver asynchronous messaging scenarios, and there are a lot of other technical details that are not included in the scope of this book.

There are several other brokers, such as ActiveMQ and RabbitMQ, that can offer you the same set of pub-sub messaging capabilities for simple-to-medium scale asynchronous messaging scenarios. However, if you need a fully distributed and scalable asynchronous messaging infrastructure, then Kafka is a good choice. There are various asynchronous messaging and event stream processing solutions that are built on top of Kafka such as Confluent Open Source Platform, which may give you some additional capabilities for your microservices implementation.

Other Asynchronous Communication Protocols

There are quite a few asynchronous messaging protocols that we haven’t discussed. MQTT12, STOMP13, and CoAP14 are also quite popular. If you think you have the expertise and that one of these is the best technology choice for your microservices messaging, then go for it. We do not cover those protocols in detail, as they don’t have any explicit characteristics that are important in the microservices context.

Synchronous versus Asynchronous Communication

At this point, you should have a solid understanding of the synchronous and asynchronous messaging techniques. It’s very important to understand when to use those communication styles in your microservices architecture implementation.

The main difference between synchronous and asynchronous communication is that there are scenarios in which it is not possible to keep a connection open between a client and a server. A durable one-way communication is more practical in those scenarios. Synchronous communication is more suitable in scenarios where you need to build services that provide request-response style interactions. If the messages are event-driven and the client doesn’t expect a response immediately, asynchronous communication is more suitable.

However, in many books and articles, it is emphasized that synchronous communication is evil, and we should only use asynchronous communication for microservices as it enables the service autonomy. In many cases, the comparison between synchronous and asynchronous communication is being interpreted in such a way that, in synchronous communication a given thread will be blocked until it waits for a response. This is completely false, because most synchronous messaging implementations are based on fully non-blocking messaging architectures. (That is, the client-side sender sends the request and registers a callback, and the thread is returned. When the response is received, it is correlated with the original request and the response is processed.)

So, coming back to the pragmatic use of these communication styles, we need to select the communication style based on our use case.

In scenarios where short-running interactions require a client and service to communicate in the request-response style, the synchronous communication is mandatory. For example, in our online retail use case, a product search service is communicated in a synchronous fashion, because you want to submit a search query and want to see the search results immediately. Scenarios such as order placing or processing are inherently asynchronous. Hence, the asynchronous messaging style is the best fit for such scenarios.

Tip

Embracing only synchronous (e.g. REST) or asynchronous messaging between microservices is a complete myth. When adopting microservices to your enterprise scenarios, you may be required to use a mix or a hybrid of both of those communication styles.

Message Formats/Types

We have discussed about several communication styles and protocols that are commonly used in the microservices architecture. The information exchange between the microservices is based on exchanging messages. Determining which message formats to use in those scenarios is important.

JSON and XML

In most of the REST-based microservices implementations, JSON is the de-facto message interchange format, owing to its simplicity, readability, and performance. However, there are some services based on XML (not SOAP). However, they lack some important features, such as robust type handling and compatibility between schema versions. In most of the scenarios where your services are exposed to the external consumers, we heavily use JSON as the message type, while usage of XML is quite limited to specific use cases. As part of the service development, the service has to process the incoming JSON or XML messages and map them to types that are used in the service code.

Protocol Buffers

With a communication style such as gRPC, the message formats are well-defined and use a dedicated data interchange format such as protocol buffers. The service developer doesn’t need to worry about processing the messages in a given message format. During marshaling and un-marshaling all the required type mapping is done and users only deal with the well-defined types. Owing to the flexible, efficient, automated mechanism of data serialization and deserialization, protocol buffers are ideal for high throughput synchronous messaging between internal services. Currently, the application of protocol buffers in microservices communication is heavily done using gRPC-based services.

Avro

Apache Avro15 addresses the key limitations of most of the conventional data interchange formats such as JSON and XML. Avro is a data serialization system that provides rich data structures for data representations, a compact format, great bindings for a wide variety of programming languages, direct mapping to and from JSON, and an extensible schema language defined in pure JSON.

Therefore, Avro is heavily used in most of the asynchronous messaging scenarios in microservices implementations. For example, most of the Kafka-based messaging systems leverage Avro to define the schema for producing and consuming messages.

Service Definitions and Contracts

When you have a business capability that’s implemented as a service, you need to define and publish the service contract. In traditional monolithic applications, we rarely have such features to define the business capabilities of an application. In SOA/web services world, WSDL is used to define the service contract, but as we all know, WSDL is not the ideal solution for defining a microservices contract, as WSDL is insanely complex and tightly coupled to SOAP.

Since we build microservices on top of the REST architectural style, we can use the same REST API definition techniques to define the contract of the microservices. Therefore, microservices use the standard REST API definition languages such as OpenAPIs16 to define the service contracts. For services that don’t fit the resource-based architecture, we can leverage GraphQL schemas.

As you saw in the gRPC section, protocol buffer definitions of your service are the service definition and contracts for your gRPC-based service (the same applies to other RPC styles such as Thrift). Service definitions are applicable to synchronous communication; there is no standard way of defining a service contract for asynchronous event-based communication.

Summary

In this chapter, we discussed the inter-service communication patterns and protocols that are used to implement those patterns in a microservices context. We took a closer look at the fundamentals of REST and gRPC, which are most commonly used in synchronous messaging between microservices. As asynchronous messaging styles, we learned about the AMQP protocol, which is a common choice for single consumer/point-to-point asynchronous messaging with reliability. For publisher-subscriber/multiple receiver type communication, Kafka is commonly used as the messaging infrastructure.

However, we haven’t discussed the details of how we can implement service interactions, compositions, orchestration, or choreography. Chapter 7 is dedicated to those topics and further discusses the commodity features of service interactions, such as resilient communication with circuit breakers, timeouts, etc. In addition, we have dedicated chapters on service governance (Chapter 6) and service definitions and APIs (Chapter 10, “APIs, Events, and Streams”).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset