72 comments

[ 2.3 ms ] story [ 141 ms ] thread
I've been seeing gRPC support in the various api-gateway solutions for a decent while now, was always interested in its use in the abstract, but having a human readable interface would be very difficult to give up, and something I personally probably wouldn't do unless I was bumping against some hard efficiency limits.
Depends on what your goals are. Protos give a nice API definition if you want to make sure you don't screw up that data that's on the wire (field name misspelled or something similar). Obviously JSON has OpenAPI (swagger), but I'm still a fan of protobufs (probably stockholm syndrome).
How do you replace Postman? I feel like it's pretty easy to test text encoding formats that are human readable, but not sure how I'd do that with gRPC.
I personally use BloomRPC — just point it at your protos and go. (plus it supports grpc-web)
+1 for Bloom. The only thing it’s missing is support for the gRPC introspection API. It’s not bad if you have all of your organizations protos in one repo (which is what we do), but otherwise annoying to have to clone a repo and point Bloom at the .proto file just to talk to the API. Other tools (grpcui I believe is one) support introspection but aren’t as polished at Bloom.
I would recommend https://kreya.app as the Postman alternative for gRPC. It supports much more features than BloomRPC or Insomnia and has some additional goodies that will help advanced users. For example global authentication configurations or support for easy git syncing.

Disclaimer: I am one of the creators of Kreya

Swagger is handy here. Auto-generate your API model objects, interfaces, and REST clients in any language (e.g. Typescript).
Is this the path to a tightly coupled distributed monolith?
Yeah imagine if instead of just using plain text JSON for your web application you could use a proprietary binary format that web browsers know nothing about and require a translation layer to talk to. It's awesome.
Agreed. Use JSON, proprietary binary formats are terrible, and 99% of the time the bandwidth savings aren't worth it. For messaging, I really like NATS (incubating in the K8S landscape - probably graduating soon). My fav thing is the wildcard usage in NATS.
Unless you are communicating C++ services for example and you prefer a lib that does the parsing and transformation to native object for you
Bandwidth isn't the primary concern here. CPU time spent parsing/unparsing is. Depending on how your application is structured, it can actually dominate time spent in application logic. Whatever the case, it's simply waste heat being added to the universe. Use a little-endian, 64-bit-aligned binary format, and parsing overhead simply goes away.
Great point. I'm not one to over optimize, but seems like parsing messages for internet sized apps is worth spending a little effort to save energy and environment.
Please elaborate on the ways in which protocol buffers conforms to the definition of the word "proprietary".
What does this (client to web service communication) has to do with interservice communication?

You still have to unmarshal json into some sort of object/struct.

There's no reason you couldn't use gRPC with json as a serialized message format. For example grpc-gateway [0] provides a very effective way of mapping a gRPC concept to HTTP/JSON. The thing is, after moving to gRPC, I've never really felt a desire to move back to JSON. While it may be correct to say "parsing json is fast enough" it's important to note that there's a "for most use cases" after that. Parsing protos is fast enough for even more use cases. You also get streams which are amazing for APIs where you have to sync some large amounts of data (listing large collections from a DB for example) across two services.

With gRPC you also have a standardized middleware API that is implemented for "all" languages. The concepts cleanly map across multiple languages and types are mostly solved for you.

Adding to that you can easily define some conventions for a proto and make amazing libraries for your team. At a previous job I made this: https://github.com/CaperAi/pronto/

Made it super easy to prototype multiple services as if you mock a service backed by memory we could plop it into a DB with zero effort.

I think this "gRPC vs X" method of thinking isn't appropriate here because protos are more like a Object.prototype in JavaScript. They're a template for what you're sending. If you have the Message you want to send you can serialize that to JSON or read from JSON or XML or another propriety format and automatically get a host of cool features (pretty printing, serialization to text/binary, sending over the network, etc).

[0] - https://github.com/grpc-ecosystem/grpc-gateway

> The thing is, after moving to gRPC, I've never really felt a desire to move back to JSON.

Second this. I think its also really important to consider the "trap" of going in on gRPC, but using something like grpc-gateway to also spit out JSON as a "backup". We did this for a project I was on, and the JSON API was the thing everyone else used (because up until then, everything there was a JSON API, naturally). As a result, unless a consuming team was willing/able to get involved in the protobuf definition internals, most of our consumers didn't reap any benefits from our typed protobuf API.

I know it really isn't hard to do, but lowest friction denominator wins far too often in a feature-focused environment :-\",

One thing that was doable in my last company where I used gRPC was I made interfacing with the gRPC API dead simple. Something like `connect(ServiceBlockingStub.class)` was in your code and it would instantly pull configs, build the service, wire up all of our middle wares + logging, etc.

You can automate a lot of this which essentially makes the normal level of friction easier.

That's pretty cool. Was there a particular local environment needed to make this work? I recall auto generation being a part of the whole suite, but sounds like you took it up a notch.
The special sauce was establishing standards that were used across the code base that we could detect. For example, all configs were from environment variables in the form of `<ServiceName>_<DestinationName>_URL` or something. If the foobar services wants to talk to the auth service it would read the environment variable `FOOBAR_AUTH_URL`. Then, from conventions like this you can build libraries/tools and layer them on to simplify.

The way you write your deployments can also know about this convention and automatically generate the environment variables you need (didn't get to this point). I hope one day I get a chance to work on building some tooling like this, open sourcing it, and making it easy for others to adopt a similar mentality.

Why are you using a browser to test microservices, for which browsers are an atypical-at-best client?

Amiga got right what Unix got wrong: A well-structured, open binary protocol is almost always preferable to a "plain text" protocol because the parsing overhead really adds up.

Try telling that to a platform with a different endian format.
Little endian won. If you're developing modern software for modern machines, you pretty much needn't even consider big endian.
Almost 100% of microservice implementations I've come across are exactly this.
Why would it lead to any tighter of a coupling that other styles of APIs?
"In the olden days, a software application was built as a large monolith (...)" - people should stop writing this kind of shit.

You can hear this nonsense from some poor quality bloggers. People who are serious about it say things like "for most startups microservices is bad idea", "monolith should be starting point in most cases", "there is a price that comes with microservices", "modularised monoliths are often pefect fit" etc. this is coming not from random internet dudes but from Sam Newman author of "Building Microservices" and "Monolith to Microservices", Martin Fowler etc.

Microservices/services/monoliths are one of design kinds, they best fit into different setups.

References:

* https://www.theregister.com/2020/03/04/microservices_last_re...

* https://www.youtube.com/watch?v=GBTdnfD6s5Q

etc.

you can see a similar lack of understanding when they use a browser-based test of http/1.1 vs http/2 to justify the improved performance of http/2 for service-to-service communication. what that test is really demonstrating is the fact that your browser hard limits the number of http/1.1 connections it will make much moreso than it limits the number of http/2 connections it makes.
sure maybe the browser is limiting the number of http/1.1 connections, but it's not true that it doesn't limit the number of http/2 connections in a similar manner. it only creates one http/2 connection but due to multiplexing that is all it needs.
I especially dislike when people talk about microservices and scalability like they are a solution. Most companies won't see the kind of scale Facebook/Google/etc sees, not because they won't have that much clients, but also because there's a good chance that they don't need all clients in the same system. Most business don't benefit from the network effect. In that case, your scale is not the total sum of the size of all your clients like Facebook/Google/etc, it's just your biggest client!

Also, I don't like how the discussion about "microservices" is drowning the discussion about plain services. Services are a natural consequence of Conway's law when your organization gets bigger. You can see parallels with the first wave of OO programming for software projects, where the goal of having a public interface and private internals was to make projects scale when 10/100/1000 times more people are working on it. This is exactly what's happening with services: a team maintain a public API, and private internals (and the developpers from the other team depend on the internals anyways because reality isn't that easy). Then it's easy to see what microservices are: ravioli code.

In a way, services and microservices are OO programming in the Alan Kay sense: late binding, message passing, local retention and hiding of state.

(comment deleted)
>the discussion about "microservices" is drowning the discussion about plain services.

Yeah I think this really sets back the conversation when, in my mind, microservices are just services with the acknowledgement that minimum size should not factor into breaking off a new service.

> In a way, services and microservices are OO programming in the Alan Kay sense: late binding, message passing, local retention and hiding of state.

> This is exactly what's happening with services: a team maintain a public API, and private internals (and the developpers from the other team depend on the internals anyways because reality isn't that easy). Then it's easy to see what microservices are: ravioli code.

This is a moot point. Following up on you OO example, there is not reason you can have separate public and private implementations within a (modularized) monolith.

The biggest downsides of (modularized) monoliths lie in 1) scaling - e.g., you can independently scale and load balancer each service, 2) releases - you can have independent releases (at the risk of maintaining version compatibility).

> Most companies won't see the kind of scale Facebook/Google/etc sees, not because they won't have that much clients, but also because there's a good chance that they don't need all clients in the same system.

I feel this line of argument is disingenuous and is based on a mix of strong personal opinions and very limited to non-existent insight onto which problems other orgs experience.

For starters, the main selling point of microscopes is not performance. It is organizational advantages. It is a way to draw clear and crisp lines regarding ownership and ops. Small teams are able to design and deploy and run and troubleshoot small bits of a large machine without wasting time in back-and-forths with other teams, and they can put in place safeguards so that if other teams screw up then their blast radius is limited.

But regarding performance, let's not fool ourselves into believing that there's always a fatter network pipe and a beefier box to deploy to. Often there is, but often there simply isn't. Once you are forced to deploy your monolith into multiple instances, most of the criticism directed at microservices is rendered moot. Also, beefier boxes are far more expensive to operate, so let's not pretend there are no costs.

Also, there's the reliability aspect. If you want to have a reliable service then you need redundancy, which means multiple deployments. Once you have to deal with load balancers serving traffic to multiple instances of your monolith, there is no longer a significant architectural and ops difference in whether you operate a monolith or shave a service or two out of it. Thus most of the criticism directed at microservices is rendered moot.

Lastly, some orgs are ok with serving users in a very limited geographical region. That's perfectly fine. Other orgs might have users in different parts of the globe. Do you expect them to tolerate latencies in the 200-500ms range routinely, or do you find acceptable to reel in those latencies back to the 50ms territory by deploying a part of the service closer?

I get the Monolith first principle, but you're fooling yourself if you believe that only FAANG-level companies can benefit from it.

> For starters, the main selling point of microscopes is not performance. It is organizational advantages. It is a way to draw clear and crisp lines regarding ownership and ops. Small teams are able to design and deploy and run and troubleshoot small bits of a large machine without wasting time in back-and-forths with other teams, and they can put in place safeguards so that if other teams screw up then their blast radius is limited.

That's true, but at this point we should clarify what we mean by microservices. By "microservices", I mean "you end up with more services than engineers". If you've got a team for each service, then I would just call them services. I also don't agree that back and forth with other teams is always a waste of time, especially since just after this you talk about limiting their blast radius if they screw up. That's probably necessary if your org doubles every year or already has thousands of engineers, but that's not for everyone.

> But regarding performance, let's not fool ourselves into believing that there's always a fatter network pipe and a beefier box to deploy to. Often there is, but often there simply isn't.

I don't think we are talking about the same thing here. My point was that if you don't have network effect (like Twitter or Facebook), there's no need to put all of your users into the same box. You could have the extreme opposite, one instance for each users. So you only need to be able to handle your biggest client.

> Once you are forced to deploy your monolith into multiple instances, most of the criticism directed at microservices is rendered moot.

I don't think that's true. Having multiple instance of a monolith is not the same as having multiple different services.

> Also, there's the reliability aspect. If you want to have a reliable service then you need redundancy, which means multiple deployments. Once you have to deal with load balancers serving traffic to multiple instances of your monolith, there is no longer a significant architectural and ops difference in whether you operate a monolith or shave a service or two out of it. Thus most of the criticism directed at microservices is rendered moot.

> Lastly, some orgs are ok with serving users in a very limited geographical region. That's perfectly fine. Other orgs might have users in different parts of the globe. Do you expect them to tolerate latencies in the 200-500ms range routinely, or do you find acceptable to reel in those latencies back to the 50ms territory by deploying a part of the service closer?

Again, I don't think that's true. You can have one/multiple monolith instances in each region. You may argue that it's not strictly a monolith if there is a routing and load-balancing service in front of it, and that's true, but it's also not a microservice architecture.

> That's true, but at this point we should clarify what we mean by microservices. By "microservices", I mean "you end up with more services than engineers".

That's not what "microservices" is.

If you are not familiar with the concept then you should refrain from post personal assertions on guidelines or best practices.

> That's not what "microservices" is.

That's how it sometimes ends up. You said:

> For starters, the main selling point of microscopes is not performance. It is organizational advantages. It is a way to draw clear and crisp lines regarding ownership and ops.

But that's just regular old services. The "micro" in "microservices" usually implies smaller services, and one extreme of that is having more services than engineers. That's something I saw happen, heard about and read about. Microservices can be abused by making them too small and making too many of them. Uber is an example of that.

Sometimes you just need the scalability. I work on a 2 programmer team making a data integration product. There's a whole lot of features, but one is that you can push a button and move like 10 years of ERP data from 1 system to another. This is done as a scalable microservice because running big migration can up our resources like 50X from baseline, and sometimes we're running 5 to 10 at once. Usually we're running 0. Between all services we utilize on average less than a gig of memory, but regularly allocate 10 or even 100s of gigs at once for short periods of time.
That's a great example that compliments nicely the remark of jayd16 here https://news.ycombinator.com/item?id=28636152

> microservices are just services with the acknowledgement that minimum size should not factor into breaking off a new service

Thanks to you both for the perspective.

I've seen "confirmed" developers create one microservice for each entity/model they have. Everything is simple CRUD, absolutely no domain logic. Literally, a "user" microservice, with its User class and repository, 100 LOC, a "message" microservice with 80, etc. Aggressively arguing the superiority of their monster. Of course the services all shared the same db, completely missing the actual benefit of microservices. And every service was completey coupled to others and could not run on its own.

How do you fix that ?

> How do you fix that?

You run away as fast as you can is what you do.

eventually they are just going to invent an actor framework
The user service team told us we were querying them too much, so we just stored usernames in addition to user IDs to avoid querying them, then later documents would have the wrong username on them if the user changed their username.

One perspective is that we were negligent and caused a bug, but another possible perspective is that maybe the user service team should have caching instead of every other team building their own caching for the user service.

They should send a cache-control header in the response or 304 Not-modified when queried with not-modified-since. The calling service should always be the one responsible for caching to save the network round-trip. And if you're going to duplicate data like usernames, get a message channel setup to propagate the changes into the system cloning data.
This blog is not on why you should choose microservices, it's just about microservices communication, but let me start with saying I don't agree. I know a couple of friends from startups and they all build software using microservices. I have personally worked in large monoliths for close to 12 years (and last 4/5 years microservices). IMO, the biggest benefit of microservices is agility, you can build and deploy features incrementally and fast. Nowadays with slick frameworks like Spring boot, Nest.js and all the tooling around (like Kubernetes) building microservices is much easier than building monolith. In the monolith world, we had developed a sophisticated, in-house framework for concurrent and distributed task management and I don' see the need for that kind of framework in microservices. In the last 3 years, I had never come across a single developer working on a "cloud-native" microservices project who can confidently answer a single thread question - but developing an auto-scaling application based on the Kubernetes is a piece of cake. How do you expect them to write a well-behaved monolith? It's much easier to quote Sam Newman and Martin fowler, but sorry, I don't find all advice from pundits very practical. From Sam Newman's book "One of the things I see people having the hardest time with is the idea that microservices should avoid the use of shared databases. If a microservice wants to access data held by another microservice, it should go and ask that second microservice for the data. This gives the microservices the ability to decide what is shared and what is hidden, which allows us to clearly separate functionality that can change freely (our internal implementation) from the functionality that we want to change infrequently (the external contract that the consumers use)." How is this practical for each microservices to have its own database? Is it even possible in the cloud world where we consume databases as a service? The bottom line is - nowadays developing microservices is much easier than a monolith. You get lots of tooling - such as observability stack, resiliency out of the box. This just needs to be well supported by organization culture and practices (DevOps, SRE, incident management).
Have you guys run into the issue of gRPC being used for ML inference engines/platforms, and having the message size be an issue?

Recently, I was working on building the backend for our Speech Recognition engine and it seems like the message size limit came in the way of being able to perform batch/offline inference. I might be lacking some of the intuition behind designing and deploying such services, and am curious if you guys have run into this issue. The platform doesn't seem to accept the channel options from the client.

That and the issue of no easy way to do gRPC calls from a browser yet, and the resulting architecture then needing to have been built the ground up with considerations for an extra piece - a gateway, or something like Envoy to make it more accessible over the web. I feel like that would be okay if I was working on a project with k8 ingress set up already and already utilizing microservices. But for getting running it seems like added complexity. Not hating on it or anything, just sharing my experiences and thoughts, hoping that some of you might've had experience reasoning and dealing with such designs and can offer some insight.

You can configure the message size limit on your gRPC server, doesn't that fix your issue?
Another option could be to "chunk" the messages with client-/server-streaming or bi-directional calls. But if you call your API from a browser, that may not be possible yet
Yes, but that would involve us digging into the code of the gRPC server, which is unfortunately closed to us :(
Ahh, we don't have access to the server. It's closed - an NVIDIA inference engine. Which under the hood talks to their Triton engine. Unfortunately, while Triton allows configuring the limit, the layer in front of it eats our channel options which have the message size configurations.
Related question: is gRPC the anointed successor to WCF for .NET? I'm looking at a re-write of our mostly monolithic code at work, which uses C# on an old framework with a little WCF to communicate with one service, and got a bit concerned as WCF appears to be on the way out in the new .NET Core versions.
I think it basically is. I've done work for a company using .Net Core since about 2018. Back then, you had to use the Google stubs and work around a lot of the Microsoft host setup to get it to work. These days, gRPC seems to be on par with REST as a framework.

That said, I'd like to see better support for C# code generation from proto files. For example, the support for nullable types works ok but is a bit cumbersome.

Note that Grpc.Core is deprecated, and will be entirely unsupported some time next year. grpc-dotnet, which is AFAICT the only replacement, by design requires ASP.NET Core for the gRPC server. That is, there is no upgrade path for the .NET Framework or in situations where you don't want to pull in ASP.NET.

(Please, someone tell me I'm wrong. Please.)

You're not wrong. I was researching this exact topic just this afternoon. We are creating new clients using .NET 5 and talking to legacy services via gRPC. Unfortunately, the legacy services are .NET Framework 4.8 or 4.6, so there is no option to use grpc-net, only grpc.core.
I love the cyclical nature of everything in remote execution world. Corba, Remoting, some XML sauce, WCF, Http methods and Url's, some Json sauce, and than gRPC. Same ideas. Just like having flared trousers every 10 years or so.

Payload is binary, it's fast -> Anybody can read the payload but it's structured -> Structure is redundant, look how lightweight is this -> Payload is binary, it's fast

The problem in software world is, there's no silver bullet for any particular problem but there are technology fashions claiming otherwise.

Flared trouser periodicity is 20 years.
Haha, that was my thought when I started reading about gRPC. Waaay back in the day I remember programming "distributed" systems using Java RMI. For some whatever reason we are coming back full circle to that approach.
I couldn't help but read this article through the eyes of CORBA, and the bit about "the olden days" made me crack up even harder.
SOAP. I'm waiting for gRPC and OpenAPI to get me where WSDLs were 20 years ago.
SOAP was the successor to CORBA really, and then everyone decided it was too complicated and heavy weight. ;-)
On the other hand, past technologies like CORBA, DCOM, and RMI were all object-oriented, whereas gRPC is data-oriented.

CORBA (as was DCOM) provided an object model where the client would make method calls to object by identity. This provided "locality transparency" — an object looks identical in the client as in the server. The client object is just a "stub" in CORBA parlance (or "proxy" in DCOM, if I remember correctly), and method calls are transparently translated to RPC calls. The consequence of this is that a client can hold onto entire object graphs, and gets very tightly coupled with the server. For example, a single client can keep tons of objects "open" inside the server just by virtue of having references to them. Very careful tracking of object references would be needed. Both CORBA ans DCOM use explicit reference counting; if a client gets this wrong, it can cause memory leaks inside the server. Not great for scalability, and a big weakness in terms of security.

gRPC is more like DCE RPC or Sun RPC, while leveraging a bunch of the web stack which didn't exist back then. There's no denying that technology is cyclical in some ways, of course. But we're not exactly coming full circle. We bring back some things and integrate it into the new stuff. The tooling around gRPC is miles ahead of what we had back then, while being based more or less on the same principles.

Interestingly, there's a similar (but maybe more depressing) cyclical story with dynamic vs. static typing in programming language. Python, Ruby, PHP, etc. allowed more people to shift away from commercial tech stacks (especially the horrific Java app server stuff in the early 2000s) while building out super-productive web frameworks. JavaScript allowed us to write web apps that were capable of competing with desktop apps. But in the process we gave up static typing. Millions of person hours have been spent in blogs and discussion boards defending, and extolling the superiority of, dynamic typing — yet this was found (in my view) to be a fallacy, and something of a shared delusion among developers drunk on web frameworks like Rails.

> But in the process we gave up static typing. Millions of person hours have been spent in blogs and discussion boards defending, and extolling the superiority of, dynamic typing — yet this was found (in my view) to be a fallacy, and something of a shared delusion among developers drunk on web frameworks like Rails.

Bingo. I learned C++ and then Java first, so I've always been biased towards static typing. When the whole MEAN stack (including non-relational databases i.e. just shoving tons of JSON back and forth) become the cool thing, I refused to buy into the hype and continued using Java, even if it didn't make me popular with the newer developers.

And now I make good money cleaning up Node / Python monstrosities that grew too big for dynamic typing to handle. The worst thing about these is that somewhere in the last 4-5 years, the dynamic advocates began to realize that static types really did make sense, and so now they've added all sorts of hacks like Typescript or Python typing libs (Pydantic) and annotations that try to bridge the gap.

But it doesn't work. The few actual benefits of "simple" languages like Python and Javascript (e.g. One Way to Do Things and quick prototyping) are a thing of the past. Instead, a typical Python or Express web app now has just as many unintelligible annotations and verbose "interfaces" or model objects as an "enterprise" Java / C# app had 10 years ago.

And the pseudo-types aren't even all that helpful. The IDEs can sometimes understand things, but usually not 100% like they would with a Java app. Rarely does auto-complete or large refactoring work. Typing errors aren't always found at "compile" time - still plenty of surprises discovered only at runtime.

Readability is just as bad as the typical Java-Spring app with all sorts of magic going on underneath the covers at runtime. There is no way to just glance at a simple controller and understand what's going on without knowledge of increasingly bloated frameworks.

It's just a mess. At least I'll have plenty of work for the next decade.

CORBA usually evolved into a very service / data oriented approach moving type value pairs around. Really only example problems were OO. At least that's how it worked on the number CORBA projects I was on.
Again a bit closer to GIOP and DCE RPC. The circle closes.
gRPC is quite counter-ergonomic with its own set of issues when trying to map a business domain to actual solutions.

protoforce.io has better modeling and implementation design, supports also 2-way communication, while remaining sane to read and debug.

/disclaimer: one of the authors

Does protoforce have sum types / tagged unions?

And is it open source? (apparently not, and is only free for non-commercial uses)

Kinda hard to compete when grpc and cap'n'proto are fully FOSS