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

  • 305
Loading comments...

More from Walter Schulze