关于负载均衡的一切:总结与思考
正文
?
古人云,不患寡而患不均。
在計算機的世界,這就是大家耳熟能詳?shù)呢撦d均衡(load balancing),所謂負載均衡,就是說如果一組計算機節(jié)點(或者一組進程)提供相同的(同質的)服務,那么對服務的請求就應該均勻的分攤到這些節(jié)點上。負載均衡的前提一定是“provide a single Internet service from multiple servers”, 這些提供服務的節(jié)點被稱之為server farm、server pool或者backend servers。
這里的服務是廣義的,可以是簡單的計算,也可能是數(shù)據(jù)的讀取或者存儲。負載均衡也不是新事物,這種思想在多核CPU時代就有了,只不過在分布式系統(tǒng)中,負載均衡更是無處不在,這是分布式系統(tǒng)的天然特性決定的,分布式就是利用大量計算機節(jié)點完成單個計算機無法完成的計算、存儲服務,既然有大量計算機節(jié)點,那么均衡的調度就非常重要。
負載均衡的意義在于,讓所有節(jié)點以最小的代價、最好的狀態(tài)對外提供服務,這樣系統(tǒng)吞吐量最大,性能更高,對于用戶而言請求的時間也更小。而且,負載均衡增強了系統(tǒng)的可靠性,最大化降低了單個節(jié)點過載、甚至crash的概率。不難想象,如果一個系統(tǒng)絕大部分請求都落在同一個節(jié)點上,那么這些請求響應時間都很慢,而且萬一節(jié)點降級或者崩潰,那么所有請求又會轉移到下一個節(jié)點,造成雪崩。
事實上,網(wǎng)上有很多文章介紹負載均衡的算法,大多都是大同小異。本文更多的是自己對這些算法的總結與思考。
一分鐘了解負載均衡的一切
本章節(jié)的標題和內容都來自一分鐘了解負載均衡的一切這一篇文章。當然,原文的標題是夸張了點,不過文中列出了在一個大型web網(wǎng)站中各層是如何用到負載均衡的,一目了然。
常見互聯(lián)網(wǎng)分布式架構如上,分為客戶端層、反向代理nginx層、站點層、服務層、數(shù)據(jù)層。可以看到,每一個下游都有多個上游調用,只需要做到,每一個上游都均勻訪問每一個下游,就能實現(xiàn)“將請求/數(shù)據(jù)【均勻】分攤到多個操作單元上執(zhí)行”。
(1)【客戶端層】到【反向代理層】的負載均衡,是通過“DNS輪詢”實現(xiàn)的
(2)【反向代理層】到【站點層】的負載均衡,是通過“nginx”實現(xiàn)的
(3)【站點層】到【服務層】的負載均衡,是通過“服務連接池”實現(xiàn)的
(4)【數(shù)據(jù)層】的負載均衡,要考慮“數(shù)據(jù)的均衡”與“請求的均衡”兩個點,常見的方式有“按照范圍水平切分”與“hash水平切分”。
數(shù)據(jù)層的負載均衡,在我之前的《帶著問題學習分布式系統(tǒng)之數(shù)據(jù)分片》中有詳細介紹。
算法衡量
在我看來,當我們提到一個負載均衡算法,或者具體的應用場景時,應該考慮以下問題
第一,是否意識到不同節(jié)點的服務能力是不一樣的,比如CPU、內存、網(wǎng)絡、地理位置
第二,是否意識到節(jié)點的服務能力是動態(tài)變化的,高配的機器也有可能由于一些突發(fā)原因導致處理速度變得很慢
第三,是否考慮將同一個客戶端,或者說同樣的請求分發(fā)到同一個處理節(jié)點,這對于“有狀態(tài)”的服務非常重要,比如session,比如分布式存儲
第四,誰來負責負載均衡,即誰充當負載均衡器(load balancer),balancer本身是否會成為瓶頸
下面會結合具體的算法來考慮這些問題
負載均衡算法
輪詢算法(round-robin)
思想很簡單,就是提供同質服務的節(jié)點逐個對外提供服務,這樣能做到絕對的均衡。Python示例代碼如下
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
可以看到,所有的節(jié)點都是以同樣的概率提供服務,即沒有考慮到節(jié)點的差異,也許同樣數(shù)目的請求,高配的機器CPU才20%,低配的機器CPU已經(jīng)80%了
加權輪詢算法(weight round-robin)
加權輪訓算法就是在輪訓算法的基礎上,考慮到機器的差異性,分配給機器不同的權重,能者多勞。注意,這個權重的分配依賴于請求的類型,比如計算密集型,那就考慮CPU、內存;如果是IO密集型,那就考慮磁盤性能。Python示例代碼如下
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)
這個就更好理解了,隨機選擇一個節(jié)點服務,按照概率,只要請求數(shù)量足夠多,那么也能達到絕對均衡的效果。而且實現(xiàn)簡單很多
1 def random_choose(server_lst):2 import random
3 ? ?random.seed()
4 return random.choice(server_lst)
?
加權隨機算法(random)
如同加權輪訓算法至于輪訓算法一樣,也是在隨機的時候引入不同節(jié)點的權重,實現(xiàn)也很類似。
?import randomrandom.seed()weighted_list = [] ?
? ?for k, v in servers.iteritems():weighted_list.extend([k] * v) ?
? ?return random.choice(weighted_list)
?
當然,如果節(jié)點列表以及權重變化不大,那么也可以對所有節(jié)點歸一化,然后按概率區(qū)間選擇
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)
根據(jù)客戶端的IP,或者請求的“Key”,計算出一個hash值,然后對節(jié)點數(shù)目取模。好處就是,同一個請求能夠分配到同樣的服務節(jié)點,這對于“有狀態(tài)”的服務很有必要
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結果足夠分散,也是能做到絕對均衡的。
一致性哈希
哈希算法的缺陷也很明顯,當節(jié)點的數(shù)目發(fā)生變化的時候,請求會大概率分配到其他的節(jié)點,引發(fā)到一系列問題,比如sticky session。而且在某些情況,比如分布式存儲,是絕對的不允許的。
為了解決這個哈希算法的問題,又引入了一致性哈希算法,簡單來說,一個物理節(jié)點與多個虛擬節(jié)點映射,在hash的時候,使用虛擬節(jié)點數(shù)目而不是物理節(jié)點數(shù)目。當物理節(jié)點變化的時候,虛擬節(jié)點的數(shù)目無需變化,只涉及到虛擬節(jié)點的重新分配。而且,調整每個物理節(jié)點對應的虛擬節(jié)點數(shù)目,也就相當于每個物理節(jié)點有不同的權重
最少連接算法(least connection)
以上的諸多算法,要么沒有考慮到節(jié)點間的差異(輪訓、隨機、哈希),要么節(jié)點間的權重是靜態(tài)分配的(加權輪訓、加權隨機、一致性hash)。
考慮這么一種情況,某臺機器出現(xiàn)故障,無法及時處理請求,但新的請求還是會以一定的概率源源不斷的分配到這個節(jié)點,造成請求的積壓。因此,根據(jù)節(jié)點的真實負載,動態(tài)地調整節(jié)點的權重就非常重要。當然,要獲得接節(jié)點的真實負載也不是一概而論的事情,如何定義負載,負載的收集是否及時,這都是需要考慮的問題。
每個節(jié)點當前的連接數(shù)目是一個非常容易收集的指標,因此lease connection是最常被人提到的算法。也有一些側重不同或者更復雜、更客觀的指標,比如最小響應時間(least response time)、最小活躍數(shù)(least active)等等。
一點思考
有狀態(tài)的請求
首先來看看“算法衡量”中提到的第三個問題:同一個請求是否分發(fā)到同樣的服務節(jié)點,同一個請求指的是同一個用戶或者同樣的唯一標示。什么時候同一請求最好(必須)分發(fā)到同樣的服務節(jié)點呢?那就是有狀態(tài) -- 請求依賴某些存在于內存或者磁盤的數(shù)據(jù),比如web請求的session,比如分布式存儲。怎么實現(xiàn)呢,有以下幾種辦法:
(1)請求分發(fā)的時候,保證同一個請求分發(fā)到同樣的服務節(jié)點。
這個依賴于負載均衡算法,比如簡單的輪訓,隨機肯定是不行的,哈希法在節(jié)點增刪的時候也會失效。可行的是一致性hash,以及分布式存儲中的按范圍分段(即記錄哪些請求由哪個服務節(jié)點提供服務),代價是需要在load balancer中維護額外的數(shù)據(jù)。
(2)狀態(tài)數(shù)據(jù)在backend servers之間共享
保證同一個請求分發(fā)到同樣的服務節(jié)點,這個只是手段,目的是請求能使用到對應的狀態(tài)數(shù)據(jù)。如果狀態(tài)數(shù)據(jù)能夠在服務節(jié)點之間共享,那么也能達到這個目的。比如服務節(jié)點連接到共享數(shù)據(jù)庫,或者內存數(shù)據(jù)庫如memcached
(3)狀態(tài)數(shù)據(jù)維護在客戶端
這個在web請求中也有使用,即cookie,不過要考慮安全性,需要加密。
?
?關于load balancer
接下來回答第四個問題:關于load balancer,其實就是說,在哪里做負載均衡,是客戶端還是服務端,是請求的發(fā)起者還是請求的3。具體而言,要么是在客戶端,根據(jù)服務節(jié)點的信息自行選擇,然后將請求直接發(fā)送到選中的服務節(jié)點;要么是在服務節(jié)點集群之前放一個集中式代理(proxy),由代理負責請求求分發(fā)。不管哪一種,至少都需要知道當前的服務節(jié)點列表這一基礎信息。
如果在客戶端實現(xiàn)負載均衡,客戶端首先得知道服務器列表,要么是靜態(tài)配置,要么有簡單接口查詢,但backend server的詳細負載信息,就不適用通過客戶端來查詢。因此,客戶端的負載均衡算法要么是比較簡單的,比如輪訓(加權輪訓)、隨機(加權隨機)、哈希這幾種算法,只要每個客戶端足夠隨機,按照大數(shù)定理,服務節(jié)點的負載也是均衡的。要在客戶端使用較為復雜的算法,比如根據(jù)backend的實際負載,那么就需要去額外的負載均衡服務(external load balancing service)查詢到這些信息,在grpc中,就是使用的這種辦法
可以看到,load balancer與grpc server通信,獲得grpc server的負載等具體詳細,然后grpc client從load balancer獲取這些信息,最終grpc client直連到被選擇的grpc server。
而基于Proxy的方式是更為常見的,比如7層的Nginx,四層的F5、LVS,既有硬件路由,也有軟件分發(fā)。集中式的特點在于方便控制,而且能容易實現(xiàn)一些更精密,更復雜的算法。但缺點也很明顯,一來負載均衡器本身可能成為性能瓶頸;二來可能引入額外的延遲,請求一定先發(fā)到達負載均衡器,然后到達真正的服務節(jié)點。
load balance proxy對于請求的響應(response),要么不經(jīng)過proxy,如LVS;要么經(jīng)過Proxy,如Nginx。下圖是LVS示意圖(來源見水印)
而如果response也是走load balancer proxy的話,那么整個服務過程對客戶端而言就是完全透明的,也防止了客戶端去嘗試連接后臺服務器,提供了一層安全保障!
值得注意的是,load balancer proxy不能成為單點故障(single point of failure),因此一般會設計為高可用的主從結構
?其他
在這篇文章中提到,負載均衡是一種推模型,一定會選出一個服務節(jié)點,然后把請求推送過來。而換一種思路,使用消息隊列,就變成了拉模型:空閑的服務節(jié)點主動去拉取請求進行處理,各個節(jié)點的負載自然也是均衡的。消息隊列相比負載均衡好處在于,服務節(jié)點不會被大量請求沖垮,同時增加服務節(jié)點更加容易;缺點也很明顯,請求不是事實處理的。
?
想到另外一個例子,比如在gunicorn這種pre-fork模型中,master(gunicorn 中Arbiter)會fork出指定數(shù)量的worker進程,worker進程在同樣的端口上監(jiān)聽,誰先監(jiān)聽到網(wǎng)絡連接請求,誰就提供服務,這也是worker進程之間的負載均衡。
原文:http://www.cnblogs.com/xybaby/p/7867735.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的关于负载均衡的一切:总结与思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VS Tools for AI全攻略
- 下一篇: Asp.net Core中SignalR