What’s in a transport layer?
Understanding gRPC in the dawn of microservices.
Microservices are small programs, each with a specific and narrow scope, that are glued together to produce what appears from the outside to be one coherent web application. This architectural style is used in contrast with a traditional “monolith” where every component and sub-routine of the application is bundled into one codebase and not separated by a network boundary. In recent years microservices have enjoyed increased popularity, concurrent with (but not necessarily requiring the use of) enabling new technologies such as Amazon Web Services and Docker. In this article, we will take a look at the “what” and “why” of microservices and at gRPC, an open source framework released by Google, which is a tool organizations are increasingly reaching for in their migration towards microservices.
Why Use Microservices?
To understand the general history and structure of microservices emerging as an architectural pattern, this Martin Fowler article is a good and fairly comprehensive read. It’s worth noting Fowler’s caveat near the end that:
“One reasonable argument we’ve heard is that you shouldn’t start with a microservices architecture. Instead begin with a monolith, keep it modular, and split it into microservices once the monolith becomes a problem.”
This is good advice. However, especially in a world where developers are becoming increasingly empowered to operate their own applications, microservices are becoming the “accidental default” in some cases. For instance, at my employer Honeycomb, for a variety of reasons including the quality of the operations team, the architecture has leaned towards this type of modularity from the start (without any deliberate attempt to introduce microservices specifically).
Additionally, there are a variety of good reasons to use microservices, if you are prepared to accept the tradeoffs:
- Programs are more scalable. Instead of requiring many copies of a large application just to “scale” one part, apps can be scaled with more granularity. This translates to cost savings. In a world with ever-increasing cloud spend, this is tempting.
- Teams are more scalable. A team can iterate on only their service and worry less about having too many cooks in the “kitchen” of their codebase. Cleaner interfaces can be defined (since “cheating” using in-process data accesses is no longer possible) and defensive coding is encouraged because downstream services become potential “attackers” (e.g., by accidentally making too many requests).
- You gain flexibility to use different technologies to solve different problems. For instance, a machine learning classifier service could be deployed using Python by a team who primarily writes Java.
- General debugging and performance engineering can become easier. If one service is bottlenecked by something, this can be easier to spot in a microservice than in a rapidly changing monolith. Additionally, since each service is deployed separately, the number of deploys to examine as possible locations of breakage is reduced.
Do Microservices Require Brand New Tooling?
While it’s tempting to throw out everything existing and start from scratch when undergoing a shift such as the move to microservices, this is not required. As one example, the use of Linux containers and microservices together is frequently presented as an all-or-nothing proposition: it is implied that if you are using one you are definitely using the other. This is not actually the case.
While there are certainly potential advantages from adopting a microservice architecture and a container technology such as Docker together, one does not require the other. In fact, it’s potentially wiser to make only one change at a time. Large monolithic applications are just as happy to be containerized as small ones, and microservices will fit into a world using more mature DevOps tools quite snugly.
However, there are plenty of instances where reaching for a new tool in a microservices architecture does provide benefits. While, as noted above, it’s not requisite to use containers, depending on the situation of the team they could fit in nicely. For instance, they are frequently a fantastic fit in CI and CD pipelines even if you are not ready to use them in production.
In addition to your deployment practices, a likely area of your infrastructure you might take aim at is the transport layer your services use to communicate with each other. Since the communication is mostly internal, it has a slightly different set of parameters and needs than one that is exposed directly to the outside world. Thus, models like pulling from a message queue or using service-to-service RPC could be a better fit than what you might do if exposing your service to external consumers. One technology gaining rapid mindshare in this area is gRPC. gRPC shares some similarities with earlier systems such as Apache Thrift. Let’s take a look at why we might want to use gRPC and what it is like.
What’s in a Transport Layer? Reaching for gRPC
gRPC, as noted by its website, is:
“A general purpose, open-source universal RPC framework.”
Why might we want one of those? Well, if we are going to be making network calls from program to program across the network, we are going to need to use some sort of protocol. Many shops would reach for a “REST-ish” usage of HTTP/1.1, frequently passing around JSON. This is a perfectly valid approach, but especially for internal usage, it has some shortcomings:
- Version compatibility issues rapidly rear their ugly head. It won’t take long in the services’ lifecycle before developers want to deprecate fields, refactor old methods into new ones and more.
- Client libraries for various languages are difficult. If another team wants to communicate with your service, they will have to roll their own client library for it, including annoying (but important) authentication logic.
- It’s a poor fit for some methods of communication such as bi-directional streaming. If you want to rapidly “stream” data (e.g., the bytes representing a video call) back and forth, HTTP/1.1 calls are an awkward fit.
- It’s not optimally performant. Since services are likely to be located on computers geographically close to each other, it’s less likely that the network will be a bottleneck. With JSON+HTTP/1.1, a lot of processor effort is spent encoding and decoding messages passed to or received from other services due to the loose structure.
gRPC offers solutions to these problems, in respective order:
- Schemas are specified in a format called Protocol Buffers, which eases compatibility issues. Errors due to field name mismatches, removed methods, and more will become less frequent (and more obvious to spot the source of).
- Code can be automatically generated for your language of choice. Support exists to generate client bindings for many popular languages very rapidly. While not perfect, it’s a far cry better than hand-crafted bindings that need to be meticulously maintained over time, wasting precious developer cycles.
- Bidirectional streaming and authentication are supported out of the box. Real-time communication between services is supported as a first class citizen, as are essential security features like authentication between services.
- It’s high performance. For instance, this user reports in their benchmark that DevOps project etcd served requests about 40 times as fast (down to ~7ms of request response time from ~292ms) when they switched from HTTP/1.1 to gRPC.
gRPC Workflow and Tradeoffs
Let’s take a quick look into what usage of gRPC locally is like:
- The developer writes out a service definition in one or more “proto” files, defining the methods used to access their service and the data structures it accepts and returns.
- The developer invokes the Protobuf compiler, which reads in these files and generates code for their language of choice to use.
- The developer uses the automatically generated code to serve, and communicate between, services.
Naturally, once deployed in production, gRPC services communicate with each other in an internal subnet authenticated using something like TLS. Locally, however, the average developer’s workflow is likely to be unauthenticated, and services will need to be reloaded constantly. This is one possible hurdle to using gRPC effectively and it remains to be seen if increased tooling in this area will help alleviate this problem over time.
Other tradeoffs include opacity of the framework (consider: little access to general purpose tools like curl which can be used to rapidly introspect many HTTP-based services) and lack of full maturity (although the 1.0 version was recently declared). Regardless, the cheery young technology is likely to be invaluable to teams who are capable of using it in the intersection of correct utility weighed against ease of use.
Will gRPC Flourish in the Dawn of Microservices?
Will gRPC continue to grow in this Cambrian era of innovation in tooling and deployment practices that we are currently experiencing? It remains to be seen. The backing of a giant like Google has proven to be useful in other tools such as Golang, and that certainly doesn’t hurt in this instance. However, we have seen many trends and fashions come and go in the technology space, and only a few stubborn stalwarts stick around. I’m optimistic for this technology and feel that, even if gRPC and microservices themselves do not live on, their ideas and influence will continue to impact the collective DNA of the space.
Have fun out there and microservice on.