(四)網(wǎng)絡(luò)編程之請求分發(fā)篇:負載均衡靜態(tài)調(diào)度算法怎燥、平滑輪詢加權(quán)、一致性哈希蜜暑、最小活躍數(shù)算法實踐铐姚!

引言

? ?先如今所有的技術(shù)棧中,只要一談關(guān)于高可用肛捍、高并發(fā)處理相關(guān)的實現(xiàn)隐绵,必然會牽扯到集群這個話題,也就是部署多臺服務(wù)器共同對外提供服務(wù)拙毫,從而做到提升系統(tǒng)吞吐量依许,優(yōu)化系統(tǒng)的整體性能以及穩(wěn)定性等目的。

當多臺機器上部署相同服務(wù)節(jié)點時缀蹄,客戶端的發(fā)送請求訪問就會出現(xiàn)一個必須要解決的問題:客戶端的請求到底該交由哪臺服務(wù)器處理峭跳?這點則由負載均衡策略來決定,也就是說:請求具體會被分發(fā)到哪臺服務(wù)器缺前,是調(diào)度算法來決定的蛀醉。

? ?在上篇《Nginx》的文章中,我們首次引出了負載均衡的概念衅码,在通過Nginx對后端做集群時拯刁,里面可以配置負載均衡策略,從而能夠做到讓訪問系統(tǒng)的大規(guī)模流量均勻分散到集群中的每臺服務(wù)器上逝段,但在上篇Nginx的文章中并未對此展開敘述垛玻,但負載相關(guān)的內(nèi)容對于后續(xù)的分布式、中間件專題尤為重要奶躯,因此也來詳細剖析一下帚桩。

負載均衡這個概念,幾乎在所有支持高可用的技術(shù)棧中都存在巫糙,例如微服務(wù)朗儒、分庫分表、各大中間件(MQ参淹、Redis醉锄、MyCat、Nginx浙值、ES)等恳不,也包括云計算、云調(diào)度开呐、大數(shù)據(jù)中也是炙手可熱的詞匯烟勋。

負載均衡策略主要分為靜態(tài)與動態(tài)兩大類:

  • 靜態(tài)調(diào)度算法:指配置后只會依據(jù)配置好的策略進行請求分發(fā)的算法规求。
  • 動態(tài)調(diào)度算法:指配置后會根據(jù)線上情況(網(wǎng)絡(luò)/CPU負載/磁盤IO等)來分發(fā)請求。

但負載均衡算法數(shù)量并不少卵惦,本篇主要對于一些常用且高效的負載策略進行剖析阻肿。

一、基本的負載算法

? ?如果聊到最基本的負載均衡算法沮尿,那么相信大家多少都有了解丛塌,例如:輪詢、隨機畜疾、權(quán)重等這類算法赴邻。特點就在于實現(xiàn)簡單,先來快速過一遍基本的算法實現(xiàn)啡捶。

1.1姥敛、輪詢算法

? ?輪詢算法是最為簡單、也最為常見的算法瞎暑,也是大多數(shù)集群情況下的默認調(diào)度算法彤敛,這種算法會按照配置的服務(wù)器列表,按照順序依次分發(fā)請求金顿,所有服務(wù)器都分發(fā)一遍后臊泌,又會回到第一臺服務(wù)器循環(huán)該步驟,Java代碼實現(xiàn)如下:

// 服務(wù)類:主要用于保存配置的所有節(jié)點
public class Servers {
    
    // 模擬配置的集群節(jié)點
    public static List<String> SERVERS = Arrays.asList(
            "44.120.110.001:8080",
            "44.120.110.002:8081",
            "44.120.110.003:8082",
            "44.120.110.004:8083",
            "44.120.110.005:8084"
    );
}

// 輪詢策略類:實現(xiàn)基本的輪詢算法
public class RoundRobin{
    // 用于記錄當前請求的序列號
    private static AtomicInteger requestIndex = new AtomicInteger(0);

    // 從集群節(jié)點中選取一個節(jié)點處理請求
    public static String getServer(){
        // 用請求序列號取余集群節(jié)點數(shù)量揍拆,求得本次處理請求的節(jié)點下標
        int index = requestIndex.get() % Servers.SERVERS.size();
        // 從服務(wù)器列表中獲取具體的節(jié)點IP地址信息
        String server = Servers.SERVERS.get(index);
        // 自增一次請求序列號渠概,方便下個請求計算
        requestIndex.incrementAndGet();
        // 返回獲取到的服務(wù)器IP地址
        return server;
    }
}

// 測試類:測試輪詢算法
public class Test{
    public static void main(String[] args){
        // 使用for循環(huán)簡單模擬10個客戶端請求
        for (int i = 1; i <= 10; i++){
            System.out.println("第"+ i + "個請求:" + RoundRobin.getServer());
        }
    }
}

/******輸出結(jié)果*******/
第1個請求:44.120.110.001:8080
第2個請求:44.120.110.002:8081
第3個請求:44.120.110.003:8082
第4個請求:44.120.110.004:8083
第5個請求:44.120.110.005:8084
第6個請求:44.120.110.001:8080
第7個請求:44.120.110.002:8081
第8個請求:44.120.110.003:8082
第9個請求:44.120.110.004:8083
第10個請求:44.120.110.005:8084

上述案例中,整個算法的實現(xiàn)尤為簡單嫂拴,就是通過一個原子計數(shù)器記錄當前請求的序列號播揪,然后直接通過%集群中的服務(wù)器節(jié)點總數(shù),最終得到一個具體的下標值筒狠,再通過這個下標值猪狈,從服務(wù)器IP列表中獲取一個具體的IP地址裁赠。

輪詢算法的優(yōu)勢:

  • ①算法實現(xiàn)簡單茧痒,請求分發(fā)效率夠高。
  • ②能夠?qū)⑺姓埱缶鶖偟郊褐械拿總€節(jié)點上钉跷。
  • ③易于后期彈性伸縮灶伊,業(yè)務(wù)增長時可以拓展節(jié)點疆前,業(yè)務(wù)萎靡時可以縮減節(jié)點。

輪詢算法的劣勢:

  • ①對于不同配置的服務(wù)器無法合理照顧聘萨,無法將高配置的服務(wù)器性能發(fā)揮出來竹椒。
  • ②由于請求分發(fā)時,是基于請求序列號來實現(xiàn)的米辐,所以無法保證同一客戶端的請求都是由同一節(jié)點處理的胸完,因此需要通過session記錄狀態(tài)時书释,無法確保其一致性。

輪詢算法的應(yīng)用場景:

  • ①集群中所有節(jié)點硬件配置都相同的情況赊窥。
  • ②只讀不寫爆惧,無需保持狀態(tài)的情景。

1.2锨能、隨機算法

? ?隨機算法的實現(xiàn)也非常簡單检激,也就是當客戶端請求到來時,每次都會從已配置的服務(wù)器列表中隨機抽取一個節(jié)點處理腹侣,實現(xiàn)如下:

// 隨機策略類:隨機抽取集群中的一個節(jié)點處理請求
public class Random {
    // 隨機數(shù)產(chǎn)生器,用于產(chǎn)生隨機因子
    static java.util.Random random = new java.util.Random();

    public static String getServer(){
        // 從已配置的服務(wù)器列表中齿穗,隨機抽取一個節(jié)點處理請求
        return Servers.SERVERS.get(random.nextInt(Servers.SERVERS.size()));
    }
}

上述該算法的實現(xiàn)傲隶,非常明了,通過java.util包中自帶的Random隨機數(shù)產(chǎn)生器窃页,從服務(wù)器列表中隨機抽取一個節(jié)點處理請求跺株,該算法的結(jié)果也不測試了,大家估計一眼就能看明白脖卖。

隨機算法的優(yōu)勢:個人看來該算法單獨使用的意義并不大乒省,一般會配合下面要講的權(quán)重策略協(xié)同使用。

隨機算法的劣勢:

  • ①無法合理的將請求均攤到每臺服務(wù)器節(jié)點畦木。
  • ②由于處理請求的目標服務(wù)器不明確袖扛,因此也無法滿足需要記錄狀態(tài)的請求。
  • ③能夠在一定程度上發(fā)揮出高配置的機器性能十籍,但充滿不確定因素蛆封。

1.3、權(quán)重算法

? ?權(quán)重算法是建立在其他基礎(chǔ)算法之上推出的一種概念勾栗,權(quán)重算法并不能單獨配置惨篱,因為權(quán)重算法無法做到請求分發(fā)的調(diào)度,所以一般權(quán)重會配合其他基礎(chǔ)算法結(jié)合使用围俘,如:輪詢權(quán)重算法砸讳、隨機權(quán)重算法等,這樣可以讓之前的兩種基礎(chǔ)調(diào)度算法更為“人性化”一些界牡。
? ?權(quán)重算法是指對于集群中的每個節(jié)點分配一個權(quán)重值簿寂,權(quán)重值越高,該節(jié)點被分發(fā)的請求數(shù)也會越多欢揖,反之同理陶耍。這樣做的好處十分明顯,也就是能夠充分考慮機器的硬件配置她混,從而分配不同權(quán)重值烈钞,做到“能者多勞”泊碑。那如何實現(xiàn)呢,先來看看隨機權(quán)重的實現(xiàn):

public class Servers{
    // 在之前是Servers類中再加入一個權(quán)重服務(wù)列表
    public static Map<String, Integer> WEIGHT_SERVERS = new LinkedHashMap<>();
    static {
        // 配置集群的所有節(jié)點信息及權(quán)重值
        WEIGHT_SERVERS.put("44.120.110.001:8080",17);
        WEIGHT_SERVERS.put("44.120.110.002:8081",11);
        WEIGHT_SERVERS.put("44.120.110.003:8082",30);
    }
}

// 隨機權(quán)重算法
public class Randomweight {
    // 初始化隨機數(shù)生產(chǎn)器
    static java.util.Random random = new java.util.Random();

    public static String getServer(){
        // 計算總權(quán)重值
        int weightTotal = 0;
        for (Integer weight : Servers.WEIGHT_SERVERS.values()) {
            weightTotal += weight;
        }

        // 從總權(quán)重的范圍內(nèi)隨機生成一個索引
        int index = random.nextInt(weightTotal);
        System.out.println(index);

        // 遍歷整個權(quán)重集群的節(jié)點列表毯欣,選擇節(jié)點處理請求
        String targetServer = "";
        for (String server : Servers.WEIGHT_SERVERS.keySet()) {
            // 獲取每個節(jié)點的權(quán)重值
            Integer weight = Servers.WEIGHT_SERVERS.get(server);
            // 如果權(quán)重值大于產(chǎn)生的隨機數(shù)馒过,則代表此次隨機分配應(yīng)該落入該節(jié)點
            if (weight > index){
                // 直接返回對應(yīng)的節(jié)點去處理本次請求并終止循環(huán)
                targetServer = server;
                break;
            }
            // 如果當前節(jié)點的權(quán)重值小于隨機索引,則用隨機索引減去當前節(jié)點的權(quán)重值酗钞,
            // 繼續(xù)循環(huán)權(quán)重列表腹忽,與其他的權(quán)重值進行對比,
            // 最終該請求總會落入到某個IP的權(quán)重值范圍內(nèi)
            index = index - weight;
        }
        // 返回選中的目標節(jié)點
        return targetServer;
    }

    public static void main(String[] args){
        // 利用for循環(huán)模擬10個客戶端請求測試
        for (int i = 1; i <= 10; i++){
            System.out.println("第"+ i + "個請求:" + getServer());
        }
    }
}

/********運行結(jié)果********/
第1個請求:44.120.110.003:8082
第2個請求:44.120.110.001:8080
第3個請求:44.120.110.003:8082
第4個請求:44.120.110.003:8082
第5個請求:44.120.110.003:8082
第6個請求:44.120.110.003:8082
第7個請求:44.120.110.003:8082
第8個請求:44.120.110.001:8080
第9個請求:44.120.110.001:8080
第10個請求:44.120.110.002:8081

上面這個算法對比之前的基本實現(xiàn)砚作,可能略微有些復(fù)雜難懂窘奏,我們先上個圖:

隨機權(quán)重算法

仔細觀看上圖后,邏輯應(yīng)該會清晰很多葫录,大體捋一下思路:

  • 先求和所有的權(quán)重值着裹,再隨機生成一個總權(quán)重之內(nèi)的索引。
  • 遍歷之前配置的服務(wù)器列表米同,用隨機索引與每個節(jié)點的權(quán)重值進行判斷骇扇。
    • 如果小于,則代表當前請求應(yīng)該落入目前這個節(jié)點面粮。
    • 如果大于少孝,則代表隨機索引超出了目前節(jié)點的權(quán)重范圍,則減去當前權(quán)重熬苍,繼續(xù)與其他節(jié)點判斷稍走。
  • 最終隨機出的索引總會落入到一個節(jié)點的權(quán)重范圍內(nèi),最后返回對應(yīng)的節(jié)點IP冷溃。

這樣一分析下來钱磅,估摸著各位小伙伴應(yīng)該都理解了,接著再來看看輪詢權(quán)重算法的實現(xiàn):

// 輪詢權(quán)重算法
public class RoundRobinweight {
    private static AtomicInteger requestCount = new AtomicInteger(0);

    public static String getServer(){
        int weightTotal = 0;
        for (Integer weight : Servers.WEIGHT_SERVERS.values()) {
            weightTotal += weight;
        }
        
        String targetServer = "";
        int index = requestCount.get() % weightTotal;
        requestCount.incrementAndGet();

        for (String server : Servers.WEIGHT_SERVERS.keySet()) {
            Integer weight = Servers.WEIGHT_SERVERS.get(server);
            if (weight > index){
                targetServer = server;
                break;
            }
            index = index - weight;
        }
        return targetServer;
    }

    public static void main(String[] args){
        for (int i = 1; i <= 10; i++){
            System.out.println("第"+ i + "個請求:" + getServer());
        }
    }
}

/********運行結(jié)果*********/
第1個請求:44.120.110.001:8080
第2個請求:44.120.110.001:8080
第3個請求:44.120.110.001:8080
第4個請求:44.120.110.001:8080
第5個請求:44.120.110.001:8080
第6個請求:44.120.110.001:8080
第7個請求:44.120.110.001:8080
第8個請求:44.120.110.001:8080
第9個請求:44.120.110.001:8080
第10個請求:44.120.110.001:8080

觀察上述中的案例似枕,此刻會發(fā)現(xiàn)出端倪盖淡,代碼實現(xiàn)過程相同,但此刻的輸出結(jié)果凿歼,竟然全部請求都被分發(fā)到了44.120.110.001:8080這個節(jié)點褪迟,這是為什么呢?因為此時是通過請求序列號去進行判斷的答憔,所以最終效果會成為:

  • 17個請求會交給44.120.110.001:8080節(jié)點味赃。
  • 后續(xù)11個請求會交給44.120.110.002:8081節(jié)點。
  • 最后30個請求會交給44.120.110.003:8082節(jié)點虐拓。
  • 然后持續(xù)重復(fù)該過程.....

此時似乎離我們預(yù)期的負載效果發(fā)生了偏離心俗,如果采用這種方案去實現(xiàn)輪詢權(quán)重算法,最終會將一個集群變?yōu)閱吸c服務(wù),這顯然并不是期待中的效果城榛,因此需要一種新的方式去實現(xiàn)揪利,那么又該如何去做呢?此時需要牽扯到一種請求調(diào)度的高級算法:平滑加權(quán)輪詢算法狠持。

二疟位、平滑加權(quán)輪詢算法

? ?平滑輪詢加權(quán)算法的本質(zhì)就是為了解決之前實現(xiàn)方式中所存在的問題,能夠?qū)⒄埱缶鶆虻陌凑諜?quán)重值分發(fā)到每臺機器喘垂。這種算法設(shè)計的非常巧妙甜刻,實現(xiàn)過程也尤為有趣,我們一起來看看:

// 權(quán)重服務(wù)器的配置類
public class Servers {
    public static Map<String, Integer> WEIGHT_SERVERS = new LinkedHashMap<>();
    static {
        // 權(quán)重值設(shè)置的略微小一點正勒,方便后續(xù)理解算法
        WEIGHT_SERVERS.put("44.120.110.001:8080",3);
        WEIGHT_SERVERS.put("44.120.110.002:8081",2);
        WEIGHT_SERVERS.put("44.120.110.003:8082",1);
    }
}

// 權(quán)重類
public class Weight {
    // 節(jié)點信息
    private String server;
    // 節(jié)點權(quán)重值
    private Integer weight;
    // 動態(tài)權(quán)重值
    private Integer currentWeight;

    // 構(gòu)造方法
    public Weight() {}
    public Weight(String server, Integer weight, Integer currentWeight) {
        this.server = server;
        this.weight = weight;
        this.currentWeight = currentWeight;
    }

    // 封裝方法
    public String getServer() {
        return server;
    }
    public void setServer(String server) {
        this.server = server;
    }
    public Integer getWeight() {
        return weight;
    }
    public void setWeight(Integer weight) {
        this.weight = weight;
    }
    public Integer getCurrentWeight() {
        return this.currentWeight;
    }
    public void setCurrentWeight(Integer currentWeight) {
        this.currentWeight = currentWeight;
    }
}

public class RoundRobinWeight {
    // 初始化存儲每個節(jié)點的權(quán)重容器
    private static Map<String,Weight> weightMap = new HashMap<>();

    // 計算總權(quán)重值得院,只需要計算一次,因此放在靜態(tài)代碼塊中執(zhí)行
    private static int weightTotal = 0;
    static {
        sumWeightTotal();
    }

    // 求和總權(quán)重值章贞,后續(xù)動態(tài)伸縮節(jié)點時尿招,再次調(diào)用該方法即可。
    public static void sumWeightTotal(){
        for (Integer weight : Servers.WEIGHT_SERVERS.values()) {
            weightTotal += weight;
        }
    }

    // 獲取處理本次請求的具體服務(wù)器IP
    public static String getServer(){
        // 判斷權(quán)重容器中是否有節(jié)點信息
        if (weightMap.isEmpty()){
            // 如果沒有則將配置的權(quán)重服務(wù)器列表挨個載入容器
            Servers.WEIGHT_SERVERS.forEach((servers, weight) -> {
                // 初始化時阱驾,每個節(jié)點的動態(tài)權(quán)重值都為0
                weightMap.put(servers, new Weight(servers, weight, 0));
            });
        }

        // 每次請求時,更改動態(tài)權(quán)重值
        for (Weight weight : weightMap.values()) {
            weight.setCurrentWeight(weight.getCurrentWeight()
                    + weight.getWeight());
        }

        // 判斷權(quán)重容器中最大的動態(tài)權(quán)重值
        Weight maxCurrentWeight = null;
        for (Weight weight : weightMap.values()) {
            if (maxCurrentWeight == null || weight.getCurrentWeight()
                    > maxCurrentWeight.getCurrentWeight()){
                maxCurrentWeight = weight;
            }
        }

        // 最后用最大的動態(tài)權(quán)重值減去所有節(jié)點的總權(quán)重值
        maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight()
                - weightTotal);

        // 返回最大的動態(tài)權(quán)重值對應(yīng)的節(jié)點IP
        return maxCurrentWeight.getServer();
    }

    public static void main(String[] args){
        // 使用for循環(huán)模擬6次請求
        for (int i = 1; i <= 6; i++){
            System.out.println("第"+ i + "個請求:" + getServer());
        }
    }
}

/********輸出結(jié)果********/
第1個請求:44.120.110.001:8080
第2個請求:44.120.110.002:8081
第3個請求:44.120.110.001:8080
第4個請求:44.120.110.003:8082
第5個請求:44.120.110.002:8081
第6個請求:44.120.110.001:8080

先看結(jié)果怪蔑,對比之前的實現(xiàn)方式而言里覆,該算法在分發(fā)請求時,確實均勻了很多很多缆瓣,而且請求分發(fā)的數(shù)量與我們配置的權(quán)重值也恰巧相符合:

  • 44.120.110.001:80803
  • 44.120.110.002:80812
  • 44.120.110.003:80821

這是不是很神奇喧枷?如何做到的呢,接下來簡單聊一下該算法的核心思想弓坞。

在之前的權(quán)重算法中隧甚,服務(wù)器列表中只有兩個值:服務(wù)器IP、對應(yīng)的權(quán)重值渡冻,而在當前這種算法中戚扳,需要再引入一個動態(tài)權(quán)重值的概念,所以我們再上述案例中族吻,將服務(wù)器的列表抽象成了一個Weight類帽借,在該類中除開原本的servers、weight之外超歌,多添加了一個字段currentWeight砍艾,用于記錄每個節(jié)點的動態(tài)權(quán)重(該值是變化的)。

在該算法中巍举,會先計算已配置的權(quán)重值總和脆荷,然后第一次請求,會初始化權(quán)重容器weightMap,將每個配置的節(jié)點都封裝成一個Weight對象蜓谋,并將其動態(tài)權(quán)重值初始化為0梦皮,如下:

  • Weight("server":"44.120.110.001:8080","weight":3,"currentWeight":0)
  • Weight("server":"44.120.110.002:8081","weight":2,"currentWeight":0)
  • Weight("server":"44.120.110.003:8082","weight":1,"currentWeight":0)

OK,至此準備工作就緒孤澎,接下來是算法的核心過程届氢,主要分為三步:

  • ①用原本的動態(tài)權(quán)重值加一次每個節(jié)點的靜態(tài)權(quán)重值,計算出新的動態(tài)權(quán)重值覆旭。
  • ②遍歷權(quán)重容器退子,找出動態(tài)權(quán)重值最大的節(jié)點,將其作為處理本次請求的節(jié)點型将。
  • ③用最大的動態(tài)權(quán)重值減去已配置的靜態(tài)權(quán)重值總和寂祥,為一下輪分發(fā)做準備。

結(jié)合上述的算法過程和前面給出的案例七兜,把整個過程攤開剖析一次:

序列號 計算動態(tài)權(quán)重 尋找最大權(quán)重 最大權(quán)重減總權(quán)重 本次目標IP
3,2,1 3 -3,2,1 44.120.110.001:8080
0,4,2 4 0,-2,2 44.120.110.002:8081
3,0,3 3 -3,0,3 44.120.110.001:8080
0,2,4 4 0,0,-2 44.120.110.003:8082
3,4,-1 4 3,-2,-1 44.120.110.002:8081
6,0,0 6 0,0,0 44.120.110.001:8080

上表中列出了六次請求的處理過程丸凭,整個過程到最后,動態(tài)權(quán)重值又會回歸初始值:0,0,0腕铸,然后開啟新的一輪計算惜犀,周而復(fù)始之,格外的神奇^_^狠裹。

平滑加權(quán)輪詢算法也是應(yīng)用最為廣泛的輪詢算法虽界,在Dubbo、Robbin涛菠、Nginx莉御、Zookeeper等一些集群環(huán)境中,當你配置了權(quán)重時俗冻,默認采用的就是該算法作為請求分發(fā)的策略礁叔。

三、一致性哈希算法

? ?其實平滑加權(quán)輪詢算法對于請求分發(fā)而言迄薄,是一種比較優(yōu)秀的策略了琅关,不過前面分析的所有策略,都存在一個致命問題:不能確保同一客戶端的所有請求都分發(fā)在同一臺服務(wù)器處理讥蔽,因此無法實現(xiàn)有狀態(tài)的請求死姚,好比最簡單的登錄功能,客戶端發(fā)送請求登錄成功勤篮,然后將其登錄的狀態(tài)保存在session中都毒,結(jié)果客戶端的第二次請求被分發(fā)到了另外一臺機器,由于第二臺服務(wù)器session中沒有相關(guān)的登錄信息碰缔,因此會要求客戶端重新登錄账劲,這顯然造成的用戶體驗感是極差的,那么對于這種問題又該如何解決呢?主要有兩種方案:

  • ①采用外部中間件存儲session瀑焦,例如Redis腌且,然后從Redis中獲取登錄狀態(tài)。
  • ②采用特殊的請求分發(fā)策略榛瓮,確保同一客戶端的所有請求都會去到同一臺機器上處理铺董。

一致性哈希算法 就是一種能夠能夠確保同一客戶端的所有請求都會被分發(fā)到同一臺機器的策略,不過一致性哈希算法依舊會存在問題禀晓,就是當集群中某個節(jié)點下線精续,或者集群出現(xiàn)拓展時,那么也會影響最終分發(fā)的目標機器粹懒,所以一般一致性哈希算法并不能100%解決session一致性的問題重付,因此該算法一般很少用于網(wǎng)關(guān)層的請求分發(fā),更多的場景是應(yīng)用在分布式緩存等情況凫乖,接下來一起來看看确垫。

3.1、通過其他分發(fā)算法實現(xiàn)緩存

? ?在講解一致性哈希算法之前帽芽,大家先來簡單理解一下一致性哈希算法的產(chǎn)生背景删掀。

先思考一個問題:假設(shè)目前單臺緩存服務(wù)器無法承擔外部的訪問壓力,此刻會如何去做呢导街?

答案是增加新的緩存服務(wù)器節(jié)點爬迟,拓展出一個集群對外提供服務(wù)。

好的菊匿,那問題又來了,現(xiàn)在緩存服務(wù)器是一個集群環(huán)境计福,此刻來了一個請求后該落入哪個節(jié)點呢跌捆?

? ?假設(shè)采用輪詢策略,那么寫入xxx緩存信息的請求被分發(fā)到了第一個節(jié)點象颖,客戶端讀取xxx時佩厚,請求又被分發(fā)到了第三個節(jié)點上,那么顯然是讀不到之前的緩存说订。而且最關(guān)鍵的是抄瓦,一般的輪詢策略都是需要基于集群的節(jié)點數(shù)量進行請求分發(fā)的,因此集群中的節(jié)點一旦出現(xiàn)伸縮陶冷,最終會導(dǎo)致所有緩存內(nèi)容全部失效钙姊。
? ?就拿最基本的取模輪詢來說,原本集群是3個節(jié)點埂伦,所以是基于取模3去分發(fā)請求煞额,結(jié)果有臺節(jié)點宕機了,成為了取模2,那最后整個緩存系統(tǒng)分發(fā)請求完全亂套.....

如果采用隨機策略.....膊毁,更不靠譜.....

因此在這種需求背景下胀莹,大名鼎鼎的一致性哈希算法問世了,一致性哈希算法其實也使用的取模方式婚温,只是描焰,剛才描述的取模輪詢法是對服務(wù)器的數(shù)量進行取模,而一致性哈希算法是對2^32取模栅螟,什么意思呢荆秦?我們一點點來講。

3.2嵌巷、一致性哈希核心-哈希環(huán)

? ?實現(xiàn)一致性哈希算法的核心結(jié)構(gòu)在于哈希環(huán)萄凤,前面講到過一致性哈希是基于2^32做取模,那么首先可以將二的三十二次方想象成一個圓搪哪,這個圓總共由2^32個點組成靡努,如下:

哈希環(huán)-V1

圓環(huán)的正上方第一個點代表00右側(cè)的點按照1晓折、2惑朦、3、4....的順序依此類推漓概,直到2^32-1漾月,也就是說0左側(cè)的第一個點代表著2^32-1

最終這個在邏輯上由2^32個點組成的圓胃珍,被稱為哈希環(huán)梁肿。

結(jié)合之前的緩存案例,假設(shè)有四臺緩存服務(wù)器A觅彰、B吩蔑、C、D填抬,然后再通過每臺服務(wù)器的IP哈希值取模2^32烛芬,最終必然會得到一個2^32范圍之內(nèi)的整數(shù),這個數(shù)在哈希環(huán)上定然也對應(yīng)著一個點飒责,那么每臺服務(wù)器的IP就可以映射到哈希環(huán)上赘娄,如下:

哈希環(huán)-V2

到此時,服務(wù)器已經(jīng)和哈希環(huán)建立起了聯(lián)系宏蛉,那么此時當客戶端發(fā)送請求時遣臼,又可以通過相同的計算方式,將客戶端需要操作的緩存Key進行相同的哈希取模拾并,然后同樣將其映射到哈希環(huán)上暑诸,例如寫入一條緩存name=竹子蚌讼,如下:
哈希環(huán)-V3

那么此時該緩存糾結(jié)要落入到哪臺服務(wù)器呢?答案是B个榕,為什么篡石?因為在哈希環(huán)結(jié)構(gòu)中,沿著順時針方向走西采,遇到的第一臺服務(wù)器是B凰萨,所以最終會落到B服務(wù)器上。

當然械馆,如果一致性哈希算法被用于請求分發(fā)胖眷,那么就以用戶的IP作為哈希取模的條件,這樣就能確保同一個客戶端的所有請求都會被分發(fā)到同一臺服務(wù)器霹崎。

一致性哈希算法中珊搀,就利用哈希環(huán)結(jié)構(gòu)+哈希取模判斷每個請求該落入的服務(wù)器,由于服務(wù)器IP尾菇、客戶端IP或緩存的Key都是相同的境析,所以在服務(wù)器數(shù)量不變的情況,相同的哈希條件進行哈希取模派诬,最終計算出來的值永遠都是相同的劳淆,然后再通過計算出的值,在哈希環(huán)結(jié)構(gòu)上進行順時針查找默赂,能夠定位到的服務(wù)器也是相同的沛鸵,所以相同屬性的請求永遠會落入到同一服務(wù)器。

3.3缆八、哈希環(huán)的映射偏移問題

? ?經(jīng)過上述分析后曲掰,好像發(fā)現(xiàn)一致性哈希算法沒啥大毛病,但上述中屬于“理想狀態(tài)”:

理想vs現(xiàn)實

可偏偏理想很豐滿奈辰,現(xiàn)實卻很骨感栏妖,實際映射服務(wù)器IP的過程中,可能會出現(xiàn)如下情況:
映射偏移

? ?由于服務(wù)器IP哈希取模后冯挎,無法確保哈希得到的數(shù)字能夠均勻分布,因此就有可能造成如上情況咙鞍,所有的服務(wù)器IP都被映射在“一塊兒”房官,最終導(dǎo)致A服務(wù)器承載了90%以上的訪問壓力。

3.4续滋、映射偏移造成的宕機連鎖反應(yīng)

? ?接上述翰守,如果服務(wù)器IP映射在哈希環(huán)上出現(xiàn)偏移,在大流量的沖擊下疲酌,這種情況很容易導(dǎo)致整個集群崩塌蜡峰,首先是A扛不住并發(fā)沖擊了袁,宕機下線,緊接著流量交給B湿颅,B也扛不住载绿,接著宕機,然后C.....油航,因此哈希環(huán)映射偏移問題可能會造成的一系列連鎖反應(yīng)崭庸,所以在一致性哈希算法中,為了確保整個集群的健壯性谊囚,提出了一種虛擬節(jié)點的概念來解決此問題怕享。

虛擬節(jié)點其實本質(zhì)上就是真實服務(wù)器節(jié)點的復(fù)制品,虛擬節(jié)點映射的IP都是指向于真實服務(wù)器的镰踏,就類似平時.EXE軟件的快捷方式函筋,現(xiàn)在為QQ創(chuàng)建了一個快捷方式,然后拷貝到了十個不同的目錄下奠伪,但本質(zhì)上這十個快捷方式指向的啟動文件都是相同exe程序跌帐,哈希環(huán)中的虛擬節(jié)點也同理,如下:

虛擬節(jié)點

從上圖中可以看出芳来,A含末、B、C即舌、D四臺服務(wù)器分別都映射出了一個虛擬節(jié)點佣盒,引入虛擬節(jié)點后會明顯感覺出來,原本A服務(wù)器需要承載90%以上的流量顽聂,但此刻映射出的虛擬節(jié)點大大減輕了A的壓力肥惭,將流量均攤到了集群中的每個節(jié)點。

在一致性哈希算法的實際應(yīng)用場景中紊搪,絕非只映射一個虛擬節(jié)點蜜葱,往往會為一個真實節(jié)點映射數(shù)十個虛擬節(jié)點,以便于減小哈希環(huán)偏移所帶來的影響耀石。同時牵囤,虛擬節(jié)點的數(shù)量越多,請求在分發(fā)時也能更均勻的分布滞伟,哈希環(huán)最終結(jié)構(gòu)如下:

哈希環(huán)最終態(tài)

3.5揭鳞、Java實現(xiàn)一致性哈希算法

? ?講了這么多,那么一致性哈希算法究竟如何實現(xiàn)呢梆奈?接下來一起看看:

public class Servers {
    public static List<String> SERVERS = Arrays.asList(
            "44.120.110.001:8080",
            "44.120.110.002:8081",
            "44.120.110.003:8082",
            "44.120.110.004:8083",
            "44.120.110.005:8084"
    );
}

public class ConsistentHash {
    // 使用有序的紅黑樹結(jié)構(gòu)野崇,用于實現(xiàn)哈希環(huán)結(jié)構(gòu)
    private static TreeMap<Integer,String> virtualNodes = new TreeMap<>();
    // 每個真實節(jié)點的虛擬節(jié)點數(shù)量
    private static final int VIRTUAL_NODES = 160;

    static {
        // 對每個真實節(jié)點添加虛擬節(jié)點,虛擬節(jié)點會根據(jù)哈希算法進行散列
        for (String serverIP : Servers.SERVERS) {
            // 將真實節(jié)點的IP映射到哈希環(huán)上
            virtualNodes.put(getHashCode(serverIP), serverIP);
            // 根據(jù)設(shè)定的虛擬節(jié)點數(shù)量進行虛擬節(jié)點映射
            for (int i = 0; i < VIRTUAL_NODES; i++){
                // 計算出一個虛擬節(jié)點的哈希值(只要不同即可)
                int hash = getHashCode(serverIP + i);
                // 將虛擬節(jié)點添加到哈希環(huán)結(jié)構(gòu)上
                virtualNodes.put(hash, serverIP);
            }
        }
    }

    public static String getServer(String IP){
        int hashCode = getHashCode(IP);
        // 得到大于該Hash值的子紅黑樹
        SortedMap<Integer, String> sortedMap = virtualNodes.tailMap(hashCode);
        // 得到該樹的第一個元素亩钟,也就是最小的元素
        Integer treeNodeKey = sortedMap.firstKey();
        // 如果沒有大于該元素的子樹了乓梨,則取整棵樹的第一個元素鳖轰,相當于取哈希環(huán)中的最小元素
        if (sortedMap == null)
            treeNodeKey = virtualNodes.firstKey();
        // 返回對應(yīng)的虛擬節(jié)點名稱
        return virtualNodes.get(treeNodeKey);
    }

    // 哈希方法:用于計算一個IP的哈希值
    public static int getHashCode(String IP){
        final int p = 1904390101;
        int hash = (int)1901102097L;
        for (int i = 0; i < IP.length(); i++)
            hash = (hash ^ IP.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;

        // 如果算出來的值為負數(shù)則取其絕對值
        if (hash < 0)
            hash = Math.abs(hash);
        return hash;
    }

    public static void main(String[] args){
        // 用for循環(huán)模擬五個不同的IP訪問
        for (int i = 1; i <= 5; i++){
            System.out.println("第"+ i + "個請求:" + getServer("192.168.12.13"+i));
        }
        System.out.println("-----------------------------");
        // 用for循環(huán)模擬三個相同的IP訪問
        for (int i = 1; i <= 3; i++){
            System.out.println("第"+ i + "個請求:" + getServer("192.168.12.131"));
        }
    }
}

/********輸出結(jié)果*******/
第1個請求:44.120.110.002:8081
第2個請求:44.120.110.003:8082
第3個請求:44.120.110.004:8083
第4個請求:44.120.110.003:8082
第5個請求:44.120.110.004:8083
-----------------------------
第1個請求:44.120.110.002:8081
第2個請求:44.120.110.002:8081
第3個請求:44.120.110.002:8081

上述便是Java實現(xiàn)一致性哈希算法的全過程,其實并不難理解扶镀,里面用到了TreeMap實現(xiàn)了哈希環(huán)結(jié)構(gòu)蕴侣,并且指定了每個服務(wù)器節(jié)點的虛擬節(jié)點數(shù)量,同時實現(xiàn)了一個簡單的哈希方法狈惫,用于計算入?yún)⒌墓V稻χ耄惴ㄟ^程如下:

  • ①啟動時先根據(jù)指定的數(shù)量,映射對應(yīng)的虛擬節(jié)點數(shù)量在哈希環(huán)上胧谈。
  • ②通過計算客戶端哈希值忆肾,然后在哈希環(huán)上取得大于該值的節(jié)點,然后返回對應(yīng)的IP菱肖。
    • 由于哈希環(huán)是取順時針方向的第一個節(jié)點作為處理請求的目標服務(wù)器客冈,所以獲取大于該哈希值的節(jié)點中的第一個節(jié)點即可。
  • ③如果哈希環(huán)中沒有大于客戶端哈希值的節(jié)點稳强,那么則將這些客戶端的請求分發(fā)到整個Map上的第一臺服務(wù)器场仲,從此實現(xiàn)哈希閉環(huán)。

一致性哈希算法由于其特性退疫,因此一般多被用于分布式緩存中的集群分片渠缕,尤其是MemCache的緩存分片,就是采用一致性哈希算法實現(xiàn)的褒繁,而Redis自身推出的RedisCluster分片集群中亦鳞,也借用了一致性哈希算法的思想,不過進行了改版實現(xiàn)棒坏,內(nèi)部采用CRC16+HashSolt實現(xiàn)了緩存分片燕差,但核心思想也是相同的。

當然坝冕,文中給出的算法過程都是較為簡單的實現(xiàn)徒探,如若想要參考完整的實現(xiàn),可以參考Dubbocom.alibaba.dubbo.rpc.cluster.loadbalance包喂窟,或參考SpringCloudRibboncom.netflix.loadbalancer包下的實現(xiàn)测暗。

四、最小活躍數(shù)算法

? ?上述分析的基本算法磨澡、平滑輪詢加權(quán)爪膊、一致性哈希等算法都屬于靜態(tài)算法追逮,也就是說這些算法配置后寓辱,并不會根據(jù)線上的實際運行情況進行調(diào)整勤家,只會根據(jù)已配置的規(guī)則進行請求分發(fā)吠架。
? ?最小活躍數(shù)算法則會根據(jù)線上的實際情況進行分發(fā)器躏,能夠靈活的檢測出集群中各個節(jié)點的狀態(tài)波俄,能夠自動尋找并調(diào)用活躍度最低的節(jié)點處理請求侥加,Java實現(xiàn)如下:

// 節(jié)點類:用于封裝集群中的每個節(jié)點
public class Server {
    private String IP;
    private AtomicInteger active;
//    private Integer weight;

    public Server(){}
    public Server(String IP,int active) {
        this.IP = IP;
        // 將外部傳遞的活躍數(shù)作為默認活躍數(shù)
        this.active = new AtomicInteger(active);
    }

    public String getIP() {
        // 每分發(fā)一個請求時自增一次活躍數(shù)
        active.incrementAndGet();
        return IP;
    }

    public AtomicInteger getActive() {
        return active;
    }
}

// 集群類:用于模擬集群節(jié)點列表
public class Servers {
    // 活躍度衰減器
    public static void attenuator(){
        new Thread(()->{
            // 遍歷集群中的所有節(jié)點
            for (Server server : Servers.SERVERS) {
                // 如果活躍度不為0
                if (server.getActive().get() != 0){
                    // 則自減一個活躍度
                    server.getActive().getAndDecrement();
                }
            }
            try {
                // 每隔 2 秒中衰減一次活躍度
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

    // 模擬的集群節(jié)點信息,活躍數(shù)最開始默認為0
    public static List<Server> SERVERS = Arrays.asList(
            new Server("44.120.110.001:8080",0),
            new Server("44.120.110.002:8081",0),
            new Server("44.120.110.003:8082",0)
    );
}

// 最小活躍數(shù)算法實現(xiàn)類
public class LeastActive {

    public static String getServer(){
        // 初始化最小活躍數(shù)和最小活躍數(shù)的節(jié)點
        int leastActive = Integer.MAX_VALUE;
        Server leastServer = new Server();
        // 遍歷集群中的所有節(jié)點
        for (Server server : Servers.SERVERS) {
            // 找出活躍數(shù)最小的節(jié)點
            if (leastActive > server.getActive().get()){
                leastActive = server.getActive().get();
                leastServer = server;
            }
        }

        // 返回活躍數(shù)最小的節(jié)點IP
        return leastServer.getIP();
    }

    public static void main(String[] args){
        Servers.attenuator();
        for (int i = 1; i <= 10; i++){
            System.out.println("第"+ i + "個請求:" + getServer());
        }
    }
}

/********運行結(jié)果*********/
第1個請求:44.120.110.001:8080
第2個請求:44.120.110.002:8081
第3個請求:44.120.110.003:8082
第4個請求:44.120.110.001:8080
第5個請求:44.120.110.002:8081
第6個請求:44.120.110.003:8082
第7個請求:44.120.110.001:8080
第8個請求:44.120.110.002:8081
第9個請求:44.120.110.003:8082
第10個請求:44.120.110.001:8080

觀察如上案例的運行結(jié)果弃锐,似乎結(jié)果好像是輪詢的效果呀袄友?確實是的,這是因為在最開始霹菊,所有節(jié)點的活躍數(shù)都為0剧蚣,三個節(jié)點的活躍數(shù)都相同,所以默認會先取集群中的第一個活躍數(shù)為0的節(jié)點處理請求旋廷,第一個節(jié)點的活躍數(shù)會變成1鸠按,第二次請求時最小活躍數(shù)也為0,然后取第二個節(jié)點處理請求饶碘,依此類推......

在線上環(huán)境下目尖,不會出現(xiàn)輪詢的效果,因為每臺服務(wù)器隨著運行時間的增長扎运,活躍數(shù)必然會不同瑟曲,因此該算法總會取活躍數(shù)最小的節(jié)點提供服務(wù)。

當然豪治,上述案例中實現(xiàn)的最小活躍數(shù)洞拨,是比較簡易的版本,對于完善的實現(xiàn)可以參考Dubbo框架中的com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance類负拟,其中也實現(xiàn)了權(quán)重機制烦衣,簡單闡述一下其中的原理實現(xiàn):

  • ①先從注冊中心中拉取所有的服務(wù)實例,然后找出活躍數(shù)最小的節(jié)點齿椅。
  • ②如果只有一個琉挖,那么則直接返回對應(yīng)的實例節(jié)點處理本次請求。
  • ③如果存在多個涣脚,則根據(jù)每個節(jié)點配置的權(quán)重值來決定本次處理請求的具體節(jié)點示辈。
  • ④如果權(quán)重值不同,優(yōu)先選取權(quán)重值最大的實例遣蚀,作為處理本次請求的節(jié)點矾麻。
  • ⑤如果存在相同的最大權(quán)重值,那么則通過隨機的方式選擇一個節(jié)點提供服務(wù)芭梯。

當然险耀,由于需要對每個節(jié)點去實現(xiàn)活躍數(shù)監(jiān)聽,所以在Dubbo框架中玖喘,想要配置最小活躍數(shù)策略甩牺,那么需要首先啟用ActiveLimitFilter記錄每個節(jié)點的活躍數(shù)。

或者也可以參考Ribbon框架com.netflix.loadbalancer包下面的BestAvailableRule最小活躍數(shù)算法實現(xiàn)類累奈。

從最小活躍數(shù)算法特性不難得知贬派,該算法帶來的優(yōu)勢極為明顯急但,永遠都能選取節(jié)點列表中最空閑的那臺服務(wù)器處理請求,從而避免某些負載過高的節(jié)點搞乏,還依舊承擔需要承擔新的流量訪問波桩,造成更大的壓力。

五请敦、最優(yōu)響應(yīng)算法

? ?與前面分析的最小活躍數(shù)算法一樣镐躲,最優(yōu)響應(yīng)算法也是一種動態(tài)算法,但它比最小活躍數(shù)算法更加智能侍筛,因為最小活躍數(shù)算法中萤皂,如果一臺節(jié)點存在故障,導(dǎo)致它自身處理的請求數(shù)比較少匣椰,那么它會遭受最大的訪問壓力敌蚜,這顯然是并不合理的。

最小活躍數(shù)算法就類似于平時的搬磚工作窝爪,誰事情做的最少誰留下來加班弛车,在正常情況下,這種算法都能夠找到“摸魚”最厲害的員工留下來加班蒲每,但如果有一天纷跛,某個員工由于身體出問題了,導(dǎo)致自己做的工作量比較少邀杏,但按照這種算法的邏輯贫奠,依舊會判定為該員工今天最閑,所以留下來加班望蜡。

從上述這個案例中唤崭,大家略微能夠感受出來最小活躍數(shù)算法的不合理性。

而最優(yōu)響應(yīng)算法則更加智能脖律,該算法在開始前谢肾,會對服務(wù)列表中的各節(jié)點發(fā)出一個探測請求(例如Ping或心跳包檢測),然后根據(jù)各節(jié)點的響應(yīng)時間來決定由哪臺服務(wù)器處理客戶端請求小泉,該算法能較好根據(jù)節(jié)點列表中每臺機器的當前運行狀態(tài)分發(fā)請求芦疏,Java實現(xiàn)如下:

public class Servers {
    // 模擬的集群節(jié)點信息,活躍數(shù)最開始默認為0
    public static List<Server> SERVERS = Arrays.asList(
            new Server("44.120.110.001:8080"),
            new Server("44.120.110.002:8081"),
            new Server("44.120.110.003:8082")
    );
}

public class Server {
    private String IP;

    public Server(){}
    public Server(String IP) {
        this.IP = IP;
    }
    public String getIP() {
        return IP;
    }
    public void setIP(String IP){
        this.IP = IP;
    }

    public String ping(){
        // 生成一個1000~3000之間的隨機數(shù)
        int random = ThreadLocalRandom.current().nextInt(1000, 2000);
        try {
            // 隨機休眠一段時間微姊,模擬不同的響應(yīng)速度
            Thread.sleep(random);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 最后返回自身的IP
        return this.IP;
    }
}

public class ResponseTime {
    // 創(chuàng)建一個定長的線程池酸茴,用于去執(zhí)行ping任務(wù)
    static ExecutorService pingServerPool = 
        Executors.newFixedThreadPool(Servers.SERVERS.size());

    public static String getServer() throws InterruptedException {
        // 創(chuàng)建一個CompletableFuture用于拼接任務(wù)
        CompletableFuture cfAnyOf;
        // 創(chuàng)建一個接收結(jié)果返回的server節(jié)點對象
        final Server resultServer = new Server();
        // 根據(jù)集群節(jié)點數(shù)量初始化一個異步任務(wù)數(shù)組
        CompletableFuture[] cfs = new CompletableFuture[Servers.SERVERS.size()];

        // 遍歷整個服務(wù)器列表,為每個節(jié)點創(chuàng)建一個ping任務(wù)
        for (Server server : Servers.SERVERS) {
            // 獲取當前節(jié)點在集群列表中的下標
            int index = Servers.SERVERS.indexOf(server);
            // 為每個節(jié)點創(chuàng)建一個ping任務(wù)兢交,并交給pingServerPool線程池執(zhí)行
            CompletableFuture<String> cf =
                    CompletableFuture.supplyAsync(server::ping,pingServerPool);
            // 將創(chuàng)建好的異步任務(wù)加入數(shù)組中
            cfs[index] = cf;
        }

        // 將創(chuàng)建好的多個Ping任務(wù)組合成一個聚合任務(wù)并執(zhí)行
        cfAnyOf = CompletableFuture.anyOf(cfs);

        // 監(jiān)聽執(zhí)行完成后的回調(diào)薪捍,誰先執(zhí)行完成則返回誰
        cfAnyOf.thenAccept(resultIP -> {
             System.out.println("最先響應(yīng)檢測請求的節(jié)點為:" + resultIP);
            resultServer.setIP((String) resultIP);
        });
        //  阻塞主線程一段時間,防止CompletableFuture退出
        Thread.sleep(3000);

        // 返回最先響應(yīng)檢測請求(ping)的節(jié)點作為本次處理客戶端請求的節(jié)點
        return resultServer.getIP();
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 5; i++){
            System.out.println("第"+ i + "個請求:" + getServer());
        }
    }
}

/******運行結(jié)果:******/
最先響應(yīng)檢測請求的節(jié)點為:44.120.110.002:8081
第1個請求:44.120.110.002:8081
最先響應(yīng)檢測請求的節(jié)點為:44.120.110.002:8081
第2個請求:44.120.110.002:8081
最先響應(yīng)檢測請求的節(jié)點為:44.120.110.003:8082
第3個請求:44.120.110.003:8082
最先響應(yīng)檢測請求的節(jié)點為:44.120.110.003:8080
第4個請求:44.120.110.001:8080
最先響應(yīng)檢測請求的節(jié)點為:44.120.110.002:8081
第5個請求:44.120.110.002:8081

在該案例中,其實現(xiàn)過程對比之前的算法略微復(fù)雜一些酪穿,首先在Server實例類中定義了一個Ping()方法与倡,該方法中使用隨機數(shù)+線程休眠的方式簡單模擬了一下節(jié)點的不同的響應(yīng)速度,然后在算法實現(xiàn)類中昆稿,利用CompletableFuture分別對每一個節(jié)點都創(chuàng)建了對應(yīng)的Ping任務(wù),然后同時執(zhí)行息拜,又通過thenAccept()回調(diào)方法監(jiān)聽了執(zhí)行結(jié)果溉潭,誰最先響應(yīng),則取其作為處理本次請求的節(jié)點少欺。

這個算法的實現(xiàn)過程中喳瓣,唯一難理解的就是CompletableFuture,它是JDK8中推出的一種異步任務(wù)赞别,具體的可參考之前的并發(fā)文章:《CompletableFuture詳解》畏陕。

這里只是舉例實現(xiàn),所以通過CompletableFuture實現(xiàn)了檢測請求仿滔,但實際過程中如果要選擇這種算法惠毁,那么基于Netty會更為合適。

從上述案例的運行結(jié)果中也可以得知:最優(yōu)響應(yīng)算法無論在何種情況下崎页,都能從集群中選取性能最好的節(jié)點對外服務(wù)鞠绰,Nginx中也支持配置這種算法,但需要先安裝對應(yīng)的nginx-upstream-fair模塊飒焦。

六蜈膨、請求分發(fā)篇總結(jié)

? ?在本文中,對于比較常用的請求分發(fā)算法進行了剖析及手寫實踐牺荠,其中提到了較為傳統(tǒng)的靜態(tài)調(diào)度算法:輪詢翁巍、隨機、加權(quán)休雌、一致性哈希等灶壶,也談到了一些較為智能的動態(tài)算法:最小活躍數(shù)、最優(yōu)響應(yīng)等杈曲,但需要牢記的一點是:

并非越智能的算法越好例朱,越是并發(fā)高、流量大的場景下鱼蝉,反而選用最基本的算法更合適洒嗤,例如微信的紅包業(yè)務(wù),就是采用最基本的輪詢算法進行集群調(diào)度魁亦。

那這又是為何呢渔隶?因為越智能的調(diào)度算法,進行節(jié)點選擇時的開銷會更大,如果你對于文中給出的調(diào)度算法實現(xiàn)都一一運行過间唉,那么大家會明顯感知出:越到后面的算法绞灼,分發(fā)請求的速度越慢。

因此在面臨巨大訪問壓力的情景中呈野,選擇最簡單的算法反而帶來的收益更高低矮,但前提是需要集群中所有的節(jié)點硬件配置都一致,所有節(jié)點分配的資源都相同被冒,輪詢算法則是最佳的調(diào)度算法军掂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市昨悼,隨后出現(xiàn)的幾起案子蝗锥,更是在濱河造成了極大的恐慌,老刑警劉巖率触,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件终议,死亡現(xiàn)場離奇詭異,居然都是意外死亡葱蝗,警方通過查閱死者的電腦和手機穴张,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來两曼,“玉大人陆馁,你說我怎么就攤上這事『嫌” “怎么了叮贩?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長佛析。 經(jīng)常有香客問我益老,道長,這世上最難降的妖魔是什么寸莫? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任捺萌,我火速辦了婚禮,結(jié)果婚禮上膘茎,老公的妹妹穿的比我還像新娘桃纯。我一直安慰自己,他們只是感情好披坏,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布态坦。 她就那樣靜靜地躺著,像睡著了一般棒拂。 火紅的嫁衣襯著肌膚如雪伞梯。 梳的紋絲不亂的頭發(fā)上玫氢,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音谜诫,去河邊找鬼漾峡。 笑死,一個胖子當著我的面吹牛喻旷,可吹牛的內(nèi)容都是我干的生逸。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼且预,長吁一口氣:“原來是場噩夢啊……” “哼槽袄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辣之,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎皱炉,沒想到半個月后怀估,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡合搅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年多搀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灾部。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡康铭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赌髓,到底是詐尸還是另有隱情从藤,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布锁蠕,位于F島的核電站夷野,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏荣倾。R本人自食惡果不足惜悯搔,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舌仍。 院中可真熱鬧妒貌,春花似錦、人聲如沸铸豁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽节芥。三九已至平匈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背增炭。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工忍燥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隙姿。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓梅垄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親输玷。 傳聞我的和親對象是個殘疾皇子队丝,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容