Twirp: A new RPC framework for Go
blog.twitch.tv
blog.twitch.tv
You can also reach me directly, if you like, email is in my profile.
> If there is an error, it will be JSON encoded, including a message and a standardized error code.
Why not to return standard protobuf error when the source was a protobuf? It massively complicates things when you have to expect errors in one format and responses in the other.
Hopefully, the complexity is encapsulated by the generated client.
But what would the benefit(s) to users be? If they are deserializing a protobuf error, they are almost certainly using a generated client, so I don’t think they will know or care how the error was encoded.
(This might be better as a github issue to keep a visible record of the design for others.)
I also like when things are consistent without surprising behavior.
Adding another transport encoding would be good, but its important that any Twirp server can support all of the encodings. We would need to be able to map a message defined in capnproto to a protobuf message definition, which didn't seem completely trivial when we looked at it, since capnproto uses its own IDL, I believe.
I don't know a ton about capnproto, though, and I'm open to learning more about it. We would just need to work under the constraints that JSON and Protobuf requests would need to still work.
EDIT: the blog post covers this in the protocol section. Every request is a POST :)
TCP reconnection and stuff like that is at a lower level than Twirp. When you make a client of a Twirp service, the constructor accepts a http.Client which it'll use to send the requests. http.Client has a "Transport" field which is responsible for opening connections, that sort of thing. The Go standard library's defaults are pretty good, but you can tune it as you like.
> The core design of Twirp is language-agnostic and we’re planning to expand into new languages, but our Go implementation is already stable and capable of serving heavy production loads.
I was wondering, is Rust on the roadmap?
It'll take someone who is very fluent in Rust and who is motivated to do it, but I'm all for it.
Elb with layer 4 and proxy protocol enabled. Behind Elb sits nghttpx (not nginx) doing TLS termination and request forwarding to gRPC.
Proxy protocol is used to keep the source IP.
This setup is all done with Kubernetes using kops for the cluster setup, nghttpx-ingress-lb as the ingress controller. Also we have multiple namespaces/environments in Kubernetes (staging/dev..), so nghttpx does routing based on the hostname.
We tried linkerd before but somehow failed using it as an ingress controller doing TLS termination and upstream HTTP2. Doing the other way of routing everything through a dedicated linkerd port and a dtab worked, but mixing in TLS termination + upstream HTTP2 in a single dtab stopped us.
So for now we keep this simpler setup and we probably are going to check out Istio/Heptio/Envoy
That's why projects such as Envoy exist. I'd link it, but I'm on mobile.
Keep an eye on it.
Great work.
Twirp is everything I wanted in an RPC framework and I'm looking forward to implementing it ASAP. Thanks Twitch team :)
Debugging with lower-level tools like strace and tcpdump is also something that's trivial with JSON-over-HTTP/1.1, but virtually impossible with gRPC. (I mean, you could grab the payload and run it through gRPC deserialization, but would you?)
I'm a big fan of gRPC, but it is pretty top-heavy — lots of code to accomplish relatively little. If you have a language that can build client bindings dynamically from .proto files without recompilation, that would ease things a lot, but if you're using Go, for example, the bar is pretty high going from zero to productive.
For a few years inside Google I experienced zero discussions about almost every aspect of RPC. It took a trivial amount of time to implement stuff interfaces, clients and servers in multiple languages and it was trivial to understand the interface of, and implement a client for, other people's code.
I didn't necessarily like everything in Stubby, but I absolutely loved not needing to have pointless discussions about RPC mechanisms or protocol design.
Since I left Google, anything even remotely resembling RPC (including REST) has been an utter waste of time mostly spent bickering over this crap solution or that crap solution – mostly with people who don't care about the same things you care about.
REST is a crap solution in my eyes because it invites absolutely endless discussions on an endless list of subtopics. From the dogmatic/fundamentalist HATEOAS end of the spectrum to the RPC-using-HTTP-and-JSON-and-let's-call-it-REST camp. Not to mention that in addition you need to have an IDL and toolchain discussion. (Of course, none of the toolchains or ways to describe interfaces are very good. In fact, they all suck in part because the attention is being spread across so many efforts that don't get the job done).
I have yet to see an IDL that works better than a commented .proto file from a "get stuff done" point of view.
I completely understand where you are coming from when it comes to having human readable wire format. For 20 years I was a strong believer in the same, and for some systems I still believe in human readable formats.
But RPC and RPC-like mechanisms is no longer among them. RPC is for computers to talk to each other and not for humans trying to manually repair stuff.
(I'm a pragmatist, so I'm allowed to both change my mind and have seemingly inconsistent opinions :-))
For RPC you should encourage the creation of tools. If you need to look at the traffic manually: fine, make a Wireshark plugin or a proxy that can view calls in real time. That's annoying, but cheaper than going off and inventing yet another mechanism. And once it is done, it is done and there's one more thing that is sane.
We should really encourage people to build tools so we can automate things and have more precise and predictable control over what we are doing without having to reimplement parsing (which is what happens if people think they understand your protocol - which they often don't)
Also, make sure it works for a large enough set of implementation languages and understand how to work in mechanical sympathy with build systems. I don't care if Cap'n Proto is marginally better than Protobuf if it lacks decent support for languages I have to care about.
I have no idea how much time we wasted on trying to get Thrift to work in a Java project that needed to build on Windows, Linux and OSX back in the day, but I was ready to strangle the makers of Thrift for not paying attention to this.
At this point I'm beyond caring about the design of RPC systems. I just want something that works for software development and doesn't have to be a discussion. Hence, I get annoyed every time I see a new RPC mechanism instead of attempts to make some of the existing mechanisms work by just making just one aspect of them a bit more sane and exhibit a bit more empathy with programmers rather than the egos of protagonists of various libraries, frameworks and formats.
I imagine part of the lack of friction around Stubby was that Google was the only consumer, and could maintain client and server bindings/tools for the strict subset of the languages that Google standardized on.
I wouldn't say the lack of friction was mostly due to Google being the only consumer. It was mostly because there was a clear path from A to B when you wanted to give something an RPC interface and that this path was made efficient.
Or at least more efficient than trying to use REST-like stuff in a large organization with lots of different teams using different technologies.
It also helped that it wasn't a democracy. You had to use it. If you didn't like that you were free to leave. As a result people will focus more effort on making the tools better and make friction points go away.
In practical terms: we can spend weeks on getting a REST-like interface to work with other projects because everyone has an opinion on every bit of the design, and everyone uses different, and quirky libraries and tools. For Stubby in Google back then, it was mostly about defining the data structures, the RPC calls, discuss semantics and then the mechanics were taken care of. This is far, far, far from the actual case for many other technologies.
(And while I appreciate HATEOAS as a design philosophy, and I've tried to make use of it several times, it just is not worth the effort. It is just takes too much time to do right and to get everyone on the same page. Most proponents are more keen on telling everyone how they are using it wrong, than on writing good tools that actually help people use it right. There's very little empathy with the developer).
[1] https://cloud.google.com/endpoints/docs/grpc/about-grpc
(Disclaimer: I work on Google Cloud.)
This reminds of of the old saw "Those who ignore history are doomed to repeat it."
If you are unaware of the benefits, here are a just few links that can explain it:
- https://www.quora.com/What-are-the-advantages-of-REST-over-a...
- https://www.quora.com/What-is-the-difference-between-REST-an...
- http://duncan-cragg.org/blog/post/getting-data-rest-dialogue...
- https://apihandyman.io/do-you-really-know-why-you-prefer-res...
- https://www.quora.com/What-are-the-pros-and-cons-of-REST-ver...
POST /api/user/:username/update_email
But you change the application to require API clients to send the user_id instead of the username. POST /api/user/:user_id/update_email
Wouldn't you still need to mandate that all clients are updated regardless of whether you use this tool as an abstraction layer to your API?And protobufs: https://developers.google.com/protocol-buffers/docs/proto3
Example, one benefit is that you're defining your API by using language neutral protobufs which then generate code consistently (including types!) into many languages. Your entire communication procedure can be easily and succinctly described in a single small, human readable file.
Here's how we did it in CockroachDB: https://github.com/cockroachdb/cockroach/blob/24ed8df04719a1...
The supporting code (protoutil) is https://godoc.org/github.com/cockroachdb/cockroach/pkg/util/... and https://godoc.org/github.com/cockroachdb/cockroach/pkg/util/....
Anyone done similar with Twirp?
When you create a Twirp server, you get a `net/http.Handler`. You can mount it on a `http.Server` with its `TLSConfig` field set to the right policy.
The client constructor similarly takes a `*net/http.Client`. You could provide a Client that uses a `http.Transport` with its `TLSClientConfig` field set to something using the right value (like in https://gist.github.com/michaljemala/d6f4e01c4834bf47a9c4, say).
While the article identifies some operational issues (e.g. the reliance on HTTP/2), there are several considerable deficiencies with gRPC today, at least when using it with Go:
1. The JSON mapping (jsonpb.go) is clumsy at best, and by this I mean that it produces JSON that often doesn't look anything like how you'd hand-design your structures. "oneof" structs, for example, generate an additional level of indirection that you typically wouldn't have. Proto3's decision to forego Proto2's optional values (in Proto3 everything is optional) cause Go's zero value semantics to leak into gRPC [1]. (We had to fork jsonpb.go to fix some of these issues, but as far as I can tell, upstream is still very awkward.)
2. The Go code generator usually produces highly unidiomatic Go. "oneof" is yet again an offender here. The gogoprotobuf [2] project tries to fix some of go-grpc's deficiencies, but it's still not sufficient. Ideally you should be able to use the Proto structs directly, but our biggest gRPC project we basically gave up here, and decided to limit Proto usage to the server layer, with a translation layer in between that translates all the Proto structs to/from native structs. That keeps things clean, but it's pretty exhausting work, which lots of type switches (which are hampered by Go's lack of switch exhaustiveness checking; we use BurntSushi's go-sumtype [3] a lot, but I don't think it can work for Proto structs, as it requires that a struct also implements an interface).
3. Proto3 has very limited support for expressing "free-form" data. By this I mean if you need to express a Protobuf field that contains a structured set of data such as {"foo": {"bar": 42}}. For this, you have the extension google.protobuf.Value [4], which supports some basic primitives, but not all (no timestamps, for example) and cannot be used to serialize actual gRPC messages; you can't serialize {"foo": MyProtoMessage{...}}. Free-form structured data is important for systems that accept foreign data where the schema isn't known; for example, a system that indexes analytics data.
From what I can tell, though, Twirp doesn't "disrupt" gRPC as much as I'd like, since it appears to rely on the existing JSON mapping.
[1] https://github.com/gogo/protobuf/issues/218
[2] https://github.com/gogo/protobuf
[3] https://github.com/BurntSushi/go-sumtype
[4] https://developers.google.com/protocol-buffers/docs/referenc...
> 1. The JSON mapping (jsonpb.go) is clumsy at best
The best thing for optional fields in jsonpb is to use the protobuf wrapper types [1]. They have special support in jsonpb to serialize and deserialize as you would expect, without the indirection. But the Go structs you get on the other end are a little weird, so its a tradeoff.
> 2. The Go code generator usually produces highly unidiomatic Go.
Yeah, using the generated structs as the main domain types in your code can be up-and-down. I agree that gogoprotobuf can help, but it's rough. We definitely use Getter methods on generated structs quite a bit for stuff like oneofs.
> 3. Proto3 has very limited support for expressing "free-form" data.
There's always `repeated byte` :) It sounds like a joke, but we've used it in some spots where the input is totally schema-less.
The Any type is also designed for this sort of thing. Still clumsy, though.
[1] https://github.com/google/protobuf/blob/master/src/google/pr...
> using the generated structs as the main domain types in your code can be up-and-down
At $DAYJOB we solve this by doing code generation outward from our domain types. The RPC layer is idiomatic Go because that's what we began with.
Some go/token and regexes take our structs and produce a server-side router implementation for net/http (endpoints from magic comments), some client-side libraries for Go / C++ (Qt) / PHP / JS, and documentation in markdown.
Our system is in a pretty reusable state, but nobody has the free cycles to open it. If Twirp had been available 24mo ago our project might have been different.
Also, regarding point 3, I'm confused with two things:
- You want "free form" data, but you're talking about protos in the context of Go. How would you define this "free form" data in Go?
- You explain that "free form structured data is important for systems that accept foreign data ... where the schema isnt known". Why are you using protobufs for this usecase? Protobufs are specifically meant to make the schema known, and be enforced by serialization.
As for free-form data, it should be representable as map[string]interface{}. Our specific use case is a document store that stores documents on behalf of clients. The format of documents cannot be not known by the store, but the API to the store is gRPC. Also, we have a desire for documents to contain higher-level types such as dates, but we're forced to use google.protobuf.Value for this, and treat dates as strings, since Value cannot contain Proto types.
(Our next step is probably to model this explicitly, by defining our own Value message that uses "oneof" to represent all the possible types we need to support, and then using a map of these. But it would be nicer if Protobuf had first-class support.)
Serialization/deserialization of a typical protobuf struct takes a microsecond or two, but it generates some garbage, so GC ends up slowing you down if you try to go really crazy and push past 100k req/s in one process with non-trivial message structures.
I've unfortunately been bitten before by choosing json as a serialization format, specifically in go, due to JSON performance dominating processing.
No criticism though, JSON is the right choice for many types of APIs.
Protobuf serialization isn't free, but it's definitely cheaper than JSON serialization.
The CloudFlare outage was related to leap second handling... while the particulars of the Go library contributed, this is also far, far from the only time that a leap second caused havoc online. Hell, in 2008, Zunes were crippled by a leap day bug.
RPC and time handling are notoriously tricky problems to get right.
Fix and optimize throughput for services which actually have those problems.
Mostly people use HTTP because there are so many services and components that already support it (load balancers, proxies, sidecars etc), it has well-supported options for authentication, conveys metadata seamlessly through multiple layers, and its overhead isn't an issue for the kind of payloads they are sending.
Plus, you know, curl.