負載均衡算法

背景:隨着互聯網的快速發展和演進,當系統達到一定規模,應用之間相互交互、互相調用便不可避免,爲了防止重疊業務的重複實現,此時將相對核心的業務抽取出來,作爲單獨的系統對外提供服務,達到業務之間相互複用,系統也因此演變成爲分佈式應用架構體系。分佈式系統所要面臨的首要問題,便是如何實現應用之間的遠程調用(RPC),以及當服務越來越多時,如何實現服務的負載均衡?

對於負載較高的服務來說,往往對應着由多臺服務器組成的集羣。在請求到來時,爲了將請求均衡地分配到後端服務器,負載均衡程序將從服務對應的地址列表中,通過相應的負載均衡算法和規則,選取一臺服務器進行訪問,這個過程稱爲服務的負載均衡,如下圖:


當服務的規模較小時,可以採用硬編碼的方式將服務地址和配置寫在代碼中,通過編碼的方式來解決服務的路由和負載均衡,也可通過傳統的硬件負載均衡設備如F5等,而當服務越來越多,規模越來越大時,對應的機器數量也越來越龐大,單靠人工來管理和維護服務及地址的配置信息,已經越來越困難,此時,需要一個能夠動態註冊和獲取服務信息的地方,來統一管理服務名稱和其對應的服務器列表信息,稱之服務配置中心,服務配置中心的核心,則是負載均衡算法。

負載均衡的算法種類很多,常見的負載均衡算法包括輪詢法、隨機法、源地址哈希法、加權輪詢法、加權隨機法、最小連接法等,應根據具體使用場景選取對應的算法。

1. 輪詢法(Round Robin)

輪詢法很容易理解,將請求按順序輪流地分配到後端服務器上,它均衡的對待後端每一臺服務器,而不關心服務器實際的連接數和當前的系統負載。

這裏通過初始化一個serverWeightMap的變量來表示服務器地址和權重的映射,以此來模擬各負載均衡算法的實現:

/**
 * 負載均衡算法
 * @author yejianwen 
 *
 */
public class LoadBalancingAlgorithm
{
    public static HashMap<String, Integer> serverWeightMap = null;
    private static Integer pos = 0;
    
    static {
        serverWeightMap = new HashMap<String, Integer>();
        serverWeightMap.put("192.168.1.100", 1);
        serverWeightMap.put("192.168.1.101", 1);
        serverWeightMap.put("192.168.1.102", 2);
        serverWeightMap.put("192.168.1.103", 1);
        serverWeightMap.put("192.168.1.104", 4);
        serverWeightMap.put("192.168.1.105", 3);
        serverWeightMap.put("192.168.1.106", 2);
        serverWeightMap.put("192.168.1.107", 1);
    }
    
    /**
     * 負載均衡--輪詢算法
     * @return
     */
    public static String roundRobin() {
        //重新創建一個Map,避免出現由於服務器上線和下線導致的併發問題
        Map<String, Integer> serverMap = new HashMap<String, Integer>();
        serverMap.putAll(serverWeightMap);
        
        //獲得IP地址list
        Set<String> keySet = serverMap.keySet();
        ArrayList<String> keyList = new ArrayList<String>();
        keyList.addAll(keySet);
        
        String server = null;
        synchronized (pos) {
            if(pos >= keySet.size()) {
                pos = 0;
            }
            server = keyList.get(pos);
            pos ++;
        }
        
        return server;
    }
}

其中IP地址192.168.1.102的權重爲2,192.168.1.105的權重爲3.

由於serverWeightMap中的地址列表是動態的,隨時可能有機器上線、下線或者宕機,因此,爲了避免可能出現的併發問題,通過新建方法內的局部變量serverMap,先將域變量複製到線程本地,以避免被多個線程修改。這樣可能會引入新的問題,複製以後serverWeightMap的修改將無法反映給serverMap,也就是說,在這一輪選擇服務器的過程中,新增或者下線服務器,負載均衡算法中將無法獲得,新增比較好處理,而當服務器下線或者宕機時,服務消費者將有可能訪問到不存在的地址。因此,在服務消費者的實現端需要考慮該問題,並且進行相應的容錯處理,比如重新發起一次調用。

對於當前輪詢的位置變量pos,爲了保證服務器選擇的順序性,需要在操縱時對其加上synchronized鎖,使得在同一時刻只有一個線程能夠修改pos的值,否則當pos變量被併發修改,則無法保證服務選擇的順序性,甚至可能導致keyList數組越界。

使用輪詢側綠的目的在於,希望做到請求轉移的絕對均衡,但付出的性能代價也是相當大的。爲了保證pos變量修改的互斥性,需要引入重量級悲觀鎖synchronized,將會導致該段輪詢代碼的併發吞吐量明顯的下降。

2. 隨機法(Random)

通過系統隨機函數,根據後端服務器列表的大小值來隨機選取其中一臺進行訪問。由概率統計理論可以的得知,隨着調用量的增大,其實效果越來越接近於平均分配流量到每一臺服務器,也就是輪詢的效果。隨機算法的部分關鍵代碼如下:

/**
     * 負載均衡--隨機法
     * @return
     */
    public static String random() {
        //重新創建一個Map,避免出現由於服務器上線和下線導致的併發問題
        Map<String, Integer> serverMap = new HashMap<String, Integer>();
        serverMap.putAll(serverWeightMap);
        
        //獲得IP地址list
        Set<String> keySet = serverMap.keySet();
        ArrayList<String> keyList = new ArrayList<String>();
        keyList.addAll(keySet);
        
        Random random = new Random();
        int randomPos = random.nextInt(keyList.size());
        
        String server = keyList.get(randomPos);
        return server;
    }

當前,跟前面類似,爲了避免可能出現的併發問題,需要將serverWeightMap複製到serverMap中。通過Random的nextInt方法,取到在0~keyList.size()區間的一個隨機整數,從而從服務器列表中隨機獲取到一臺服務器地址,進行返回。基於概率論知識,吞吐量越大,隨機算法的效果越接近輪詢算法的效果,因此,你還會考慮一定要使用需要付出一定性能代價的輪詢算法嗎?

3. 源地址哈希法(hash)

源地址哈希的思想是獲取客戶端的IP地址值,通過哈希函數計算得到一個數值,用該數值對服務器列表的大小進行取模運算,得到的結果便是要訪問的服務器序號。採用哈希法進行負載均衡,同一個IP地址的客戶端,當後端服務器列表不變時,它每次都會被映射到同一臺後端服務器進行房訪問。

/**
     * 負載均衡--源地址哈希算法
     * @return
     */
    public static String comsumerHash(String remoteip) {
        //重新創建一個Map,避免出現由於服務器上線和下線導致的併發問題
        Map<String, Integer> serverMap = new HashMap<String, Integer>();
        serverMap.putAll(serverWeightMap);
        
        //獲得IP地址list
        Set<String> keySet = serverMap.keySet();
        ArrayList<String> keyList = new ArrayList<String>();
        keyList.addAll(keySet);
        
        int hashCode = remoteip.hashCode();
        int serverListSize = keyList.size();
        int serverPos = hashCode % serverListSize;
        
        return keyList.get(serverPos);
    }

4. 加權輪詢法(Weight Round Robin)

不同的後端服務器可能機器的配置和當前系統的負載並不相同,因此他們的抗壓能力也不盡相同,給配置高、負載低的機器配置更高的權重,讓其處理更多的需求,而低配置、高負載的機器,則給其分配較低的權重,降低其系統負載。

/**
     * 負載均衡--加權輪詢算法
     * @return
     */
    public static String weightRoundRobin() {
        //重新創建一個Map,避免出現由於服務器上線和下線導致的併發問題
        Map<String, Integer> serverMap = new HashMap<String, Integer>();
        serverMap.putAll(serverWeightMap);
        
        //獲得IP地址list
        Set<String> keySet = serverMap.keySet();
        Iterator<String> it = keySet.iterator();
        
        ArrayList<String> serverList = new ArrayList<String>();
        
        while(it.hasNext()) {
            String server = it.next();
            Integer weight = serverMap.get(server);
            for(int i = 0; i < weight; i++) {
                serverList.add(server);
            }
        }
        
        String server = null;
        synchronized (pos) {
            if(pos >= keySet.size()) {
                pos = 0;
            }
            server = serverList.get(pos);
            pos ++;
        }
        
        return server;
    }

與輪詢算法類似,只是在獲取服務器地址之前增加了一段權重計算的代碼,根據權重的大小,將地址重複的增加到服務器地址列表中,權重越大,該服務器每輪所獲得的請求數量越大。

5. 加權隨機法(Weight Random)

與加權輪詢法類似,加權隨機法也根據後端服務器不同的配置和負載情況,配置不同的權重。不同的是,它是按權重來隨機選取服務器的,而非順序。

/**
     * 負載均衡--加權隨機法
     * @return
     */
    public static String weightRandom() {
        //重新創建一個Map,避免出現由於服務器上線和下線導致的併發問題
        Map<String, Integer> serverMap = new HashMap<String, Integer>();
        serverMap.putAll(serverWeightMap);
        
        //獲得IP地址list
        Set<String> keySet = serverMap.keySet();
        Iterator<String> it = keySet.iterator();
        
        ArrayList<String> serverList = new ArrayList<String>();
        
        while(it.hasNext()) {
            String server = it.next();
            Integer weight = serverMap.get(server);
            for(int i = 0; i < weight; i++) {
                serverList.add(server);
            }
        }
        
        Random random = new Random();
        int randomPos = random.nextInt(serverList.size());
        
        String server = serverList.get(randomPos);
        return server;
    }


6. 最小連接數法(Least Connections)
    最小連接數法算法比較靈活和智能,由於後端服務器的配置不盡相同,對於請求的處理有快有慢,它正是根據後端服務器當前連接數情況,動態的選取其中當前積壓連接數最少的一臺服務器來處理當前請求,儘可能地提高後端服務器的利用效率,將負載合理地分流到每一臺機器。由於最小連接數涉及服務器連接數彙總和感知,設計與實現較爲繁瑣,此處,不做細說。

負載均衡的算法介紹到此。

參考文獻:阿里總架構師 陳康賢 所著《大型分佈式網站架構設計與實踐》



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