Fast and Maintainable APIs
HTTP/2 for Performance
single connection
7 round trips
HTTP/2 Header Compression
HPack => 1 Packet
HTTP/2 vs HTTP/1
HTTP/2 | HTTP/1 |
---|---|
binary | textual |
fully multiplexed | ordered and blocking |
one connection with parallelism | multiple connections |
header compression | lots of overhead |
push from server to client | polling |
TLS by default | TLS optional |
23% of websites already served with HTTP/2
https://w3techs.com/technologies/details/ce-http2/all/all
- Backwards Compatible
- Fast
- Polyglot Contract enforced with generated code
Protobuf: Encoding
- varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum
- fixed32: fixed32, sfixed32, float
- fixed64: fixed64, sfixed64, double
- length delimited: string, bytes, embedded messages, repeated fields (lists)
varint - Variable Length Integer Encoding
func EncodeVarint(x uint64) []byte {
var buf [10]byte
var n int
for n = 0; x > 127; n++ {
buf[n] = 0x80 | uint8(x&0x7F)
x >>= 7
}
buf[n] = uint8(x)
n++
return buf[0:n]
}
10101100 00000010
more
done
32+8+4
256
= 300
func DecodeVarint(buf []byte) (uint64, int) {
var x uint64
var n int
for shift := uint(0); ; shift += 7 {
b := buf[n]
n++
x |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
return x, n
}
01010100
done
64+16+4
= 84
Length Delimited
length|bytes
varint encoded length
- encoded string
- just plain bytes
- encoded message
Wire Type
varint | 0 |
fixed32 | 5 |
fixed64 | 1 |
length delimited | 2 |
Field
field number|wire type
string myfieldname = 4;
encodeVarint(fieldNumber << 3 | wireType)
encodeVarint(4 << 3 | 2 )
00100010
done
4<<3
| 2
Message
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
Message
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
{
"myfieldname": "",
"values": [300,84],
"recursive": {
"myfieldname": "cde"
}
}
Message
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
{
"myfieldname": "",
"values": [300,84],
"recursive": {
"myfieldname": "cde"
}
}
00100010
c:01100011
d:01100100
e:01100101
00000011
Message
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
{
"myfieldname": "",
"values": [300,84],
"recursive": {
"myfieldname": "cde"
}
}
00110010
00100010
c:01100011
d:01100100
e:01100101
00000101
00000011
Message
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
{
"myfieldname": "",
"values": [300,84],
"recursive": {
"myfieldname": "cde"
}
}
00110010
00100010
c:01100011
d:01100100
e:01100101
00000101
00000011
10101100 00000010
01010100
00101010
00000011
Message
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
{
"myfieldname": "",
"values": [300,84],
"recursive": {
"myfieldname": "cde"
}
}
00110010
00100010
c:01100011
d:01100100
e:01100101
00000101
00000011
10101100 00000010
01010100
00101010
00000011
Backwards Exercise 1
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
double myNewField = 7;
}
Old
New
Backwards Exercise 1
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
double myNewField = 7;
}
Old
New
Backwards Exercise 2
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string newName = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
Old
New
Backwards Exercise 2
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
Old
New
message MyMessage {
string newName = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
Backwards Exercise 3
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
repeated fixed64 values = 5;
MyMessage recursive = 6;
}
Old
New
Backwards Exercise 3
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
repeated fixed64 values = 5;
MyMessage recursive = 6;
}
Old
New
Backwards Exercise 4
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
MyMessage recursive = 6;
}
Old
New
Backwards Exercise 4
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
// used to be values
reserved 5;
MyMessage recursive = 6;
}
Old
New
Backwards Exercise 5
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
MyMessage recursive = 6;
bool mybool = 5;
}
Old
New
Backwards Exercise 5
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
MyMessage recursive = 6;
bool mybool = 5;
}
Old
New
Backwards Exercise 6
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
repeated bool mybool = 5;
MyMessage recursive = 6;
}
Old
New
Backwards Exercise 6
message MyMessage {
string myfieldname = 4;
repeated int64 values = 5;
MyMessage recursive = 6;
}
message MyMessage {
string myfieldname = 4;
repeated bool mybool = 5;
MyMessage recursive = 6;
}
Old
New
More Types
- HashMap
- Timestamp
- Duration
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
message Timestamp {
// Represents seconds of UTC time since Unix epoch
// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
// 9999-12-31T23:59:59Z inclusive.
int64 seconds = 1;
// Non-negative fractions of a second at nanosecond resolution. Negative
// second values with fractions must still have non-negative nanos values
// that count forward in time. Must be from 0 to 999,999,999
// inclusive.
int32 nanos = 2;
}
message Duration {
int64 seconds = 1;
int32 nanos = 2;
}
Your First Service
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
Java, Python, Go, C++, Node.js, Ruby, C#, Android Java, Objective-C, PHP, Swift, etc.
Generated Go Client
type GreeterClient interface {
SayHello(ctx context.Context, in *HelloRequest,
opts ...grpc.CallOption) (*HelloReply, error)
}
type greeterClient struct {
cc *grpc.ClientConn
}
func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest,
opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...)
return out, err
}
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
type HelloReply struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}
protoc --go_out=plugins=grpc:. helloworld.proto
Go Client
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "World"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
Remember REST?
func main() {
enc := json.NewEncoder(bytes.NewBuffer())
if err := enc.Encode(&pb.HelloRequest{Name: "World"}); err != nil {
log.Fatalf("could not marshal: %v", err) // dynamically typed
}
resp, err := http.Post("localhost:50051/helloworld.Greeter/SayHello",
"application/json", enc)
if err != nil {
log.Fatalf("post failed: %v", err) // stringly typed
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
log.Fatalf("not ok")
}
dec := json.NewDecoder(resp.Body)
r := &pb.HelloResponse{}
if err := dec.Decode(r); err != nil {
log.Fatalf("count not unmarshal: %v", err) // dynamically typed
}
log.Printf("Greeting: %s", r.Message)
}
Generated Python Server
def add_GreeterServicer_to_server(servicer, server):
rpc_method_handlers = {
'SayHello': grpc.unary_unary_rpc_method_handler(
servicer.SayHello,
request_deserializer=helloworld__pb2.HelloRequest.FromString,
response_serializer=helloworld__pb2.HelloReply.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'helloworld.Greeter', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
HelloReply = _reflection.GeneratedProtocolMessageType('HelloReply', (_message.Message,), dict(
DESCRIPTOR = _HELLOREPLY,
__module__ = 'helloworld_pb2'
# @@protoc_insertion_point(class_scope:helloworld.HelloReply)
))
_sym_db.RegisterMessage(HelloReply)
_HELLOREPLY = _descriptor.Descriptor(
name='HelloReply',
full_name='helloworld.HelloReply',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
...
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. helloworld.proto
Python Server
class Greeter(helloworld_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)\
if __name__ == '__main__':
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:50051')
server.start()
Demo
Streaming
service BuySide {
rpc Search (Query) returns (stream ItemSummary) {}
rpc Item (ItemId) returns (Item) {}
}
service Chat {
rpc Bidi (stream Chat) returns (stream Update) {}
}
Streaming Demo
Meta => Tools
protoc \
--descriptor_set_out=parseTree.pb \
--proto_path=.
Command Line Interface
$ echo <json-request> | java -jar polyglot.jar \
--command=call \
--endpoint=<host>:<port> \
--full_method=<some.package.Service/doSomething>
Better
Command Line Interface
REST Gateway
package example;
+
+import "google/api/annotations.proto";
+
message StringMessage {
string value = 1;
}
service YourService {
- rpc Echo(StringMessage) returns (StringMessage) {}
+ rpc Echo(StringMessage) returns (StringMessage) {
+ option (google.api.http) = {
+ post: "/v1/example/echo"
+ body: "*"
+ };
+ }
}
GRPC Gateway
protoc -I/usr/local/include -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:. \
path/to/your_service.proto
Swagger/OpenAPI
protoc -I/usr/local/include -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--swagger_out=logtostderr=true:. \
path/to/your_service.proto
Gateway Demo
GUI
enum Instrument {
Voice = 0;
...
}
enum Genre {
Pop = 0;
...
}
message Artist {
//Pick something original
string Name = 1;
Instrument Role = 2;
}
message Song {
string Name = 1;
uint64 Track = 2;
double Duration = 3;
repeated Artist Composer = 4;
}
message Album {
string Name = 1;
repeated Song Song = 2;
Genre Genre = 3;
string Year = 4;
repeated string Producer = 5;
bool Mediocre = 6;
bool Rated = 7;
string Epilogue = 8;
repeated bool Likes = 9;
}
service Label {
rpc Produce(Album) returns (Album);
}
Docs
/**
* Represents the status of a vehicle booking.
*/
message BookingStatus {
int32 id = 1; /// Unique booking status ID.
string description = 2; /// Booking status description. E.g. "Active".
}
/**
* Represents the booking of a vehicle.
*
* Vehicles are some cool shit. But drive carefully!
*/
message Booking {
int32 vehicle_id = 1; /// ID of booked vehicle.
int32 customer_id = 2; /// Customer that booked the vehicle.
BookingStatus status = 3; /// Status of the booking.
/** Has booking confirmation been sent? */
bool confirmation_sent = 4;
/** Has payment been received? */
bool payment_received = 5;
}
Security
- SSL/TLS by Default
- JWT
- OAuth2
Service Discovery /
Load Balancing
func main() {
// Create Consul client
cli, err := api.NewClient(api.DefaultConfig())
if err != nil {
log.Fatal(err)
}
// Create a resolver for the "echo" service
r, err := lb.NewConsulResolver(cli, "echo", "")
if err != nil {
log.Fatal(err)
}
// Notice you can use a blank address here
conn, err := grpc.Dial("", grpc.WithBalancer(grpc.RoundRobin(r)))
if err != nil {
log.Fatal(err)
}
Interceptors
conn, err := grpc.Dial("localhost:9001", grpc.WithInsecure(),
grpc.WithUnaryInterceptor(
openTracingGrpc.OpenTracingClientInterceptor(
tracer, openTracingGrpc.LogPayloads())))
import "github.com/grpc-ecosystem/go-grpc-prometheus"
...
// Initialize your gRPC server's interceptor.
myServer := grpc.NewServer(
grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor),
grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor),
)
// Register your gRPC service implementations.
myservice.RegisterMyServiceServer(s.server, &myServiceImpl{})
// After all your registrations, make sure all of the Prometheus metrics are initialized.
grpc_prometheus.Register(myServer)
// Register Prometheus metrics handler.
http.Handle("/metrics", promhttp.Handler())
...
Cost Saving
I'm a former Google engineer working at another company now, and we use http/json rpc here. This RPC is the single highest consumer of cpu in our clusters, and our scale isn't all that large. I'm moving over to gRPC asap, for performance reasons.
https://news.ycombinator.com/item?id=12344995
BenchmarkGRPCProtobuf 197919 ns/op
BenchmarkJSONHTTP 1720124 ns/op
http://pliutau.com/benchmark-grpc-protobuf-vs-http-json/
GRPC vs REST
gRPC/HTTP2/Protobuf | REST/HTTP1/JSON |
---|---|
statically typed | error prone |
fully multiplexed | ordered and blocking |
backwards compatible | forced to version |
fast (binary, hpack) | slow (textual, overhead) |
push from server to client | polling |
generated code | development time |
cost saving | consumes all the cpus |
Oh but wait there is more...
If you call now ...
waschulze@ebay.com
awalterschulze@gmail.com
Walter Schulze
gRPC Fast and Maintainable APIs
By Walter Schulze
gRPC Fast and Maintainable APIs
- 2,069