Currently there is not a single discussion about cloud native architectures where the term "microservices" is not brought up. With more and more developers and architects considering leveraging this architectural style, a lot of great content is showing up, but some of this new content misses the point on microservices completely. A good example of the latter is content that suggests one is building microservices just by making the applications package smaller ("micro").
Don't miss Part 2: Containers and Microservices.
This blog post is the start of a four-part series of posts that aims to provide a better understanding of what microservices are and how to work with them productively. In this introductory post, we'll cover some of the advantages of microservices and discuss some of the challenges you should be aware of when building microservices-based applications. The second part covers how microservices and containers play together, and how to take advantage of orchestration and container management tools such as Kubernetes. The third part discusses microservices design principles and microservices devops best practices. The series will wrap up with a blog post about Oracle's current and exciting future support for microservices-based applications.
Advantages of Microservices
Before looking into what makes microservices so appealing we should have a quick look at what they are. While there is no standard for microservices architectures, the industry mostly agrees on a couple of design points and characteristics that a microservices architecture should follow which are mentioned below.
In general, you can think of applications designed using the microservices architecture as applications composed of small autonomous services. These services are very loosely coupled, only communicate through APIs, and are less complex and thus smaller as they generally focus only on a single functionality centered around a business capability (bounded context) of the application. Basically, instead of building a big single codebase for an entire application (monolith), the application is composed of services, each with its own code base and state that are managed independently by smaller agile teams. This allows companies to develop, deploy and update parts of their application in a faster, more agile way, and thereby react to new market requirements and competition in a more timely and flexible manner. Netflix and other "born in the cloud companies" serve as prime examples of successful microservices architectures.
Figure 1 shows a simple application that is built using microservices:
Figure 1: A microservices based application with three microservices
With time to market and agility being the main drivers for using microservices, the big question is what enables that agility and speed? The answer to this question leads to the main advantages of microservices.
With a large monolithic application, fast reliable deployments can be problematic. Think about a scenario where you want to introduce a new feature, for example adding a new field for a user profile, or simply fixing a bug. A monolithic application is typically built and deployed as a single, entire application, requiring the need to build and test the entire application to make sure that a small change does not break any other component in the application. The entire application must also then be redeployed, including all those other components that have not changed. Depending on the size, technologies, and processes used, building and deploying an update can take quite some time.
In a microservice-based architecture, such as shown in Figure 1, you would only need to update and deploy the service to which the feature update or a critical bug fix applies, assuming you follow the best practice of loose coupling. If a change to a service needs to be rolled back, it can be done without impacting the other services in production. This makes it possible for large applications to remain agile, and deploy updates more quickly and more often.
In the cloud, applications are commonly scaled out by increasing the number of machines, called instances. Basically, one creates more machine instances, each with an instance of the application, and then applies load balancing across those instances. In the case of a monolith, even if only one feature of the application requires additional scale, the entire application needs to be scaled out, and thus more machine instances need to be added at a much higher cost, compared to the microservices model.
A microservices architecture allows the ability to scale each service as needed and deploy the services to instances that better match their resource requirements. If the order service needs to be scaled out to meet demand it can be done without having to scale the other services that compose the application. If the profile service requires a lot of memory it can simply be deployed to instances with more memory. It's still possible to deploy the various services to the same instances, but we have the ability to better optimize for cost and scale in our deployments.
Different Technology Stacks and Conflicting Dependencies
Monolithic applications are typically developed in one language and on one technology stack, often even on a specific version of a stack. If one component of an application can benefit from some new features in a more recent version of the technology stack, they may not be able to adopt it because another component in the application may be unable to support it. So the ability to take advantage of more modern capabilities can lag seriously behind in a monolithic application.
With a microservices approach the application can be composed of independent services that leverage different frameworks, versions of libraries or framework, and even completely different OS platforms. This allows you to pick the best technology for the feature and the team and to avoid stack version conflicts between features or libraries.
For example, in Figure 1 the profile service may be implemented in Java and benefit from Elasticsearch features while the order service can use Node.js and a transactional database with strong consistency and reporting features like Oracle database. You can even use different versions of the same language or stack in your services without worrying about dependency conflicts.
Considerations When Dealing with Microservices
While microservices architectures offer great benefits, it is important to be aware that you are entering a distributed computing world. Distributed computing has always been a complex topic and microservices are no exception to this. There are a couple of things that you need to consider when starting with microservices.
Due to the many moving parts in such an architecture, management of the services becomes much more complex. Instead of one deployment, you are dealing with deployments of tens or hundreds of services, which need to work seamlessly together. That requires service registry and service discovery solutions, so that a new or updated service can make itself known to the system, and be detected by others. You also need to ensure all the services are up and running, not running out of disk space or other resources, and remain performant. All the services will generally require load balancing and communication over synchronous and asynchronous messaging. Cluster management and orchestration tools help with some of those tasks but it also requires you to understand how those tools work. Part 2 of this blog post series will have a closer look at cluster management and orchestration and how that helps with microservices.
Network Congestion and Latency
As microservices communicate via APIs through standard protocols, such as HTTP, networking becomes an important factor. Considering that there are hundreds of services in one application, and that requests typically span multiple services, it's not hard to see how this can impact the overall performance of your application if networking isn't given a lot of consideration. Another aspect that is often overlooked at first is data serialization and deserialization. Sometimes the same data is passed from one service to the next where it's de-serialized and serialized multiple times which can substantially increase the latency. There are several patterns that can be used, such as data caching or replication, to limit the number of requests. Regarding the serialization issues, you can use efficient serialization formats and mandate a common serialization format across the services. This may reduce some of these steps by allowing one service to pass data along to another service without having to reserialize it again.
As each microservice typically has its own state store you must deal with the consistency and integrity challenges that come with decentralized data. Consider the following scenario. An order service references data in another service, say a product catalog service that you need to maintain integrity for. You now have some of the same data in both services that must remain consistent; if it changes in one it must change in the other. What if some data in the catalog service is deleted or changes, e.g. number of available items, and the order service needs to be made aware of this? Dealing with these consistency challenges, and concepts like eventual consistency, can be extremely hard to get right, but luckily the problem has been tackled already. For example, you can use patterns such as event sourcing, or notifications services to publish changes to data where consumers can subscribe to learn of these changes and updates.
Fault Tolerance and Resiliency
One characteristic of microservices applications is that they are typically fault tolerant even in the case of catastrophic failures. As there are many microservices with networks in between, faults can be more prevalent and more of a challenge in a microservices architecture. You may wonder how this can be the case as microservices are usually running in their own process or container and another would not directly affect its process. So how can one bad microservice bring down the entire application? Well, for example if a microservice takes too long to respond, exhausting all the threads in the calling service, it can cause cascading failures in the entire call chain. Not handling faults properly can also have an impact on your application's uptime SLA. Let's assume that your application needs to comply with a 99.9% uptime SLA, which relates to roughly 44 minutes of downtime per month, and that your application consists of three microservices, each offering 99.9% uptime. As each microservice can feasibly go down at a different time, you are now looking at a potential downtime of about 132 minutes, which would obviously violate the application's uptime SLA.
To handle faults and make your application fault tolerant you need to implement resiliency patterns such as timeouts, retries, circuit breakers and bulkheads, which can be quite challenging for developers being new to those types of patterns.
Logging and tracing require a sound strategy in microservices based applications. Log aggregation and analysis require serious thought, as there can be hundreds and thousands of microservices in an application producing a huge number of logs. Furthermore, requests typically span multiple services, so it's important to find a way to tag a request through the system, enabling you to look at the entire request across all services. This is typically done by using correlation or activity IDs that get passed on to all downstream services, and each service includes this ID in its logs. But given that services are developed by different teams, it is also important to agree on a common log format. Overall diagnostics and debugging of microservices can be challenging, and must be planned for right at the beginning. In part three of this series we'll cover some diagnostics best practices.
In monolithic systems, the code consuming an interface will typically be deployed with the implementation of the interface. Breaking changes in interfaces are typically caught during integration testing or during build time. In a microservices world, changes to an interface of a microservice are not necessarily handled by a consuming microservice right away, as they may be on different release cadences. To make sure that the consuming services can still work as expected requires all teams to think through and agree on service versioning techniques.
Advanced devops, automation, and monitoring are key to successful Microservices operations. Testing in production is generally the goal, which requires more emphasis on monitoring to enable detection of anomalies and issues very quickly, and roll back as needed. Investment in automation, and use of tools and practices such as blue-green deployments, canaries, A/B testing, and feature flags are vitally important. Setting up a well-defined workflow where development and operations work together to produce agile, high quality releases can be challenging. Part three of this blog series will cover the entire devops process in more detail.
This introductory post discussed some the benefits of using microservices and explained why microservices are so appealing to many companies. We also covered some of the challenges that come with microservices-based applications. Going forward in this series we will discuss how to cope with those challenges in more detail. The next part will look into how containers and microservices play together and what role orchestrators and container management tools such as Kubernetes play in a microservices world.