5
头图

introduce

I guess most developers who have used Java for a long time should have less contact with gRPC . After all, most of the Java circles Dubbo/SpringClound these two service frameworks.

I also had the opportunity to rebuild the business from scratch recently 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 Go circles.
  • Some businesses in the company use Python for development, and gRPC supports multi-language compatibility very well.

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 in gRPC to:

  • Based on Protocol Buffer serialization protocol, the performance is efficient.
  • Based on HTTP/2 standard protocol development, it comes stream , multiplexing and other features; 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 codes.

HTTP/2

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

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

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 done 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 to the same header repeatedly.

Protocol

gRPC uses Protocol serialization, which was released 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 used for data interaction when developing https://github.com/crossoverJie/cim .

package order.v1;

service OrderService{

  rpc Create(OrderApiCreate) returns (Order) {}

  rpc Close(CloseApiCreate) returns (Order) {}

  // 服务端推送
  rpc ServerStream(OrderApiCreate) returns (stream Order) {}

  // 客户端推送
  rpc ClientStream(stream OrderApiCreate) returns (Order) {}
  
  // 双向推送
  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) {
    // 获取 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 to 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 the regular gRPC calls, unary 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 of the server is shown above, and the Send function can be called to push to the client.

    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 client is optimized, which 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.

// 服务端代码
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
}

    // 客户端代码
    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.

// 服务端
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
}
// 客户端
    for i := 0; i < 5; i++ {
        messages, _ := GetMsg(data)
        // 发送消息
        rpc.SendMsg(messages[0])
        // 接收消息
        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 HTTP in header .

    // 客户端写入
    metaStr := `{"lang":"zh"}`
    var m map[string]string
    err := json.Unmarshal([]byte(metaStr), &m)
    md := metadata.New(m)
    // 调用时将 ctx 传入即可
    ctx := metadata.NewOutgoingContext(context.Background(), md)
    
    // 服务端接收
    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 APPs is not as extensive as REST applications (the browsers also support it, but there are very few applications).

To this end, the community created the https://github.com/grpc-ecosystem/grpc-gateway project, which can expose gRPC services as RESTFUL APIs.

In order to allow the testers to be accustomed to using postman for interface testing, we also proxy the gRPC service 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 obtaining the service name and pb file.

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

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

load balancing

Since gRPC is 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 is the client load balancing used.

gRPC also provides related interfaces, please refer to the official demo for details.

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 content in gRPC . This article is only an introductory material. I hope that those who do not know gRPC can have a basic understanding; this is indeed a necessary 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

crossoverJie
5.4k 声望4k 粉丝