How golang uses native RPC and microservices

Time:2021-12-30

[TOC]

How golang uses native RPC and microservices

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

How golang uses native RPC and microservices

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

  • usedocker+k8sTo solve

2. Communication layer

  • Network transmission, using RPC (remote procedure call)

    • HTTP transport,GET POST PUT DELETE
    • TCP basedAt the bottom, RPC is based on TCP, Dubbo (changed to support various languages at the end of 18), grpc and thrift
  • You need to know who to call, register and discover with the service

    • Need distributed data synchronization: etcd, consumer, 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 requires services above 100ms, XML based SOAP ProtocolIt is a plan worth considering.

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

3、When there are high requirements for performance and simplicity, protobuf, thrift and Avro have a certain competitive relationship.

4. ForProtobuf and Avro are the first choice for T-level data persistence application scenarios。 If the persistent data is stored in the Hadoop subproject, Avro will be a better choice.

5. If requiredFor 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 that need to be accessed across firewalls,ProtobufPriority can be given.

RPC mechanism and implementation process

1. RPC mechanism

Lightweight remote procedure calls are used between servicesHTTP,RPC

  • 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 usingC/SMode, 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 function 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 call. It is insensitive in the whole process

The basic communication of RPC is as follows:

How golang uses native RPC and microservices

For RPC remote procedure calls, the following issues need to be considered:four o’clock

  • 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,Simply passing a reference to a parameter is completely meaningless, 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, and the data format can be differentImplicit typeandExplicit type

  • 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 of ISO standard one
  • JSON
  • PROTOBUF
  • XML

3. Communication protocol mechanism

In a broad sense, the protocol stack is divided intoMutual agreementandPrivate agreement

  • Mutual agreement

for exampleHTTP, SMPP and WebService are common protocols. They are universal and have advantages in the ability of public network transmission

  • Private agreement

The agreement formed by internal agreement has many disadvantages, butIt 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

How golang uses native RPC and microservices

  • 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

How golang uses native RPC and microservices

  • 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

How golang uses native RPC and microservices

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 algorithmPolling consistency hashweighting
    • Exception tolerance:Health detection fuse current limiting
  • Service monitoring

    • Log collection
    • Dot sampling

1. Introduction to RPC

  • remote procedure callRemote 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

  • Data interaction under microservice architecture is generallyInternal RPC, external rest
  • Split the business into micro services according to functional modules,It 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 is usedencoding/gobEncoding and decoding,Support 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()
   //Monitoring 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

How golang uses native RPC and microservices

Golang uses jsonrpc

JSON RPC uses JSON for data encoding and decoding, supports cross language calls,The JSON RPC library is implemented based on the TCP protocol and does not support the HTTP transmission mode

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)
   //Monitoring 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, a piece of data,The first two bytes are data headers, and the following bytes are real data,For example:

How golang uses native RPC and microservices

  • Now that the protocol has been customized, weWhen sending and reading data, we need to abide by our agreement, or something will go wrong
  • Then we will be involved in data transmissionEncoding and decoding, we also need to encapsulate the encoding and decoding functions
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 designedThe binding method between string command and specific called function, so for the nextserver3.goThe realization of RPC lays a good foundation

//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
}

//Decryption
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
}

//Decryption
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 {// 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 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 has 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))
        }

        //Reflection
        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"}

    //Inquiry
    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 dials successfully and starts 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 {}
}

result:

How golang uses native RPC and microservices

The above are all learning results. Please correct any errors

Technology is open, so should our mentality. Embrace change and move forward bravely on the road of the future. Come on, everybody!

I am Na Zha, welcome to make complaints about communication.