5
votes

I am trying to understand GoLang "Go" together with gRPC and to make a simple service scalable.

Lets say I have a client1 that calls a service1(adds numbers) that calls service2(determines if the result is prime), and service2 returns the result to service1 that returns the result to client1 all via gRPC.

When I use protocol buffers "proto3" and generate the Go code via protoc. I get generated methods that call the service in one particular way. I see no distinction to call the methods asynchronously "Go".

And the underlying call seems to be "Invoke" which I believe is synchronous,the call returns once a result is received.

How do I make service1 "performant", I know I can run this in a cluster and have copies, but that would mean I can only serve clients as per the amount of instances within the cluster.

I want a "single" service to be able to serve multiple clients(e.g. 1000) .

Here is a simple server and I am not sure if this is performant or not: I do know that the getprime function does dial every time, and this could probably be moved to make this dial persist and be re-used; But more importantly I want to make a simple performant scaleable service and get a good understanding.

(A) Perhaps the whole design is incorrect and the service1 should just return as soon as the instruction is received "ack", do the addition and send the next request to sercice2 which determines if the answer is prime or not; again service2 just responds with an acknowledgement of the request being received. Once prime is determined by the service2 a call is made to the client with an answer.

If (A) above is the better approach, then still please explain the bottlenecks below; what happens when multiple clients are processed? The call to "Listen" does what, "blocks, or does not block", etc.

package main

import (
    pb "demo/internal/pkg/proto_gen/calc"
    "fmt"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
    "log"
    "net"
)

const (
    port = ":8080"
)

type service struct {
}

func (s *service) Calculate(ctx context.Context, req *pb.Instruction) (*pb.Response, error) {

    var answer float64
    answer = req.Number1 + req.Number2

    // call service prime
    p := getprime(int(answer))
    pa := pb.PrimeAnswer{Prime: p}
    return &pb.Response{Answer: answer, Prime: &pa}, nil
}

const (
    primeAddress = "127.0.0.1:8089"
)

func getprime(number int) bool {
    conn, err := grpc.Dial(primeAddress, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Did not connect to prime service: %v", err)
    }
    defer conn.Close()

    client := pb.NewPrimeServiceClient(conn)
    p := pb.PrimeMessage{"", float64(number)}

    r, err := client.Prime(context.Background(), &p)
    if err != nil {
        log.Fatalf("Call to prime service failed: %v", err)
    }
    return r.Prime
}

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

    s := grpc.NewServer()
    pb.RegisterCalculatorServer(s, &service{})
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
2
Go doesn't generally need asynchronous APIs, you manage concurrency with goroutines.JimB
Or, if you're asking how to handle concurrent requests, that's what the grpc.Server does. Unless you're implementing your own, it's not something you need to worry about.JimB
Thanks Jim; and what about the callback, where the gRPC calls your gRPC method implementation to perform some operation. In my example it is the Calculate method. Would you need to use channels and go routines inside this method if it would perform long running operations?Wayne
I'm afraid I still don't understand. The Go grpc API isn't structured around callbacks. If you want to do long running operations, you do long running operations. Channels and goroutines are just constructs of the language -- it's kind of like asking if you would use functions and variables to do long running operations.JimB
if you're asking wether the Go gRPC server will handle concurrent connections; yes it will, it wouldn't work very well if it didn't.JimB

2 Answers

5
votes

Thanks for your question. It is true that gRPC-Go is sync only; that is your Unary RPC(the one in your example) will return only when the RPC has finished (got a response from the server).

About performance:

  1. The Dial operation establishes an underlying connection which may be expensive. So it not wise to do it every time getprime is called. A better way is to create a client, keep it around and make calls to the prime server on it. This way only first RPC incurs the cost of connection.
  2. For each RPC request a server gets we launch a goroutine to process that request. So in general, this should scale fairly well.

About (A): It is not uncommon for a service handler to make an RPC call to yet another server and wait for its response before returning back. Note that there's no way for a server to make call to the client.

3
votes

To phrase what JimB said as an answer: "Synchronous" means that the function that makes the remote call waits for a reply before continuing, not that the whole server or client does. The server is normally multithreaded, even when processing synchronous calls; it can accept and work on a second call while it's responding to the first.

And similarly, if a client has multiple concurrent tasks that each have a gRPC call running, that won't block the process. Clients like that could include net/http servers serving end users, or gRPC servers handling multiple RPCs.

Where you might add explicit go statements is if you want to do something else from the specific function making the RPC call. For example, if you want to issue several RPC calls at once then wait for all their results to come in, you could write code following the examples of fan-out calls.