文章目录
一、什么是负载均衡
- 负载均衡(Load Balance) 主要作用是把请求分摊到多个服务进行处理,从字面意思我们可以知道,负载均衡是需要多台服务器的,两台或两台以上服务才可以完成我们负载均衡的需要。
- 建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。负载均衡有两方面的含义:首先,大量的并发访问或数据流量分担到多台节点设备上分别处理,减少用户等待响应的时间;其次,单个重负载的运算分担到多台节点设备上做并行处理,每个节点设备处理结束后,将结果汇总,返回给用户,系统处理能力得到大幅度提高。
1.1 负载均衡主要软件
- 负载均衡的软件主要有:LV、Nginx、HAProxy
- 三大主流软件负载均衡器适用业务场景:
1、网站建设初期,可以选用Nigix/HAproxy作为反向代理负载均衡(或者流量不大都可以不选用负载均衡),因为其配置简单,性能也能满足一般的业务场景。如果考虑到负载均衡器是有单点问题,可以采用Nginx+Keepalived/HAproxy+Keepalived避免负载均衡器自身的单点问题。
2、网站并发达到一定程度之后,为了提高稳定性和转发效率,可以使用LVS、毕竟LVS比Nginx/HAproxy要更稳定,转发效率也更高。不过维护LVS对维护人员的要求也会更高,投入成本也更大。
注:Niginx与Haproxy比较:Niginx支持七层、用户量最大,稳定性比较可靠。Haproxy支持四层和七层,支持更多的负载均衡算法,支持session保存等。具体选型看使用场景,目前来说Haproxy由于弥补了一些Niginx的缺点用户量也不断在提升。
1.2 七层负载和四层负载
- 四层负载: 即从第四层"传输层"开始, 使用"ip+port"接收请求,再转发到对应的机器。
- 四层的负载均衡就是基于IP+端口的负载均衡:在三层负载均衡的基础上,通过发布三层的IP地址(VIP),然后加四层的端口号,来决定哪些流量需要做负载均衡,对需要处理的流量进行NAT处理,转发至后台服务器,并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理。
- 对应的负载均衡器称为四层交换机(L4 switch),主要分析IP层及TCP/UDP层,实现四层负载均衡。此种负载均衡器不理解应用协议(如HTTP/FTP/MySQL等等)。
- 实现四层负载均衡的软件有:
F5:硬件负载均衡器,功能很好,但是成本很高。
lvs:重量级的四层负载软件
nginx:轻量级的四层负载软件,带缓存功能,正则表达式较灵活
haproxy:模拟四层转发,较灵活
- 七层负载: 从第七层"应用层"开始, 根据虚拟的url或IP,主机名接收请求,再转向相应的处理服务器。
-
七层的负载均衡就是基于虚拟的URL或主机IP的负载均衡:在四层负载均衡的基础上(没有四层是绝对不可能有七层的),再考虑应用层的特征,比如同一个Web服务器的负载均衡,除了根据VIP加80端口辨别是否需要处理的流量,还可根据七层的URL、浏览器类别、语言来决定是否要进行负载均衡。举个例子,如果你的Web服务器分成两组,一组是中文语言的,一组是英文语言的,那么七层负载均衡就可以当用户来访问你的域名时,自动辨别用户语言,然后选择对应的语言服务器组进行负载均衡处理。
-
对应的负载均衡器称为七层交换机(L7 switch),除了支持四层负载均衡以外,还有分析应用层的信息,如HTTP协议URI或Cookie信息,实现七层负载均衡。此种负载均衡器能理解应用协议。
-
实现七层负载均衡的软件有:
haproxy:天生负载均衡技能,全面支持七层代理,会话保持,标记,路径转移;
nginx:只在http协议和mail协议上功能比较好,性能与haproxy差不多;
apache:功能较差
Mysql proxy:功能尚可。
-
二、负载均衡实现方式
2.1 集中式负载均衡
- LB上存储有所有服务的地址映射表,通常通过运维配置注册,当服务消费方调用某个目标服务时,他向LB发起请求,由LB 做负载均衡后将请求转发到目标服务。
- LB一般具备健康检查能力,能自动摘除不健康的服务。
- 服务消费方如何发现LB,通常做法是通过DNS,运维人员为服务配置一个DNS域名,这个域名指向LB。
- 这种方法存在致命缺点,所有服务调用流量都经过load balance 服务器,所以load balance服务器成了系统的单点,一旦LB服务器发生故障对整个系统来说是灾难性的,为了解决这个问题,必须要对load balance服务进行分布式处理。
2.2 进程内负载均衡
- 这里引入了第三方服务,第三方服务提供节点列表,并检测这些节点是否健康,检测的方式是,每个节点部署成功,都通知服务注册中心,一直和服务注册中心保持心跳。
- 服务注册中心维护所有节点。
- 任何一个节点想要订阅其他服务提供方的节点列表,需要向服务注册中心注册。
- 服务注册中心将服务提供方的列表推送到消费方。
- 消费方接收到消息后再本地维护一个列表,并且自己做负载均衡算法。
注:在这里服务注册中心的角色尤为重要,他是唯一一个知道集群内部所有节点的运行情况,所以对他的性能要求会很高,这个组件可以使用zookeeper实现。
这种方案的缺点也很明确,每个语言都要研究一套自己的sdk,如果公司内部使用的语言很多,那么他的成本会很高。后续如果对客户库进行升级,势必要求服务调用方修改代码并重新发布,所以该方案的升级推广会有很大的阻力
2.3 独立进程负载均衡
- 该方案是针对第二种方案的不足提出的一种折中的方案,原理和第二种方案类似,不同之处在于他将LB和服务发现功能从进程中移出来了,变成一个独立的进程,主机上的一个活多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。
- 这个方案解决了上一种方案的问题,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。
注:但引入了新的问题:这个组件本身是需要维护的,还需要写一个watch dog 来监控这个组件,另外多了一个环节就多了一个出错的环节,多了一个排查的环节
三、负载均衡常见算法
- 轮询法(Round Robin): 询策略按照顺序将每个新的请求分发给后端服务器,依次循环。这是一种最简单的负载均衡策略,适用于后端服务器的性能相近,且每个请求的处理时间大致相同的情况。
- 加权轮询法(Weight Round Robin): 加权轮询策略给每个后端服务器分配一个权重值,然后按照权重值比例来分发请求。这可以用来处理后端服务器性能不均衡的情况,将更多的请求分发给性能更高的服务器。
- 随机法(Random): 随机选择策略随机选择一个后端服务器来处理每个新的请求。这种策略适用于后端服务器性能相似,且每个请求的处理时间相近的情况,但不保证请求的分发是均匀的。
- 加权随机法(Weight Random): 加权随机选择策略与加权轮询类似,但是按照权重值来随机选择后端服务器。这也可以用来处理后端服务器性能不均衡的情况,但是分发更随机。
- 最少链接数(Least Connections): 最少连接策略将请求分发给当前连接数最少的后端服务器。这可以确保负载均衡在后端服务器的连接负载上均衡,但需要维护连接计数。
- 源地址哈希法(IP Hash): IP 哈希策略使用客户端的 IP 地址来计算哈希值,然后将请求发送到与哈希值对应的后端服务器。这种策略可用于确保来自同一客户端的请求都被发送到同一台后端服务器,适用于需要会话保持的情况。
- 最短响应时间(Least Response Time): 最短响应时间策略会测量每个后端服务器的响应时间,并将请求发送到响应时间最短的服务器。这种策略可以确保客户端获得最快的响应,适用于要求低延迟的应用。
四、gRPC使用负载均衡
4.1 安装环境
# 1. 安装docker
#安装docker
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
# 设置开机自启
systemctl enable docker
# 启动docer
systemctl start docker
# 2. 安装consul
docker pull consul
# 启动
docker run -d --name myconsul -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 9302:8302 -p 8600:8600/udp consul consul agent -dev -client=0.0.0.0
# 访问
:8500 #http端口
:8600 #dns端口
4.2 目录结构
4.3 创建proto文件
syntax = "proto3";
option go_package = ".;proto";
service User {
rpc SayHello (Request) returns (Reply) {}
}
message Request {
string name = 1;
}
message Reply {
string message = 1;
}
4.4 生成go文件
# 进入proto文件目录
cd /proto
protoc -I . *.proto --go_out=plugins=grpc:.
4.5 grpc服务端代码
package main
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"syscall"
"gitee.com/deardai/shop/test/consul_test/proto"
"github.com/hashicorp/consul/api"
uuid "github.com/satori/go.uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
type UserServer struct {
}
func (u *UserServer) SayHello(ctx context.Context, req *proto.Request) (*proto.Reply, error) {
//可以根据不同的port 返回不同的数据
return &proto.Reply{Message: fmt.Sprintf("Port:%d, Hello, %s!",serverConfig.Port, req.Name)}, nil
}
//consul配置信息
type ConsulConfig struct {
Host string //consul ip地址
Port int //conusl 端口号 8500
}
type ServerConfig struct {
Host string //服务地址
Port int //服务端口号
Name string //服务名称
Id string //服务ID 此处用作服务注销 优雅退出
}
var serverConfig = ServerConfig{
Host: "192.168.12.212",
Port: 50051, //两次执行修改端口号 50051 50052 可以负载均衡
Name: "user",
Id: "",
}
func main() {
serverConfig.Id = uuid.NewV4().String()
// 创建一个gRPC服务器实例
grpcServer := grpc.NewServer()
// 创建一个UserServer实例
userServer := &UserServer{}
// 注册UserServer服务
proto.RegisterUserServer(grpcServer, userServer)
// 监听端口
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port))
if err != nil {
panic("监听端口失败")
}
//设置consul 健康检查'
grpc_health_v1.RegisterHealthServer(grpcServer, health.NewServer())
//注册到consul
err = RegisterConsul(serverConfig)
if err != nil {
fmt.Printf("注册到consul失败:%v\n", err)
return
}
//启动gRPC服务器
fmt.Printf("gRPC服务启动成功,监听地址:%s:%d\n", serverConfig.Host, serverConfig.Port)
//优雅退出
var exitCh = make(chan os.Signal, 1)
signal.Notify(exitCh, os.Interrupt, syscall.SIGTERM)
go func() {
exit := <-exitCh
fmt.Printf("接收到退出信号:%v,准备退出...\n", exit)
err = UnregisterConsul(serverConfig)
if err != nil {
fmt.Printf("注销consul失败:%v\n", err)
}
fmt.Printf("Received signal: %v\n", exit)
os.Exit(0)
}()
if err := grpcServer.Serve(lis); err != nil {
panic("服务启动失败")
}
}
func RegisterConsul(item ServerConfig) error {
config := api.DefaultConfig()
consulConfig := ConsulConfig{
Host: "192.168.213.128",
Port: 8500,
}
config.Address = fmt.Sprintf("%s:%d", consulConfig.Host, consulConfig.Port)
client, err := api.NewClient(config)
if err != nil {
return err
}
check := &api.AgentServiceCheck{
GRPC: fmt.Sprintf("%s:%d", item.Host, item.Port),
Interval: "5s",
Timeout: "5s",
DeregisterCriticalServiceAfter: "30s",
}
registration := &api.AgentServiceRegistration{
ID: item.Id,
Name: item.Name,
Address: item.Host,
Port: item.Port,
Check: check,
}
err = client.Agent().ServiceRegister(registration)
if err != nil {
return err
}
return nil
}
func UnregisterConsul(item ServerConfig) error {
config := api.DefaultConfig()
config.Address = fmt.Sprintf("%s:%d", "192.168.213.128", 8500)
client, err := api.NewClient(config)
if err != nil {
return err
}
err = client.Agent().ServiceDeregister(item.Id)
if err != nil {
return err
}
return nil
}
运行命令
go run server/main.go
成功
4.6 grpc 客户端代码
package main
import (
"context"
"fmt"
_ "github.com/mbobakov/grpc-consul-resolver" //注意这个如果不导入会报错
"gitee.com/deardai/shop/test/consul_test/proto"
"google.golang.org/grpc"
)
type ConsulConfig struct {
Host string
Port int
}
type ServerConfig struct {
Host string
Port int
Name string
Id string
}
func main() {
consulConfig := ConsulConfig{
Host: "192.168.213.128",
Port: 8500,
}
serverConfig := ServerConfig{
Host: "192.168.12.212",
Port: 50051,
Name: "user",
Id: "",
}
conn, err := grpc.Dial(
fmt.Sprintf("consul://%s:%d/%s?wait=14s", consulConfig.Host, consulConfig.Port, serverConfig.Name),
grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
)
if err != nil {
fmt.Printf("暂无可用服务: %v", err)
return
}
defer conn.Close()
client := proto.NewUserClient(conn)
resp, err := client.SayHello(context.Background(), &proto.Request{Name: "deardai"})
if err != nil {
fmt.Printf("调用服务失败: %v", err)
return
}
fmt.Println(resp.Message)
}
运行
go run client/main.go
结果
4.7 尝试负载均衡
- 修改服务端代码
//修改代码 启动两次服务端
serverConfig := ServerConfig{
Host: "192.168.12.212",
Port: 50051, //两次执行修改端口号 50051 50052 可以负载均衡
Name: "user",
Id: "",
}
- 服务端启动
# 50051
go run server/main.go
# 50052
go run server/main.go
运行结果
consul查看两个服务
3. 启动客户端
启动客户端进行负载均衡,这里我们启动10个客户端作为并发请求来测试
package main
import (
"context"
"fmt"
_ "github.com/mbobakov/grpc-consul-resolver" //注意这个如果不导入会报错
"gitee.com/deardai/shop/test/consul_test/proto"
"google.golang.org/grpc"
)
type ConsulConfig struct {
Host string
Port int
}
type ServerConfig struct {
Host string
Port int
Name string
Id string
}
func main() {
consulConfig := ConsulConfig{
Host: "192.168.213.128",
Port: 8500,
}
serverConfig := ServerConfig{
Host: "192.168.12.212",
Port: 50051,
Name: "user",
Id: "",
}
for i := 0; i < 10; i++ {
conn, err := grpc.Dial(
fmt.Sprintf("consul://%s:%d/%s?wait=14s", consulConfig.Host, consulConfig.Port, serverConfig.Name),
grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
)
if err != nil {
fmt.Printf("暂无可用服务: %v", err)
return
}
defer conn.Close()
client := proto.NewUserClient(conn)
resp, err := client.SayHello(context.Background(), &proto.Request{Name: "deardai"})
if err != nil {
fmt.Printf("调用服务失败: %v", err)
return
}
fmt.Println(resp.Message)
}
}
运行:
go run client/main.go
结果:
当你看到这个结果时,恭喜你,grpc负载均衡已经完成,加油