gRPC的負載均衡

BigSun丶發表於2024-03-19

目錄
  • 一、什麼是負載均衡
    • 1.1 負載均衡軟體
    • 1.2 七層和四層負載
  • 二、負載均衡策略
    • 2.1 輪循
    • 2.2 加權輪循
    • 2.3 最少連線數
    • 2.4 最少連線數慢啟動時間
    • 2.5 加權最少連線
    • 2.6 基於代理的自適應負載均衡
    • 2.7 固定權重
    • 2.8 加權響應
    • 2.9 源 IP 雜湊
  • 三、go-rpc負載均衡演示
    • 3.1 proto
    • 3.2 生成go檔案
    • 3.3 grpc服務端
    • 3.4 grpc 客戶端
  • 四、gin服務呼叫rpc服務-負載均衡
    • 4.1 同時啟動多個rpc服務
      • (1)ResolveTCPAddr
      • (2)ListenTCP
    • 4.2 多個rpc服務註冊到consul
    • 4.3 gin呼叫rpc負載均衡
      • (1)服務端程式碼
      • (2)utils.go
      • (3)gin程式碼

一、什麼是負載均衡

  • 負載均衡(Load Balance)意思就是分攤到多個操作單元上進行執行,例如Web伺服器、FTP伺服器、企業關鍵應用伺服器和其它關鍵任務伺服器等,從而共同完成工作任務。

  • 單從字面上的意思來理解就可以解釋N臺伺服器平均分擔負載,不會因為某臺伺服器負載高當機而某臺伺服器閒置的情況。那麼負載均衡的前提就是要有多臺伺服器才能實現,也就是兩臺以上即可。

  • 負載均衡建立在現有網路結構之上,它提供了一種廉價有效透明的方法擴充套件網路裝置和伺服器的頻寬、增加吞吐量、加強網路資料處理能力、提高網路的靈活性和可用性

1.1 負載均衡軟體

  • 負載均衡軟體有Nginx、LVS、HaProxy等是目前使用最廣泛的三種負載均衡軟體

1.2 七層和四層負載

  • 四層負載均衡工作在OSI模型的傳輸層,主要工作是轉發,它在接收到客戶端的流量以後透過修改資料包的地址資訊將流量轉發到應用伺服器。

  • 七層負載均衡工作在OSI模型的應用層,因為它需要解析應用層流量,所以七層負載均衡在接到客戶端的流量以後,還需要一個完整的TCP/IP協議棧。七層負載均衡會與客戶端建立一條完整的連線並將應用層的請求流量解析出來,再按照排程演算法選擇一個應用伺服器,並與應用伺服器建立另外一條連線將請求傳送過去,因此七層負載均衡的主要工作就是代理。 七層負載均衡 也稱為“內容交換”,也就是主要透過報文中的真正有意義的應用層內容,再加上負載均衡裝置設定的伺服器選擇方式,決定最終選擇的內部伺服器。

  • 七層負載均衡的優點:這種方式可以對客戶端的請求和伺服器的響應進行任意意義上的修改,極大的提升了應用系統在網路層的靈活性;安全性高。

  • **七層負載均衡:主要是著重於應用廣泛的HTTP協議,所以其應用範圍主要是眾多的網站或者內部資訊平臺等基於B/S開發的系統 **

  • 四層負載均衡:對應其他TCP應用,例如基於C/S開發的ERP等系統

二、負載均衡策略

2.1 輪循

  • Round Robin: 這種方法會將收到的請求迴圈分配到伺服器叢集中的每臺機器,即有效伺服器。如果使用這種方式,所有的標記進入虛擬服務的伺服器應該有相近的資源容量 以及負載形同的應用程式。如果所有的伺服器有相同或者相近的效能那麼選擇這種方式會使伺服器負載形同。基於這個前提,輪循排程是一個簡單而有效的分配請求 的方式。然而對於伺服器不同的情況,選擇這種方式就意味著能力比較弱的伺服器也會在下一輪迴圈中接受輪循,即使這個伺服器已經不能再處理當前這個請求了。 這可能導致能力較弱的伺服器超載。

2.2 加權輪循

  • Weighted Round Robin: 這種演算法解決了簡單輪循排程演算法的缺點:傳入的請求按順序被分配到叢集中伺服器,但是會考慮提前為每臺伺服器分配的權重。管理員只是簡單的透過服務 器的處理能力來定義各臺伺服器的權重。例如,能力最強的伺服器 A 給的權重是 100,同時能力最低的伺服器給的權重是 50。這意味著在伺服器 B 接收到第一個 請求之前前,伺服器 A 會連續的接受到 2 個請求,以此類推。

2.3 最少連線數

  • Least Connection: 以上兩種方法都沒有考慮的是系統不能識別在給定的時間裡保持了多少連線。因此可能發生,伺服器 B 伺服器收到的連線比伺服器 A 少但是它已經超載,因為 伺服器 B 上的使用者開啟連線持續的時間更長。這就是說連線數即伺服器的負載是累加的。這種潛在的問題可以透過 “最少連線數” 演算法來避免:傳入的請求是根據每 臺伺服器當前所開啟的連線數來分配的。即活躍連線數最少的伺服器會自動接收下一個傳入的請求。接本上和簡單輪詢的原則相同:所有擁有虛擬服務的伺服器資源 容量應該相近。值得注意的是,在流量率低的配置環境中,各伺服器的流量並不是相同的,會優先考慮第一臺伺服器。這是因為,如果所有的伺服器是相同的,那麼 第一個伺服器優先,直到第一臺伺服器有連續的活躍流量,否則總是會優先選擇第一臺伺服器。

2.4 最少連線數慢啟動時間

  • Least Connection Slow Start Time: 對最少連線數和帶權重的最小連線數排程方法來說,當一個伺服器剛加入線上環境是,可以為其配置一個時間段,在這段時間內連線數是有限制的而且是緩慢 增加的。這為伺服器提供了一個‘過渡時間’以保證這個伺服器不會因為剛啟動後因為分配的連線數過多而超載。這個值在 L7 配置介面設定。

2.5 加權最少連線

  • Weighted Least Connection: 如果伺服器的資源容量各不相同,那麼 “加權最少連線” 方法更合適:由管理員根據伺服器情況定製的權重所決定的活躍連線數一般提供了一種對伺服器非常 平衡的利用,因為他它借鑑了最少連線和權重兩者的優勢。通常,這是一個非常公平的分配方式,因為它使用了連線數和伺服器權重比例;叢集中比例最低的伺服器 自動接收下一個請求。但是請注意,在低流量情況中使用這種方法時,請參考 “最小連線數” 方法中的注意事項。

2.6 基於代理的自適應負載均衡

  • Agent Based Adaptive Balancing: 除了上述方法之外,負載主機包含一個自適用邏輯用來定時監測伺服器狀態和該伺服器的權重。對於非常強大的 “基於代理的自適應負載均衡” 方法來說,負 載主機以這種方式來定時檢測所有伺服器負載情況:每臺伺服器都必須提供一個包含檔案,這個檔案包含一個 0~99 的數字用來標明改伺服器的實際負載情況 (0 = 空前,99 = 超載,101 = 失敗,102 = 管理員禁用),而伺服器同構 http get 方法來獲取這個檔案;同時對叢集中伺服器來說,以二進位制檔案形式提供自身負載情況也是該伺服器工作之一,然而,並沒有限制伺服器如何計算自身的負載 情況。根據伺服器整體負載情況,有兩種策略可以選擇:在常規的操作中,排程演算法透過收集的伺服器負載值和分配給該伺服器的連線數的比例計算出一個權重比 例。因此,如果一個伺服器負載過大,權重會透過系統透明的作重新調整。和加權輪循排程方法一樣,不正確的分配可以被記錄下來使得可以有效的為不同伺服器分 配不同的權重。然而,在流量非常低的環境下,伺服器報上來的負載值將不能建立一個有代表性的樣本;那麼基於這些值來分配負載的話將導致失控以及指令震盪。 因此,在這種情況下更合理的做法是基於靜態的權重比來計算負載分配。當所有伺服器的負載低於管理員定義的下限時,負載主機就會自動切換為加權輪循方式來分 配請求;如果負載大於管理員定義的下限,那麼負載主機又會切換回自適應方式。

2.7 固定權重

  • Fixed Weighted: 最高權重只有在其他伺服器的權重值都很低時才使用。然而,如果最高權重的伺服器下降,則下一個最高優先順序的伺服器將為客戶端服務。這種方式中每個真實伺服器的權重需要基於伺服器優先順序來配置。

2.8 加權響應

  • Weighted Response: 流量的排程是透過加權輪循方式。加權輪循中所使用的權重是根據伺服器有效性檢測的響應時間來計算。每個有效性檢測都會被計時,用來標記它響應成功花 了多長時間。但是需要注意的是,這種方式假定伺服器心跳檢測是基於機器的快慢,但是這種假設也許不總是能夠成立。所有伺服器在虛擬服務上的響應時間的總和 加在一起,透過這個值來計算單個服務物理伺服器的權重;這個權重值大約每 15 秒計算一次。

2.9 源 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 包中的的以下兩個方法:

(1)ResolveTCPAddr

// func ResolveTCPAddr(net, addr string) (*TCPAddr, error)
ResolveTCPAddr 能將 addr 作為TCP 地址解析並返回
引數addr格式為 host:port 或 [ipv6-host%zone]:port
解析得到網路名和埠名
net 可選的值必須是 tcp、tcp4、tcp6其中一個

(2)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負載均衡

(1)服務端程式碼

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

}

(2)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
}

(3)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()
}

相關文章