The other day I paid a king's ransom for a small upcoming ebook called “Building Microservices” by Sam Newman. Overall it's an interesting read, chock-full of information on microservices, how to adopt and use them, architecture details, and how they're evolving. But there's a glaring omission: while the first chapter covers multiple benefits provided by microservices, explaining their superiority over other approaches such as monolithic apps, SOA, modules, and shared libraries, it completely neglects to mention any downsides, challenges, pitfalls, or disadvantages to adopting a microservices architecture.
This is a dangerous omission in my opinion, and has prompted me to dedicate the third post in this microseries on microservices to this very topic: the Challenges of Microservices.
The Challenges of Microservices
Moving from a traditional monolithic architecture to a microservices approach is an enormous change for any organization, and fraught with multiple challenges. I'll cover a number of these here. My intent is not to exhaustively catalog these countless challenges, but instead to make sure that the reader isn't lulled into a false sense of security, and to emphasize that a microservices approach isn't one to be taken lightly.
Microservices Begets Complexity
A complex application will be complex, no matter how the architecture looks. A microservices-based app is just as complex as a monolithic app, but instead of all the complexity being in one place (one source code repository, one application container, one replicated application instance) the complexity is now distributed all over the network.
Consider a large monolithic application instance running in a single Tomcat container. This Tomcat instance has its own configuration files, logs, failover policies, quotas, monitoring, and required resources like disk space.
Which is complex in itself, but now compare it with a microservices-based app which might have dozens of Tomcat containers, one per microservice. The configuration for each container is just as involved, with its own configuration file, logs, failover policies, etc.
And all of these containers must be installed, configured, managed, monitored, and replicated. The old centralized IT approach just won't work, but even with a fully-engaged and committed DevOps culture, this is a lot of extra complexity to deal with.
Polyglot: Tread Carefully
That's just the beginning. Where we have multiple instances of polyglot containers, there are multiple polyglot languages to deal with as well. Not only do these languages (and associated frameworks and libraries) need to be installed/configured/managed, but the entire ecosystem associated with these: tooling, artifact repositories, IDEs, test frameworks, coverage tools, and documentation systems has to be dealt with.
For instance, each language has its own way of handling dates, parsing text, and invoking external REST calls. Often multiple services must accomplish similar tasks, such as date calculations. With multiple languages in play, it gets tricky to determine what functionality should appear in what service.
DRY Doesn't Mean Simple
This raises the larger-reaching issue of code replication, and the related duplication of effort that it entails. It's common for multiple services to share common functionality. In the monolithic world this is simple: just build a library that ecompasses that functionaility, make it available across the application, and you're done.
How does a microservices architect deal with this? Do they extract the common functionality into yet another service? Or create a shared library and figure out a mechanism to share it, potentially across multiple languages? Or just duplicate the code across the services? There's no clear-cut answer, and each of these approaches has drawbacks. And each adds complexity when compared with the old way of simply sharing a library across a monolithic application.
I remember first coming across The Fallacies of Distributed Computing, which came out of Sun back in the early days of JavaSpaces and Jini. This list was intended to drive home the point that network computing is just not the same as local computing, and point out the significant differences between network computing and local computing. Admittedly Jini came out a long time ago, and much has changed since the list was first formulated, such as ten generations of Moore's law which has mitigated some of the fallacies. But they still exist, and a microservices architecture by definition is particularly susceptible to the consequences of believing these fallacies.
In particular, while a monolithic application exists inside a single host, a single OS, and single address space, a microservices-based app resides across servers and networks, and thus the application as a whole is susceptible to the constraints outlined in "The Fallacies of Distributed Computing" such as latency, unreliability, and bandwidth limits.
Fan Out, Man
A particularly poignant example of this is fan out. In a monolithic application, a single incoming likely results in cascades of inter-object messages and library calls. But often these calls occur between components residing in a common memory space, built with the same software stack, having identical dependencies, and running on the same server or servers close-by. Not so with microservices: a single incoming request might result in dozens, or even scores of inter-service communications. This is known as "fan-out" and results in an order of magnitude increase in network traffic. If your site is servicing two billion requests a day, these could easily fan out to 20 billion network requests.
This can be a big problem, and one that might not manifest until traffic starts to approach two billion requests a day. Trust me, that's a bad time to find out that your network can't handle the load.
Versioning is yet another situation that is simpler in the monolithic world. A monolith has a version, and all its dependencies, with their own versions, are specified in the single application manifest that's checked into source control, tied to the overall application version. This system of versioning has been around for a long time, it works well, and it's easily understood by everyone on the team.
Each microservice, by contrast, has its own distinct version number. Often multiple versions of individual services are run side-by-side to allow graceful degradation and instant redeployment.
Understanding, and managing, the version numbers of dozens of loosely coupled microservices is challenging. Processes and tooling must be put in place to answer questions like: Which versions of a service are currently in use? When is it safe to remove an old version? Which microservices depend on which specific versions of other microservices?
A related challenge is routing service requests to specific versions of a microservice. A powerful allure of microservices is the practice of running multiple service versions concurrently, allowing instant rollback and graceful degradation in the event of a failure or disruption. A mechanism to route between service versions must be designed into the system from the outset.
This dynamic routing capability can be thought of as sophisticated A/B testing.
There are no widespread tools or practices available today to deal with this, so it's up to each organization to determine a mechanism to reroute requests to specific service versions on the fly, in real time, in response to application failure or feature upgrades/downgrades.
As the number of microservices making up an application increases, so does the number of integration points. Associated with each integration point is a public interface allowing other microservices and applications to access it. Each of these interfaces require a naming scheme, error handling protocol, serialization mechanism, and routing facility. Each of these aspects is dramatically more complex when the intercommunication is over the network as compared to via a library call or object message invocation.
Monitoring and Logging
As mentioned in a previous article, microservices monitoring must be instantly responsive, able to detect failure or situations requiring rollback, in seconds, and must be completely automated, no human intervention required at any time. The tooling must enable this, provisioning agent-based logging and allowing services to be deployed with all monitoring hooks and logging facilities prewired and ready to go. As with most things microservices-related, doing this correctly results in more complexity and overhead in the tooling.
The Challenges Could Fill a Book
I'm just getting started here, and I've only covered a few pitfalls of microservices above. There are many more challenges which are worth a mention:
- the vastly increased operational complexity and overhead
- the absolute requirement for a transition to devops mindset and culture
- the current lack of developers, architects, and devops folks with real microservices experience
- the lack of mature tooling designed for microservices
- the rapid evolution of microservices practices
- the challenges with testing environments and procedures
- known and unknown security concerns
The list goes on. Even understanding and conceptualizing a microservices architecture is difficult. Dozens of cooperating microservices might result several hundred distinct communication paths between them. Just diagramming these interconnections, and worse, visualizing how they change over time, is a formidable task. Netflix and others are pioneering innovative ways to represent these complex systems, but tools to accomplish this are in their infancy and not yet in widespread use.
My main point here is to drive home the fact that building microservices systems is challenging and the decision to do so must not be taken lightly. But it's not a hopeless task, and many companies are taking the microservices leap with great success.
Tooling to the Rescue
One area that will significantly help here is in tooling, which is naturally evolving almost as quickly as microservices are. Netflix is a prolific contributor to tools and utilities that ease the construction and delivery of microservice-based applications. Similarly, Platform-as-a-Service (PaaS) offerings are quickly adapting themselves to allow rapid development, delivery, deployment, and maintenance of microservices-based applications.
So finally, we're ready for the long-promised and final installment of this series blog which will cover Microservices and PaaS, and show how Platform-as-a-Service is evolving to adapt to a microservices approach.