關於負載均衡的一切:總結與思考

 

  古人云,不患寡而患不均。html

  在計算機的世界,這就是你們耳熟能詳的負載均衡(load balancing),所謂負載均衡,就是說若是一組計算機節點(或者一組進程)提供相同的(同質的)服務,那麼對服務的請求就應該均勻的分攤到這些節點上。負載均衡的前提必定是「provide a single Internet service from multiple servers」, 這些提供服務的節點被稱之爲server farm、server pool或者backend servers。nginx

  這裏的服務是廣義的,能夠是簡單的計算,也多是數據的讀取或者存儲。負載均衡也不是新事物,這種思想在多核CPU時代就有了,只不過在分佈式系統中,負載均衡更是無處不在,這是分佈式系統的自然特性決定的,分佈式就是利用大量計算機節點完成單個計算機沒法完成的計算、存儲服務,既然有大量計算機節點,那麼均衡的調度就很是重要。git

  負載均衡的意義在於,讓全部節點以最小的代價、最好的狀態對外提供服務,這樣系統吞吐量最大,性能更高,對於用戶而言請求的時間也更小。並且,負載均衡加強了系統的可靠性,最大化下降了單個節點過載、甚至crash的機率。不難想象,若是一個系統絕大部分請求都落在同一個節點上,那麼這些請求響應時間都很慢,並且萬一節點降級或者崩潰,那麼全部請求又會轉移到下一個節點,形成雪崩。github

  事實上,網上有不少文章介紹負載均衡的算法,大多都是大同小異。本文更多的是本身對這些算法的總結與思考。web

  本文地址:http://www.cnblogs.com/xybaby/p/7867735.html算法

一分鐘瞭解負載均衡的一切

  本章節的標題和內容都來自一分鐘瞭解負載均衡的一切這一篇文章。固然,原文的標題是誇張了點,不過文中列出了在一個大型web網站中各層是如何用到負載均衡的,一目瞭然。數據庫

  

  常見互聯網分佈式架構如上,分爲客戶端層、反向代理nginx層、站點層、服務層、數據層。能夠看到,每個下游都有多個上游調用,只須要作到,每個上游都均勻訪問每個下游,就能實現「將請求/數據【均勻】分攤到多個操做單元上執行」。安全

  (1)【客戶端層】到【反向代理層】的負載均衡,是經過「DNS輪詢」實現的
  (2)【反向代理層】到【站點層】的負載均衡,是經過「nginx」實現的
  (3)【站點層】到【服務層】的負載均衡,是經過「服務鏈接池」實現的
  (4)【數據層】的負載均衡,要考慮「數據的均衡」與「請求的均衡」兩個點,常見的方式有「按照範圍水平切分」與「hash水平切分」。服務器

  數據層的負載均衡,在我以前的《帶着問題學習分佈式系統之數據分片》中有詳細介紹。cookie

算法衡量

  在我看來,當咱們提到一個負載均衡算法,或者具體的應用場景時,應該考慮如下問題

  第一,是否意識到不一樣節點的服務能力是不同的,好比CPU、內存、網絡、地理位置

  第二,是否意識到節點的服務能力是動態變化的,高配的機器也有可能因爲一些突發緣由致使處理速度變得很慢

  第三,是否考慮將同一個客戶端,或者說一樣的請求分發到同一個處理節點,這對於「有狀態」的服務很是重要,好比session,好比分佈式存儲

  第四,誰來負責負載均衡,即誰充當負載均衡器(load balancer),balancer自己是否會成爲瓶頸

  下面會結合具體的算法來考慮這些問題

負載均衡算法

輪詢算法(round-robin)

  思想很簡單,就是提供同質服務的節點逐個對外提供服務,這樣能作到絕對的均衡。Python示例代碼以下

 1 SERVER_LIST = [
 2     '10.246.10.1',
 3     '10.246.10.2',
 4     '10.246.10.3',
 5 ]
 6 def round_robin(server_lst, cur = [0]):
 7     length = len(server_lst)
 8     ret = server_lst[cur[0] % length]
 9     cur[0] = (cur[0] + 1) % length
10     return ret

  能夠看到,全部的節點都是以一樣的機率提供服務,即沒有考慮到節點的差別,也許一樣數目的請求,高配的機器CPU才20%,低配的機器CPU已經80%了

加權輪詢算法(weight round-robin)

  加權輪訓算法就是在輪訓算法的基礎上,考慮到機器的差別性,分配給機器不一樣的權重,能者多勞。注意,這個權重的分配依賴於請求的類型,好比計算密集型,那就考慮CPU、內存;若是是IO密集型,那就考慮磁盤性能。Python示例代碼以下

 1 WEIGHT_SERVER_LIST = {
 2     '10.246.10.1': 1,
 3     '10.246.10.2': 3,
 4     '10.246.10.3': 2,
 5 }
 6 
 7 def weight_round_robin(servers, cur = [0]):
 8     weighted_list = []
 9     for k, v in servers.iteritems():
10         weighted_list.extend([k] * v)
11 
12     length = len(weighted_list)
13     ret = weighted_list[cur[0] % length]
14     cur[0] = (cur[0] + 1) % length
15     return ret

 

隨機算法(random)

  這個就更好理解了,隨機選擇一個節點服務,按照機率,只要請求數量足夠多,那麼也能達到絕對均衡的效果。並且實現簡單不少

1 def random_choose(server_lst):
2     import random
3     random.seed()
4     return random.choice(server_lst)

 

加權隨機算法(random)

  如同加權輪訓算法至於輪訓算法同樣,也是在隨機的時候引入不一樣節點的權重,實現也很相似。

def weight_random_choose(servers):
    import random
    random.seed()
    weighted_list = []
    for k, v in servers.iteritems():
        weighted_list.extend([k] * v)
    return random.choice(weighted_list)

 

  固然,若是節點列表以及權重變化不大,那麼也能夠對全部節點歸一化,而後按機率區間選擇

 1 def normalize_servers(servers):
 2     normalized_servers = {}
 3     total = sum(servers.values())
 4     cur_sum = 0
 5     for k, v in servers.iteritems():
 6         normalized_servers[k] = 1.0 * (cur_sum + v) / total
 7         cur_sum += v
 8     return normalized_servers
 9 
10 def weight_random_choose_ex(normalized_servers):
11     import random, operator
12     random.seed()
13     rand = random.random()
14     for k, v in sorted(normalized_servers.iteritems(), key = operator.itemgetter(1)):
15         if v >= rand:
16             return k
17     else:
18         assert False, 'Error normalized_servers with rand %s ' % rand 

 

哈希法(hash)

  根據客戶端的IP,或者請求的「Key」,計算出一個hash值,而後對節點數目取模。好處就是,同一個請求可以分配到一樣的服務節點,這對於「有狀態」的服務頗有必要

1 def hash_choose(request_info, server_lst):
2     hashed_request_info = hash(request_info)
3     return server_lst[hashed_request_info % len(server_lst)]

  只要hash結果足夠分散,也是能作到絕對均衡的。

一致性哈希

  哈希算法的缺陷也很明顯,當節點的數目發生變化的時候,請求會大機率分配到其餘的節點,引起到一系列問題,好比sticky session。並且在某些狀況,好比分佈式存儲,是絕對的不容許的。

  爲了解決這個哈希算法的問題,又引入了一致性哈希算法,簡單來講,一個物理節點與多個虛擬節點映射,在hash的時候,使用虛擬節點數目而不是物理節點數目。當物理節點變化的時候,虛擬節點的數目無需變化,只涉及到虛擬節點的從新分配。並且,調整每一個物理節點對應的虛擬節點數目,也就至關於每一個物理節點有不一樣的權重

最少鏈接算法(least connection)

  以上的諸多算法,要麼沒有考慮到節點間的差別(輪訓、隨機、哈希),要麼節點間的權重是靜態分配的(加權輪訓、加權隨機、一致性hash)。

  考慮這麼一種狀況,某臺機器出現故障,沒法及時處理請求,但新的請求仍是會以必定的機率源源不斷的分配到這個節點,形成請求的積壓。所以,根據節點的真實負載,動態地調整節點的權重就很是重要。固然,要得到接節點的真實負載也不是一律而論的事情,如何定義負載,負載的收集是否及時,這都是須要考慮的問題。

  每一個節點當前的鏈接數目是一個很是容易收集的指標,所以lease connection是最常被人提到的算法。也有一些側重不一樣或者更復雜、更客觀的指標,好比最小響應時間(least response time)、最小活躍數(least active)等等。

一點思考

有狀態的請求  

  首先來看看「算法衡量」中提到的第三個問題:同一個請求是否分發到一樣的服務節點,同一個請求指的是同一個用戶或者一樣的惟一標示。何時同一請求最好(必須)分發到一樣的服務節點呢?那就是有狀態 -- 請求依賴某些存在於內存或者磁盤的數據,好比web請求的session,好比分佈式存儲。怎麼實現呢,有如下幾種辦法:

  (1)請求分發的時候,保證同一個請求分發到一樣的服務節點。

  這個依賴於負載均衡算法,好比簡單的輪訓,隨機確定是不行的,哈希法在節點增刪的時候也會失效。可行的是一致性hash,以及分佈式存儲中的按範圍分段(即記錄哪些請求由哪一個服務節點提供服務),代價是須要在load balancer中維護額外的數據。

  (2)狀態數據在backend servers之間共享

  保證同一個請求分發到一樣的服務節點,這個只是手段,目的是請求能使用到對應的狀態數據。若是狀態數據可以在服務節點之間共享,那麼也能達到這個目的。好比服務節點鏈接到共享數據庫,或者內存數據庫如memcached

  (3)狀態數據維護在客戶端

  這個在web請求中也有使用,即cookie,不過要考慮安全性,須要加密。

 

 關於load balancer

  接下來回答第四個問題:關於load balancer,其實就是說,在哪裏作負載均衡,是客戶端仍是服務端,是請求的發起者仍是請求的3。具體而言,要麼是在客戶端,根據服務節點的信息自行選擇,而後將請求直接發送到選中的服務節點;要麼是在服務節點集羣以前放一個集中式代理(proxy),由代理負責請求求分發。無論哪種,至少都須要知道當前的服務節點列表這一基礎信息。

  若是在客戶端實現負載均衡,客戶端首先得知道服務器列表,要麼是靜態配置,要麼有簡單接口查詢,但backend server的詳細負載信息,就不適用經過客戶端來查詢。所以,客戶端的負載均衡算法要麼是比較簡單的,好比輪訓(加權輪訓)、隨機(加權隨機)、哈希這幾種算法,只要每一個客戶端足夠隨機,按照大數定理,服務節點的負載也是均衡的。要在客戶端使用較爲複雜的算法,好比根據backend的實際負載,那麼就須要去額外的負載均衡服務(external load balancing service)查詢到這些信息,在grpc中,就是使用的這種辦法

  

  能夠看到,load balancer與grpc server通訊,得到grpc server的負載等具體詳細,而後grpc client從load balancer獲取這些信息,最終grpc client直連到被選擇的grpc server。

  而基於Proxy的方式是更爲常見的,好比7層的Nginx,四層的F五、LVS,既有硬件路由,也有軟件分發。集中式的特色在於方便控制,並且能容易實現一些更精密,更復雜的算法。但缺點也很明顯,一來負載均衡器自己可能成爲性能瓶頸;二來可能引入額外的延遲,請求必定先發到達負載均衡器,而後到達真正的服務節點。

  load balance proxy對於請求的響應(response),要麼不通過proxy(三角傳輸模式),如LVS;要麼通過Proxy,如Nginx。下圖是LVS示意圖(來源見水印)

  

  而若是response也是走load balancer proxy的話,那麼整個服務過程對客戶端而言就是徹底透明的,也防止了客戶端去嘗試鏈接後臺服務器,提供了一層安全保障!

  值得注意的是,load balancer proxy不能成爲單點故障(single point of failure),所以通常會設計爲高可用的主從結構

 其餘

  在這篇文章中提到,負載均衡是一種推模型,必定會選出一個服務節點,而後把請求推送過來。而換一種思路,使用消息隊列,就變成了拉模型:空閒的服務節點主動去拉取請求進行處理,各個節點的負載天然也是均衡的。消息隊列相比負載均衡好處在於,服務節點不會被大量請求沖垮,同時增長服務節點更加容易;缺點也很明顯,請求不是事實處理的。

 

  想到另一個例子,好比在gunicorn這種pre-fork模型中,master(gunicorn 中Arbiter)會fork出指定數量的worker進程,worker進程在一樣的端口上監聽,誰先監聽到網絡鏈接請求,誰就提供服務,這也是worker進程之間的負載均衡。

references

wiki:Load balancing  

一分鐘瞭解負載均衡的一切

grpc load-balancing.md

相關文章
相關標籤/搜索