How golang uses native RPC and microservices

1. What is micro service

  • A set of small services is used to develop a single application. Each service runs in an independent process. It is generally interconnected by lightweight communication mechanism, and they can be deployed in an automatic way

Micro service is a design idea, not a reflection of quantity

  • Specific function
  • A lot of code
  • Complex architecture

2. What are the characteristics

  • Specific responsibilities, such as focusing on rights management
  • Lightweight communication, which is independent of platform and language. For example, http is lightweight
  • Data isolation
  • Have their own data
  • Technical diversity

image

3. Advantages of microservice architecture

  • Independence
  • Easy for users to understand
  • Flexible technology stack
  • Efficient team

4. Deficiencies of microservice architecture

  • Additional work, service splitting
  • Ensure data consistency
  • Increased communication costs

Micro service ecology

1. Hardware layer

  • Use docker+k8s to solve it

2. Communication layer

  • Network transmission, using RPC (remote procedure call)
* **HTTP transmission** ,GET POST PUT DELETE
* **be based on TCP** ,Closer to the bottom, RPC be based on TCP,Dubbo(18 Change to support various languages at the end of the year), Grpc,Thrift You need to know who to call, register and discover with the service
* Distributed data synchronization required: etcd,consul,zk Data transmission may be various languages, technologies and transmission

Suggestions on data transmission protocol selection

1. For inter company system calls, if the performance requirements are more than 100ms, XML based SOAP protocol is a scheme worthy of consideration.

2. For scenes with bad debugging environment, using JSON or XML can greatly improve debugging efficiency and reduce system development cost.

3. When there are high requirements for performance and simplicity, Protobuf, Thrift and Avro have a certain competitive relationship.

4. Protobuf and Avro are the first choice for the T-level data persistence application scenario

. If the persistent data is stored in the Hadoop subproject, Avro will be a better choice.

5. If you need to provide a complete RPC solution, Thrift is a good choice.

6. If different transport layer protocols need to be supported after serialization, or high-performance scenarios requiring cross firewall access, Protobuf can be given priority.

RPC mechanism and implementation process

1. RPC mechanism

Lightweight remote procedure calls are used between services. Generally, HTTP and RPC are used

  • HTTP calls the application layer protocol, and the structure is relatively fixed
  • The network protocol of RPC is relatively flexible and can be customized

RPC remote procedure call generally adopts C/S

Mode, client server mode, client process, the program that calls the server process. The execution result of the server process is returned to the client. The client is awakened from the blocking state, receives data and extracts data.

In the above process, the client calls the functions of the server to execute the task. It does not know whether the operation is carried out in the local operating system or through remote procedure calls. It has no sense in the whole process.

The basic communication of RPC is as follows:

image

For RPC remote procedure calls, the following four points should be considered:

  • Parameter transfer
  • Communication protocol mechanism
  • Error handling
  • timeout handler

2. Parameter transfer

  • pass by value

Generally, the default is value transfer. You only need to copy the value in the parameter to the data in the network message

  • Reference passing

It's difficult. It's meaningless to simply pass a reference to a parameter

, because the referenced address is given to the remote server, the memory address on the server is not the data that the client wants at all. If this is not the case, the client must also pass copies of the data to the remote server and put them into the memory of the remote server. After the server copies the referenced address, the data can be read.

However, the above method is troublesome and error prone. Generally, RPC does not support direct reference passing

  • Data format unification

There needs to be a standard to encode and decode all data types. Data formats can have implicit types and explicit types

  • Implicit type

Pass only the value, not the name or type of the variable

  • Explicit type

Pass the type and value of the field

Common transmission data formats are:

  • ASN.1 of ISO standard
  • JSON
  • PROTOBUF
  • XML

3. Communication protocol mechanism

In a broad sense, the protocol stack is divided into common protocol and private protocol

  • Mutual agreement

For example, HTTP, SMPP and WEBSERVICE are common protocols, which have the advantages of universal type and public network transmission capability

  • Private agreement

The agreement formed by internal agreement has many disadvantages, but it can be highly customized, improve performance, reduce cost, and improve flexibility and efficiency. Private protocol development is often used within enterprises

For the formulation of the agreement, the following five aspects need to be considered:

  • Protocol design

What issues need to be considered

image

  • Encoding and decoding of private protocols

Business specific encoding and decoding methods are required, as shown in the following examples

  • Definition of command and selection of command processor

There are generally two kinds of protocol processes

  1. Load command

Transmit service specific data, such as request parameters and commands in response to results

  1. control command

It is generally a function management command, such as heartbeat command

  • Command protocol

Serialization protocols are generally used. Different protocols have different coding efficiency and transmission efficiency, such as

image

  • Communication mode
  1. oneway -- don't care about the response, and the request thread won't be blocked
  2. sync -- the call will be blocked until the result is returned
  3. future -- the county thread will not be blocked when calling, and the thread will be blocked when obtaining the result
  4. callback -- an asynchronous call that does not block threads

Error handling and timeout handling

Remote procedure calls have a higher probability of error than local procedure calls. Therefore, various scenarios of call failure need to be considered:

  • What should I do if there is an error on the server
  • An error or timeout occurs when the client requests a service. You need to set an appropriate retry mechanism

image

4. Simple GO language native RPC

It is roughly divided into the following four steps:

  • Design data structure and method
  • Implementation method
  • Registration service
  • The client connects to the server and calls the method of the server

Let's look at the case of how golang uses native rpc

rpc invocation and service monitoring

  • RPC related content
    • Data transmission: JSON Protobuf thrift
    • Load: random algorithm polling consistency hash weighting
    • Abnormal fault tolerance: health detection fuse current limiting
  • Service monitoring
    • Log collection
    • Dot sampling

1. Introduction to RPC

  • Remote Procedure Call (RPC) is a computer communication protocol
  • The protocol allows a program running on one computer to call a subroutine of another computer without extra programming for this interaction
  • If the software involved adopts object-oriented programming, remote procedure call can also be called remote call or remote method call

2. RPC call process

In general, we will call the function code directly locally. In the microservice architecture, we need to run this function as a separate service, and the client calls it through the network

  • Under the microservice architecture, data interaction is generally internal RPC and external REST
  • Splitting the business into micro services according to functional modules has the following advantages
    • Improve project collaboration efficiency
    • Reduce module coupling
    • Improve system availability
  • It has the following disadvantages:
    • The development threshold is relatively high, such as the use of RPC framework and later service monitoring

3.rpc golang native processing method

The simplest use of golang native rpc

The official net/rpc Library of golang uses encoding/gob for encoding and decoding, and supports tcp and http data transmission

server1.go

package main
import (
   "log"
   "net/http"
   "net/rpc"
)
type Happy struct{}
// Calculate happy
func (r *Happy) CalHappy(num int, ret *int) error {
   *ret = num * 10
   return nil
}
// Main function
func main() {
   // new a service
   ha := new(Happy)
   // Register a Happy service
   rpc.Register(ha)
   // Service processing is bound to the http protocol
   rpc.HandleHTTP()
   // Listener Service 
   err := http.ListenAndServe(":9999", nil)
   if err != nil {
      log.Panicln(err)
   }
}

client1.go

package main
import (
   "fmt"
   "log"
   "net/rpc"
)
// Main function
func main() {
   //Connect to remote rpc service
   conn, err := rpc.DialHTTP("tcp", ":9999")
   if err != nil {
      log.Fatal(err)
   }
   // Call server method
   ret := 0
   err2 := conn.Call("Happy.CalHappy", 10, &ret)
   if err2 != nil {
      log.Fatal(err2)
   }
   fmt.Println("Happy index:", ret)
}

result

image

golang uses jsonrpc

Jsonrpc uses JSON for data encoding and decoding and supports cross language calls. The jsonrpc library is implemented based on tcp protocol and does not support http transmission temporarily

server2.go

package main
import (
   "fmt"
   "log"
   "net"
   "net/rpc"
   "net/rpc/jsonrpc"
)
type Happy struct{}
// Calculate happy
func (r *Happy) CalHappy(num int, ret *int) error {
   *ret = num * 10
   return nil
}
// Main function
func main() {
   // new a service
   ha := new(Happy)
   // Register a Happy service
   rpc.Register(ha)
   // Listener Service 
   listen, err := net.Listen("tcp", ":9999")
   if err != nil {
      log.Panicln(err)
   }
   // Processing requests
   for {
      con, err := listen.Accept()
      if err != nil {
         continue
      }
      // A special collaboration process is opened to handle the corresponding request
      go func(con net.Conn) {
         fmt.Println("process new client")
         jsonrpc.ServeConn(con)
      }(con)
   }
}

client2.go

package main
import (
   "fmt"
   "log"
   "net/rpc/jsonrpc"
)
// Main function
func main() {
   //Connect to remote rpc service
   conn, err := jsonrpc.Dial("tcp", ":9999")
   if err != nil {
      log.Fatal(err)
   }
   // Call server method
   ret := 0
   err2 := conn.Call("Happy.CalHappy", 10, &ret)
   if err2 != nil {
      log.Fatal(err2)
   }
   fmt.Println("Happy index:", ret)
}

golang native rpc custom protocol

For example, we customize the protocol. For a piece of data, the first two bytes are the data header, and the following is the real data, such as:

image

  • Since the protocol is customized, we need to abide by our protocol when sending and reading data, otherwise there will be problems
  • Then when we do data transmission, coding and decoding will be involved. We also need to encapsulate the functions of coding and decoding
Function encapsulation for writing data and reading data
// Write data
func MyWriteData(con net.Conn, data []byte) (int, error) {
    if con == nil {
        log.Fatal("con is nil")
    }
    buf := make([]byte, 2+len(data))
    // Write the header first, and write the length of the real data into the header
    binary.BigEndian.PutUint16(buf[:2], uint16(len(data)))
    // Re write data
    copy(buf[2:], data)
    n, err := con.Write(buf)
    if err != nil {
        log.Fatal("Write error", err)
    }
    return n, nil
}
//Read data
func MyReadData(con net.Conn) ([]byte, error) {
    if con == nil {
        log.Fatal("con is nil")
    }
    // Protocol header 2 bytes
    myheader := make([]byte, 2)
    // Read 2-byte protocol header
    _, err := io.ReadFull(con, myheader)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    //Read real data
    // Read the length of real data from the inside
    len := binary.BigEndian.Uint16(myheader)
    data := make([]byte, len)
    _, err = io.ReadFull(con, data)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    return data, nil
}
Write function encapsulation for encoding and decoding

We designed the binding method between the string command and the specific called function, so as to lay a good foundation for the implementation of server3.go rpc

// Specific data structure
type MyData struct {
    Name   string
    MyArgs []interface{} // parameter list
}
// encryption
func MyEncode(data *MyData) ([]byte, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    var bb bytes.Buffer
    buf := gob.NewEncoder(&bb)
    if err := buf.Encode(data); err != nil {
        log.Fatal("Encode error ", err)
    }
    return bb.Bytes(), nil
}
// decrypt
func MyDecode(data []byte) (MyData, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    buf := bytes.NewBuffer(data)
    myDe := gob.NewDecoder(buf)
    var res MyData
    if err := myDe.Decode(&res); err != nil {
        log.Fatal("Decode error ", err)
    }
    return res, nil
}
Integrating the above functions, the implementation of the server side_ server.go:
package main
import (
    "bytes"
    "encoding/binary"
    "encoding/gob"
    "fmt"
    "io"
    "log"
    "net"
    "reflect"
)
// Write data
func MyWriteData(con net.Conn, data []byte) (int, error) {
    if con == nil {
        log.Fatal("con is nil")
    }
    buf := make([]byte, 2+len(data))
    // Write the header first, and write the length of the real data into the header
    binary.BigEndian.PutUint16(buf[:2], uint16(len(data)))
    // Re write data
    copy(buf[2:], data)
    n, err := con.Write(buf)
    if err != nil {
        log.Fatal("Write error", err)
    }
    return n, nil
}
//Read data
func MyReadData(con net.Conn) ([]byte, error) {
    if con == nil {
        log.Fatal("con is nil")
    }
    // Protocol header 2 bytes
    myheader := make([]byte, 2)
    // Read 2-byte protocol header
    _, err := io.ReadFull(con, myheader)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    //Read real data
    // Read the length of real data from the inside
    len := binary.BigEndian.Uint16(myheader)
    data := make([]byte, len)
    _, err = io.ReadFull(con, data)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    return data, nil
}
// Specific data structure
type MyData struct {
    Name   string
    MyArgs []interface{} // parameter list
}
// encryption
func MyEncode(data *MyData) ([]byte, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    var bb bytes.Buffer
    buf := gob.NewEncoder(&bb)
    if err := buf.Encode(data); err != nil {
        log.Fatal("Encode error ", err)
    }
    return bb.Bytes(), nil
}
// decrypt
func MyDecode(data []byte) (MyData, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    buf := bytes.NewBuffer(data)
    myDe := gob.NewDecoder(buf)
    var res MyData
    if err := myDe.Decode(&res); err != nil {
        log.Fatal("Decode error ", err)
    }
    return res, nil
}
// A global map, corresponding to commands and functions
var myFun = make(map[string]reflect.Value)
// Register command binding functions
func MyRegister(name string, fn interface{}) {
    if _, ok := myFun[name]; ok { // This indicates that the command has been bound to a function
        return
    }
    myFun[name] = reflect.ValueOf(fn)
    log.Println("reflect.ValueOf(fn) == ", myFun[name])
}
// Method of server execution
func MyRun(addr string) {
    listen, err := net.Listen("tcp", addr)
    if err != nil {
        log.Fatal("Listen is nil")
    }
    log.Println("Start the server....")
    // Start blocking connections waiting for clients
    for {
        con, err := listen.Accept()
        if err != nil {
            log.Println("Accept is nil")
            return
        }
        // Read data
        b, err := MyReadData(con)
        if err != nil {
            log.Println("MyReadData error ", err)
            return
        }
        log.Println("MyReadData =============== ")
        // Parse data
        my, err := MyDecode(b)
        if err != nil {
            log.Println("MyDecode =============== ")
            log.Println("MyDecode error ", err)
            return
        }
        f, ok := myFun[my.Name]
        if !ok {
            fmt.Printf("command %s No binding function\n", my.Name)
            return
        }
        // Get parameters
        args := make([]reflect.Value, 0, len(my.MyArgs))
        for _, arg := range my.MyArgs {
            args = append(args, reflect.ValueOf(arg))
            log.Println("reflect.ValueOf(arg) - ", reflect.ValueOf(arg))
        }
        //reflex
        res := f.Call(args)
        log.Println("f.Call(args) == ", res)
        // Packaging result data to the client
        out := make([]interface{}, 0, len(res))
        for _, arg := range res {
            log.Println("arg  == ", arg)
            out = append(out, arg.Interface())
        }
        log.Println("out  == ", out)
        // Coded data
        bb, err := MyEncode(&MyData{Name: my.Name, MyArgs: out})
        if err != nil {
            log.Println("MyEncode error ", err)
            return
        }
        // Write data to client
        _, err = MyWriteData(con, bb)
        if err != nil {
            log.Println("MyWriteData ======== ")
            log.Println("MyWriteData error ", err)
            return
        }
    }
}
// The client calls the function through the command
func CallRPCFun(con net.Conn, rpcName string, args interface{}) {
    // Get the uninitialized function prototype of args through reflection
    fn := reflect.ValueOf(args).Elem()
    log.Println("fn == ", fn)
    // Another function is required to operate on the first function parameter
    f := func(args []reflect.Value) []reflect.Value {
        // Processing parameters
        inArgs := make([]interface{}, 0, len(args))
        for _, arg := range args {
            inArgs = append(inArgs, arg.Interface())
        }
        // connect
        // Coded data
        reqRPC := &MyData{Name: rpcName, MyArgs: inArgs}
        b, err := MyEncode(reqRPC)
        if err != nil {
            log.Println("MyEncode =============== ")
            log.Println("MyEncode error ", err)
        }
        // Write data
        _, err = MyWriteData(con, b)
        if err != nil {
            log.Println("MyWriteData =============== ")
            log.Fatal("MyWriteData error ", err)
        }
        // The return value sent by the server should be read and parsed at this time
        respBytes, err := MyReadData(con)
        if err != nil {
            log.Fatal("MyReadData error ", err)
        }
        // decode
        res, err := MyDecode(respBytes)
        if err != nil {
            log.Println("MyDecode =============== ")
            log.Fatal("MyDecode error ", err)
        }
        // Process the data returned by the server
        outArgs := make([]reflect.Value, 0, len(res.MyArgs))
        for i, arg := range res.MyArgs {
            // nil conversion is required
            if arg == nil {
                // reflect.Zero() returns the value of the zero value of the type
                // . out() returns the parameter type of the function output
                outArgs = append(outArgs, reflect.Zero(fn.Type().Out(i)))
                continue
            }
            outArgs = append(outArgs, reflect.ValueOf(arg))
        }
        return outArgs
    }
    v := reflect.MakeFunc(fn.Type(), f)
    // Assign value to function f
    fn.Set(v)
}
// Define user objects
type Data struct {
    CmdName string
    Param   string
}
// Method for testing user queries
func GetData(id int) (Data, error) {
    data := make(map[int]Data)
    // False data
    data[0] = Data{"PullInfo", "xiaoxiong"}
    data[1] = Data{"PutInfo", "daxiong"}
    // query
    if u, ok := data[id]; ok {
        return u, nil
    }
    return Data{}, fmt.Errorf("%d err", id)
}
// Main function
func main() {
    // Simply set the log parameter
    log.SetFlags(log.Lshortfile | log.LstdFlags)
    // rpc server
    // Register when one of the fields in the code is interface {}
    gob.Register(Data{})
    addr := "127.0.0.1:9999"
    // Create server
    // Register the server method
    MyRegister("GetData", GetData)
    // Server waiting for call
    go MyRun(addr)
    //-------------I am the dividing line-----------
    // rpc client get connection
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        fmt.Println("Dial err")
        return
    }
    log.Println("The client succeeded in dialing and started calling the function...")
    // Create client object
    // A function prototype needs to be declared
    var getdata func(int) (Data, error)
    CallRPCFun(conn, "GetData", &getdata)
    // Get query results
    u, err := getdata(1)
    if err != nil {
        fmt.Println("getdata err")
        return
    }
    log.Println(u)
    select {}
}

Posted by Tomz on Sun, 21 Nov 2021 23:25:43 -0800