gRPC with Go-Microservices

17.03.2021 Raffael Schneider
Cloud Tech golang k8s microservices handson

In the cloud environment, there are numerous ways in which a service-oriented architecture can be implemented using microservices. The focus is often on resource efficiency and thus cost efficiency, as well as scalability with different load windows. Today we're looking at how to run cost-effectively microservices in the cloud.

Basically there are two established ways of running microservices, either as a serverless cloud function or as a container in a Kubernetes cluster. Serverless functions such as the popular AWS Lambda can be used with low tariffs and are easy to set up. Serverless functions become significantly more expensive above a certain load limit. The variant with containers on Kubernetes, on the other hand, is more complex, but can be scaled more cost-effectively in the long term. The Kubernetes solution is therefore dependent on rolling out containers that are built with a lowest possible footprint and available response power.

In the past, we have already looked at Quarkus as the framework of choice, with which one can quickly write a microservice in Java without any problems. But is there more? For this reason, we are focusing today on microservcies that are written in Go and provide endpoints with a different protocol that achieve even higher throughput.

This post is inspired by https://itnext.io/grpc-go-microservices-on-kubernetes-bcb6267e9f53.

gRPC - The universal remote procedure call framework

With the term Remote Procedure Call, one might vaguely recall Java’s Remote Method Invocation (shortly RMI) or EJBs (Enterprise JavaBeans), with which a Java application could call a specific method of a second Java application remotely via a network connection.

gRPC is a protocol developed by Google for calling functions in distributed systems. gRPC stands for Google Remote Procedure Call. gRPC is rated by the Cloud Native Computing Foundation as an ‘Incubating Project’. The following two main components are used:

  • Protocol Buffers (shortly Protobuf) as _ Interface Description Language _: uses gRPC as serialization for communication. A binary format is used here, which allows the interface to respond more quickly to requests. The interface is defined semantically (and not binary) in a .proto \ file. This definition generates the binary format, which then functions as Interface Description Language (like e.g. JSON).
  • HTTP/2 as transport: The new HTTP standard has existed since 2015. This second iteration allows bidirectional communication, server pushes, header compression, multiplexing and more. gRPC uses these new features to make data transfer even faster.

For microservices, a REST interface is typically used to create communication between applications. With gRPC Google has implemented a solution with which one can communicate with each other even faster.

Features and Benefits

  • Simple service definition
  • Efficient because binary, scalable
  • Language-independent, polyglot
  • Bidirectional data exchange / streaming
  • authentication
  • Perfect for internal communication under services

gRPC can be used in different programming languages. Thus, a gPRC interface can be implemented relatively easily on REST endpoints, which ensures better performance with lower power requirements. It should be noted that HTTP / 2 is not yet widely used, as outdated browsers are still used on a daily basis. With gRPC Web , a JavaScript implementation for browser clients, you can also address gRPC with older browsers. If you are still using applications that can only address REST interfaces, you can use gRPC Gateway to create parallel operation via the definition file, whereby the service exposes REST and gRPC. We won't go into more detail about this technology here, but it is certainly good to know how flexible an interface with gRPC can be.

It is important to know that you write the interface declarative in a .proto \ - file and then use this to compile _ stub _ \ - files (Code-fragments) in the desired language. Regardless of the programming language, these blocks can be implemented in the respective project and communicate with each other language-independent in the running application.

Supported languages for gRPC

C, C++, C#, Dart, Go, Java, Kotlin, Node.js, Objective-C, PHP, Python, Ruby.

Service definition with Google’s Protobuf

The gRPC interface uses Protobuf as IDL. This interface is written declarative in a .proto \ - file and then compiled with this _stub _ \ - Files (Code-fragments) in the desired language. Regardless of the programming language, these blocks can be implemented in the respective project and communicate with each other language-independent in the running application.

An example of a .proto file for a geographic-aware chat API could look like this:

syntax = "proto3";

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

message Feature {
  string name = 1;
  Point location = 2;
}

message RouteNote {
  Point location = 1;
  string message = 2;
}

service RouteGuid {
  rpc GetFeature(Point) returns (Feature);
  rpc RouteChat(stream RouteNote) returns (stream RouteNote);
}

Install and use Protobuf

How can we now build a microservice with gRPC in Go? It's pretty easy. First we install Protobuf as follows:

❯ brew install protobuf

For this you have to load the Go-Plugin protoc-gen-go from:

❯ go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

Also ensures that the bash paths for Go are set correctly:

❯ echo 'export GOPATH=$HOME/Go' >> $HOME/.bashrc
❯ source $HOME/.bashrc

And also :

❯ echo 'export PATH=$PATH:$GOPATH/bin' >> $HOME/.bashrc
❯ source $HOME/.bashrc

The following snippet is an exemplary grpcapi.proto. We use Protobuf's Interface Description Language to describe our interface.

// grpcapi.proto

syntax = "proto3";

package grpcapi;
option go_package = ".;grpcapi";

message GrpcRequest {
    string input = 1;
}

message GrpcResponse {
    string response = 1;
}

service GrpcService {
    rpc grpcService(GrpcRequest) returns (GrpcResponse) {};
}

With this command the Protobuf compiler protoc compiles the schema into a usable Go-File:

❯ protoc -I=. --go_out=. grpcapi.proto

The output of protoc is a go-file protocol.pb.go which is placed in the same directory. The created Go-File has over 200 characters, but to ensure that we have the same output, the first line blocks can be seen here:

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
//     protoc-gen-go v1.25.0-devel
//     protoc        v3.14.0
// source: grpcapi.proto

package grpcapi

import (
   protoreflect "google.golang.org/protobuf/reflect/protoreflect"
   protoimpl "google.golang.org/protobuf/runtime/protoimpl"
   reflect "reflect"
   sync "sync"
)

const (
   // Verify that this generated code is sufficiently up-to-date.
   _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
   // Verify that runtime/protoimpl is sufficiently up-to-date.
   _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

// . . .

Microservice architecture with Go

The server

Now we write a server.go \ microservice that can receive a GrpcService \ invocation from a client. Take a closer look at the function func (*grpcServer) GrpcService().

// server.go
package main

import (
	"context"
	"fmt"
	"os/signal"

	"log"
	"net"
	"os"

	"github.com/b-nova-techhub/go-grpc-api"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

type grpcServer struct{}

func (*grpcServer) GrpcService(ctx context.Context, req *grpcapi.GrpcRequest) (*grpcapi.GrpcResponse, error) {
	fmt.Printf("grpcServer %v\n", req)
	name, _ := os.Hostname()

	input := req.GetInput()
	result := "Got input " + input + " server host: " + name
	res := &grpcapi.GrpcResponse{
		Response: result,
	}
	return res, nil
}

func main() {
	fmt.Println("Starting Server...")

	log.SetFlags(log.LstdFlags | log.Lshortfile)

	hostname := os.Getenv("SVC_HOST_NAME")

	if len(hostname) <= 0 {
		hostname = "0.0.0.0"
	}

	port := os.Getenv("SVC_PORT")

	if len(port) <= 0 {
		port = "50051"
	}

	lis, err := net.Listen("tcp", hostname+":"+port)
	if err != nil {
		log.Fatalf("Failed to listen: %v", err)
	}

	opts := []grpc.ServerOption{}
	s := grpc.NewServer(opts...)
	grpcapi.RegisterGrpcServiceServer(s, &grpcServer{})

	// reflection service on gRPC server.
	reflection.Register(s)

	go func() {
		fmt.Println("Server running on ", (hostname + ":" + port))
		if err := s.Serve(lis); err != nil {
			log.Fatalf("failed to serve: %v", err)
		}
	}()

	// Wait for Control C to exit
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt)

	// Block until a signal is received
	<-ch
	fmt.Println("Stopping the server")
	s.Stop()
	fmt.Println("Closing the listener")
	lis.Close()
	fmt.Println("Server Shutdown")

}

Before the server.go can be compiled, the external dependencies must first be drawn. Simply execute the following go commands in the console:

❯ go get -u github.com/b-nova-techhub/go-grpc-api
❯ go get -u	google.golang.org/grpc
❯ go get -u google.golang.org/grpc/reflection

Then you can compile and start the server.

❯ go run server.go
Starting Server...
Server running on  0.0.0.0:50051

The client

Now we write a client.go \ microservice, which carries out a GrpcService \ invocation on a server. Take a closer look at the function func callService().

// client.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/b-nova-techhub/go-grpc-api"
	"google.golang.org/grpc"
)

func main() {

	fmt.Println("Starting client...")

	hostname := os.Getenv("SVC_HOST_NAME")

	if len(hostname) <= 0 {
		hostname = "0.0.0.0"
	}

	port := os.Getenv("SVC_PORT")

	if len(port) <= 0 {
		port = "50051"
	}

	cc, err := grpc.Dial(hostname+":"+port, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("could not connect: %v", err)
	}
	defer cc.Close()

	c := grpcapi.NewGrpcServiceClient(cc)
	fmt.Printf("Created client: %f", c)

	callService(c)

}

func callService(c grpcapi.GrpcServiceClient) {
	fmt.Println("callService...")
	req := &grpcapi.GrpcRequest{
		Input: "test",
	}
	res, err := c.GrpcService(context.Background(), req)
	if err != nil {
		log.Fatalf("error while calling gRPC: %v", err)
	}
	log.Printf("Response from Service: %v", res.Response)
}

Now we can also compile and run the client:

❯ go run client.go
Starting client...
Created client: &{%!f(*grpc.ClientConn=&{0xc00007eb40 0x1094060 0.0.0.0:50051 {passthrough  0.0.0.0:50051} 0.0.0.0:50051 {<nil> <nil> [] [] <nil> <nil> {{1000000000 1.6 0.2 120000000000}} false false true 0 <nil>  {grpc-go/1.37.0-dev <nil> false [] <nil> <nil> {0 0 false} <nil> 0 0 32768 32768 0 <nil> true} [] <nil> 0 false true false <nil> <nil> <nil> <nil> 0x13fb280 []} 0xc00000ef60 {<nil> <nil> <nil> 0 grpc-go/1.37.0-dev {passthrough  0.0.0.0:50051}} 0xc0001928d0 {{{0 0} 0 0 0 0} 0xc0000102d8} {{0 0} 0 0 0 0} 0xc000032a80 0xc000088690 map[0xc0000d7340:{}] {0 0 false} pick_first 0xc00007ec40 {<nil>} 0xc00000ef40 0 0xc000024860 {0 0} <nil>})}callService...
grpcServer input:"test" 
2021/02/18 16:12:36 Response from Service: Got input test server host: spacegrey.home

: white_check_mark: Great, it worked!

Here we see the response from the server. In doing so, we printed the entire binary output.

Grpc server on Kubernetes

Now we can deploy the whole construct in our Kubernetes cluster.

Requirements

  • Docker knowledge and access to a Docker repository
  • Knowledge of Kubernetes and access to a Kubernetes cluster
  • If you don't have that, feel free to see what we're doing here

The Dockerfile for the grpc_server:

FROM iron/go
WORKDIR /app

ADD grpc_server /app/

CMD [ "./grpc_server" ]

Kubernetes Manifesto:

apiVersion: v1
kind: Service
metadata:
  name: grpcserver
spec:
  ports:
    - port: 50051
      protocol: TCP
      targetPort: 50051
  selector:
    run: grpcserver
status:
  loadBalancer: { }
---
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: grpcserver
  name: grpcserver
spec:
  replicas: 1
  selector:
    matchLabels:
      run: grpcserver
  strategy: { }
  template:
    metadata:
      labels:
        run: grpcserver
    spec:
      containers:
        - image: javierramos1/grpc_server:latest
          name: grpcserver
          ports:
            - containerPort: 50051
          resources: { }
status: { }

As soon as you are connected to the Kubernetes cluster, you can apply the manifest as follows:

❯ kubectl apply -f deployment.yaml 
service/grpcserver configured
deployment.apps/grpcserver configured

Now you can find out the external IP address in order to align the client accordingly:

❯ kubectl get services
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)          AGE
grpcserver   ClusterIP      10.245.215.85   <none>           50051/TCP        9m41s
kubernetes   ClusterIP      10.245.0.1      <none>           443/TCP          15d
my-nginx     LoadBalancer   10.245.195.94   164.90.243.164   8080:32336/TCP   13d

Now it is your turn! How much better is a gRPC interface compared to a CRUD interface?

Stay tuned!


This text was automatically translated with our golang markdown translator.

Raffael Schneider – crafter, disruptor, free spirit. As a fervent software craftsmanship, Raffael likes to write about programming languages and software resilience in modern distributed systems. Be it DevOps, SRE or systems architecture, he always got a new way of approaching things.