CTO Notes: Granularity of microservices in the context of project management
Midjourney: "a diagram of information flowing through many servers in cyberspace"

CTO Notes: Granularity of microservices in the context of project management

Microservices are a popular topic in software architecture, especially for SaaS, chiefly because of their potential for scalability. But not every project needs that kind of scalability, and not every organisation / team is ready for microservices even if high scalability is a requirement. Like with everything in life, microservices have certain trade-offs which might even kill a project if not handled properly.

The Holy Grail of scalability is to achieve a "shared nothing" state, where no two modules are tightly coupled - or ideally, not coupled at all. The theoretical condition for perfect scalability is if every operation can be done completely independently from every other operation.

With web apps, we have pretty much completely separated the user interface from business code, and made web browsers to be the ultimate UI platforms, working on their own, and providing UI to the data prepared (and stored) by the servers. This naturally leads to structuring the backend side of projects in terms of a collection of orthogonal REST-like APIs, to be called by the frontend (and possibly 3rd party services) as they see fit.

It doesn't have to be that way - there are multiple frameworks on the web which basically control the UI and respond to user-generated events from the server side. This approach is usually called Server-Driven UI, and it too can be done so the UI part is somewhat separated from the rest. But still, there needs to be a scalable backend infrastructure in the first place to feed the generated HTML to the frontend, and this again is an API.

I see the design of microservice apps mostly from two angles:

  • making clean, orthogonal APIs
  • creating an organisational structure which can maintain those APIs

As always, there are many ways to achieve those goals, and early decisions will have a huge impact on the future development of the product.

The enlightened monolith

For now, let's pretend we didn't hear about microservices. The classic way to implement scalable web apps would be to use a web stack that includes a "proper" database like PostgreSQL, to make the database (possibly in a combination with an in-memory cache server) a single source of truth, and to make each server request - usually an API call - as idempotent as possible.

The backend app will have a single code base in this case, probably all in the same source code repo, but that's not a downside yet. With a clean API design and avoidance of state management within instances, it should be possible to create dozens of instances of the same backend, and with the help of a load balancer, achieve very high levels of scalability. There is nothing wrong with this approach. It will usually work perfectly fine to serve hundreds of thousands of users.

A load balancer routes requests to undifferentiated monolithic instances

Some positive sides to the monolith approach are:

  • It's simple(r) compared to the alternatives. There are a lot of senior developers capable of architecting and creating such a project from scratch.
  • The shared codebase can make many operations more efficient in the short term.
  • At this scale, complex SQL queries with multiple joins and grouping are usually par for the course.

But an app which grows to serve hundreds of thousands of users usually has some growing concerns:

  • The development team will usually grow, and working on the same code base might introduce unnecessary friction
  • Redeployments (updates) of the monolith might mean downtime for the whole app
  • Upgrades of APIs might be tricky since the whole monolith will eventually contain a lot of legacy code
  • The monolith might take too much resources to be instantiated over a certain limit, because it needs to support all the functionalities of the whole app from a single process.

I call this pattern the "enlightened monolith" if it serves a clean API, rather than ad-hoc endpoints needed for a tightly-coupled front-end. This approach is not exactly a 100% fit to wear the name "microservice" but it is probably good enough for 99% of web apps out there.

A key aspect of the enlightened monolith approach is that the load balancer can route every request to a different instance, as the request stat is not managed by instances

If the web app server get clogged, just spawn a new one (horizontal scaling). If the database is getting clogged, migrate to a bigger db server (vertical scaling) - this will take modern app a long way in terms of scalability.

Classic microservices

A big step above the monolith is the classic microservices architecture, whose major attribute is that the app functionalities are split into separate processes by some criteria, and the database is designed to take a much higher load. Each microservice is its own process (or a group of undifferentiated processes), serving a set of endpoints.

The criteria by which the functionalities are split into microservices are a major architectural and organisational decision.

One aspect of the criteria is performance oriented, and beings by asking the question: what will be the hotspots of the app? Or, in more detail, to determine which endpoints can be clustered together based on their usage pattern. Presumably, endpoints which serve user profile data might not be as frequently hit as endpoints which serve product information. And endpoints which perform sales operations must be hardened against failure (and possibly against security issues) much more than the others. Often, this criteria can be matched to specific groups of functionalities an app implements. For example, there might be multiple endpoints serving product data, and most of them have a similar usage pattern.

The other important aspect is organisational: which teams, or parts of the teams, will be managing each group of endpoints? While the company or the product are young, it might not be important. Maybe there are some services which can be delegated to junior developers, but some must be handled by more experienced ones, and that's it. But as the company grows, this will begin to change, and more specialised teams will emerge out of necessity. Maybe the product team does not have to know the details of the user profiles.

This allows grouping the implementation of endpoints into a single service process - or a container, or a pod. At this point, it is also beneficial if they are based on the same URL prefix.

In Kubernetes jargon, a "service" is the name for a group of pods with containers created from the same image. In this case, the pods are usually undifferentiated and interchangeable. The number of running pods can depend on load balancing. There might be dozens of product pods, but only a few pods of the user profile service.

The load balancer routes requests to services which themselves are created as a load balanced group of identical processes (pods) serving endpoint for a specific URL path prefix

A common variant of this architecture is to split it into gateway services (aka business services, external services) and internal services (aka API services). This approach tends to build a library of internal APIs which can be meshed and combined into more complex APIs served by the gateways.

One common upgrade is to split the services into those reachable from the public network (usually providing endpoints to a frontend), from the internal services which are not accessible from the public network (but can access the database)

The benefits of the classic microservice approach in either case are:

  • Easy to scale horizontally (at least the services themselves - the database needs a special treatment)
  • Can be made less sensitive to bugs and code problems - deployment of new code can be staged so that the number of services running new code is gradually increased, rather than switching them over all at once; if there are problems, fewer users will be affected before the rollback
  • If an entire app service goes down for whatever reason, only one app feature could be disabled, while the rest of them will keep running (especially effective with a feature database)

The downsides:

  • This is a complex setup which needs a dedicated devops team to support it
  • Costs more than the monolith approach to develop and maintain
  • It's not geared for single request efficiency, but for scalability, especially in the two-tiered approach. It will truly shine only with a large number of requests.

In short: the classic microservice approach scales very well for millions of active users, at the expense of being more complicated to implement than the monolithic approach, and needing a large volume of users to actually show its benefits.

Lambda functions

Finally, lambda functions in the context of code deployment can be seen as the ultimate expression of the microservice architecture. Here, the functionality of the app is not even grouped into processes serving similar endpoints, but there is complete deconstruction of the app into individual, small functions, serving a single end-point. Or not even that - the lambda functions might not even know what transport protocol is used - HTTP, or gRPC, or protobuf, or anything else; they might just look like ordinary functions, and the environment which calls them takes care of the transport.

The diagram is pretty much the same as for the microservice case, only much more granular. Every single endpoint will have its own box, and load balancing will happen at the granularity of a single endpoint. This approach is often labelled "serverless" as we are no longer even concerned where and how the containers with our code are running.

The benefits of this approach are:

  • The operating environment (e.g. AWS Lambda, Google Functions as a Service) takes care of scalability.
  • App features, migrations, and resiliency is granular to the level of individual functions

But the downsides can be severe:

  • This approach leaves the app at the mercy of the service provider
  • It can be more costly than the other approaches, depending on the exact use

I'd say than this approach is still reserved for special casees - not many projects can benefit from it.

What about the database?

All of the microservice approaches described previously have excellent horizontal scalability, and that also includes resilience to common problems like device, network and data centre failures. Just create some instances somewhere else.

However, there are two general bottlenecks still present: the ingress load balancing and the database. All cloud services offer reasonably good solutions for ingress load balancing, but database scalability is kind of an ongoing issue.

The ideal solution, and the one I have been happy to employ in a couple of times, is to have completely independent databases for each service. With a two-tiered microservice architecture, the internal services can call each other if they need "foreign" data. In this way, we are introducing lightweight horizontal scalability for databases, and also having the opportunity to configure each database differently, depending on scalability requirements.

Generally, data storage (which goes beyond databases and includes caches, long term storage and backups) is a separate topic which deserves a separate article.

要查看或添加评论,请登录

Ivan Voras的更多文章

  • The selfish AI model

    The selfish AI model

    The Selfish Gene is an idea that can be applied to many things. Half tongue-in-cheek, half serious, I think "the…

  • Infinite growth in infinite complexity

    Infinite growth in infinite complexity

    As a young engineer, I had a comfortable bubble of friends and colleagues in which we collectively scoffed at the…

  • Not-so-hidden risks in AI code generation

    Not-so-hidden risks in AI code generation

    With AI's (such as they currently are - notably LLMs), it looks like we have approached one of the holy grails of…

  • What is money?

    What is money?

    Blast from the past - I was reminded of an article I wrote 6+ years ago - about how to conceptualize money. Here's the…

    2 条评论
  • When intelligence? AGI soon?

    When intelligence? AGI soon?

    Not a day goes buy in the last couple of weeks where I don't get a question like "is ChatGPT intelligent?" The answer…

    4 条评论
  • Sam Altman - Lex Fridman interview on ChatGPT notes - an abundance of optimism

    Sam Altman - Lex Fridman interview on ChatGPT notes - an abundance of optimism

    Just watched the interview with Sam Altman by Lex Fridman about GPT and ChatGPT, it's available here: Some short notes…

    2 条评论
  • Singularity is robots building robots?

    Singularity is robots building robots?

    The technological singularity is the (future) historical point in time where the technological progress gets literally…

    1 条评论
  • Doing Business in a Metaverse

    Doing Business in a Metaverse

    I saw a question in a forum recently, which boils down to: What is the incentive for someone to do business in the…

  • How "conversational AIs" like ChatGPT work?

    How "conversational AIs" like ChatGPT work?

    I see a lot of confusion and optimism with the recent advances in conversational AI. It is indeed impressive and it…

  • Problems we don't want to solve - decentralisation and the free lunch

    Problems we don't want to solve - decentralisation and the free lunch

    As is appropriate for this time of year, yesterday my sister started enthusiastically describing a new app by the…

社区洞察

其他会员也浏览了