This article explores the differences between gRPC and RabbitMQ RPC, two leading frameworks in remote procedure calls (RPC) for microservices architecture.
Comparison overview #
Feature | gRPC | RabbitMQ RPC |
---|---|---|
Service Definition | Yes (.proto file) | No |
Error Handling | Predefined errors list | Requires custom implementation |
Security | TLS, Token | TLS, Username/Password |
Orchestration | Required | Not necessary |
Performance | Fast | Slower |
Requires direct connection between server and client | Yes | No |
What is RPC? #
RPC (Remote Procedure Call) enables program functions to be executed remotely. This article focuses on comparing two popular RPC frameworks in microservices: gRPC and RabbitMQ RPC.
RPC stands for Remote Procedure Call and is a protocol that allows a program to execute a function on a remote machine. It is similar to a function call, but the function is executed on a remote machine.
The major difference between a function call and a remote produce call is that the RPC is usually slower and can fail due to network issues.
What is gRPC? #
gRPC is a RPC framework developed by Google. It is based on the HTTP/2 protocol and uses the Protocol Buffers as the default payload format.
An RPC is defined by a service definition, which is written in a .proto
file. The service definition defines the
methods that can be called remotely.
Such a .proto
file can be compiled to a client and server stub, which can be used to implement the service.
Here is an example of such a .proto
file. It defines a fibonacci service, which offers one function to calculate the
nth fibonacci based on the input.
syntax = "proto3";
package example;
service Fibonacci {
rpc CalcFib (FibRequest) returns (FibResponse) {}
}
message FibRequest {
int64 input = 1;
}
message FibResponse {
int64 output = 1;
}
That proto file generates the stub code, which then requires the implementation of a server and client. Which could look like the following:
// FibonacciServerImpl implements the server interface for our Fibonacci service.
type FibonacciServerImpl struct {
grpc_comparison.UnimplementedFibonacciServer
}
func (FibonacciServerImpl) CalcFib(ctx context.Context, request *grpc_comparison.FibRequest) (*grpc_comparison.FibResponse, error) {
n := base.Fib(request.Input)
result := &grpc_comparison.FibResponse{Output: n}
return result, nil
}
What is RabbitMQ RPC? #
RabbitMQ RPC is a RPC library based on the RabbitMQ message broker. It uses the AMQP protocol to communicate between the client and server. The client sends a message to the server, which then processes the message and sends a response back to the client.
Compared to gRPC, RabbitMQ RPC does not necessary have a server/client distinction. RPC provider and consumer will need to connect to the same RabbitMQ instance.
Besides that, there is no service definition, which means, the RPC provider and consumer need to agree on the payload format.
Comparison #
Service definition #
gRPC relies on a service definition, which is written in a .proto
file. The service definition defines the methods
that can be called remotely, as well as the types for parameters and return values.
RabbitMQ offers no service definition. That means, the RPC provider and consumer need to agree on the payload and routing format.
Error handling #
gRPC #
For gRPC, there is a list of predefined errors , which can either indicate an error regarding gRPC or the actual remote procedure call.
The UNKNOWN
error code is used to indicate unknown errors. It is also used if the error code is not specified, such as
in case of errors raised by the RPC API.
RabbitMQ #
For RabbitMQ RPC, there is also a list of predefined errors
. But
it is not as extensive as the gRPC error list. It is
mainly concerned about channel, connection, and queue thematics, such as no-route
or not-found
for example. Though, it does not provide API error capabilities.
That means, API error reporting need to be implemented as part of the API response.
Security #
There are two aspects of security, transport security and authentication.
Transport #
Both gRPC and RabbitMQ support transport security via TLS/SSL.
To read more about RabbitMQ TLS support, please refer to the RabbitMQ TLS documentation .
For more about gRPC TLS support, please refer to the gRPC TLS documentation , also this is a good example.
Authentication #
RabbitMQ supports authentication via username and password. It is also possible to use TLS client certificates for authentication, although username and password are easier to get started with.
gRPC on the other hand relies by default on TLS client certificates for authentication. Besides that, it also supports token based authentication but please check out here for more information.
Orchestration #
RabbitMQ does not need orchestration, as it is a message broker. It is possible to run multiple instances of RabbitMQ and connect to them via a load balancer.
gRPC on the other hand, requires orchestration. It is possible to run multiple instances of the gRPC server and connect to them via a load balancer. But the client needs to know about all the instances.
An example of such an orchestration is Apache ZooKeeper.
That means, gRPC requires more setup than RabbitMQ.
Performance #
The performance test involved sending 1000 requests to calculate the 5th Fibonacci number. These tests were both run on a MacBook Pro M1 Max to ensure a fair comparison.
Here are the results:
gRPC #
Average request time was 98.27µs.
=== RUN TestGRPCClient
2023/09/04 00:15:11 [x] Requesting run: 0
2023/09/04 00:15:11 [x] Requesting run: 100
2023/09/04 00:15:11 [x] Requesting run: 200
2023/09/04 00:15:11 [x] Requesting run: 300
2023/09/04 00:15:11 [x] Requesting run: 400
2023/09/04 00:15:11 [x] Requesting run: 500
2023/09/04 00:15:11 [x] Requesting run: 600
2023/09/04 00:15:11 [x] Requesting run: 700
2023/09/04 00:15:11 [x] Requesting run: 800
2023/09/04 00:15:11 [x] Requesting run: 900
Execution time: 98.270333ms avg: 98.27µs
RabbitMQ RPC #
Average request time was 6.00ms.
=== RUN TestRabbitRPCClient
2023/09/04 00:17:41 [x] Requesting run: 0
2023/09/04 00:17:42 [x] Requesting run: 100
2023/09/04 00:17:42 [x] Requesting run: 200
2023/09/04 00:17:43 [x] Requesting run: 300
2023/09/04 00:17:44 [x] Requesting run: 400
2023/09/04 00:17:44 [x] Requesting run: 500
2023/09/04 00:17:45 [x] Requesting run: 600
2023/09/04 00:17:46 [x] Requesting run: 700
2023/09/04 00:17:46 [x] Requesting run: 800
2023/09/04 00:17:47 [x] Requesting run: 900
Execution time: 6.002085917s avg: 6.002085ms
Test Definition #
package go_rpc_comparison
import (
"fmt"
"go-rpc-comparison/grpcimpl"
"go-rpc-comparison/rabbitimpl"
"go-rpc-comparison/utils"
"log"
"testing"
"time"
)
func measureRunTime(f func()) {
// Start measuring time
start := time.Now()
for i := 0; i < utils.TestRuns; i++ {
if i%100 == 0 {
log.Printf(" [x] Requesting run: %d", i)
}
f()
}
// Stop measuring time
duration := time.Since(start)
average := duration / utils.TestRuns
// Print the execution time
fmt.Println("Execution time:", duration, " avg: ", average)
}
func TestGRPCClient(t *testing.T) {
grpcimpl.InitClient()
measureRunTime(func() {
err := grpcimpl.RpcFib(utils.TestFibN)
utils.FailOnError(err, "Failed to handle RPC request")
})
}
func TestRabbitRPCClient(t *testing.T) {
rabbitimpl.InitClient()
measureRunTime(func() {
_, err := rabbitimpl.FibonacciRPC(utils.TestFibN)
utils.FailOnError(err, "Failed to handle RPC request")
})
}
RabbitMQ RPC Server
The RabbitMQ RPC server implementation requires a queue definition and consumption.
package go_rpc_comparison
import (
"context"
amqp "github.com/rabbitmq/amqp091-go"
"go-rpc-comparison/base"
"go-rpc-comparison/utils"
"log"
"strconv"
"testing"
"time"
)
func TestRabbitRPCServer(t *testing.T) {
conn, err := amqp.Dial("amqp://rabbitmq:rabbitmq@localhost:5672/")
utils.FailOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
utils.FailOnError(err, "Failed to open a channel")
defer ch.Close()
q, err := ch.QueueDeclare(
rabbitimpl.RABBIT_QUEUE, // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
utils.FailOnError(err, "Failed to declare a queue")
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
utils.FailOnError(err, "Failed to set QoS")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
utils.FailOnError(err, "Failed to register a consumer")
var forever chan struct{}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for d := range msgs {
n, err := strconv.Atoi(string(d.Body))
utils.FailOnError(err, "Failed to convert body to integer")
log.Printf(" [.] fib(%d)", n)
response := base.Fib(int64(n))
err = ch.PublishWithContext(ctx,
"", // exchange
d.ReplyTo, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: d.CorrelationId,
Body: []byte(strconv.Itoa(int(response))),
})
utils.FailOnError(err, "Failed to publish a message")
d.Ack(false)
}
}()
log.Printf(" [*] Awaiting RPC requests")
<-forever
}
RabbitMQ RPC Client
The RabbitMQ RPC client implementation requires, for each request, a new queue definition and consumption.
package rabbitimpl
import (
"context"
amqp "github.com/rabbitmq/amqp091-go"
"go-rpc-comparison/utils"
"math/rand"
"strconv"
"time"
)
var conn *amqp.Connection
var channel *amqp.Channel
func InitClient() {
connection, err := amqp.Dial("amqp://rabbitmq:rabbitmq@localhost:5672/")
utils.FailOnError(err, "Failed to connect to RabbitMQ")
conn = connection
ch, err := conn.Channel()
utils.FailOnError(err, "Failed to open a channel")
channel = ch
}
func randInt(min int, max int) int {
return min + rand.Intn(max-min)
}
func randomString(l int) string {
bytes := make([]byte, l)
for i := 0; i < l; i++ {
bytes[i] = byte(randInt(65, 90))
}
return string(bytes)
}
func FibonacciRPC(n int) (res int, err error) {
// Declare a receiver queue per request
q, err := channel.QueueDeclare(
"", // name
false, // durable
false, // delete when unused
true, // exclusive
false, // noWait
nil, // arguments
)
utils.FailOnError(err, "Failed to declare a queue")
// Consume messages for that queue
msgs, err := channel.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
utils.FailOnError(err, "Failed to register a consumer")
// Define a random correlation ID for the request
corrId := randomString(32)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Publish the request to the queue, pushing n as the body
err = channel.PublishWithContext(ctx,
"", // exchange
RABBIT_QUEUE, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: corrId,
ReplyTo: q.Name,
Body: []byte(strconv.Itoa(n)),
})
utils.FailOnError(err, "Failed to publish a message")
// Listen to messages on the queue and wait for the response with the same correlation ID
for d := range msgs {
if corrId == d.CorrelationId {
res, err = strconv.Atoi(string(d.Body))
utils.FailOnError(err, "Failed to convert body to integer")
break
}
}
return
}
gRPC Server
package go_rpc_comparison
import (
"flag"
"fmt"
grpc_comparison "go-rpc-comparison/ecostack.dev/project/grpc-comparison"
"go-rpc-comparison/grpcimpl"
"google.golang.org/grpc"
"log"
"net"
"testing"
)
type FibonacciServerImpl struct {
grpc_comparison.UnimplementedFibonacciServer
}
func (FibonacciServerImpl) CalcFib(ctx context.Context, request *grpc_comparison.FibRequest) (*grpc_comparison.FibResponse, error) {
n := base.Fib(request.Input)
result := &grpc_comparison.FibResponse{Output: n}
return result, nil
}
func TestGRPCServer(t *testing.T) {
port := grpcimpl.RpcPort
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
grpcServer := grpc.NewServer(opts...)
server := &grpcimpl.FibonacciServerImpl{}
grpc_comparison.RegisterFibonacciServer(grpcServer, server)
grpcServer.Serve(lis)
}
gRPC Client
package grpcimpl
import (
"context"
grpc_comparison "go-rpc-comparison/ecostack.dev/project/grpc-comparison"
"google.golang.org/grpc"
"log"
"strconv"
"time"
)
var conn *grpc.ClientConn
const RpcPort = 44553
func InitClient() {
serverAddr := "localhost:" + strconv.Itoa(RpcPort)
connection, err := grpc.Dial(serverAddr, grpc.WithInsecure())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
conn = connection
}
func RpcFib(n int64) error {
client := grpc_comparison.NewFibonacciClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
_, err := client.CalcFib(ctx, &grpc_comparison.FibRequest{Input: n})
if err != nil {
log.Fatalf("Error calling SayHello: %v", err)
}
return err
}
Conclusion #
In conclusion, choose RabbitMQ RPC for simpler setups and when ease of use is a priority. Opt for gRPC for more robust, scalable solutions, especially in environments where performance is critical.
Feature | gRPC | RabbitMQ RPC |
---|---|---|
Service Definition | Yes (.proto file) | No |
Error Handling | Predefined errors list | Requires custom implementation |
Security | TLS, Token | TLS, Username/Password |
Orchestration | Required | Not necessary |
Performance | Fast | Slower |
Requires direct connection between server and client | Yes | No |
Further reading #
In case you want to learn more about RabbitMQ RPC, please refer to the RabbitMQ RPC tutorial .
If you are interested in gRPC, please refer to the gRPC documentation .