【负载均衡】Consul中同步服务信息并进行负载均衡——基于「Golang」「gRPC」的负载均衡策略


前言

负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件,目标是 尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。
这里我只做微服务里的负载均衡 nginx&网关不做
在这里插入图片描述


一、什么是负载均衡,负载均衡的策略有哪些?

可以根据导图整理思路

在这里插入图片描述

0.负载均衡之前先设置一下动态端口

python篇

import socket
#动态获取端口号
def get_free_tcp_port():
    tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp.bind(("", 0))
    _, port = tcp.getsockname()
    tcp.close()
    return port

golang篇

import (
	"net"
)
//动态获取端口号
func GetFreePort() (int, error) {
	addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
	if err != nil {
		return 0, err
	}
	l, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return 0, err
	}
	defer l.Close()
	return l.Addr().(*net.TCPAddr).Port, nil
}
func main() {
	port, _ := GetFreePort()
	fmt.Println(port)
}

1.集中式load balance

集中式LB方案,如下图。首先,服务的消费方和提供方不直接耦合,而是在服务消费者和服务提供者之间有一个独立的LB(LB通常是专门的硬件设备如F5,或者基于软件如LVS,HAproxy等实现)。
在这里插入图片描述

  • LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略(比如Round-Robin)做负载均衡后将请求转发到目标服务。

  • LB一般具备健康检查能力,能自动摘除不健康的服务实例。

  • 服务消费方如何发现LB呢?通常的做法是通过DNS,运维人员为服务配置一个DNS域名,这个域名指向LB。

这种方案基本可以否决,因为它有致命的缺点:所有服务调用流量都经过load balance服务器,所以load balance服务器成了系统的单点,一旦LB发生故障对整个系统的影响是灾难性的。为了解决这个问题,必然需要对这个load balance部件做分布式处理(部署多个实例,冗余,然后解决一致性问题等全家桶解决方案),但这样做会徒增非常多的复杂度。

2.进程内load balance

进程内load balance。将load balance的功能和算法以sdk的方式实现在客户端进程内。先看架构图:
在这里插入图片描述
可看到引入了第三方:服务注册中心。它做两件事:

  • 维护服务捷供方的节点列表,并检测这些节点的健康度。检测的方式是:每个节点部署成功,都通知服务注册中心;然后一直和注册中心保持心跳。
  • 允许服务调用方注册感兴趣的事件,把服务提供方的变化情况推送到服务调用方。这种方案下,整个load balance的过程是这样的:
  • 服务注册中心维护所有节点的情况。
  • 消费方接收到消息后,在本地维护一份这个列表,并自己做load balance.

可见,服务注册中心充当什么角色?它是唯一一个知道整个集群内部所有的节点情况的中心。所以对它的可用性要求会非常高,这个组件可以用Zookeeper实现。

这种方案的缺点是:每个语言都要研究一套sdk,如果公司内的服务使用的语言五花八门的话,这方案的成本会很高。第二点是:后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,所以该方案的升级推广有不小的阻力。

3.独立进程load balance

该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似,不同之处是
他将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程,主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。如图
在这里插入图片描述
这个方案解决了上一种方案的问题,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。但新引入的问题是:这个组件本身的可用性谁来维护?还要再写一个watchdog去监控这个组件?另外,多了一个环节,就多了一个出错的可能,线上出问题了,也多了一个需要排查的环节。

二、常用负载均衡策略

在分布式系统中,多台服务器同时提供一个服务,并统一到服务配置中心进行管理,消费者通过查询服务配置中心,获取到服务到地址列表,需要选取其中一台来发起RPC远程调用。如何选择,则取决于具体的负载均衡算法,对应于不同的场景,选择的负载均衡算法也不尽相同。负载均衡算法的种类有很多种,常见的负载均衡算法包括轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接法等,应根据具体的使用场景选取对应的算法。

1.轮询(Round Robin)法

轮询很容易实现,将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

2.随机法

通过系统随机函数,根据后台服务器列表的大小值来随机选取其中一台进行访问。由概率概率统计理论可以得知,随着调用量的增大,其实际效果越来越接近于平均分配流量到后台的每一台服务器,也就是轮询法的效果。

3.源地址哈希法

源地址哈希法的思想是根据服务消费者请求客户端的IP地址,通过哈希函数计算得到一个哈希值,将此哈希值和服务器列表的大小进行取模运算,得到的结果便是要访问的服务器地址的序号。采用源地址哈希法进行负载均衡,相同的IP客户端,如果服务器列表不变,将映射到同一个后台服务器进行访问。

4.加权轮询(Weight Round Robin)法

不同的后台服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不一样。跟配置高、负载低的机器分配更高的权重,使其能处理更多的请求,而配置低、负载高的机器,则给其分配较低的权重,降低其系统负载,加权轮询很好的处理了这一问题,并将请求按照顺序且根据权重分配给后端。

5.加权随机(Weight Random)法

加权随机法跟加权轮询法类似,根据后台服务器不同的配置和负载情况,配置不同的权重。不同的是,它是按照权重来随机选取服务器的,而非顺序。

6.最小连接数法

前面我们费尽心思来实现服务消费者请求次数分配的均衡,我们知道这样做是没错的,可以为后端的多台服务器平均分配工作量,最大程度地提高服务器的利用率,但是,实际上,请求次数的均衡并不代表负载的均衡。因此我们需要介绍最小连接数法,最小连接数法比较灵活和智能,由于后台服务器的配置不尽相同,对请求的处理有快有慢,它正是根据后端服务器当前的连接情况,动态的选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后台服务器利用率,将负载合理的分流到每一台服务器。

三、grpc从consul中同步服务信息并进行负载均衡

grpc官方提供的负载均衡策略
github.com/grpc/grpc/blob/master/doc/load-balancing.md

需要grpc的基础知识
服务器(虚拟机)运行consul

以前写的Consul注册中心文章:https://blog.csdn.net/the_shy_faker/article/details/127425125

目录:

在这里插入图片描述

生成proto所需的文件

protoc -I .  --go_out=plugins=grpc:. --validate_out="lang=go:." helloworld.proto

proto模板:

syntax = "proto3";
//option go_package = ".;proto";
package proto;
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

consul的调用文件:

package consul

import (
	"fmt"

	"github.com/hashicorp/consul/api"
)

type Registry struct {
	Host string
	Port int
}

type RegistryClient interface {
	Register(address string, port int, name string, tags []string, id string) error
	DeRegister(serviceId string) error
}

func NewRegistryClient(host string, port int) RegistryClient {
	return &Registry{
		Host: host,
		Port: port,
	}
}

func (r *Registry) Register(address string, port int, name string, tags []string, id string) error {
	cfg := api.DefaultConfig()
	cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)

	client, err := api.NewClient(cfg)
	if err != nil {
		panic(err)
	}
	//生成对应的检查对象
	check := &api.AgentServiceCheck{
		GRPC:                           fmt.Sprintf("%s:%d", address, port),
		Timeout:                        "5s",
		Interval:                       "5s",
		DeregisterCriticalServiceAfter: "10s",
	}

	//生成注册对象
	registration := new(api.AgentServiceRegistration)
	registration.Name = name
	registration.ID = id
	registration.Port = port
	registration.Tags = tags
	registration.Address = address
	registration.Check = check

	err = client.Agent().ServiceRegister(registration)
	if err != nil {
		panic(err)
	}
	return nil
}

func (r *Registry) DeRegister(serviceId string) error {
	cfg := api.DefaultConfig()
	cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)

	client, err := api.NewClient(cfg)
	if err != nil {
		return err
	}
	err = client.Agent().ServiceDeregister(serviceId)
	return err
}

server端:

package main

import (
	"context"
	"fmt"
	"net"
	"os"
	"os/signal"
	"syscall"

	"github.com/satori/go.uuid"
	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"

	"oldpackagetest/grpc_test/proto"
	"oldpackagetest/grpclb_test/consul"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
	fmt.Println("已调用", request.Name)
	return &proto.HelloReply{
		Message: "hello" + request.Name,
	}, nil
}

func GetFreePort() (int, error) {
	addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
	if err != nil {
		return 0, err
	}
	l, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return 0, err
	}
	defer l.Close()
	return l.Addr().(*net.TCPAddr).Port, nil
}
func main() {
	Port, _ := GetFreePort()
	g := grpc.NewServer()
	//grpc注册方法
	proto.RegisterGreeterServer(g, &Server{})
	lis, err := net.Listen("tcp", fmt.Sprintf("10.231.72.37:%d", Port))

	//注册服务健康检查
	grpc_health_v1.RegisterHealthServer(g, health.NewServer())
	//consul服务注册
	register_client := consul.NewRegistryClient("192.168.10.130", 8500)
	serviceId := fmt.Sprintf("%s", uuid.NewV4())
	err = register_client.Register("10.231.72.37", Port, "user_srv", []string{"srv"}, serviceId)
	if err != nil {
		fmt.Printf("【srv】服务注册失败:%s\n", err.Error())
	} else {
		fmt.Println("【srv】注册成功")
	}
	//运行server
	//启动服务
	go func() {
		err = g.Serve(lis)
		if err != nil {
			panic("failed to start grpc:" + err.Error())
		}
	}()
	//接受终止信号 用于注销consul服务
	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	if err = register_client.DeRegister(serviceId); err != nil {
		fmt.Printf("【srv】注销失败:%s\n", err.Error())
	} else {
		fmt.Println("【srv】注销成功")
	}
}

client端:

package main

import (
	"context"
	"fmt"
	"log"

	_ "github.com/mbobakov/grpc-consul-resolver" // It's important
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"awesomeProject3/consul_test/proto"
)

func main() {
	conn, err := grpc.Dial(
		//consul网络必须是通的   user_srv表示服务 wait:超时 tag是consul的tag  可以不填
		"consul://192.168.10.130:8500/user_srv?wait=14s&tag=srv",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		//轮询法   必须这样写   grpc在向consul发起请求时会遵循轮询法
		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	//发起10次请求
	for i := 0; i < 10; i++ {
		//从这开始用自己的protobuf   
		userSrvClient := proto.NewUserClient(conn)
		rsp, err := userSrvClient.GetUserList(context.Background(), &proto.PageInfo{
			Pn:    1,
			PSize: 2,
		})
		if err != nil {
			panic(err)
		}
		for index, data := range rsp.Data {
			fmt.Println(index, data)
		}
	}
}

这里开启2个server服务然后运行client端,看一下2个server服务是否遵循轮询

开启两个server实例
在这里插入图片描述

然后运行client
再看控制台
srv1:
在这里插入图片描述
srv2:
在这里插入图片描述
这里srv1运行了6次是因为我以前已经测试过了,轮询法会从第2个开始,所以没有问题

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jzin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值