Building gRPC services on AWS

RPC (remote procedure call) is the mechanism whereby an application client can invoke a function call to a server running on distributed hardware as if it were calling a local function.

Why is RPC important in today’s massively distributed computing environment? Simple answer: (micro)services. Gone is the era where your entire technology stack runs as a monolithic application on one computer. With big data needs, application are increasingly being broken down into distributed services and encapsulate computation units at the service level. For example, as shoppers check out on an e-commerce website, the backend that handles the checkout flow will interact with several services, including the shopping cart service which gathers the items put into the cart, the user account service that gets shipping and billing addresses and a payment gateway service to authorize card payment. RPC allows for a programming model where client and server in the services can interact seamlessly based on a well defined contract of service.

In this blog post we will build a distributed service on Amazon Web Services(AWS) using one of the most popular modern RPC frameworks, gRPC from Google.

Let’s say your friend is moving to the moon. Before he left for the moon, you have agreed to keep in touch using a interstellar communication device. The device is simple. You can only talk to your friend in one of four ways

  1. Say hello to your friend and receive a hello back

  2. Ask your friend to send you a stream of words as a message

  3. Send your friend a stream of words as a message and get an acknowledgement back

  4. Send your friend a stream of words as a message and receive a stream of words back

Before your friend leaves for the moon, you agreed that these four mode of communications are sufficient for you to keep in touch. In other words, you have established a contract of how you and your friend will communicate. That contract, in gRPC, is defined in protocol buffers in a proto file. Protocol Buffer is a language independent way to serialize data structure and send them across wires in distributed systems.

syntax = "proto3";

package interstellar;

service InterstellarCommunication {
  // Say hello and receive a hello back
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
  // Get a stream of words from Mars
  rpc GetMessageFromMars(MessageRequest) returns (stream StreamMessageFromMars) {}
  // Send a stream of words from earth and get a quick reply back from Mars
  rpc SendMessageFromEarth(stream StreamMessageFromEarth) returns (ReplyFromMars) {}
  // Send a stream of words from earth and get a stream of words back from Mars
  rpc SendAndReceiveMessage(stream StreamMessageFromEarth) returns (stream StreamMessageFromMars) {}
}

message HelloRequest {
  string hello_from_earth = 1;
}

message HelloResponse {
  string hello_from_mars = 1;
}

message MessageRequest {
  string request = 1;
}

message StreamMessageFromEarth {
  string message = 1;
}

message ReplyFromMars {
  string reply = 1;
}

message StreamMessageFromMars {
  string message = 1;
}

Because proto file contains the protocol buffer message types and service interfaces, we can use it to auto-generate code in various languages. gRPC supports most major languages, including python, java, c++, go, etc. We are going to generate python code.

python -m grpc_tools.protoc -Iprotos/ --python_out=. --grpc_python_out=. protos/interstellar.proto

There are two files generated, together they contain the concrete classes of messages and service interfaces that client and server can communicate with remotely.

Now let’s create the server implementation which will reside on Mars. Later we will create the client which will be on Earth.

In this case, creating the server just means implement the logic of the service interfaces we have defined in the proto file and start it up.

import time

import grpc
from concurrent import futures

import interstellar_pb2 as pb2
import interstellar_pb2_grpc


class InterstellarServer(interstellar_pb2_grpc.InterstellarCommunicationServicer):

    def __init__(self):
        self.earth_message_history = []

    def SayHello(self, request, context):
        """
        Receive a hello request from Earth and returns a hello from Mars
        """
        self.earth_message_history.append(request.hello_from_earth)
        return pb2.HelloResponse(hello_from_mars='Hello from Mars!')

    def GetMessageFromMars(self, request, context):
        """
        Receive a message request from Earth and returns a iterator of words from Mars
        """
        self.earth_message_history.append(request.request)
        for word in 'Message from Mars!'.split(' '):
            yield pb2.StreamMessageFromMars(message=word)

    def SendMessageFromEarth(self, request_iterator, context):
        """
        Receive an iterator of requests with words from Earth and returns a simple acknowledgement
        """
        for earth_request in request_iterator:
            self.earth_message_history.append(earth_request.message)

        return pb2.ReplyFromMars(reply='Copy!')

    def SendAndReceiveMessage(self, request_iterator, context):
        """
        Receive an iterator of requests with words from Earth and
        returns an iterator of words from Mars
        """
        for earth_request in request_iterator:
            self.earth_message_history.append(earth_request.message)

        for word in 'Live on Mars is great!'.split(' '):
            yield pb2.StreamMessageFromMars(message=word)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    interstellar_pb2_grpc.add_InterstellarCommunicationServicer_to_server(
        InterstellarServer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(60 * 60 * 24)
    except KeyboardInterrupt:
        server.stop(0)

Now we can run the server on Mars.

Now let’s create the client. For the client to work we need to create a stub first. Without going into too much details, a stub is a way for the client to call the server interface as if it were a local function. gRPC hides the implementation of the stub. As far as the client is concerned it can use the stub to call the service interface directly.

import grpc

import interstellar_pb2 as pb2
import interstellar_pb2_grpc


def get_message_iterator():
    earth_message = 'Live on Earth in fantastic!'

    for word in earth_message.split(' '):
        yield pb2.StreamMessageFromEarth(message=word)


def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = interstellar_pb2_grpc.InterstellarCommunicationStub(channel)

    # say hello
    print('Calling SayHello...')
    print(stub.SayHello(pb2.HelloRequest(hello_from_earth='Hello!')))

    # get a message from Mars
    print('Calling GetMessageFromMars...')
    for msg in stub.GetMessageFromMars(pb2.MessageRequest(request='Requesting Mars message')):
        print(msg)

    # send message from Earth to Mars asynchronously
    print('Calling SendMessageFromEarth...')
    print(stub.SendMessageFromEarth(get_message_iterator()))

    # send and receive messages between Earth and Mars asynchronously
    print('Calling SendAndReceiveMessage...')
    for msg in stub.SendAndReceiveMessage(get_message_iterator()):
        print(msg)

Now we can start up the client on Earth and see it communicates with Mars.

A great feature of gRPC is that it supports both synchronous (blocking) and asynchronous (non-blocking) communication between client and server. Asynchronous mode is denoted by the keyword “stream” in the proto file. In synchronous mode, a request to the server will block until the server returns a response, but the asynchronous mode, the server immediately returns an iterator whose content will be available some time in the future. The next() all on the iterator will block until the next result is generated by the server. gRPC supports both asynchronous request from the client and response from the server.

In today’s distributed computing environment, gRPC is a powerful tool that provides a clean client server framework and supports synchronous and asynchronous modes of communication.

As always you can find the full code discussed in this post on Cloudbox Labs github.