preface

background

Recently, I decided to start learning Go. However, due to the lack of practical application scenarios, MY learning has been stuck in the Level of Hello World, and the tutorials and materials I have read are not very impressive. So I decided to start with the RPC implementation of GO to understand how go language is used in actual scenarios, including exception handling, proxy and filtering, and the usage of GO routine, etc. At the same time, I also briefly learned about other GO language implementations of RPC, such as Thrift and GRPC. A cursory tour deepened my impression, and I began to realize the differences and commonalities between GO language and Java language. Next, in order to further consolidate the learning effect and also to make a review and report on my career so far, I decided to use go language to build a relatively complete RPC (or micro-service) framework from scratch.

Microservices framework and RPC framework

The RPC framework mentioned in this article refers to the framework that provides basic RPC call support; The microservices framework mentioned in this article refers to the RPC framework that contains some service governance related functions such as service registry discovery, load balancing, link tracing, and so on.

research

Before starting to do, it is necessary to learn about other existing products, from which you can learn excellent experience and methods. Here are some frameworks that you have learned preliminarily:

  • GRPC is a microservices framework that supports 10 languages and two-way streaming communication based on HTTP2
  • Go-micro is an open source microservices framework, which is unique in supporting Async Messaging, subpub like MQ
  • Thrift – Go Thrift is an RPC framework donated by Facebook to Apache (without service governance related functionality). According to the official documentation, Thrift supports RPC calls in 20 languages
  • RPCX RPCX is a micro service framework developed by Chinese and open source, advertised as “fast, easy to use but powerful”, the introduction of the official website mentioned that the performance is twice that of GRPC. Attached here is the author’s blog

The above are several existing frameworks that we have known at present. It is a shame that we have not understood them deeply enough. We need to continue to learn in the future.

Pluggable Interfaces

It’s worth noting that apart from thrift, the other three microservices frameworks all feature Pluggable Interfaces, which allow you to replace some functionality with plug-ins. Replaceable functionality via plug-ins is actually a minimum requirement in a microservices framework, otherwise it becomes very difficult to extend functionality later on, and trust me, I speak from the experience of blood and tears.

Demand analysis

Before we start designing or even writing code, let’s first analyze our requirements (from learning software engineering). At the same time, for some students who may not be familiar with the details of RPC, we can also have a general idea of what we will do later. Here are just a few functional requirements:

  • Supports RPC calls, including synchronous and asynchronous calls
  • Support service governance related functions, including:
    • Service registration and discovery
    • Service load balancing
    • Current limiting and fusing
    • The identity authentication
    • Monitoring and link tracing
    • Health checks, including end-to-end heartbeat and registry checks of service instances
  • Support plug-ins. For functions that have multiple implementations (such as load balancing), they need to be implemented in the form of plug-ins. At the same time, they need to support custom plug-ins.

The system design

layered

Now that you have a rough idea of what you need, you can start designing. First we divide the framework into several layers, and each layer interacts with the other through interfaces. Don’t ask why stratification is needed, but experience. Layering, as a classic design mode, is almost ubiquitous in software development and is also very applicable in RPC framework. The following is a general layering diagram:

  • A service is a user-facing interface, such as the initialization and running of client and server instances
  • Client and Server represent instances of the client and server, which are responsible for making requests and returning responses
  • Selector represents a load balancer, or loadBanlancer, which decides which server to send the request to
  • Registery indicates a registry. A server must register its information with the registry after initialization or even at runtime, so that the client can find the desired server from the registry
  • Codec stands for codec, or converting objects and binary data to and from each other
  • Protocol refers to the communication protocol, that is, how the binary data is composed. Many functions in RPC framework require the support of the protocol layer
  • Transport is responsible for network communication. It sends binary data assembled by Protocol over the network and reads data from the network in a manner specified by Protocol

Each of the layers mentioned above, in addition to Services, can actually provide multiple implementations, so they should all be implemented as plugins.

Thus, according to our hierarchy, a client’s process from sending a request to receiving a response looks something like this:

Filter chain

As you can see from the hierarchy above, a request or response actually passes through each layer and then is sent over the network or arrives at the user logic, so we use a filter chain approach to handle requests and responses to achieve the effect of being open for extension and closed for modification. This allows additional functions such as fusible downgrading and limiting traffic, and authentication to be implemented in filters.

Message protocol

The next step is to design specific messaging protocols, which are basically the conventions that two computers make to communicate with each other. For example, TCP specifies the format of a TCP packet, such as the first two bytes for the source port, the third and fourth bytes for the destination port, followed by sequence numbers and confirmation numbers, and so on. In our RPC framework, we also need to define our own protocol. Generally speaking, network protocols are divided into head and body parts. Head is some metadata, which is the data required by the protocol itself, and body is the data transmitted from the previous layer, which only needs to be passed on unchanged.

Let’s try to define our own protocol:

-------------------------------------------------------------------------------------------------
|2byte|1byte  |4byte       |4byte        | header length |(total length - header length - 4byte)|
-------------------------------------------------------------------------------------------------
|magic|version|total length|header length|     header    |                    body              |
-------------------------------------------------------------------------------------------------
Copy the code

According to the protocol above, a message body consists of the following parts in strict order:

  • With a magic number starting with two bytes, we can quickly identify illegal requests
  • A byte indicates the protocol version. Currently, it can be set to 0 uniformly
  • 4 bytes indicates the total length of the rest of the message body.
  • 4 bytes indicates the header length.
  • The length of the message header is determined by the previously resolved header length
  • Body, the length of which is the total length of the previous resolution minus the length of the message header (total length-4-header length)

The data of the message header in the protocol is mainly the metadata in the RPC call process. The metadata has nothing to do with method parameters and response, and mainly records additional information and implements ancillary functions such as link tracing and identity authentication. The data in the message body is encoded by the actual request parameters or response. In actual processing, the header is usually a structure at the sender, which is encoded as a binary before the header when it is sent, and decoded as a structure when it is received by the receiver, which is handed to the program for processing. Try enumerating the information contained in the message header:

type Header struct {
        Seq uint64 // Serial number, which uniquely identifies the request or response
        MessageType byte // Message type, which identifies whether a message is a request or a response
        CompressType byte // Compression type, which identifies how a message is compressed
        SerializeType byte // Serialization type, which identifies how the message body is encoded
        StatusCode byte // Status type, which identifies whether a request is normal or abnormal
        ServiceName string / / service name
        MethodName string  / / the method name
        Error string // The exception occurred on the method call
        MetaData map[string]string // Other metadata
}

Copy the code

conclusion

This is the end of the first article, mainly to do the first preparation, sort out the ideas, if there is incorrect or unreasonable part, please give us more advice.