Learn gRPC in 5 minutes

introduce

I guess that most developers who have used Java for a long time should have less exposure to gRPC. After all, most of the Java circles use Dubbo/SpringClound service frameworks.

I also recently had the opportunity to refactor my business from scratch before I came into contact with gRPC. There were several reasons for choosing gRPC at that time:

  • Develop and deploy projects based on cloud-native ideas, and gRPC is almost a standard communication protocol in cloud-native.
  • The development language chose Go, and gRPC is obviously a better choice in the Go circle.
  • Some businesses within the company use Python development, and gRPC supports very well in terms of multi-language compatibility.

After more than a year of stable online operation, it can be seen that gRPC is still very stable and efficient; the core points of the rpc framework:

  • Serialization
  • letter of agreement
  • IDL (Interface Description Language)

These correspond to the following in gRPC:

  • Based on the Protocol Buffer serialization protocol, the performance is efficient.
  • Developed based on the HTTP/2 standard protocol, with features such as stream and multiplexing; at the same time, because it is a standard protocol, the compatibility of third-party tools will be better (such as load balancing, monitoring, etc.)
  • Write a .proto interface file to generate common language code.

HTTP/2

Before learning gRPC, you must first know what protocol it communicates through. The HTTP/1.1 protocol is basically the most exposed to our daily development or application.

Since HTTP/1.1 is a text protocol, it is very friendly to humans, but on the contrary, the performance of machines is relatively low.

It is necessary to parse the text repeatedly, and the efficiency is naturally low; to be more friendly to the machine, it is necessary to use binary, which is naturally achieved by HTTP/2.

Apart from that, there are other advantages:

  • Multiplexing: messages can be sent and received in parallel without affecting each other
  • HPACK saves header space and avoids HTTP1.1 sending the same header repeatedly.

Protocol

gRPC uses Protocol serialization, and the release time is earlier than gRPC, so it is not only used for gRPC, but can be used in any scenario that requires serialized IO operations.

It will be more space-saving and high-performance; it was developed before https://github.com/crossoverJie/cim use it for data interaction.

package order.v1;

service OrderService{

  rpc Create(OrderApiCreate) returns (Order) {}

  rpc Close(CloseApiCreate) returns (Order) {}

  // Server push
  rpc ServerStream(OrderApiCreate) returns (stream Order) {}

  // Client push
  rpc ClientStream(stream OrderApiCreate) returns (Order) {}
  
  // Two-way push
  rpc BdStream(stream OrderApiCreate) returns (stream Order) {}
}

message OrderApiCreate{
  int64 order_id = 1;
  repeated int64 user_id = 2;
  string remark = 3;
  repeated int32 reason_id = 4;
}

It is also very simple to use. You only need to define your own .proto file, and you can use the command line tool to generate the SDK of the corresponding language.

For details, please refer to the official documentation:
https://grpc.io/docs/languages/go/generated-code/

transfer

    protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    test.proto


After generating the code, writing the server is very simple, you only need to implement the generated interface.

func (o *Order) Create(ctx context.Context, in *v1.OrderApiCreate) (*v1.Order, error) {
    // get metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
    }
    fmt.Println(md)
    fmt.Println(in.OrderId)
    return &v1.Order{
        OrderId: in.OrderId,
        Reason:  nil,
    }, nil
}

The client is also very simple, it only needs to rely on the server code, create a connection and then it is the same as calling a local method.

This is a classic unary (unary) call, similar to the request-response mode of http, one request corresponds to one response.

Server stream

In addition to regular unary calls, gRPC also supports server push, which is useful in some specific scenarios.

func (o *Order) ServerStream(in *v1.OrderApiCreate, rs v1.OrderService_ServerStreamServer) error {
    for i := 0; i < 5; i++ {
        rs.Send(&v1.Order{
            OrderId: in.OrderId,
            Reason:  nil,
        })
    }
    return nil
}

The push on the server side is shown above, and the push function can be pushed to the client side by calling the Send function.

    for {
        msg, err := rpc.RecvMsg()
        if err == io.EOF {
            marshalIndent, _ := json.MarshalIndent(msgs, "", "\t")
            fmt.Println(msg)
            return
        }
    }

The client obtains the server message by judging whether the currently received data packet has expired through a loop.

In order to show this process more intuitively, a previously developed gRPC was optimized client , you can debug stream calls intuitively.

The figure above is an example of server push.

Client Stream

In addition to supporting server push, the client also supports it.

The client keeps sending data to the server in the same connection, and the server can process the messages in parallel.

// server code
func (o *Order) ClientStream(rs v1.OrderService_ClientStreamServer) error {
    var value []int64
    for {
        recv, err := rs.Recv()
        if err == io.EOF {
            rs.SendAndClose(&v1.Order{
                OrderId: 100,
                Reason:  nil,
            })
            log.Println(value)
            return nil
        }
        value = append(value, recv.OrderId)
        log.Printf("ClientStream receiv msg %v", recv.OrderId)
    }
    log.Println("ClientStream finish")
    return nil
}

    // client code
    for i := 0; i < 5; i++ {
        messages, _ := GetMsg(data)
        rpc.SendMsg(messages[0])
    }
    receive, err := rpc.CloseAndReceive()

The code is similar to server push, but the roles are reversed.

Bidirectional Stream

Similarly, it is also supported when both the client and the server are sending messages at the same time.

// Server
func (o *Order) BdStream(rs v1.OrderService_BdStreamServer) error {
    var value []int64
    for {
        recv, err := rs.Recv()
        if err == io.EOF {
            log.Println(value)
            return nil
        }
        if err != nil {
            panic(err)
        }
        value = append(value, recv.OrderId)
        log.Printf("BdStream receiv msg %v", recv.OrderId)
        rs.SendMsg(&v1.Order{
            OrderId: recv.OrderId,
            Reason:  nil,
        })
    }
    return nil
}
// client
    for i := 0; i < 5; i++ {
        messages, _ := GetMsg(data)
        // Send a message
        rpc.SendMsg(messages[0])
        // receive message
        receive, _ := rpc.RecvMsg()
        marshalIndent, _ := json.MarshalIndent(receive, "", "\t")
        fmt.Println(string(marshalIndent))
    }
    rpc.CloseSend()

In fact, the two appeals are combined into one.

It is easy to understand by calling an example.

metadata

gRPC also supports metadata transfer, similar to header s in HTTP.

    // client write
    metaStr := `{"lang":"zh"}`
    var m map[string]string
    err := json.Unmarshal([]byte(metaStr), &m)
    md := metadata.New(m)
    // Just pass in ctx when calling
    ctx := metadata.NewOutgoingContext(context.Background(), md)
    
    // Server receives
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
    }
    fmt.Println(md)    

gRPC gateway

Although gRPC is powerful and easy to use, its support for browsers and APP s is not as extensive as REST applications (browser also supports it, but there are very few applications).

created for this community https://github.com/grpc-ecosystem/grpc-gateway Project that can expose gRPC services as RESTFUL API s.

In order to make the testers accustomed to using postman for interface testing, we also proxy the gRPC service out for easier testing.

reflection call

As an rpc framework, generalization calls must also be supported, which can facilitate the development of supporting tools; gRPC is supported through reflection, and reflection calls are made by getting the service name and pb file.

https://github.com/jhump/protoreflect This library encapsulates common reflection operations.

The visual stream call seen in the image above is also implemented through this library.

load balancing

Since gRPC is implemented based on HTTP/2, the client and server will maintain a long connection; at this time, load balancing is not as simple as HTTP.

And we use gRPC to achieve the same effect as HTTP, and we need to load balance requests instead of connections.

There are usually two approaches:

  • Client Load Balancing
  • Server Load Balancing

Client load balancing is widely used in rpc calls. For example, Dubbo uses client load balancing.

Related interfaces are also provided in gRPC. For details, please refer to the official demo.

https://github.com/grpc/grpc-go/blob/87eb5b7502/examples/features/load_balancing/README.md

Client-side load balancing is relatively more flexible for developers (you can customize your own strategies), but you also need to maintain this logic yourself. If there are multiple languages, you have to maintain multiple copies.

Therefore, in the context of cloud native, it is more recommended to use server-side load balancing.

Options are:

  • istio
  • envoy
  • apix

We are also working on this, and we will probably use envoy/istio.

Summarize

There is still a lot of gRPC content. This article is just an introductory material. I hope that those who don’t know gRPC can have a basic understanding; this is indeed a must-have skill in the cloud-native era.

Friends who are interested in the gRPC client in this article can refer to the source code here:
https://github.com/crossoverJie/ptg

Tags: Go grpc

Posted by goochy808 on Thu, 02 Jun 2022 02:07:49 +0530