Building a modern API with gRPC

gRPC is a free and open-source Remote Procedure Calls framework developed by Google as part of the CNCF (Cloud Native Computing Foundation). It’s a high-performance solution that can run in any environment.

gRPC is based on HTTP/2 protocol, supports low latency and streaming. It’s fast, language-independent and easy to plug in logging, monitoring, load balancing, and authentication. To start using it everything you need to do is define service with requests and responses via Protocol Buffers.

Why gRPC?

In a microservice world, we need to exchange lots of information. For this, we need to think about API, data format, performance, scalability, errors, monitoring, and many others. In simple words, the one service (client) sends the request and the other (server) need to send back the response. It’s all about the data. gRPC automatically generates client and server stubs for you in many popular languages. At the moment the gRPC supports C/C++, C#, Dart, Go, Java, Node.js, Objective-C, PHP, Python, and Ruby.

Get started with Protocol Buffers

Protocol Buffers are a powerful, language and platform agnostic automated mechanism for serializing structured data.
They are 3 to 10 times smaller and 20 to 100 times faster than XML and allows for easy API evolution (backward and forward compatibility).

You define a protocol buffer message types in .proto file to specify your data structure. Let’s look at the simple car description example below:

syntax = "proto3";

message Car {
    string brand = 1;
    string model = 2;
    Type type = 3;
    float engine_capacity = 4;
    int32 doors = 5;
}

enum Type {
    HATCHBACK = 0;
    SEDAN =1;
    SUV = 2;
    COUPE = 3;
    CONVERTIBLE =4;
}

The current proto3 standard allows us to define scalar value types (int32, double, bool, string…), enumerations and many others.

The unique number at the end of the field definition is used to identify the field in the message binary format. Enumerations start from 0 value. This number should not be changed once your message is in use!

Based on such a file, the protocol buffer compiler will generate the necessary code in the chosen language.

More details about language guide for protocol buffers proto3 syntax you can find in the documentation:

https://developers.google.com/protocol-buffers/docs/proto3

The power of HTTP/2

gRPC takes advantage of released in 2015 HTTP/2 Web protocol. Thanks to that, supports multiplexing (client and server can push messages over the same TCP connection in parallel). This significantly reduces latency.

HTTP/2 also supports client, server and bi-directional streaming.
Text-based headers can be compressed decreasing package size.
As a binary protocol is more efficient over the network. Finally, HTTP/2 can be used over TLS encrypted channels (h2 standard) or without any encryption (h2c standard).
This security option is not required, but all major browser implementations like Chrome or Safari decided that they will only supports HTTP/2 over TLS.

Here you can watch a quick HTTP/2 and HTTP/1.1 performance comparison:

https://www.youtube.com/watch?time_continue=8&v=QCEid2WCszM&feature=emb_logo

gRPC API types

Thanks to HTTP/2 gRPC have 4 types of API:

  • unary – a traditional API (like HTTP REST)
  • server streaming
  • client streaming
  • bi-directional streaming

Let’s code!

Now it’s time to see the gRPC framework in action. I’ll show you how easy we can create a simple API using Java and Python.

syntax = "proto3";

package movie;

option java_package = "com.proto.movie";
option java_multiple_files = true;

message Movie {
    string id = 1;
    string title = 2;
    repeated MovieGenre genres = 3;
    repeated MoviePerson directors = 4;
    repeated MoviePerson writers = 5;
    repeated MoviePerson stars = 6;
    int32 year_of_production = 7;
    int32 duration_minutes = 8;
}

message MoviePerson {
    string first_name = 1;
    string last_name = 2;
}

enum MovieGenre {
    ACTION = 0;
    COMEDY = 1;
    DRAMA = 2;
    HORROR = 3;
    SCI_FI = 4;
    THRILLER = 5;
    WAR = 6;
    WESTERN = 7;
}

First, we need to define a .proto file with the service definition.

For Java language additionally, we need to add some options, like the package name.

As you can see, some fields have repeated keywords – it’s nothing else like a simple list/collection.

This is only an object (message) declaration. We need also service calls definition with requests and responses. We can start by creating a basic unary call.

Unary API example

message AddMovieResponse {
    string movie_id = 1;
}

service MovieService {
    // unary API
    rpc AddMovie (Movie) returns (AddMovieResponse) {}
}

Now we finally have created a whole service definition. Based on it the Protocol Buffer compiler will automatically generate code for us. In Java, we need to add the necessary dependencies and plugins to our build.gradle file.

plugins {
    id 'com.google.protobuf' version '0.8.10'
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.10.0"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.25.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

dependencies {
    implementation 'io.grpc:grpc-netty-shaded:1.25.0'
    implementation 'io.grpc:grpc-protobuf:1.25.0'
    implementation 'io.grpc:grpc-stub:1.25.0'
}

Now under src/main, we can see the proto directory, that we can put our movie.proto file. After running generateProto task, gradle will generate for us java classes for all messages, that we declared before and ServiceGrpc with all necessary methods.

We can use them to create a client and server implementation.

Let’s look at the server example. First, we need to implement service, that extends generated gRPC MovieServiceImpBase class. Then we should override the available service methods.
In our case, it will be addMovie
(same name as we declared inside MovieService in movie.proto file for rpc call definition).

 

We have access to the Movie object from the request and the StreamObserver,
that we can use to pass the response to the caller. For a simple unary call the onNext or onError methods will execute only once. We need also call onCompleted method at the end.

public class MovieService extends MovieServiceGrpc.MovieServiceImplBase {

    @Override
    public void addMovie(Movie request, StreamObserver<AddMovieResponse> responseObserver) {
        try {
            String movieId = someMethodToPerformRequest(request);

            AddMovieResponse response = AddMovieResponse.newBuilder()
                .setMovieId(movieId)
                .build();

            responseObserver.onNext(response);
        } catch (Exception e) {
            responseObserver.onError(
                Status.ALREADY_EXISTS
                    .withDescription("???")
                    .withCause(new Throwable("???"))
                    .asRuntimeException()
            );
        }
        responseObserver.onCompleted();
    }

    private String someMethodToPerformRequest(Movie movie) {
        // logic
        return UUID.randomUUID().toString() + " from Java";
    }

Finally, we can use our service in the server class

public class MovieServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        Server server = ServerBuilder.forPort(50051)
            .addService(new MovieService())
            .build();

        server.start();

        Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown));

        server.awaitTermination();
    }
}

Now it’s time to create a client for sending our gRPC request.
We need to define a channel for TCP connection with
the server, build request and handle possible errors from the response.
For unary call, we use BlockingStub as the synchronous client.

Note that we disable SSL by using plain text. Our server site was also configured without SSL security. It’s very easy to enable it, but we cover this topic later.

public class MovieUnaryClient {
    public static void main(String[] args) {
        final Logger logger = Logger.getLogger(MovieUnaryClient.class.getName());

        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
            .usePlaintext() // disable SSL - for local development
            .build();

        MovieServiceGrpc.MovieServiceBlockingStub movieSyncClient = MovieServiceGrpc
            .newBlockingStub(channel);

        Movie movie = Movie.newBuilder()
            .setTitle("Pulp Fiction")
            .build();

        try {
            AddMovieResponse response = movieSyncClient.addMovie(movie);

            logger.info("Response: " + response.toString());
        } catch (StatusRuntimeException e) {
            // do something
        }
        channel.shutdown();
    }
}

We can also set a deadline to specify how long the client should wait for a response before the RPC is terminated with the DEADLINE_EXCEEDED error.

AddMovieResponse response = movieSyncClient
                .withDeadline(Deadline.after(1, TimeUnit.SECONDS))
                .addMovie(movie);

Our java server and client are ready to communicate with each other.
The gRPC framework is language-agnostic, so based on the definition from .proto file we can easily generate code for many popular languages. Let’s look at how to build the implementation in Python.

Python implementation

We need to install the gRPC dependencies:

python-m pipinstall grpcio
python-m pipinstall grpcio-tools

 

From the movie.proto file directory run the following command:

python -m grpc_tools.protoc --proto_path=. --python_out=. --grpc_python_out=. *.proto

 

It will generate for us the movie_pb2.py and movie_pb2_grpc.py files.
A very simple server and client implementations may look like this:

class MovieService(movie_pb2_grpc.MovieServiceServicer):

    def AddMovie(self, request, context):
        # logic to perform request
        movie_id = 'movie_id from Python'

        return movie_pb2.AddMovieResponse(movie_id=movie_id)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    movie_pb2_grpc.add_MovieServiceServicer_to_server(MovieService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    serve()
def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = movie_pb2_grpc.MovieServiceStub(channel)

        movie = movie_pb2.Movie()
        movie.title = 'Goodfellas'

        response = stub.AddMovie(movie)

    print(response.movie_id)


if __name__ == '__main__':
    run()

Of course, we should handle some errors, but this is only a start point example. Both java and python implementations are fully compatible.

Client streaming API example

Thanks to the HTTP/2 protocol we can create a streaming API in gRPC. In this blog, we’ll implement the client streaming scenario.

First, let’s add a new method definition to our movie.proto file.

message AddMultipleMoviesResponse {
    repeated string movie_ids = 1;
}

service MovieService {
    // unary API
    rpc AddMovie (Movie) returns (AddMovieResponse) {}

    // client streaming API
    rpc AddMultipleMovies (stream Movie) returns (AddMultipleMoviesResponse) {}
}

After restarting the generateProto gradle task, we need to override a newly created method in our MovieService class.
This time we don’t have direct access to the request as it will be a stream.
We need to configure the StreamObserver to handle all requests from the client.

@Override
    public StreamObserver<Movie> addMultipleMovies(StreamObserver<AddMultipleMoviesResponse> 
        responseObserver) {

        StreamObserver<Movie> requestStreamObserver = new StreamObserver<>() {

            List<String> performedMovieIds = new ArrayList<>();

            @Override
            public void onNext(Movie value) {
                String movieId = someMethodToPerformRequest(value);
                performedMovieIds.add(movieId);
                logger.info("Performing a movie with id: " + movieId);
            }

            @Override
            public void onError(Throwable t) {
                // do something
            }

            @Override
            public void onCompleted() {
                responseObserver.onNext(AddMultipleMoviesResponse.newBuilder()
                    .addAllMovieIds(performedMovieIds)
                    .build());

                responseObserver.onCompleted();
            }
        };

        return requestStreamObserver;
    }

On the client side, we’ll be using an asynchronous stub now. Similarly to the server, we need to create and configure the StreamObserver.
As this is a client streaming scenario – we expect only one response from the server (one execution of the onNext method)

        // asynchronous
        MovieServiceGrpc.MovieServiceStub movieAsyncClient = MovieServiceGrpc.newStub(channel);

        CountDownLatch latch = new CountDownLatch(1);

        StreamObserver<Movie> requestStreamObserver = movieAsyncClient
            .addMultipleMovies(new StreamObserver<>() {
            @Override
            public void onNext(AddMultipleMoviesResponse value) {
                // onNext will be called only once - client streaming -> one server response
                logger.info("Response: " + value.toString());
            }

            @Override
            public void onError(Throwable t) {
                // do something
            }

            @Override
            public void onCompleted() {
                logger.info("Response from server completed");
                latch.countDown();
            }
        });

For simulating the streaming behavior we’ll use a simple for-loop.
In the end, we need to tell the server that we have finished sending the requests by calling onComplited method.

        // streaming behaviour
        for (int i = 1; i < 4; i++) {
            Movie movie = Movie.newBuilder()
                .setTitle("Movie " + i)
                .build();

            logger.info("Sending request no " + i);
            requestStreamObserver.onNext(movie);
        }
        // information for server that client has finished sending the data
        requestStreamObserver.onCompleted();

        latch.await(3L, TimeUnit.SECONDS);

        channel.shutdown();

SSL (Secure Sockets Layer) security

Our implementation right now is based on PLAINTEXT. For security reasons, every gRPC calls should be running with encryption enabled to protect data transmitted across the network. SSL allows clients and servers to securely exchange data. With gRPC, we can easily enabled SSL encryption.
First, we need to generate the necessary certificates.

In your terminal set the host, that you are using:

SERVER_CN=localhost

 

Generate Certificate Authority + Trust Certificate:

openssl genrsa -passout pass:password -des3 -out ca.key 4096
openssl req -passin pass:password -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=${SERVER_CN}"

 

Generate the Server Private Key:

openssl genrsa -passout pass:password -des3 -out server.key 4096

 

Get a signed certificate from the CA:

openssl req -passin pass:password -new -key server.key -out server.csr -subj "/CN=${SERVER_CN}"

 

Sign the certificate with the CA we created (self sinning):

openssl x509 -req -passin pass:password -days 365 -in server.csr -CA ca.crt -CAkey ca.key 
-set_serial 01 -out server.crt

 

Convert the server certificate to .pem format usable by gRPC:

 

openssl pkcs8 -topk8 -nocrypt -passin pass:password -in server.key -out server.pem

 

Now we can configure our server like this:

Server server = ServerBuilder.forPort(50051)
            .addService(new MovieService())
            .useTransportSecurity(
                new File("path_to_your_file/server.crt"),
                new File("path_to_your_file/server.pem")
            )
            .build();

In the same easy way, we can enable SSL in our client:

ManagedChannel channel = NettyChannelBuilder.forAddress("localhost", 50051)
            .sslContext(GrpcSslContexts.forClient().trustManager(
                new File("path_to_your_file/ca.crt")
                ).build()
            ).build();

Note that this time w used NettyChannelBuilder class, which includes SSL functionality.

Testing

gRPC Reflection & CLI

From the development perspective may be useful to use gRPC reflection to check what endpoints are available on the server.

To enable reflection, add the following dependency to your build.gradle file.

compile 'io.grpc:grpc-services:1.25.0'

Then we need to configure the server:

Server server = ServerBuilder.forPort(50051)
            .addService(new MovieService())
            .addService(ProtoReflectionService.newInstance())
            .build();

Finally, we can use Evans CLI to discover information about declared gRPC services and request/response messages. This tool also can be used for testing. More information you can find here:

https://github.com/ktr0731/evans

Install the evans CLI. For macOS:

$ brew tap ktr0731/evans
$ brew install evans

 

Run the server and use the Evans CLI in your terminal. Enter following command to start:

evans -p 50051 -r

 

CLI supports IntelliSense. To see available commands use help keyword. By typing show service you should see all created before methods for our MovieService:

You can also use evans CLI to manually call the gRPC server:

BloomRPC – GUI Client for gRPC services

BloomRPC is a great tool for testing your RPC services. Here is a Github page with description and installation guides:

https://github.com/uw-labs/bloomrpc

Configure your running server port and import .proto file with service definition. You can also set up SSL security using a build-in TLS/SSL Manager.

gRPC vs REST

A popular way to build API is REST. In simplified terms, it’s an architectural style where everything is focused on the resource. I won’t go to the deep dive of the REST because this blog is about gRPC :)

In the table below you can see what are the main differences between those ways of building API:

REST gRPC
HTTP/1.1, HTTP/2 HTTP/2 – lower latency
JSON – text data, slow Protocol Buffers – binary data, fast
CRUD Oriented (Resources + http verbs) API Oriented (Messages + Services)
Request – Response only Streaming & Async
OpenAPI + third-party tools for code generation Client code-generation out of the box

Performance comparison

To compare REST and gRPC efficiency I created several simulations using Gatling.

Gatling is a great developer tool to load test your applications and generate results in the form of legible charts.
I had to use a special plugin for gRPC because currently, Gatling doesn’t provide official support for it.

Gatling documentation
gRPC Gatling plugin

Tests conditions:

  • MacBook Pro 2017, Processor 2,5 GHz Dual-Core Intel Core i7, RAM 16 GB
  • one local server instance for each technology
  • for gRPC – the same basic Python example as presented before in this blog
  • for REST – simple Python Flask route as below
@app.route('/', methods=['POST'])
def rest():
    print(request)
    return Response('{\"movie_id\": \"id from python\"}',
                    status=200, content_type="application/json")
  • insecure connection
  • both servers consume requests and simply return a small response without any additional logic between
  • RPC message / POST request with JSON body
  • default Gatling timeouts – connect (for establishing a TCP socket) 10 seconds, request 60 seconds

Scenarios:

    • one single user repeated 100 times

Gatling measures the response time as an elapsed time between the instant a request connection to the target host has been established and the instant the response is received.

As you can see, for one request at the time with a small payload results for gRPC and REST are almost identical.

    • 50 users repeated 100 times

For handling 50 requests at once, the gRPC has better results. Over 80% response times are below 50 ms with an average of 42 ms, when REST average is 281 ms.

    • 10.000 users over 30 seconds

Under more stressful conditions such as 10.000 requests over 30 seconds
(~ 333 requests per second), the gRPC outclasses the REST solution.

All gRPC calls have finished successfully with response time average 46 ms, where over 90% of the REST calls gave connection timeout exception!

    • only for gRPC – 10.000 users over 15 seconds

As a REST give up during the previous scenario I decided to make one more only for gRPC. For around 667 requests per second, gRPC failed in 16% cases with a specific UNAVAILABLE gRPC status code. More details on the chart below:

Conclusion

gRPC framework is modern, still being developed and offers unique benefits compared to the HTTP APIs (like REST). Takes advantages from Protocol Buffers and HTTP/2 protocol significantly increasing performance. gRPC provides also first-class support for code generation in many popular languages.

It can be extremely useful for the following scenarios:

  • microservices
  • multi-language environments
  • point-to-point real-time communication (bi-directional streaming)
  • network constrained environments (smaller binary payloads)

gRPC has also its limitations. By using ProtocolBuffers the RPC messages are not human-readable. We also cannot directly call a gRPC service from a browser.
It’s harder to debug.

In overall gRPC framework is definitely worth to learn. Companies like Netflix, Salesforce, Cisco and of course Google already using it for connecting multiple services in their environments.

Links to all GitHub repositories created for this blog:

Java gRPC API examples
SpringBoot examples
Python examples
Gatling performance tests

Check out other useful information about gRPC:

https://grpc.io
https://github.com/grpc
https://github.com/grpc-ecosystem/awesome-grpc
https://developers.google.com/protocol-buffers