負載均衡算法與應用詳解

引言

  • 負載均衡的定義;
  • 負載均衡算法;
  • 負載均衡的應用場景;

負載均衡的定義

負載均衡(Load Balance)是一種集羣技術,它將特定的業務(網絡服務、網絡流量等)分擔給多臺網絡設備(包括服務器、防火牆等)或多條鏈路,從而提高了業務處理能力,保證了業務的高可靠性。負載均衡建立在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備和服務器的帶寬、增加吞吐量、加強網絡數據處理能力、提高網絡的靈活性和可用性。

負載均衡有兩方面的含義:首先,大量的併發訪問或數據流量分擔到多臺節點設備上分別處理,減少用戶等待響應的時間;其次,單個重負載的運算分擔到多臺節點設備上做並行處理,每個節點設備處理結束後,將結果彙總,返回給用戶,系統處理能力得到大幅度提高。目前負載均衡技術大多數是用於提高諸如在Web服務器、FTP服務器和其它關鍵任務服務器上的Internet服務器程序的可用性和可伸縮性。

技術特點

  • 高性能負載均衡技術將業務較均衡地分配到多臺設備或多條鏈路上,提高了整個系統的性能,提升用戶請求的響應性。

  • 可擴展性:負載均衡技術可以方便地增加集羣中設備或鏈路的數量,在不降低業務質量的前提下滿足不斷增長的業務需求。

  • 高可靠性單個甚至多個設備或鏈路發生故障也不會導致業務中斷,提高了整個系統的可靠性。

  • 可管理性:大量的管理工作都集中在應用負載均衡技術的設備上,設備羣或鏈路羣只需要通常的配置和維護即可。

  • 透明性:對用戶而言,集羣等同於一個可靠性高、性能好的設備或鏈路,用戶感知不到也不必關心具體的網絡結構。增加和減少設備或鏈路均不會影響正常的業務。


負載均衡算法

  • 輪詢法(Round Robin,RR)
  • 加權輪詢法(Weighted Round Robin,WRR)
  • 源地址散列法(Source Hash)
  • 最小連接數法(Least Connections)

(平均)輪詢法

算法特點:輪詢法均衡的對待每一臺服務器,將請求按順序輪流分配到後臺服務器上,而不關心服務器實際的連接數、不同服務器的性能高低和當前的系統負載。

適用場景:每臺服務器需要處理的請求數據都是相同,適合於所有服務器硬件都相同的場景;

import java.util.ArrayList;

class RoundRobinTest {

    // 創建服務器集羣數組
    private static ArrayList<String> serverIpArray = new ArrayList<String>();
    static {
        serverIpArray.add("192.168.1.101");
        serverIpArray.add("192.168.1.102");
        serverIpArray.add("192.168.1.103");
        serverIpArray.add("192.168.1.104");
        serverIpArray.add("192.168.1.105");
    }

    // 輪詢法是將請求按順序輪流分配到後臺服務器上,均衡對待每一個服務器
    // currentIndex記錄當前輪詢的序號
    private static Integer currentIndex = 0;

    public static String getServerIp(){
        synchronized (currentIndex){
            // 輪詢一輪後,計數序號清零
            if (currentIndex >= serverIpArray.size()){
                currentIndex = 0;
            }
            // 按序號獲取服務器IP後,計數自加操作
            return serverIpArray.get(currentIndex++);
        }
    }

    public static void main(String[] args) {

        // 用戶請求次數
        int requestTimes = 100;

        // 模擬用戶請求,並按輪詢返回即將服務用戶的服務器IP
        for (int i =0; i<requestTimes; i++){
            String serverIp = getServerIp();
            System.out.println("Server: " + serverIp + " is servicing for your request...");
        }

    }

}

// 輸出結果
Server: 192.168.1.101 is servicing for your request...
Server: 192.168.1.102 is servicing for your request...
Server: 192.168.1.103 is servicing for your request...
Server: 192.168.1.104 is servicing for your request...
Server: 192.168.1.105 is servicing for your request...
Server: 192.168.1.101 is servicing for your request...
Server: 192.168.1.102 is servicing for your request...
Server: 192.168.1.103 is servicing for your request...
Server: 192.168.1.104 is servicing for your request...
Server: 192.168.1.105 is servicing for your request...

(平滑)加權輪詢

根據應用服務器硬件的性能的情況,在輪詢的基礎上,按照配置的權重將請求分發到每個服務器,高性能的服務器設置的權重較大,能分配更多的用戶請求。

加權輪詢算法一個明顯的缺陷。即在某些特殊的權重下,加權輪詢調度會生成不均勻的實例序列,這種不平滑的負載可能會使某些實例出現瞬時高負載的現象,導致系統存在宕機的風險。爲了解決這個調度缺陷,就提出了平滑加權輪詢調度算法。

平滑加權輪詢,算法描述

假設N臺服務器,S={S1, S2, S3, ... , Sn},系統配置的權重是 W = {W1, W2, W3, ..., Wn},輪詢時的當前權重記爲CW={CW1, CW2, CW3, ... , CWn}。即第 i 臺服務器的最初配置權重是Wi,而當前的實時有效權重是CWi;指示變量currentPos表示負載均衡下當前選擇的服務器ID號,所有服務器的配置權重和記爲weightSum,即weightSum = S1 + S2 + S3 +...+ Sn;

算法過程:

1. 初始狀態:CW1 = CW2 = CW3 = ... = CWn = 0;

2. 將當前權重 CW 和配置權重 W 對應相加,然後賦值給CW,即 CWi = CWi + Wi

3. 選出當前權重中的最大值CWmax,currentPos指向該位置,並將其作爲選中的服務器

4. 將選擇出的CWmax減去weightSum;即CWmax = CWmax - weightSum

5. 下一次調度重複步驟 2、3、4;

根據上述算法,舉例:

  • 服務器1 :192.168.1.101,權重是5,pos =0;
  • 服務器2: 192.168.1.102,權重是1,pos =1;
  • 服務器3: 192.168.1.103,權重是1,pos =2;
用戶請求編號 當前權重CW(步驟 2 ) currentPos(步驟 3) 選中的服務器IP(步驟 3 ) 選中後更新CW(步驟 4)
初始化時 { 0 , 0 , 0} 0 \ \
1 { 5, 1, 1 } 0 192.168.1.101 { -2 , 1, 1}
2 { 3, 2, 2 } 0 192.168.1.101 {-4 , 2, 2}
3 { 1, 3, 3 } 1 192.168.1.102 {1,-4, 3}
4 { 6, -3, 4} 0 192.168.1.101 { -1, -3, 4}
5 {4, -2, 5} 2 192.168.1.103 {4,-2,-2}
6 {9,-1,-1} 0 192.168.1.101 {2,-1,-1}
7 {7,0, 0} 0 192.168.1.101 {0,0,0}
8 {5, 1, 1} 0 192.168.1.101 { -2 , 1, 1}

可以看出上述調度序列分散是非常均勻的,且第 8 次調度時又回到第1次調度的狀態,即請求編號1-7的請求是比較平滑的輪詢,7次作爲一個週期和配置權重之和也是吻合的,所以後續可以一直重複調度操作。

import java.util.ArrayList;

class WRRTest{

    // 服務器集羣IP數組
    private static ArrayList<String> serverIpArray = new ArrayList<String>();
    // 系統配置權重數組
    private static ArrayList<Integer> W = new ArrayList<Integer>();
    // 系統配置之和
    private static int weightSum;
    // 當前選中位置遊標
    private static Integer currentPos = 0 ;
    // 初始化標誌
    private static boolean initFLag = false;
    static {
        serverIpArray.add("192.168.1.101");
        serverIpArray.add("192.168.1.102");
        serverIpArray.add("192.168.1.103");

        // 系統配置權重設置
        W.add(5);
        W.add(1);
        W.add(1);

        weightSum = 5+1+1;
    }

    // 當前有效權重數組
    private static ArrayList<Integer> CW = new ArrayList<Integer>();

    public static void main(String[] args){

        // 用戶請求次數
        int requestTimes = 100;

        // 模擬用戶請求,並按輪詢返回即將服務用戶的服務器IP
        for (int i =0; i<requestTimes; i++){
            String serverIp = getServerIp();
            System.out.println("Times at: " + (i+1) +  ", Server: " + serverIp + " is servicing for your request...");
        }


    }

    public static String getServerIp(){
        if (!initFLag){
            step1();
            initFLag = true;
        }
        step2();
        step3();
        step4();
        return serverIpArray.get(currentPos);
    }

    // 第一步驟:初始狀態:CW1 = CW2 = CW3 = ... = CWn = 0;
    public static void step1(){
        for (int i =0 ; i < W.size(); i++){
            CW.add(0);
        }
    }

    // 第二步驟:將當前權重 CW 和配置權重 W 對應相加,然後賦值給CW,即 CWi = CWi + Wi;
    public static void step2(){

        for (int i=0; i< W.size(); i++){
            CW.set(i, CW.get(i) + W.get(i));
        }

    }

    // 第三步驟:選出當前權重中的最大值CWmax,currentPos指向該位置,並將其作爲選中的服務器;
    public static void step3(){
        currentPos = 0;
        for (int i =0; i< CW.size(); i++){
            if (CW.get(i) > CW.get(currentPos)){
                currentPos = i;
            }
        }
    }

    // 第四步驟:將選擇出的CWmax減去weightSum;即CWmax = CWmax - weightSum;
    public static void step4(){
        CW.set(currentPos, CW.get(currentPos) - weightSum);
    }

}

// 輸出結果
Times at: 1, Server: 192.168.1.101 is servicing for your request...
Times at: 2, Server: 192.168.1.101 is servicing for your request...
Times at: 3, Server: 192.168.1.102 is servicing for your request...
Times at: 4, Server: 192.168.1.101 is servicing for your request...
Times at: 5, Server: 192.168.1.103 is servicing for your request...
Times at: 6, Server: 192.168.1.101 is servicing for your request...
Times at: 7, Server: 192.168.1.101 is servicing for your request...
Times at: 8, Server: 192.168.1.101 is servicing for your request...
Times at: 9, Server: 192.168.1.101 is servicing for your request...
Times at: 10, Server: 192.168.1.102 is servicing for your request...
Times at: 11, Server: 192.168.1.101 is servicing for your request...

 

輪詢(均衡、加權、平滑加權)算法的缺陷:

輪詢調度算法並不能動態感知每個實例的負載,它完全依賴於我們的工程經驗,人爲配置權重來實現基本的負載均衡,並不能保證服務的高可用性。若服務的某些實例因其他原因負載突然加重,輪詢調度還是會一如既往地分配請求給這個實例,因此可能會形成小面積的宕機,導致服務的局部不可用。

 

源地址散列法(Source Hashing)

根據請求來源的IP地址進行Hash計算,得到應用服務器,這樣來自於同一個IP地址的請求總在同一個服務器上處理,該請求的上下文信息可能存儲在這臺服務器上,在一個會話週期內重複使用,從而實現會話粘滯(Sessions Sticky)

import java.util.ArrayList;
import java.util.Random;

class SHTest {

    // 創建服務器集羣數組
    private static ArrayList<String> serverIpArray = new ArrayList<String>();
    static {
        serverIpArray.add("192.168.1.101");
        serverIpArray.add("192.168.1.102");
        serverIpArray.add("192.168.1.103");
        serverIpArray.add("192.168.1.104");
        serverIpArray.add("192.168.1.105");
    }


    public static void main(String[] args){

        // 用戶請求次數
        int requestTimes = 100;

        Random random = new Random();

        // 模擬用戶請求,並按輪詢返回即將服務用戶的服務器IP
        for (int i =0; i<requestTimes; i++){
            // 取5以內的隨機數,提升源地址的重複率,方便驗證同一源地址總會對應到同一臺服務器上
            String sourceIp = "192.168.1." + random.nextInt(5);
            System.out.println(sourceIp + " is requesting, " +
                    "server: " + getServerIp(sourceIp) + " will response...");
        }


    }

    public static String getServerIp(String sourceIp){

        // 獲取源地址的哈希值
        int hashCode = sourceIp.hashCode();
        // 將哈希值對集羣的大小取餘,得到服務器序號
        int pos = hashCode % serverIpArray.size();
        // 根據序號,得出服務器的IP
        return serverIpArray.get(pos);

    }


}

// 輸出結果:
192.168.1.1 is requesting, server: 192.168.1.104 will response...
192.168.1.1 is requesting, server: 192.168.1.104 will response...
192.168.1.2 is requesting, server: 192.168.1.105 will response...
192.168.1.3 is requesting, server: 192.168.1.101 will response...
192.168.1.1 is requesting, server: 192.168.1.104 will response...
192.168.1.0 is requesting, server: 192.168.1.103 will response...
192.168.1.1 is requesting, server: 192.168.1.104 will response...
192.168.1.2 is requesting, server: 192.168.1.105 will response...
192.168.1.1 is requesting, server: 192.168.1.104 will response...
192.168.1.2 is requesting, server: 192.168.1.105 will response...
192.168.1.2 is requesting, server: 192.168.1.105 will response...
192.168.1.4 is requesting, server: 192.168.1.102 will response...
192.168.1.3 is requesting, server: 192.168.1.101 will response...
192.168.1.0 is requesting, server: 192.168.1.103 will response...

 

最少連接法(Least Connections)

記錄每個應用服務器正在處理的連接數(請求數)將新到的請求分發到最少連接的服務器上,應該說,這是最符合負載均衡定義的算法。同樣,最少連接算法也可以實現加權最少連接。

客戶端的每一次請求服務在服務器停留的時間可能會有較大的差異,隨着工作時間加長,如果採用簡單的輪循或隨機均衡算法,每一臺服務器上的連接進程可能會產生極大的不同,並沒有達到真正的負載均衡。最少連接數均衡算法對內部中需負載的每一臺服務器都有一個數據記錄,記錄當前該服務器正在處理的連接數量,當有新的服務連接請求時,將把當前請求分配給連接數最少的服務器,使均衡更加符合實際情況,負載更加均衡。此種均衡算法適合長時處理的請求服務,如FTP。 


負載均衡的應用場景

  • HTTP重定向負載均衡;

  • DNS域名解析負載均衡;

  • 反向代理負載均衡(應用層HTTP負載均衡);

  • IP負載均衡(網絡層負載均衡)

  • 直接路由方式(數據鏈路層負載均衡);

HTTP重定向負載均衡

這種負載均衡方案的優點是比較簡單。缺點是瀏覽器需要兩次請求服務器才能完成一次訪問,性能較差;重定向服務器自身的處理能力有可能成爲瓶頸,整個集羣的伸縮性規模有限;使用HTTP302響應碼重定向,有可能使搜索引擎判斷爲SEO作弊,降低搜索排名。因此實踐中使用這種方案進行負載均衡的案例並不多見。

 

DNS域名解析負載均衡

DNS域名解析負載均衡的優點是將負載均衡的工作轉交給DNS,省掉了網站管理維護負載均衡服務器的麻煩,同時許多DNS還支持基於地理位置的域名解析,即會將域名解析成距離用戶地理最近的一個服務器地址,這樣可加快用戶訪問速度,改善性能。但是DNS域名解析負載均衡也有缺點,就是目前的DNS是多級解析,每一級DNS都可能緩存A記錄,當下線某臺服務器後,即使修改了DNS的A記錄,要使其生效也需要較長時間,這段時間,DNS依然會將域名解析到已經下線的服務器,導致用戶訪問失敗;而且DNS負載均衡的控制權在域名服務商那裏,網站無法對其做更多改善和更強大的管理。

事實上,大型網站總是部分使用DNS域名解析,利用域名解析作爲第一級負載均衡手段,即域名解析得到的一組服務器並不是實際提供Web服務的物理服務器,而是同樣提供負載均衡服務的內部服務器,這組內部負載均衡服務器再進行負載均衡,將請求分發到真實的Web服務器上。

 

反向代理負載均衡(應用層負載均衡)

大多數反向代理服務器(可以緩存資源,以改善網站性能)同時提供負載均衡的功能,管理一組Web服務器,將請求根據負載均衡算法轉發到不同Web服務器上。Web服務器處理完成的響應也需要通過反向代理服務器返回給用戶。由於Web服務器不直接對外提供訪問,因此Web服務器不需要使用外部IP地址,而反向代理服務器則需要配置雙網卡和內部外部兩套IP地址。

瀏覽器訪問請求的地址是反向代理服務器的地址114.100.80.10,反向代理服務器收到請求後,根據負載均衡算法計算得到一臺真實物理服務器的地址10.0.0.3,並將請求轉發給服務器。10.0.0.3處理完請求後將響應返回給反向代理服務器,反向代理服務器再將該響應返回給用戶。

由於反向代理服務器轉發請求在HTTP協議層面,因此也叫應用層負載均衡。其優點是和反向代理服務器功能集成在一起,部署簡單。缺點是反向代理服務器是所有請求和響應的中轉站,其性能可能會成爲瓶頸。

 

IP負載均衡(網絡層負載均衡)

在網絡層通過修改請求目標地址進行負載均衡

用戶請求數據包到達負載均衡服務器114.100.80.10後,負載均衡服務器在操作系統內核進程獲取網絡數據包,根據負載均衡算法計算得到一臺真實Web服務器10.0.0.1,然後將數據目的IP地址修改爲10.0.0.1,不需要通過用戶進程處理(直接修改客戶報文的目的IP地址)。真實Web應用服務器處理完成後,響應數據包回到負載均衡服務器,負載均衡服務器再將數據包源地址修改爲自身的IP地址(114.100.80.10)發送給用戶瀏覽器。

這裏的關鍵在於真實物理Web服務器響應數據包如何返回給負載均衡服務器。

  • 一種方案是負載均衡服務器在修改目的IP地址的同時修改源地址,將數據包源地址設爲自身IP,即源地址轉換(SNAT),這樣Web服務器的響應會再回到負載均衡服務器;
  • 另一種方案是將負載均衡服務器同時作爲真實物理服務器集羣的網關服務器,這樣所有響應數據都會到達負載均衡服務器。

IP負載均衡在內核進程完成數據分發,較反向代理負載均衡(在應用程序中分發數據)有更好的處理性能。但是由於所有請求響應都需要經過負載均衡服務器,集羣的最大響應數據吞吐量不得不受制於負載均衡服務器網卡帶寬。對於提供下載服務或者視頻服務等需要傳輸大量數據的網站而言,難以滿足需求。能不能讓負載均衡服務器只分發請求,而使響應數據從真實物理服務器直接返回給用戶呢?

 

直接路由方式(數據鏈路層負載均衡)

數據鏈路層負載均衡是指在通信協議的數據鏈路層修改mac地址進行負載均衡;

使用三角傳輸模式的鏈路層負載均衡是目前大型網站使用最廣的一種負載均衡手段;

在Linux平臺上最好的鏈路層負載均衡開源產品是LVS(Linux Virtual Server);


在圖中,用戶請求到達負載均衡服務器114.100.80.10後,負載均衡服務器將請求數據的目的mac地址修改爲00:0c:29:d2,並不修改數目包目標IP地址,由於Web服務器集羣所有服務器的虛擬IP地址都和負載均服務器的IP地址相同,因此數據可以正常傳輸到達mac地址00:0c:29:d2對應的服務器,該服務器處理完成後發送響應數據到網站的網關服務器,網關服務器直接將該數據包發送到用戶瀏覽器(通過互聯網),響應數據不需要通過負載均衡服務器

這種數據傳輸方式又稱作三角傳輸模式負載均衡數據分發過程中不修改IP地址,只修改目的mac地址,通過配置真實物理服務器集羣所有機器虛擬IP和負載均衡服務器IP地址一致,從而達到不修改數據包的源地址和目的地址就可以進行數據分發的目的,由於實際處理請求的真實物理服務器IP和數據請求目的IP一致,不需要通過負載均衡服務器進行地址轉換,可將響應數據包直接返回給用戶瀏覽器,避免負載均衡服務器網卡帶寬成爲瓶頸。這種負載均衡方式又稱作直接路由方式(DR)

 


參考文獻

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章