Nowadays, modern architecture can combine different design patterns and languages.
Traditionally, microservices are often implemented in an HTTP API interface, which can be some performance issues in distributed overloaded systems.
gRPC (Remote Procedure Calls) is a modern high-performance framework, an Open Source project, which is used to connect a large number of microservices.
Download the binary to a temporal directory
curl -L https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip -o /tmp/protoc.zip
NOTE: We'll use the "v21.12" version of protoc, which is the latest today. You can change it for another version that you can find in the following link, even though it's not necessary: https://github.com/protocolbuffers/protobuf/releases
Unzip it
unzip /tmp/protoc.zip -d /tmp/protoc/
Install the binary
mv /tmp/protoc/bin/protoc /usr/local/bin/protoc
Verify the installation
protoc --version
libprotoc 3.21.12
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
NOTE: if you can execute
protoc-gen-go
you have to add it to your PATH:export GO_PATH=~/go
andexport PATH=$PATH:/$GO_PATH/bin
A significant advantage of using protobufs is the capacity to auto-generate the code in your preferred language, even in different languages.
In this example, we'll create a .proto
file with the definition of the entity Comment.
Once we've created the file, we'll generate the source code. In this case, I have chosen the golang plugin.
protoc comment/grpc/*.proto \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative
NOTE: I've installed the
protoc-gen-go
plugin, which allow to auto-generate golang code. If you prefer, you can install another language plugin.
After the plugin execution, we can inspect our code inside the comment/grpc
folder.
As a good practice, we shouldn't modify the autogenerated code, but we can import and override it.
In this example, the folder gprc
contains the generated code. We'll create a new module. This module includes the auto-generated code and two applications: the grpc server and a grpc client used to test our application.
go mod init github.com/dbgjerez/workshop-golang-grpc/comment
It's essential to change
dbgjerez
for your own GitHub account.
We need to download the dependencies that our project need:
go mod tidy
go get google.golang.org/grpc
go get google.golang.org/grpc/reflection
Once we have our project, we'll create a folder called server
and a main.go
file inside it, where we'll implement our server application.
The DDD structure used to organize this project has a folder for handlers. A handler is a point to communicate with our application, for example, a REST endpoint or gRPC like this case. In addition, the application domain has its own folder for its definitions.
The main file contains all the necessary to initialize the application. The application initializes the server, registers the gRPC handlers and starts the server up.
The complete main function looks like the following block:
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
c.RegisterCommentServiceServer(s, handler.NewCommentHandler())
c.RegisterHealthServer(s, new(handler.HealthHandler))
c.RegisterInfoServiceServer(s, handler.NewInfoHandler())
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
If we visualize the main.go file, we can watch the following points:
- The port when the application is listening.
- How the application starts the gRPC server.
- The registration of the comment service.
- The registration of the health check service.
- The registration of the info service.
The line with reflection.Register(s)
enables the exposition of the API, so you can call to know the different functions and endpoints that expose the application.
Now, we can start our application and test it:
go run main.go
We can use the grpcurl
tool to test our server application. For example, I'm going to list the different endpoints with the following command:
grpcurl -plaintext localhost:50051 list
CommentService
HealthService
InfoService
grpc.reflection.v1alpha.ServerReflection
NOTE: change port 50051 for your application port.
Our application responds to different endpoints:
- CommentService: application business logic
- HealthService: the health check service endpoint.
- InfoService: application information, such as the name, version and build time.
- grpc.reflection.v1alpha.ServerReflection: the Reflection API exposes all the endpoint definitions
If we continue calling it, we can see the different methods that contain our endpoint:
grpcurl -plaintext localhost:50051 list CommentService
CommentService.Retrieve
We've received two methods, also the same ones that we defined in the comment.proto
file. So, it looks good. Now, I'll call the insert method:
grpcurl -plaintext localhost:50051 CommentService.Retrieve
ERROR:
Code: Unimplemented
Message: method Insert not implemented
Our application is responding, so it runs ok, but we've received an error code. This error is because we have not implemented the different methods as we have used the auto-generated code, and we only have defined an empty struct for our server.
Now, we can implement the business logic application without modifying the auto-generated code. I'll return a list of comments, but you can use whatever you want in your application.
As we defined the server struct, we only have to define the Retrieve method:
type CommentHandler struct {
c.UnimplementedCommentServiceServer
comments []*c.Comment
}
func (s *CommentHandler) Retrieve(ctx context.Context, rq *c.RetrieveRequest) (*c.Comments, error) {
log.Printf("Request: %s", rq.String())
return &c.Comments{Comments: s.FilterComments(rq.IdObject, rq.TypeObject)}, nil
}
NOTE: The
CommentHandler
struct has to contain theUninmplementedCommentServiceServer
and whatever you want, as a base de date or something similar.
Finally, we'll implement the rest of the handlers for info and health endpoints.
Now, we'll run the application again and test it by calling the Retrieve endpoint:
grpcurl -plaintext localhost:50051 CommentService.Retrieve
{
"comments": [
{
"idComment": "1",
"idObject": 12,
"typeObject": "film",
"idUser": 20
},
{
"idComment": "2",
"idObject": 12,
"typeObject": "film",
"idUser": 20
}
]
}
And now, we can see how the application responds to the comment that we return.
A client is an application that usually consumes some services or applications easily.
When we executed the plugin, it generated all the necessary code to make the server and the client. In this way, we only have to focus on the business logic of our application.
idObj = flag.Int("idObj", -1, "The object id")
typeObj = flag.String("type", "", "The server host")
comments, err := client.Retrieve(ctx, &c.RetrieveRequest{IdObject: int32(*idObj), TypeObject: *typeObj})
The complete code is in the main.go
file in the client folder.
Container runtimes offer us many advantages concerning portability, less overhead, decoupling, security, etc. In this case, we'll create a container to run our application.
Firstly, we need a file that contains the steps to generate our runtime container.
In addition, another good practice is to use a container to build the application. The Containerfile
contains two steps, the first will build the image, and the second one is liable for the application execution.
We can check all the steps to open it.
Finally, the container can be built with the following instructions:
SERVICE_NAME=comment-service
VERSION=0.2
SERVICE_BUILD_TIME=$(date '+%Y/%m/%d %H:%M:%S')
podman build \
--no-cache \
--build-arg version=$VERSION \
--build-arg serviceName=$SERVICE_NAME \
--build-arg buildTime=$SERVICE_BUILD_TIME \
-t quay.io/dborrego/$SERVICE_NAME:$VERSION \
-f Containerfile
For example, you can use my container. I've deployed it in quay.io: https://quay.io/repository/dborrego/comment-service?tab=tags&tag=latest.
If you want to run it:
podman run \
-p 8080:8080 \
-d \
--name grpc \
quay.io/dborrego/comment-service:0.1
And now, then we can test again the application:
grpcurl -plaintext localhost:8080 list
CommentService
HealthService
InfoService
grpc.reflection.v1alpha.ServerReflection
In this workshop, we've studied the easiest way to make many calls in the atomic model. Another very important advantage of gprc use is that you can implement a stream of calls both in the client and server.
In this example, you can see this example in the branch feature/client-stream