How micro is your microservice?
Microservices have been all the rage for the past several years. They’re the new way to make applications scalable, robust, and break down the old silos that kept different layers of an application at odds with each other.
But let’s not pretend they don’t have costs of their own. They do. And, in fact, they are frequently, perhaps most of the time, not the right choice. There are, however, other options besides one monolith to rule them all and microservice-all-the-things.
What is a microservice?
As usual, let’s start with the canonical source of human knowledge, Wikipedia:
“There is no industry consensus yet regarding the properties of microservices, and an official definition is missing as well.”
Well that was helpful.
Still, there are common attributes that tend to typify a microservice design:
- Single-purpose components
- Linked together over a non-shared medium (usually a network with HTTP or similar, but technically inter-process communication would qualify)
- Maintained by separate teams
- And released (or replaced) on their own, independent schedule
The separate teams part is often overlooked, but shouldn’t be. The advantages of the microservice approach make it clear why:
- Allow the use of different languages and tools for different services (PHP/MongoDB for one and Node/MySQL for another, for instance.)
- Allows small, interdisciplinary teams to manage targeted components (that is, the team has one coder, one UI person, and one DB monkey rather than having a team of coders, a team of UI people, and a team of DB monkeys)
- Allows different components to evolve and scale scale independently
- Encourages strong separation of concerns
Most of those benefits tie closely to Conway’s Law:
Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.
A microservice approach works best when you have discrete teams that can view each other as customers or vendors, despite being within the same organization. And if you’re in an organization where that’s the case then microservices are definitely an approach to consider.
However, as with any architecture there are tradeoffs. Microservices have cost:
- Adding network services to your system introduces the network as a point of failure.
- PointS of failure should always be plural, as a network, even a virtual and containerized one, has many, many points of failure.
- The network will always be 10x slower than calling a function, even a virtual network. If you’re using a shared-nothing framework like PHP you have to factor in the process startup cost of every microservice.
- If you need to move some logic from one microservice to another it’s 10x harder than from one library to another within an application.
- You need to staff multiple interdisciplinary teams.
- Teams need to coordinate carefully to avoid breaking any informal APIs
- Coarse APIs
- Needing new information from another team involves a much longer turnaround time than just accessing a database.
Or, more simply: Microservices add complexity. A lot of complexity. That means a lot more places where things can go wrong. A common refrain from microservice skeptics (with whom I agree) is
“if one of your microservices going down means the others don’t work, you don’t have a microservice; you have a distributed monolith.”
To be sure, that doesn’t mean you shouldn’t use microservices. Sometimes that is the right approach to a problem. However, the scale at which that’s the is considerably higher than most people realize.
What’s the alternative?
Fortunately, there are other options than the extremes of a single monolith and a large team of separate applications that happen to talk to each other. There’s no formal term for these yet, but I will refer to them as “clustered applications”.
A clustered application:
- Is maintained by a single interdisciplinary team
- Is split into discrete components that run as their own processes, possibly in separate containers
- Deploys as a single unit
- May be in multiple languages but usually uses a single language
- May share its datastore(s) between processes
This “in between” model has been with us for a very long time. The simplest example is also the oldest: cron tasks. Especially in the PHP world, many applications have had a separate cron process from their web request/response process for literally decades. The web process exists as, essentially, a monolith, but any tasks that can be pushed off to “later” get saved for later. The cron process, which could share, some, all, or none of the same code, takes care of the “later”. That could include sending emails, maintenance tasks, refreshing 3rd party data, and anything else that doesn’t have to happen immediately upon a user request for the response to be generated.
Moving up a level from cron are queue workers. Again, the idea is to split off any tasks that do not absolutely need to be completed before a response can be generated and push them to “later”. In the case of a queue worker “later” is generally sooner than with a cron job but that’s not guaranteed. The workers could be part and parcel of the application, or they could be a stand-alone application in the same language, or they could be in an entirely different language. A PHP application with a Node.js worker is one common pattern, but it could really be any combination.
Another variant is to make an “Admin” area of a site a separate application from the front-end. It would still be working on the same database, but it’s possible then to have two entirely separate user pools, two different sets of access control, two different caching configurations, etc. Often the admin could be built as just an API with a single-page-app frontend (since all users will be authenticated with a known set of browser characteristics and no need for SEO) while the public-facing application produces straight HTML for better performance, scalability, cacheability, accessibility, and SEO.
Similarly, one could make a website in Django but build a partner REST API in a separate application, possibly in Go to squeeze the last drop of performance out of your system.
There’s an important commonality to all of these examples: Any given web request runs through exactly one of them at a time. That helps to avoid the main pitfall of microservices, which is adding network requests to every web request. The fewer internal IO calls you have the better; just ask anyone who’s complained about an application making too many SQL queries per request. The boundaries where it’s reasonable to “cut” an application into multiple clustered services are anywhere there is, or can be, an asynchronous boundary.
There is still additional complexity overhead beyond a traditional monolith: while an individual request only needs one working service and there’s only one team to coordinate, there’s still multiple services to have to manage. The communication paths between them are still points of failure, even if they’re much more performance tolerant. There could also be an unpredictable delay between actions; an hourly cron could run 1 minute or 59 minutes after the web request that gave it an email to send. A queue could fill up with lots of traffic. Queues are not always perfectly reliable.
Still, that cost is lower than the overhead of full separate-team microservices while offering many (but not all) of the benefits in terms of separation of concerns and allowing different parts of the system to scale and evolve mostly independently. (You can always throw more worker processes at the queue even if you don’t need more resources for web requests.) It’s a model well worth considering before diving into microservices.
How do I do either of these on Platform.sh?
I’m so glad you asked! Platform.sh is quite capable of supporting both models. While our CPO might yell at me for this, I would say that if you want to do “microservices” you need multiple Platform.sh projects.
Each microservice is supposed to have its own team, its own datastore, its own release cycle, etc. Doing that in a single project, with a single Git repository, is rather counter to that design. If your system is to be built with 4 microservices, then that’s 4 Platform.sh projects; however, bear in mind that’s a logical separation. Since they’re all on Platform.sh and presumably in the same region, they’re still physically located in the same data center. The latency between them shouldn’t be noticeably different than if they were in the same project.
Clustered applications, though, are where Platform.sh especially shines. Every project can have multiple applications in a single project/Git repository, either in the same language or different language. They can share the same data store or not.
To use the same codebase for both the web front-end and a background worker (which is very common), we support the ability to spin up the same built application image as a separate worker container. Each container is the same codebase but can have different disk configuration, different environment variables, and start a different process. However, because they all run the same code base it’s only a single code base to maintain, a single set of unit tests to write, etc.
And of course cron tasks are available on every app container for all the things cron tasks are good for.
Within a clustered application processes will usually communicate either by sharing a database (be it MariaDB, PostgreSQL, or MongoDB) or through a queue server, for which we offer RabbitMQ.
Mixing and matching is also entirely possible. In a past life (in the bad old days before Platform.sh existed) I built a customer site that consisted of an admin curation tool built in Drupal 7 that pulled data in from a 3rd party, allowed users to process it, and then exported pre-formatted JSON to Elasticsearch. That exporting was done via a cron job, however, to avoid blocking the UI. A Silex application then served a read-only API off of the data in Elasticsearch, and far faster than a Drupal request could possibly have done.
Were I building that system today it would make a perfect case for a multi-app Platform.sh project: A Drupal app container, a MySQL service, an Elasticsearch service, and a Silex app container.
Please code responsibly
There are always tradeoffs in different software design decisions. Sometimes the extra management, performance, and complexity overhead of microservices is worth it. Sometimes it’s… not, and a tried-and-true monolith is the most effective solution.
Or maybe there’s an in-between that will get you a better balance between complexity, performance, and scalability. Sometimes all you need is “just” a clustered application.
Pick the approach that fits your needs best, not the one that fits the marketing zeitgeist best. Don’t worry, we can handle all of them.