The second in a series on microservices.
Part 2: The start of microservices
Out of the water, onto land…
The motivation for and benefits of a microservices architecture extend far beyond simply breaking down the monolith as mentioned, but for most engineers and companies I’ve spoken with looking to make the switch, scaling and cost savings are the primary motivation. They realize that deployments are painful, that money is wasted on power or cloud compute resources, that choke points are killing performance. But they quickly realize additional benefits: the application can be extended more easily. Services and functions can be decoupled from one another and refactored without affecting other parts. Each segment can handle it own security. Each service can dynamically scale independently of others. Let your imaginations run wild, boys and girls.
To better understand the move from a monolith to a microservices architecture, consider your own applications and how they have evolved over time. Initially, they were likely simple scripts. Probably less than 100 lines. I know mine were, at least. Over time, as the needs grew more complex, so did the code that supported them. No longer would 100 lines of straight imperative code work, so you added some functions here, functions there. Sometimes they were reused, sometimes it was just a pipeline from one function to the next. As you matured and your problems grew more complex, your solutions became more sophisticated and elegant. You embraced the DRY principle and wrote highly compact and reusable pieces of code. Helpful libraries that you were able to recycle across projects. You took each job that your code did and broke it down until it did one thing and did it very well. That my friends, is the sprit of microservices. We take services–tasks–and break them out of the monolith and into their own tiny little app, apply an interface to it so it can communicate with other things, and bam, microservice.
Now, imagine how we might do that in practice?
Killing the monolith, a little at a time
As I mentioned earlier, one of the first services that gets split off from the main monolith is the database. This makes the most sense as it’s the most un-like service from the rest of the application and data has a higher demand for integrity so a safer option is to move it off the server hosting the application. Additionally, clustering databases can be a headache, so moving the database off the application server makes administration of both easier, as the app servers can come and go without affecting the database. Already you can probably see some themes emerging that will become quite prominent soon, namely the decoupling of services.
Okay, I broke something off. What is the next thing?
This is often entirely dependent upon the organization, but let me give you a scenario:
After splitting the DB off from the main application, you get a new feature request dropped into your lap: send users notifications on pending actions in their account. Do you add this to the monolith or do you create a new, tiny little application? Why, a microservice of course! So you imagine that this mircoservice serves the same role in the overall app architecture that a function might in a program: You try to make it reusable, you write a solid API, you document it, you isolate it away from lateral movement and practice good defensive coding. You have a lovely little service now. Just one catch: You have written it in such a way (no fault of your own really, the app architecture of the old ugly monolith forced you to) that you have to call the monolith and ask it for a piece of information on every transaction you perform. It’s a terrible, no good, very bad situation. In the process, you have actually identified the next great candidate for a microservice and a prime illustration of another benefit of the microservices architecture: reusability.
Whether implicitly or explicitly, programmers understand the concept of DRY: Don’t Repeat Yourself. It’s why we have functions. Why write the same 10 lines every time I need to do something? Just write a function and pass data in and get data out. Easy. Mircoservices can serve the same purpose for us, and a well-written, reusable microservice is a lovely little gem that should be cherished.
In our situation, let’s take a look at how the notification service (let’s call him Mercury, since he’s dispatching messages) talks to Cronus (the big daddy Titan who ruled before Zeus and friends took over). Mercury needs information about the user it needs to dispatch a message to, so it has to call Cronos and ask it for some specific user info. Why? This sounds like bad design. Well, it is. That’s the nature of apps in the real world: they’re designed poorly. However, rather than giving Mercury direct access to the database, we can take something that Cronos is doing well, which is serving info about users and customers, and split it off. After all, this new service will instantly have two consumers as soon as it’s live and given what it’s serving, it will have many more.
So on your next sprint you decide to make this new service your top priority. Let’s call it Athena, after the goddess of wisdom known for her calm demeanor. Athena has one very narrow job: When asked about a user, it will authenticate and authorize the request. If the request is authorized, it will serve the requested information in a JSON object back to the requestor. Athena now has two customers: Cronos and Mercury, but more are coming. Therefore, Athena might end up being busy.
As it turns out, Athena’s job was a bit of a bottleneck. The developers were able to run it in a nice, compact server (512mb that usually ran at close to 80% capacity. She handles quite a few requests per second). However, once Athena was decoupled from Cronos and allowed to scale up to 4 instances, a total of 2gb of memory and nearly negligible cost, performance improved by 25%. Who knew! Previously, the entire app would have been scaled at enormous cost to get achieve what was possible by a surgical allocation of resources in a single service.
In terms of accuracy and precision, this is the difference between carpet bombing cities and a laser-guided JDAM flying through a window to blow up a room
Additionally, in off-peak hours, Athena easily scales back down to a single node until the traffic picks up again. We can already see the benefits of decoupling services from one another. Independent scaling is one, but what about refactor and replacement of large chunks of the application?
Decoupling gone wild
When Athena was first created, she was just cut and pasted right out of Cronos, monkeypatched a little to provide an interface, and largely left the way she originated. After a couple months, a developer who had been experimenting with Elixir in her free time had an idea. “Hey, based on what Athena is doing, we can get a performance gain and cut our codebase by half if we switch to elixir. Dropping the Node dependency will save install time and reduce RAM usage!” Excited, she codes up an MVP the next day and demos it to the team. They’re so impressed that they work on it and introduce Athena 2.0 the next week. Using the same exact API, they drop Athena 2.0 into place without any other app or service ever noticing while clawing back gains in memory utilization and request execution time.
Most people get into microservices for the benefits of scaling. “If it was just easier to deploy my app, or just this piece of my app, life would be so much better.” Once they have dipped their toes in the waters of microservices, they quickly realize the flexibility this provides for the overall design of the system and how things like decoupling can lead to independent refactoring (or even wholesale rebuilding) of services to improve performance. In the next article, we will address new problems microservices introduce.