Golang implements a load balancing case (random, rotation training)

Time:2021-10-20

Today, we use go to implement a simple load balancing algorithm. Although it is simple, we still need to write it.

1. The first is the server information


package balance
type Instance struct {
    host string
    port int
}
func NewInstance(host string, port int) *Instance {
    return &Instance{
        host: host,
        port: port,
    }
}
func (p *Instance) GetHost() string {
    return p.host
}
func (p *Instance) GetPort() int {
    return p.port
}

2. Then define the interface

package balance
type Balance interface {
    /**
    *Load balancing algorithm
    */
    DoBalance([] *Instance,...string) (*Instance,error)
}

3. Next, implement the interface, random.go


package balance
import (
    "errors"
    "math/rand"
)
func init()  {
    RegisterBalance("random",&RandomBalance{})
}
type RandomBalance struct {
}
func (p *RandomBalance) DoBalance(insts [] *Instance,key...string) (inst *Instance, err error) {
    if len(insts) == 0 {
        err = errors.New("no instance")
        return
    }
    lens := len(insts)
    index := rand.Intn(lens)
    inst = insts[index]
    return
}

roundrobin.go


package balance
import (
    "errors"
)
func init() {
    RegisterBalance("round", &RoundRobinBalance{})
}
type RoundRobinBalance struct {
    curIndex int
}
func (p *RoundRobinBalance) DoBalance(insts [] *Instance, key ...string) (inst *Instance, err error) {
    if len(insts) == 0 {
        err = errors.New("no instance")
        return
    }
    lens := len(insts)
    if p.curIndex >= lens {
        p.curIndex = 0
    }
    inst = insts[p.curIndex]
    p.curIndex++
    return
}

4 then, it is all left to the manager for management, which is why all the above files rewrite the init function


package balance
import (
    "fmt"
)
type BalanceMgr struct {
    allBalance map[string]Balance
}
var mgr = BalanceMgr{
    allBalance: make(map[string]Balance),
}
func (p *BalanceMgr) registerBalance(name string, b Balance) {
    p.allBalance[name] = b
}
func RegisterBalance(name string, b Balance) {
    mgr.registerBalance(name, b)
}
func DoBalance(name string, insts []*Instance) (inst *Instance, err error) {
    balance, ok := mgr.allBalance[name]
    if !ok {
        err = fmt.Errorf("not fount %s", name)
        fmt.Println("not found ",name)
        return
    }
    inst, err = balance.DoBalance(insts)
    if err != nil {
        err = fmt.Errorf(" %s erros", name)
        return
    }
    return
}

Test as follows:


func main() {
    var insts []*balance.Instance
    for i := 0; i < 10; i++ {
        host := fmt.Sprintf("192.168.%d.%d", rand.Intn(255), rand.Intn(255))
        port, _ := strconv.Atoi(fmt.Sprintf("880%d", i))
        one := balance.NewInstance(host, port)
        insts = append(insts, one)
    }
    var name = "round"
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    for {
        inst, err := balance.DoBalance(name, insts)
        if err != nil {
            fmt.Println("do balance err")
            time.Sleep(time.Second)
            continue
        }
        fmt.Println(inst)
        time.Sleep(time.Second)
    }
}

5. If you want to extend this without invading the original code structure, you can compare the implementation of the dobalance interface above


package add
import (
    "awesomeProject/test/balance"
    "fmt"
    "math/rand"
    "hash/crc32"
)
func init() {
    balance.RegisterBalance("hash", &HashBalance{})
}
type HashBalance struct {
    key string
}
func (p *HashBalance) DoBalance(insts [] *balance.Instance, key ...string) (inst *balance.Instance, err error) {
    defKey := fmt.Sprintf("%d", rand.Int())
    if len(key) > 0 {
        defKey = key[0]
    }
    lens := len(insts)
    if lens == 0 {
        err = fmt.Errorf("no balance")
        return
    }
    hashVal := crc32.Checksum([]byte(defKey), crc32.MakeTable(crc32.IEEE))
    index := int(hashVal) % lens
    inst = insts[index]
    return
}

In this way, it can be managed by the manager, and the original API will not be affected.

Supplement: golang grpc cooperates with nginx to realize load balancing

summary

Grpc load balancing mainly includes three methods: in-process balance, out of process balance and proxy. This paper describes the proxy method. In the past, the in-process method was more popular. The load balancing was realized by polling and random service discovery such as etcd or consumer.

Now after nginx 1.13, grpc is officially supported. Because nginx is stable, high concurrency and powerful, it is more valuable that it is easy to deploy, and unlike in-process balance, different languages need to write different implementations, so I highly recommend this method.

Configuration of nginx

After confirming that nginx with version greater than 1.13 is installed, open the configuration file and write the following configuration

upstream lb{
#Load balanced grpc server address
  server 127.0.0.1:50052;
  server 127.0.0.1:50053;
  server 127.0.0.1:50054;
  #keepalive 500;# This is the total number of long connections between nginx and the RPC server cluster. Setting this can improve efficiency and avoid the time caused by the concurrency of short connections between nginx and the RPC server by default_ Too many wait
}
server {
  listen       9527     http2;
  access_log  /var/log/nginx/host.access.log  main;
  http2_ max_ requests 10000;# The default value here is 1000. Errors will be reported in the concurrency volume, so set it larger
  #grpc_ socket_ keepalive on;# This thing will be supported after nginx1.5
  location / {
    grpc_pass grpc://lb;
    error_page 502 = /error502grpc;
  }
  location = /error502grpc {
    internal;
    default_type application/grpc;
    add_header grpc-status 14;
    add_header grpc-message "Unavailable";
    return 204;
  }
}

You can see the data forwarding record in the host.access.log log file

Proto file:

syntax = "proto3"; //  Specify proto version
package grpctest;     //  Specify package name
//Define Hello service
service Hello {
    //Define sayhello method
    rpc SayHello(HelloRequest) returns (HelloReply) {}
}
//Hellorequest request structure
message HelloRequest {
    string name = 1;
}
//Hellopreply response structure
message HelloReply {
    string message = 1;
}

client:

For the client connection address, fill in the listening address of nginx, and the relevant codes are as follows:

package main
import (
 Pb "protobuf / grpctest" // import the proto package
 "golang.org/x/net/context"
 "google.golang.org/grpc"
 "google.golang.org/grpc/grpclog"
 "fmt"
 "time"
)
const (
 //Address grpc service address
 Address = "127.0.0.1:9527"
)
func main() {
 //Connect
 conn, err := grpc.Dial(Address, grpc.WithInsecure())
 if err != nil {
  grpclog.Fatalln(err)
 }
 defer conn.Close()
 //Initialize client
 c := pb.NewHelloClient(conn)
 reqBody := new(pb.HelloRequest)
 reqBody.Name = "gRPC"
 //Call method
 for{
  r, err := c.SayHello(context.Background(), reqBody)
  if err != nil {
   grpclog.Fatalln(err)
  }
  fmt.Println(r.Message)
  time.Sleep(time.Second)
 }
}

Server:

package main
import (
 "net"
 "fmt"
 Pb "protobuf / grpctest" // import the compiled package
 "golang.org/x/net/context"
 "google.golang.org/grpc"
 "google.golang.org/grpc/grpclog"
)
const (
 //Address grpc service address
 Address = "127.0.0.1:50052"
 //Address = "127.0.0.1:50053"
 //Address = "127.0.0.1:50054"
)
var HelloService = helloService{}
type helloService struct{}
func (this helloService) SayHello(ctx context.Context,in *pb.HelloRequest)(*pb.HelloReply,error){
 resp := new(pb.HelloReply)
 resp.Message = Address+" hello"+in.Name+"."
 return resp,nil
}
func main(){
 listen,err:=net.Listen("tcp",Address)
 if err != nil{
  grpclog.Fatalf("failed to listen: %v", err)
 }
 s:=grpc.NewServer()
 pb.RegisterHelloServer(s,HelloService)
 grpclog.Println("Listen on " + Address)
 s.Serve(listen)
}

test

Start three server processes with three ports of 500525005350054 and run the client code to see the following effects:

在这里插入图片描述

Load balancing is perfectly realized. When you open the log file, you can see that the address of the post is / grpctest.hello/sayhello. Nginx is configured to forward all requests according to the default localization / routing rules. Therefore, nginx can also achieve more flexible forwarding and micro service registration, which is very convenient.

The above is my personal experience. I hope I can give you a reference, and I hope you can support developpaer. If you have any mistakes or don’t consider completely, please don’t hesitate to comment.