13-负载均衡
一 什么是负载均衡
负载均衡(Load Balance)意思就是分摊到多个操作单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。
单从字面上的意思来理解就可以解释N台服务器平均分担负载,不会因为某台服务器负载高宕机而某台服务器闲置的情况。那么负载均衡的前提就是要有多台服务器才能实现,也就是两台以上即可。
负载均衡建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性
1.1 负载均衡软件
负载均衡软件有Nginx、LVS、HaProxy等是目前使用最广泛的三种负载均衡软件
1.2 七层和四层负载
四层负载均衡工作在OSI模型的传输层,主要工作是转发,它在接收到客户端的流量以后通过修改数据包的地址信息将流量转发到应用服务器。
七层负载均衡工作在OSI模型的应用层,因为它需要解析应用层流量,所以七层负载均衡在接到客户端的流量以后,还需要一个完整的TCP/IP协议栈。七层负载均衡会与客户端建立一条完整的连接并将应用层的请求流量解析出来,再按照调度算法选择一个应用服务器,并与应用服务器建立另外一条连接将请求发送过去,因此七层负载均衡的主要工作就是代理。 七层负载均衡 也称为“内容交换”,也就是主要通过报文中的真正有意义的应用层内容,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。
七层负载均衡的优点:这种方式可以对客户端的请求和服务器的响应进行任意意义上的修改,极大的提升了应用系统在网络层的灵活性;安全性高。
**七层负载均衡:主要是着重于应用广泛的HTTP协议,所以其应用范围主要是众多的网站或者内部信息平台等基于B/S开发的系统 **
四层负载均衡:对应其他TCP应用,例如基于C/S开发的ERP等系统
二 负载均衡策略
轮循
Round Robin: 这种方法会将收到的请求循环分配到服务器集群中的每台机器,即有效服务器。如果使用这种方式,所有的标记进入虚拟服务的服务器应该有相近的资源容量 以及负载形同的应用程序。如果所有的服务器有相同或者相近的性能那么选择这种方式会使服务器负载形同。基于这个前提,轮循调度是一个简单而有效的分配请求 的方式。然而对于服务器不同的情况,选择这种方式就意味着能力比较弱的服务器也会在下一轮循环中接受轮循,即使这个服务器已经不能再处理当前这个请求了。 这可能导致能力较弱的服务器超载。
加权轮循
Weighted Round Robin: 这种算法解决了简单轮循调度算法的缺点:传入的请求按顺序被分配到集群中服务器,但是会考虑提前为每台服务器分配的权重。管理员只是简单的通过服务 器的处理能力来定义各台服务器的权重。例如,能力最强的服务器 A 给的权重是 100,同时能力最低的服务器给的权重是 50。这意味着在服务器 B 接收到第一个 请求之前前,服务器 A 会连续的接受到 2 个请求,以此类推。
最少连接数
Least Connection: 以上两种方法都没有考虑的是系统不能识别在给定的时间里保持了多少连接。因此可能发生,服务器 B 服务器收到的连接比服务器 A 少但是它已经超载,因为 服务器 B 上的用户打开连接持续的时间更长。这就是说连接数即服务器的负载是累加的。这种潜在的问题可以通过 “最少连接数” 算法来避免:传入的请求是根据每 台服务器当前所打开的连接数来分配的。即活跃连接数最少的服务器会自动接收下一个传入的请求。接本上和简单轮询的原则相同:所有拥有虚拟服务的服务器资源 容量应该相近。值得注意的是,在流量率低的配置环境中,各服务器的流量并不是相同的,会优先考虑第一台服务器。这是因为,如果所有的服务器是相同的,那么 第一个服务器优先,直到第一台服务器有连续的活跃流量,否则总是会优先选择第一台服务器。
最少连接数慢启动时间
Least Connection Slow Start Time: 对最少连接数和带权重的最小连接数调度方法来说,当一个服务器刚加入线上环境是,可以为其配置一个时间段,在这段时间内连接数是有限制的而且是缓慢 增加的。这为服务器提供了一个‘过渡时间’以保证这个服务器不会因为刚启动后因为分配的连接数过多而超载。这个值在 L7 配置界面设置。
加权最少连接
Weighted Least Connection: 如果服务器的资源容量各不相同,那么 “加权最少连接” 方法更合适:由管理员根据服务器情况定制的权重所决定的活跃连接数一般提供了一种对服务器非常 平衡的利用,因为他它借鉴了最少连接和权重两者的优势。通常,这是一个非常公平的分配方式,因为它使用了连接数和服务器权重比例;集群中比例最低的服务器 自动接收下一个请求。但是请注意,在低流量情况中使用这种方法时,请参考 “最小连接数” 方法中的注意事项。
基于代理的自适应负载均衡
Agent Based Adaptive Balancing: 除了上述方法之外,负载主机包含一个自适用逻辑用来定时监测服务器状态和该服务器的权重。对于非常强大的 “基于代理的自适应负载均衡” 方法来说,负 载主机以这种方式来定时检测所有服务器负载情况:每台服务器都必须提供一个包含文件,这个文件包含一个 0~99 的数字用来标明改服务器的实际负载情况 (0 = 空前,99 = 超载,101 = 失败,102 = 管理员禁用),而服务器同构 http get 方法来获取这个文件;同时对集群中服务器来说,以二进制文件形式提供自身负载情况也是该服务器工作之一,然而,并没有限制服务器如何计算自身的负载 情况。根据服务器整体负载情况,有两种策略可以选择:在常规的操作中,调度算法通过收集的服务器负载值和分配给该服务器的连接数的比例计算出一个权重比 例。因此,如果一个服务器负载过大,权重会通过系统透明的作重新调整。和加权轮循调度方法一样,不正确的分配可以被记录下来使得可以有效的为不同服务器分 配不同的权重。然而,在流量非常低的环境下,服务器报上来的负载值将不能建立一个有代表性的样本;那么基于这些值来分配负载的话将导致失控以及指令震荡。 因此,在这种情况下更合理的做法是基于静态的权重比来计算负载分配。当所有服务器的负载低于管理员定义的下限时,负载主机就会自动切换为加权轮循方式来分 配请求;如果负载大于管理员定义的下限,那么负载主机又会切换回自适应方式。
固定权重
Fixed Weighted: 最高权重只有在其他服务器的权重值都很低时才使用。然而,如果最高权重的服务器下降,则下一个最高优先级的服务器将为客户端服务。这种方式中每个真实服务器的权重需要基于服务器优先级来配置。
加权响应
Weighted Response: 流量的调度是通过加权轮循方式。加权轮循中所使用的权重是根据服务器有效性检测的响应时间来计算。每个有效性检测都会被计时,用来标记它响应成功花 了多长时间。但是需要注意的是,这种方式假定服务器心跳检测是基于机器的快慢,但是这种假设也许不总是能够成立。所有服务器在虚拟服务上的响应时间的总和 加在一起,通过这个值来计算单个服务物理服务器的权重;这个权重值大约每 15 秒计算一次。
源 IP 哈希
Source IP Hash: 这种方式通过生成请求源 IP 的哈希值,并通过这个哈希值来找到正确的真实服务器。这意味着对于同一主机来说他对应的服务器总是相同。使用这种方式,你不需要保存任何源 IP。但是需要注意,这种方式可能导致服务器负载不平衡
三 go-rpc负载均衡演示
https://github.com/grpc/grpc/blob/master/doc/load-balancing.md
// 下载:
go get github.com/mbobakov/grpc-consul-resolver
3.1 proto
syntax = "proto3";
option go_package = ".;proto";
service Greeter{
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// 类似于go的结构体,可以定义属性
message HelloRequest {
string name = 1; // 1 是编号,不是值
int32 age = 2;
}
// 定义一个响应的类型
message HelloResponse {
string reply =1;
}
3.2 生成go文件
protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto
3.3 grpc服务端
package main
import (
"context"
"fmt"
consulapi "github.com/hashicorp/consul/api"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"grpc_proto_demo/grpc_gin_load_balance/grpc_srv/proto"
"net"
)
type GreeterServer struct {
}
func (h GreeterServer) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
// 接收客户端发送过来的数据,打印出来
fmt.Println("客户端传入的名字是:", in.Name)
fmt.Println("客户端传入的年龄是:", in.Age)
return &proto.HelloResponse{
Reply: "gin-调用grpc,grpc给的回复",
}, nil
}
// 服务端代码
func main() {
// 第一步:new一个server
g := grpc.NewServer()
// 第二步:生成一个结构体对象
s := GreeterServer{}
// 第三步: 把s注册到g对象中
proto.RegisterGreeterServer(g, &s)
// 第四步:启动服务,监听端口
lis, error := net.Listen("tcp", "192.168.31.226:50052")
if error != nil {
panic("启动服务异常")
}
//******** 注册grpc服务和设置健康检查********
// 1 设置健康检查
//health.NewServer()具体实现grpc已经帮我们写好了
grpc_health_v1.RegisterHealthServer(g,health.NewServer())
// 2 注册grpc服务
RegisterConsul("192.168.31.226",50052,"grpc_test","grpc_test001",[]string{"grpc","lqz"})
g.Serve(lis)
}
func RegisterConsul(localIP string, localPort int, name string,id string, tags []string) error {
// 创建连接consul服务配置
config := consulapi.DefaultConfig()
config.Address = "10.0.0.102:8500"
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Println("consul client error : ", err)
}
// 创建注册到consul的服务到
registration := new(consulapi.AgentServiceRegistration)
registration.ID = id
registration.Name = name //根据这个名称来找这个服务
registration.Port = localPort
//registration.Tags = []string{"lqz", "gin_web"} //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
registration.Tags = tags //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
registration.Address = localIP
// 增加consul健康检查回调函数
check := new(consulapi.AgentServiceCheck)
check.GRPC = "192.168.31.226:50052" // 健康检查地址只需要写grpc服务地址端口即可,会自动检查
check.Timeout = "5s" //超时
check.Interval = "5s" //健康检查频率
check.DeregisterCriticalServiceAfter = "30s" // 故障检查失败30s后 consul自动将注册服务删除
registration.Check = check
// 注册服务到consul
err = client.Agent().ServiceRegister(registration)
if err != nil {
return err
}
return nil
}
3.4 grpc 客户端
package main
import (
"context"
"fmt"
_ "github.com/mbobakov/grpc-consul-resolver" // It's important
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"grpc_proto_demo/grpc_gin_load_balance/grpc_srv/proto"
"log"
)
func main() {
conn, err := grpc.Dial(
"consul://10.0.0.102:8500/grpc_test?wait=14s&tag=lqz",
//grpc.WithInsecure(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := proto.NewGreeterClient(conn)
// 测试默认值
resp,err:=client.SayHello(context.Background(),&proto.HelloRequest{
Name: "lqz",
Age: 19,
})
if err!=nil {
panic(err)
}
fmt.Println(resp.Reply)
}
四 gin服务调用rpc服务-负载均衡
4.1 同时启动多个rpc服务
由于rpc服务启动时,地址和端口写死了,我们需要用一种方式实现,端口动态获取,才可以启动多次rpc服务
主要使用 net 包中的的以下两个方法:
-
ResolveTCPAddr
// func ResolveTCPAddr(net, addr string) (*TCPAddr, error)
ResolveTCPAddr 能将 addr 作为TCP 地址解析并返回
参数addr格式为 host:port 或 [ipv6-host%zone]:port
解析得到网络名和端口名
net 可选的值必须是 tcp、tcp4、tcp6其中一个
-
ListenTCP
//func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)
ListenTCP在本地TCP地址laddr上声明并返回一个 *TCPListener,
net 可选的值必须是 tcp、tcp4、tcp6其中一个
如果laddr的端口字段为0,函数将选择一个当前可用的端口
可以用Listener的Addr方法获得该端口。
package utils
import "net"
func GetCanUsePort()(int,error) {
// 解析地址
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, nil
}
// 利用 ListenTCP 方法的如下特性
// 如果 addr 的端口字段为0,函数将选择一个当前可用的端口
listen, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, nil
}
// 关闭资源
defer listen.Close()
// 为了拿到具体的端口值,我们转换成 *net.TCPAddr类型获取其Port
return listen.Addr().(*net.TCPAddr).Port, nil
}
4.2 多个rpc服务注册到consul
注册到consul,只要名字一样,id可用随意,会注册成同一个服务,所以我们使用uuid为服务生成id
package utils
import (
"fmt"
uuid "github.com/satori/go.uuid"
)
func GetUUId() string {
// 创建
u1 := uuid.NewV4()
//fmt.Println(u1.String())
return u1.String()
}
func ParserUUID(u string) (*uuid.UUID, error) {
// 解析
//u2, err := uuid.FromString("f5394eef-e576-4709-9e4b-a7c231bd34a4")
u2, err := uuid.FromString(u)
if err != nil {
fmt.Printf("Something gone wrong: %s", err)
return nil, err
}
return &u2, nil
}
4.3 gin调用rpc负载均衡
服务端代码
package main
import (
"context"
"fmt"
consulapi "github.com/hashicorp/consul/api"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"grpc_proto_demo/grpc_gin_load_balance/grpc_srv/proto"
"grpc_proto_demo/grpc_gin_load_balance/grpc_srv/server/utils"
"net"
)
type GreeterServer struct {
}
func (h GreeterServer) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
// 接收客户端发送过来的数据,打印出来
fmt.Println("客户端传入的名字是:", in.Name)
fmt.Println("客户端传入的年龄是:", in.Age)
return &proto.HelloResponse{
Reply: "gin-调用grpc,grpc给的回复",
}, nil
}
// 服务端代码
func main() {
// 定义段端口
port,_:=utils.GetCanUsePort()
// 第一步:new一个server
g := grpc.NewServer()
// 第二步:生成一个结构体对象
s := GreeterServer{}
// 第三步: 把s注册到g对象中
proto.RegisterGreeterServer(g, &s)
// 第四步:启动服务,监听端口
lis, error := net.Listen("tcp", fmt.Sprintf("192.168.31.226:%d",port))
if error != nil {
panic("启动服务异常")
}
//******** 注册grpc服务和设置健康检查********
// 1 设置健康检查
//health.NewServer()具体实现grpc已经帮我们写好了
grpc_health_v1.RegisterHealthServer(g,health.NewServer())
// 2 注册grpc服务
grpcId:=utils.GetUUId()
RegisterConsul("192.168.31.226",port,"grpc_test",grpcId,[]string{"grpc","lqz"})
g.Serve(lis)
}
func RegisterConsul(localIP string, localPort int, name string,id string, tags []string) error {
// 创建连接consul服务配置
config := consulapi.DefaultConfig()
config.Address = "10.0.0.102:8500"
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Println("consul client error : ", err)
}
// 创建注册到consul的服务到
registration := new(consulapi.AgentServiceRegistration)
registration.ID = id
registration.Name = name //根据这个名称来找这个服务
registration.Port = localPort
//registration.Tags = []string{"lqz", "gin_web"} //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
registration.Tags = tags //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
registration.Address = localIP
// 增加consul健康检查回调函数
check := new(consulapi.AgentServiceCheck)
check.GRPC = fmt.Sprintf("192.168.31.226:%d",localPort)// 健康检查地址只需要写grpc服务地址端口即可,会自动检查
check.Timeout = "5s" //超时
check.Interval = "5s" //健康检查频率
check.DeregisterCriticalServiceAfter = "30s" // 故障检查失败30s后 consul自动将注册服务删除
registration.Check = check
// 注册服务到consul
err = client.Agent().ServiceRegister(registration)
if err != nil {
return err
}
return nil
}
utils.go
package utils
import (
"fmt"
uuid "github.com/satori/go.uuid"
"net"
)
func GetCanUsePort() (int, error) {
// 解析地址
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, nil
}
// 利用 ListenTCP 方法的如下特性
// 如果 addr 的端口字段为0,函数将选择一个当前可用的端口
listen, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, nil
}
// 关闭资源
defer listen.Close()
// 为了拿到具体的端口值,我们转换成 *net.TCPAddr类型获取其Port
return listen.Addr().(*net.TCPAddr).Port, nil
}
func GetUUId() string {
// 创建
u1 := uuid.NewV4()
//fmt.Println(u1.String())
return u1.String()
}
func ParserUUID(u string) (*uuid.UUID, error) {
// 解析
//u2, err := uuid.FromString("f5394eef-e576-4709-9e4b-a7c231bd34a4")
u2, err := uuid.FromString(u)
if err != nil {
fmt.Printf("Something gone wrong: %s", err)
return nil, err
}
return &u2, nil
}
gin代码
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
consulapi "github.com/hashicorp/consul/api"
_ "github.com/mbobakov/grpc-consul-resolver" // It's important
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"grpc_proto_demo/grpc_gin_load_balance/grpc_srv/proto"
)
// 可能有多个grpc服务,我们只返回一个
func getFirstGrpcRegister()(host string,port int,err error) {
// 创建连接consul服务配置
config := consulapi.DefaultConfig()
config.Address = "10.0.0.102:8500"
client, err := consulapi.NewClient(config)
if err != nil {
fmt.Println("consul client error : ", err)
}
//res, err := client.Agent().Services()
res, err := client.Agent().ServicesWithFilter(`Service=="grpc_test"`)
if err != nil {
return "", 0,err
}
fmt.Println(res)
for _,value:=range res{
host=value.Address
port=value.Port
}
return // 命名返回值
}
func main() {
r:=gin.Default()
r.GET("/index", func(c *gin.Context) {
// 第一步:连接服务端
conn, err := grpc.Dial(
"consul://10.0.0.102:8500/grpc_test?wait=14s&tag=lqz",
//grpc.WithInsecure(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
)
if err != nil {
fmt.Println(err)
c.JSON(200,"连接grpc服务异常")
}
//defer 关闭
defer conn.Close()
// 第二步:创建客户端调用
client := proto.NewGreeterClient(conn)
// 测试默认值
resp,err:=client.SayHello(context.Background(),&proto.HelloRequest{
Name: "lqz",
Age: 19,
})
if err != nil {
fmt.Println(err)
c.JSON(200,"服务器错误")
}
c.JSON(200,resp.Reply)
})
r.Run()
}