In a microservice-based architecture, how do microservices speak to each other?
I have some experience on a user behavior tracking application with multiple microservices speaking to each other through RabbitMQ queues, but that was a rare scenario because no client was waiting for a sync response. But what if a user is waiting for a response? Is that still a good approach or I have to consider an approach like API gateway?
There are many alternatives to answer this question and always depends on the use cases of your application.
In my experience REST apis are the easy way to go, but queues or topics are easy to implement as well. In my last project we ended up with a hybrid approach where the communication with some services was using rest and in other places where the order of the messages was important we used queues.
So the bottom line is: you need to clearly understand the requirements of the application, you really need to know the how the data fill flow into your app and base on that create the the constraints that will help you to decide what's the best solution. That being said, I have one recommendation, please start from the easy and then iterate over the solution until you get the final architecture.
Rabbit MQ does support the Request/Response style messaging rabbitmq.com/direct-reply-to.html .
Microservices that need to collaborate can do so in several ways:
Each approach has its advantages and disadvantages. Listening for topics on a message bus can require multiple queues (not really an issue); such as queue per service; or queue per topic; but it de-couples the sender from the receiver, and allows for resiliency (if a service goes down and is not listening to messages on the queue, they'll hang out there until it comes up to consume them). It also provides a measure of observability of messages in the system.
You also have to ensure that your messages have a contract; and that that contract is consistent among all of your services (has the same structure). Gilt.io's VP Engineering has a talk that is really helpful in understanding the place for code generation and code contracts for services. youtube.com/watch
RPC over AMQP has the resilience of the first approach, but is clunky in an event-driven architecture. It's synchronous and it's difficult to get around that issue. This is not a great approach for asynchronous event-driven systems; and it couples the resiliency and uptime of the system to the individual services being able to communicate with each other at the same time. NotAGreatPlan.gif.
HTTP Based communication is the 'easiest' to implement from an architectural perspective, but the least resilient. You must manually implement the circuit breaker pattern so you aren't waiting for a down service to respond; and you must limit your call depth to 1 service (A service being called must be able to handle the request; it cannot also call another service). Failure to do so will cause... problems. Jimmy Bogard talks about this in his talk "Avoiding Microservices Mega Disasters": youtube.com/watch
Other answerers have stated a few design patterns to be on the lookout for: CQRS, Publish/Subscribe, Circuit Breaker, API Gateway, and Service Locator pattern. Read up on these patterns, as they will apply in the different scenarios.
This maybe a little of topic introduction, but for your own favor, please don't implement microservices pattern if you don't have a proper use case. Otherwise you will suffer. :)
For the communication part, you can use REST APIs and sockets. Leveraging REST is rather easy. But If you don't have experience in sockets and more importantly your framework of choice doesn't have prebuilt libraries for leveraging sockets it can be problematic. Sockets are less expensive than REST APIs.
And offcourse you can use messaging queues for async communication.
Learning CQRS can help you a lot with microservices.
Command and Query are general operations we use when developing software.
Consider this task: Update status of the user with the ID 5 and send an email regarding the update.
There are three operations in this task can be simplified as follows:
Where 1 is a query and 2 and 3 are commands. Queries are mostly sync where commands are async. So first item should be in sync because you are querying some service and you want to hear back from the service with the details of the user.
For the first item we can create a query. Like this.
class FetchUserById
{
protected int userId;
constuctor(int userId) {
this.userId = userId;
}
}
Message bus is used to serialize the message (above) and send it to related source via your transport of choice. You can choose REST, Socket (which will be synchronous) or amqp to have async behaviour.
Operation 2 and 3 can be async, so we can utilize appropriate messaging bus that uses amqp as transport.
In my experience, the answer is really "it depends".
In general, I prefer to standardize on HTTP requests because they are mostly cheap, well understood, and available on every single platform. In my experience, unless you have huge performance requirements, this might be enough. There's a lot of prior art on this, and a lot of open source tools that help with the most common issues you'll encounter (timeouts, cascading failures, thundering herds, etc...). Products like Hystrix, Zuul, Eureka, Linkerd, Envoy, Istio, Zipkin, etc... can help immensely on this.
And in my personal experience, most APIs lend themselves well to the model. Also, keep in mind that having HTTP endpoints doesn't mean everything is synchronous. You can do async work on HTTP (even event-based via long polling).
That being said, if your environment or your requirements don't lend themselves well to http (e.g. having multiple producers/consumers), queues can be very useful. But make sure you understand them well. Developing an architecture based on messages, disconnected actors and asynchronous messages is hard, and if done poorly, you'll end in a worst shape than in f you were just doing HTTP requests everywhere.
Keep it simple, start with the patterns that you're most comfortable, and slowly grow your architecture as your requirements grow. Overengineering has a bigger impact on performance and productivity that a badly tuned network call.
It depends. If you can, keep things simple. But if you have a more complicated scenario (hundreds of microservices, lots of different teams, etc), you probably should consider a couple of things when gluing your services together:
It's hard to isolate communication (transport channel and message format) from other challenges inerent to microservice architectures such as: Traceability, monitoring, circuit-break, load-balancing, etc. It's desirable that your tools play well together to solve these problems.
Details:
(1) Your services will change with time and when it happens how the services will handle it? The changes are backwards compatible, do you need to orchestrate the deployment? (i.e redeploy both services with the new message format?) Is there a possibility that your service will receive messages in the old format, if yes what happens? (is your service backwards-compatible?)
Some communication schemes may help you with the questions, above: Avro, gRPC (protobuf), Thrift have their our evolution scheme. Here's a good article about the topic: Schema evolution in Avro, Protocol Buffers and Thrift
(2) Why does it matter if your services are written in different languages? Well, if you need to write a library for each language to standardize the communication, that's a lot of effort. If everything is in the same language, you can share your communication library. For example: If you are in the JVM, you can use something like Finagle, if you have lots of teams writting stuff in different languages you could use something like Istio
(3) If you have 200 microservices talking with each other in production, you need standards and order. For example: How do you handle errors? Is there a common error format? Retrying/resending messages is supported in the transport layer or you have to do that on the application side? Does it make sense to separate transport errors from application errors?
(4) If you are using some sort of message broker or queue, how are the messages routed? Is there any need to add an deadletter queue? If the messages may be delivered more than once you need some sort of idempotency mechanism.