如何用蟻群演算法實現高效的負載均衡調度?

螞蟻幾乎沒有視力,但他們卻能夠在黑暗的世界中找到食物,而且能夠找到一條從洞穴到食物的最短路徑。它們是如何做到的呢?

螞蟻尋找食物的過程

單只螞蟻的行為及其簡單,行為數量在10種以內,但成千上萬隻螞蟻組成的蟻群卻能擁有巨大的智慧,這離不開它們信息傳遞的方式——信息素。

螞蟻在行走過程中會釋放一種稱為「信息素」的物質,用來標識自己的行走路徑。在尋找食物的過程中,根據信息素的濃度選擇行走的方向,並最終到達食物所在的地方。

信息素會隨著時間的推移而逐漸揮發。

在一開始的時候,由於地面上沒有信息素,因此螞蟻們的行走路徑是隨機的。螞蟻們在行走的過程中會不斷釋放信息素,標識自己的行走路徑。隨著時間的推移,有若干只螞蟻找到了食物,此時便存在若干條從洞穴到食物的路徑。由於螞蟻的行為軌跡是隨機分布的,因此在單位時間內,短路徑上的螞蟻數量比長路徑上的螞蟻數量要多,從而螞蟻留下的信息素濃度也就越高。這為後面的螞蟻們提供了強有力的方向指引,越來越多的螞蟻聚集到最短的路徑上去。

什麼是蟻群演算法?

蟻群演算法就是模擬螞蟻尋找食物的過程,它能夠求出從原點出發,經過若干個給定的需求點,最終返回原點的最短路徑。這也就是著名的旅行商問題(Traveling Saleman Problem,TSP)。

本文使用蟻群演算法來解決分散式環境下的負載均衡調度問題。

蟻群演算法的應用——負載均衡調度

集群模式是目前較為常用的一種部署結構,也就是當單機處理能力無法滿足業務需求,那麼就增加處理節點,並由一個負載均衡器負責請求的調度。然而對於一個龐大系統而言,情況往往比較複雜。集群中節點的處理能力往往各不相同,而且不同任務的處理複雜度也不盡相同。那麼負載均衡器如何進行任務分配,使得集群性能達到最優?資源利用率達到最高呢?這是一個極具挑戰又很有價值的問題。

本文我們就採用蟻群演算法來解決這一問題。

數學建模

在開始之前,我們首先需要將「負載均衡調度」這個問題進行數學建模,量化各項指標,並映射到蟻群演算法中。

問題描述

求一種最優的任務分配策略,能夠將N個長度不等的任務按照某一種策略分配給M個處理能力不同的伺服器節點,並且N個任務的完成時間最短。

在這個問題中,我們將所有任務的完成時間作為衡量分配策略優良的指標。每一種分配策略都是這個問題的一個可行解。那麼具有最小完成時間的分配策略就是這個問題的最優解。

參數定義

var tasks = [];nvar taskNum = 100;n

  • tasks:任務數組,數組的下標表示任務的編號,數組的值表示任務的長度。比如:tasks[0]=10表示第一個任務的任務長度是10.
  • taskNum:任務的數量,也就是tasks數組的長度。這裡為了提高代碼的可讀性才專門使用taskNum來表示任務數量。

var nodes = [];nvar nodeNum = 10;n

  • nodes:處理節點的數組。數組的下標表示處理節點的編號,數組值表示節點的處理速度。比如:nodes[0]=10表示第1個處理節點的處理速度為10.
  • nodeNum:處理節點的數量,也就是nodes數組的長度。這裡也是為了提高代碼的可讀性才專門使用nodeNum來表示節點的數量。

var iteratorNum;nvar antNum;n

  • iteratorNum:蟻群演算法一共需要迭代的次數,每次迭代都有antNum只螞蟻進行任務分配。
  • antNum:每次迭代中螞蟻的數量。每隻螞蟻都是一個任務調度者,每次迭代中的每一隻螞蟻都需要完成所有任務的分配,這也就是一個可行解。

var timeMatrix = [];n

  • 任務處理時間矩陣。
    • 它是一個二維矩陣。比如:timeMatrix[i][j]就表示第i個任務分配給第j個節點所需的處理時間。
    • 這個矩陣是基於tasks數組和nodes數組計算而來的。比如task[i]表示第i個任務的任務長度,nodes[j]表示第j個節點的處理速度。所以,timeMatrix[i][j]=task[i]/nodes[j].

var pheromoneMatrix = [];nvar maxPheromoneMatrix = [];nvar criticalPointMatrix = [];n

  • pheromoneMatrix:信息素矩陣
    • 它是一個二維矩陣,用於記錄任務i分配給節點j這條路徑上的信息素濃度。
    • 比如:pheromoneMatrix[i][j]=0.5就表示任務i分配給節點j這條路徑上的信息素濃度為0.5
  • maxPheromoneMatrix:pheromoneMatrix矩陣的每一行中最大信息素的下標。
    • 比如:maxPheromoneMatrix[0]=5表示pheromoneMatrix第0行的所有信息素中,最大信息素的下標是5.
  • criticalPointMatrix:在一次迭代中,採用隨機分配策略的螞蟻的臨界編號。
    • 比如:如果將螞蟻數量設為10,那麼每次迭代中都有10隻螞蟻完成所有任務的分配工作。並且分配過程是按照螞蟻編號從小到大的順序進行的(螞蟻從0開始編號)。如果criticalPointMatrix[0]=5,那麼也就意味著,在分配第0個任務的時候,編號是0~5的螞蟻根據信息素濃度進行任務分配(即:將任務分配給本行中信息素濃度最高的節點處理),6~9號螞蟻則採用隨機分配的方式(即:將任務隨機分配給任意一個節點處理)。
    • 為什麼要這麼做? 如果每隻螞蟻都將任務分配給信息素濃度最高的節點處理,那麼就會出現停滯現象。也就是演算法過早地收斂至一個局部最優解,無法發現全局最優解。 因此需要一部分螞蟻遵循信息素最高的分配策略,還需要一部分螞蟻遵循隨機分配的策略,以發現新的局部最優解。

var p = 0.5;nvar q = 2;n

  • p:每完成一次迭代後,信息素衰減的比例。 我們知道,在真實的蟻群中,螞蟻分泌的信息素會隨著時間的推移而漸漸衰減。那麼在演算法中,我們使得信息素每完成一次迭代後進行衰減,但在一次迭代過程中,信息素濃度保持不變。
  • q:螞蟻每次經過一條路徑,信息素增加的比例。 我們也知道,在真實的蟻群中,螞蟻會在行進過程中分泌信息素。那麼在演算法中,我們使得演算法每完成一次迭代後,就將螞蟻經過的路徑上增加信息素q,但在一次迭代過程中,信息素濃度不變。

演算法初始化

// 初始化任務集合ntasks = initRandomArray(_taskNum, taskLengthRange);nn// 初始化節點集合nnodes = initRandomArray(_nodeNum, nodeSpeendRange);n

在正式開始之前,我們需要初始化任務數組和節點數組。這裡採用隨機賦值的方式,我們給tasks隨機創建100個任務,每個任務的長度是10~100之間的隨機整數。再給nodes隨機創建10個節點,每個節點的處理速度是10~100之間的隨機整數。

OK,準備工作完成,下面來看蟻群演算法的實現。

蟻群演算法

/**n * 蟻群演算法n */nfunction aca() {n // 初始化任務執行時間矩陣n initTimeMatrix(tasks, nodes);nn // 初始化信息素矩陣n initPheromoneMatrix(taskNum, nodeNum);nn // 迭代搜索n acaSearch(iteratorNum, antNum);n}n

正如你所看到的,蟻群演算法並不複雜,總體而言就是這三部:

  • 初始化任務執行時間矩陣
  • 初始化信息素矩陣
  • 迭代搜索

當然,第一第二步都較為簡單,相對複雜的代碼在「迭代搜索」中。那麼下面我們就分別來看一下這三個步驟的實現過程。

初始化任務執行時間矩陣

/**n * 初始化任務處理時間矩陣n * @param tasks 任務(長度)列表n * @param nodes 節點(處理速度)列表n */nfunction initTimeMatrix(tasks, nodes) {n for (var i=0; i<tasks.length; i++) {n // 分別計算任務i分配給所有節點的處理時間n var timeMatrix_i = [];n for (var j=0; j<nodes.length; j++) {n timeMatrix_i.push(tasks[i] / nodes[j]);n }n timeMatrix.push(timeMatrix_i);n }n}n

通過上文的學習我們已經知道,當任務長度數組tasks和節點處理速度數組nodes確定下來後,所有任務的執行時間都是可以確定下來了,用公式tasks[i]/nodes[j]計算一下即可,也就是「時間=長度/速度」,小學數學知識。OK,那麼timeMatrix矩陣的計算也就是這樣。

這裡再次介紹下timeMatrix矩陣的含義:timeMatrix[i][j]表示任務i分配給節點j處理所需要的時間,其計算公式也就是:

timeMatrix[i][j] = tasks[i]/nodes[j]n

初始化信息素矩陣

/**n * 初始化信息素矩陣(全為1)n * @param taskNum 任務數量n * @param nodeNum 節點數量n */nfunction initPheromoneMatrix(taskNum, nodeNum) {n for (var i=0; i<taskNum; i++) {n var pheromoneMatrix_i = [];n for (var j=0; j<nodeNum; j++) {n pheromoneMatrix_i.push(1);n }n pheromoneMatrix.push(pheromoneMatrix_i);n }n}n

初始化信息素矩陣也就是將信息素矩陣中所有元素置為1.

這裡再次重申一下信息素矩陣的含義,pheromoneMatrix[i][j]表示將任務i分配給節點j這條路徑的信息素濃度。

注意:我們將負載均衡調度過程中的一次任務分配當作蟻群演算法中一條路徑。如:我們將「任務i分配給節點j」這一動作,當作螞蟻從任務i走向節點j的一條路徑。因此,pheromoneMatrix[i][j]就相當於i——>j這條路徑上的信息素濃度。

迭代搜索過程

/**n * 迭代搜索n * @param iteratorNum 迭代次數n * @param antNum 螞蟻數量n */nfunction acaSearch(iteratorNum, antNum) {n for (var itCount=0; itCount<iteratorNum; itCount++) {n // 本次迭代中,所有螞蟻的路徑n var pathMatrix_allAnt = [];nn for (var antCount=0; antCount<antNum; antCount++) {n // 第antCount只螞蟻的分配策略(pathMatrix[i][j]表示第antCount只螞蟻將i任務分配給j節點處理)n var pathMatrix_oneAnt = initMatrix(taskNum, nodeNum, 0);n for (var taskCount=0; taskCount<taskNum; taskCount++) {n // 將第taskCount個任務分配給第nodeCount個節點處理n var nodeCount = assignOneTask(antCount, taskCount, nodes, pheromoneMatrix);n pathMatrix_oneAnt[taskCount][nodeCount] = 1;n }n // 將當前螞蟻的路徑加入pathMatrix_allAntn pathMatrix_allAnt.push(pathMatrix_oneAnt);n }nn // 計算 本次迭代中 所有螞蟻 的任務處理時間n var timeArray_oneIt = calTime_oneIt(pathMatrix_allAnt);n // 將本地迭代中 所有螞蟻的 任務處理時間加入總結果集n resultData.push(timeArray_oneIt);nn // 更新信息素n updatePheromoneMatrix(pathMatrix_allAnt, pheromoneMatrix, timeArray_oneIt);n }n}n

這個過程略微複雜,但也還好,且聽我一一道來。

在整個蟻群演算法中,一共要進行iteratorNum次迭代。每一次迭代都會產生當前的最優分配策略,也就是「局部最優解」。迭代的次數越多,那麼局部最優解就越接近於全局最優解。但是,迭代次數過多會造成負載均衡器大量的時間和性能上的開銷,從而無法滿足海量任務的調度。但迭代次數太少了,可能得到的並不是全局最優解。那麼這個問題如何解決呢?有兩種辦法:

  1. 限定迭代次數 為了避免過多的迭代,我們可以事先設置一個迭代次數,從而迭代了這麼多次後,就把當前的局部最優解當作全局最優解。
  2. 設置誤差允許範圍 我們還可以事先設置一個允許的誤差範圍。當迭代N此後,當前最優的任務處理時間在這個允許範圍之內了,那麼就停止迭代。

這兩種方式各有千秋,我們這裡選擇第一種——限定迭代次數。並且將迭代次數限定為1000次。

注意:收斂速度也是衡量演算法優良的一個重要指標。比如演算法1迭代10次就能找到全局最優解,而演算法2迭代1000次才能找到全局最優解。所以演算法1的收斂速度要優於演算法2.

下面介紹上述演算法的執行流程。

蟻群演算法一共要進行iteratorNum次迭代,每次迭代中,所有螞蟻都需要完成所有任務的分配。因此上述演算法採用了三層for循環,第一層用於迭代次數的循環,在本演算法中一共要循環1000次;第二層用於螞蟻的循環,本演算法一共有10隻螞蟻,因此需要進行10次循環;第三層用於所有任務的循環,本演算法一共有100個任務,因此需要循環100次,每一次循環,都將當前任務按照某一種策略分配給某一個節點,並在pathMatrix_oneAnt矩陣中記錄螞蟻的分配策略。

pathMatrix_oneAnt是一個二維矩陣,所有元素要麼是0要麼是1.比如:pathMatrix_oneAnt[i][j]=1就表示當前螞蟻將任務i分配給了節點j處理,pathMatrix_oneAnt[i][j]=0表示任務i沒有分配給節點j處理。該矩陣的每一行都有且僅有一個元素為1,其他元素均為0.

每一隻螞蟻當完成這100個任務的分配之後,就會產生一個pathMatrix_oneAnt矩陣,用於記錄該只螞蟻的分配策略。那麼當10隻螞蟻均完成任務的分配後,就會產生一個pathMatrix矩陣。這是一個三維矩陣,第一維記錄了螞蟻的編號,第二維表示任務的下標,第三維表示節點的編號,從而pathMatrix[x][i][j]=1就表示編號為x的螞蟻將任務i分配給了節點j處理;pathMatrix[x][i][j]=0就表示編號為x的螞蟻沒有將任務i分配給了節點j處理。

這10隻螞蟻完成一次任務的分配也被稱為一次迭代。每完成一次迭代後,都要使用calTime_oneIt函數在計算本次迭代中,所有螞蟻的任務處理時間,並記錄在timeArray_oneIt矩陣中。

在每次迭代完成前,還需要使用updatePheromoneMatrix函數來更新信息素矩陣。

下面就分別詳細介紹迭代搜索過程中的三個重要函數:

  • 任務分配函數:assignOneTask
  • 任務處理時間計算函數:calTime_oneIt
  • 更新信息素函數:updatePheromoneMatrix

任務分配函數

/**n * 將第taskCount個任務分配給某一個節點處理n * @param antCount 螞蟻編號n * @param taskCount 任務編號n * @param nodes 節點集合n * @param pheromoneMatrix 信息素集合n */nfunction assignOneTask(antCount, taskCount, nodes, pheromoneMatrix) {nn // 若當前螞蟻編號在臨界點之前,則採用最大信息素的分配方式n if (antCount <= criticalPointMatrix[taskCount]) {n return maxPheromoneMatrix[taskCount];n }nn // 若當前螞蟻編號在臨界點之後,則採用隨機分配方式n return random(0, nodeNum-1);n}n

任務分配函數負責將一個指定的任務按照某種策略分配給某一節點處理。分配策略一共有兩種:

  1. 按信息素濃度分配 也就是將任務分配給本行中信息素濃度最高的節點處理。比如:當前的任務編號是taskCount,當前的信息素濃度矩陣是pheromoneMatrix,那麼任務將會分配給pheromoneMatrix[taskCount]這一行中信息素濃度最高的節點。
  2. 隨機分配 將任務隨意分配給某一個節點處理。

那麼,這兩種分配策略究竟如何選擇呢?答案是——根據當前螞蟻的編號antCount。

通過上文可知,矩陣criticalPointMatrix用於記錄本次迭代中,採用不同分配策略的螞蟻編號的臨界點。比如:criticalPointMatrix[i]=5就表示編號為0~5的螞蟻在分配任務i的時候採用「按信息素濃度」的方式分配(即:將任務i分配給信息素濃度最高的節點處理);而編號為6~9的螞蟻在分配任務i時,採用隨機分配策略。

計算任務處理時間

/**n * 計算一次迭代中,所有螞蟻的任務處理時間n * @param pathMatrix_allAnt 所有螞蟻的路徑n */nfunction calTime_oneIt(pathMatrix_allAnt) {n var time_allAnt = [];n for (var antIndex=0; antIndex<pathMatrix_allAnt.length; antIndex++) {n // 獲取第antIndex只螞蟻的行走路徑n var pathMatrix = pathMatrix_allAnt[antIndex];nn // 獲取處理時間最長的節點 對應的處理時間n var maxTime = -1;n for (var nodeIndex=0; nodeIndex<nodeNum; nodeIndex++) {n // 計算節點taskIndex的任務處理時間n var time = 0;n for (var taskIndex=0; taskIndex<taskNum; taskIndex++) {n if (pathMatrix[taskIndex][nodeIndex] == 1) {n time += timeMatrix[taskIndex][nodeIndex];n }n }n // 更新maxTimen if (time > maxTime) {n maxTime = time;n }n }nn time_allAnt.push(maxTime);n }n return time_allAnt;n}n

每完成一次迭代,都需要計算本次迭代中所有螞蟻的行走路徑(即:所有螞蟻的任務處理之間),並記錄在time_allAnt矩陣中。

在實際的負載均衡調度中,各個節點的任務處理是並行計算的,所以,所有任務的完成時間應該是所有節點任務完成時間的最大值,並非所有任務完成時間的總和。

每完成一次迭代,就會產生一個time_allAnt矩陣,並且加入resultData矩陣中。當演算法完成所有迭代後,所有螞蟻的所有任務處理時間都被記錄在resultData矩陣中,它是一個二維矩陣。比如:resultData[x][y]=10代表第x次迭代中第y只螞蟻的任務處理時間是10.

更新信息素

/**n * 更新信息素n * @param pathMatrix_allAnt 本次迭代中所有螞蟻的行走路徑n * @param pheromoneMatrix 信息素矩陣n * @param timeArray_oneIt 本次迭代的任務處理時間的結果集n */nfunction updatePheromoneMatrix(pathMatrix_allAnt, pheromoneMatrix, timeArray_oneIt) {n // 所有信息素均衰減p%n for (var i=0; i<taskNum; i++) {n for (var j=0; j<nodeNum; j++) {n pheromoneMatrix[i][j] *= p;n }n }nn // 找出任務處理時間最短的螞蟻編號n var minTime = Number.MAX_VALUE;n var minIndex = -1;n for (var antIndex=0; antIndex<antNum; antIndex++) {n if (timeArray_oneIt[antIndex] < minTime) {n minTime = timeArray_oneIt[antIndex];n minIndex = antIndex;n }n }nn // 將本次迭代中最優路徑的信息素增加q%n for (var taskIndex=0; taskIndex<taskNum; taskIndex++) {n for (var nodeIndex=0; nodeIndex<nodeNum; nodeIndex++) {n if (pathMatrix_allAnt[minIndex][taskIndex][nodeIndex] == 1) {n pheromoneMatrix[taskIndex][nodeIndex] *= q;n }n }n }nn maxPheromoneMatrix = [];n criticalPointMatrix = [];n for (var taskIndex=0; taskIndex<taskNum; taskIndex++) {n var maxPheromone = pheromoneMatrix[taskIndex][0];n var maxIndex = 0;n var sumPheromone = pheromoneMatrix[taskIndex][0];n var isAllSame = true;nn for (var nodeIndex=1; nodeIndex<nodeNum; nodeIndex++) {n if (pheromoneMatrix[taskIndex][nodeIndex] > maxPheromone) {n maxPheromone = pheromoneMatrix[taskIndex][nodeIndex];n maxIndex = nodeIndex;n }nn if (pheromoneMatrix[taskIndex][nodeIndex] != pheromoneMatrix[taskIndex][nodeIndex-1]){n isAllSame = false;n }nn sumPheromone += pheromoneMatrix[taskIndex][nodeIndex];n }nn // 若本行信息素全都相等,則隨機選擇一個作為最大信息素n if (isAllSame==true) {n maxIndex = random(0, nodeNum-1);n maxPheromone = pheromoneMatrix[taskIndex][maxIndex];n }nn // 將本行最大信息素的下標加入maxPheromoneMatrixn maxPheromoneMatrix.push(maxIndex);nn // 將本次迭代的螞蟻臨界編號加入criticalPointMatrix(該臨界點之前的螞蟻的任務分配根據最大信息素原則,而該臨界點之後的螞蟻採用隨機分配策略)n criticalPointMatrix.push(Math.round(antNum * (maxPheromone/sumPheromone)));n }n}n

每完成一次迭代,都需要更新信息素矩陣,這個函數的包含了如下四步:

  1. 將所有信息素濃度降低p% 這個過程用來模擬信息素的揮發。
  2. 找出本次迭代中最短路徑,並將該條路徑的信息素濃度提高q% 每次迭代,10隻螞蟻就會產生10條路徑(即10種任務分配策略),我們需要找出最短路徑,並將該條路徑的信息素濃度提高。
  3. 更新maxPheromoneMatrix矩陣 步驟1和步驟2完成後,信息素矩陣已經更新完畢。接下來需要基於這個最新的信息素矩陣,計算每行最大信息素對應的下標,即:maxPheromoneMatrix矩陣。通過上文可知,該矩陣供函數assignOneTask在分配任務時使用。
  4. 更新criticalPointMatrix矩陣 緊接著需要更新criticalPointMatrix矩陣,記錄採用何種任務分配策略的螞蟻臨界編號。 比如:信息素矩陣第0行的元素為pheromoneMatrix[0]={1,3,1,1,1,1,1,1,1,1},那麼criticalPointMatrix[0]的計算方式如下:
  • 計算最大信息素的概率:最大信息素/該行所有信息素之和
    • 3/(1+3+1+1+1+1+1+1+1+1)=0.25
  • 計算螞蟻的臨界下標:螞蟻數量*最大信息素的概率
    • 10*0.25=3(四捨五入)
  • 所以criticalPointMatrix[0]=3
    • 也就意味著在下一次迭代過程中,當分配任務0時,0~3號螞蟻將該任務分配給信息素濃度最高的節點,而4~9號螞蟻採用隨機分配策略。

結果分析

演算法的運行結果如下圖所示:

橫坐標為迭代次數,縱坐標為任務處理時間。 每個點表示一隻螞蟻的任務處理時間。上圖的演算法的迭代次數為100,螞蟻數量為1000,所以每次迭代都會產生1000種任務分配方案,而每次迭代完成後都會挑選出一個當前最優方案,並提升該方案的信息素濃度,從而保證在下一次迭代中,選擇該方案的概率較高。並且還使用一定概率的螞蟻採用隨機分配策略,以發現更優的方案。

從圖中我們可以看到,大約迭代30次時,出現了全局最優解。

寫在最後

所有代碼我已經上傳至我的Github,大家可以隨意下載。 github.com/bz51/AntColo

上面一共有兩個問題:

  • aca.html
  • aca.js

蟻群演算法的實現均在aca.js中,你把代碼down下來之後直接在瀏覽器打開aca.html即可查看。歡迎各位star。也歡迎關注我的公眾號,我定期分享技術乾貨的地方~

推薦閱讀:

高可用架構:CLB實現跨園區容災和升級不停服
在 Linux 上簡單模擬系統負載的方法
網路爬蟲之Splash負載均衡配置
nginx快速入門之配置篇
什麼是負載均衡?

TAG:蚁群算法 | 负载均衡 | 算法 |