存儲類服務集羣的負載均衡與實戰

引言

關於負載均衡,什麼是負載均衡? 這個概念挺大的。可是咱們要理解一個大的概念,就得把化繁爲簡,把它分紅多個部分,或者是拆成多個層次的來理解。我所理解的負載均衡: 按照實現方式分紅兩種,一種是動態的負載均衡:根據當前服務的負載來分配新的請求;與之相對的就是非動態的負載均衡:用預先設計好的算法去分配新的請求。從統計學角度來看,大多數的狀況下非動態的負載均衡方案可以知足負載的平均分配了。曾經和一位資深同事瞭解過在某個非動態負載均衡的系統中,統計每臺服務器的請求量,發現每一臺機器的請求總量幾乎相等。 而動態均衡方案只是在一些非動態負載均衡不能有效分配的狀況下的補充。負載均衡若是按照業務類型來分 ,又有好多不一樣方面。好比網絡交換機系統的負載均衡,web服務系統的負載均衡,分佈式任務的負載均衡,存儲類集羣的負載均衡。 每一種系統的負載均衡的概念和方法差別都又有千差萬別。 web

問題描述

提到數據存儲類服務集羣的負載均衡方案這個問題, 主要的想法來源是和咱們組的panda同窗上個星期的一次關於數據存儲類服務如何擴容的討論。panda同窗提了一個實際的問題。把這個問題稍微從具體業務中抽象出來一點點,能夠這樣表述: 有一個數據存儲類的服務集羣一共有3臺機器,一共保存着300萬用戶的資料,也就是每臺機器保存有100萬用戶的資料。 如今要擴容一臺服務器,也就是要變成4臺機器,每臺保存75萬用戶的資料,應該怎麼作呢? 算法

非一致性哈希解決方案以及問題

問題是這樣的,咱們暫且不去討論它的答案,而是先關注一個可能被忽略的細節。"如今有3臺服務器,一共保存着這300萬用戶的資料。" 如何分這300萬用戶的資料的呢?其實這個細節問題的答案就是負載均衡的方案。對於數據存儲類服務集羣的負載均衡,其核心問題就是如何分配數據。並且通常狀況下,對於數據存儲類的服務集羣,沒法採用動態的負載均衡來實現。畢竟數據在那裏,若是用戶甲的數據在A機器上,那就沒法從B機器來獲取用戶甲的數據。所以,咱們採用非靜態的方式來分配這300萬用戶的資料,最多見的方法就是哈希。假如每一個用戶都有一個整數的uid做爲惟一表示。那麼能夠用哈希函數把用戶的uid映射成到[1,3]的區間內,再將哈希值做爲機器的ID就能夠實現負載均衡了。而最多見的哈希函數就是取模。好比有3臺服務器就模3, 用戶的具體分配方案以下圖: 服務器

圖1 取模的方式分配用戶 網絡

好比uid=3, 3%3=0,用戶就被分到server0. 在client端的存取用戶資料的代碼大概就像下面這樣的寫法: 負載均衡


GetServiceByUid(uid)
{
   server_id = uid % serverCount ;
   return Service(server_id);
}

GetUserInfo(uid)
{
    service = GetServiceByUid(uid);
    return service.GetUserInfo(uid);
}

SetUserInfo(info)
{
    service = GetServiceByUid(info.uid);
    return service.SetUserInfo(info);
}
在GetUserInfo和SetUserInfo在實現上都先用用戶的uid來取一臺服務器,再從具體的一臺服務器上存取。功能上來看已經可以知足了。不過這種哈希的方式有一個問題。當服務器的數量發生改變的時候就很麻煩了。好比如今我須要加一臺存儲服務器,則幾乎原有全部的數據都要挪動一下位置了。以下圖所示:


圖2 取模方式的擴容方案 運維

能夠看到。服務器0的數據所有都要換,服務器1也是如此。假如要擴容,就會形成大量的數據遷移。而且這樣的數據遷移幾乎無法在不停服務的方法下完成。若是必定要不停服務遷移,則須要用額外的四臺機器,一邊讓老系統提供服務,一邊遷移數據。等新老系統的數據總量徹底相等的時候,再把整個系統切到新的系統。用這樣的方法作擴容,估計運維部門聽到就會很頭痛吧。 分佈式

上面的方案很差,究其緣由在於哈希函數。當機器數+1時,絕大多數的數據的哈希值都發生了改變。這樣一來,咱們之前所分配的存儲方案所有都失效了。所以,須要哈希函數在增減機器時,儘可能少去打亂原來的數據分配。所以須要提升哈希函數的單調性。有一種算法叫一致性哈希能夠解決這個問題。  函數

一致性哈希算法

這篇 一致性哈希算法  的文章是這樣介紹的: ui

1 用戶的整數uid能夠當作是一個0 至 2^32-1 的數值空間。 能夠把這個數值空間想象成一個收尾相連的圓環。以下圖所示: spa


圖3 原型數據空間

2 把用戶的uid的hash值映射成圓環上的點。這很好理解, 下圖中的object就是uid。


圖4 圓形數據空間的哈希映射

3 將'服務器' 利用一樣的哈希函數映射到同個數值空間中。 這句話比較很差理解。 ’服務器'如何可以映射到一個數值空間中。實際上你能夠本身想辦法去實現,好比你能夠用服務器的IP作哈希。以下圖所示:

圖5 一致性哈希 

上圖中的CacheA 、CacheB 和CacheC等藍色的節點就是存儲服務器。紅色的節點是數據。能夠清楚的看到,幾個藍色的節點把數值空間清晰的分紅了多個子空間。查找數據所在服務器,看一下紅色節點在哪一個子空間就能夠了。再增長一點想象力,一臺服務器能夠分紅許多虛擬的藍色節點。查找數據所在的服務器的時候,分紅兩步走,先查數據所在的虛擬空間再查虛擬節點對應的服務器。

一致性哈希算法最大的好處就是在於增長、刪除節點時,不會大面積的影響數據所在的服務器的位置。而隻影響有限的一到兩臺服務器。 一致性哈希的精華在於虛擬節點。由於當藍色節點數量較少的時候,很難保證用戶數據是平均分配到不一樣的服務器上的。當把一個節點分紅多個虛擬節點的時候,能夠保證用戶數據在物理機器上更平均的分配。

在實踐中使用一致性哈希算法

如何在實踐中使用一致性哈希? 首先把數據空間的粒度劃分的更大,先將整個數據空間劃分爲有限個子空間做爲數據擴容最基本的單位。例如咱們將用戶的uid經過哈希函數映射成[0, 99] 這100個值,也就是劃分爲100個子空間。 這就比如先找來100個大桶,把全部用戶資料先丟到這100個大桶裏。 第二步是用一臺服務器管理多個哈希值的用戶資料,這就比如把幾個大桶放在一個服務器裏。能夠看下圖:

圖6 實踐中的一致性哈希

在上圖中,有3臺服務器,7個桶。先用哈希把用戶uid映射到7個大桶裏, 好比用模7的方式就能夠實現。而後根據配置文件查找到桶對應的服務器在哪裏。具體的代碼和非一致性哈希的代碼只有GetServiceByUid有所不一樣。配置文件和僞代碼以下: 


//file: userinfo_client.conf
[server1]
addr = 10.0.0.2
bucket = 1,2

[server2]
addr = 10.0.0.3
bucket= 3,4

[server3]
addr = 10.0.0.4
bucket = 5,6,7

//file: userinfo_client.cpp
GetServiceByUid(uid)
{
bucket_id = uid%7 +1 ;
service_id = GetServiceByBucketId(bucket_id)
return Service(service_id);
}

不停機擴容與「陰影桶"

這種方法實踐簡單實用。擴容起來也比較方便。停機擴容咱們就不談了,只要遷移完數據再修改配置文件。

若是要不停機擴容又該如何操做呢? 這裏就須要用到一個「陰影桶」的概念了。陰影桶是一個數據桶的未完成的複製品。在擴容的過程當中,咱們首先要在新機器上配置一個陰影桶。將原來的數據桶的數據逐步遷移到陰影桶中。而後將對於被遷移的數據通的寫操做所有要寫兩次。既要寫原來的數據桶,也要寫陰影桶。當陰影桶和原數據桶的數據保持徹底一致的時候,廢棄掉原數據桶,將陰影桶升級爲數據桶就完成數據遷移了。


好比在下面的系統中,咱們要擴容一臺server4, 並把數據桶 bucket7的不停機遷移到server4上。

原來系統是這樣的:

圖7 系統擴容以前

第一步,咱們在配置文件中增長一臺服務器server4, 而後在server4中配置一個bucket7的陰影桶。


[server1]
addr = 10.0.0.2
bucket = 1,2

[server2]
addr = 10.0.0.3
bucket= 3,4

[server3]
addr = 10.0.0.4
bucket = 5,6,7

[server4]
addr= 10.0.0.5
shadow_bucket=7
擴容中的系統以下圖所示:


圖8 擴容中的系統

server4是一臺新的服務器,咱們在上面設置一個bucket7的陰影桶。 對於bucket7的寫操,既要寫server3上的原數據桶,也要寫server4上的陰影桶。同時進行server3到server4的數據遷移。在數據遷移過程當中, shadow bucket7上的數據是不徹底的,因此對於bucket7的讀操做還所有是在server3上進行的。

這時的client程序全部的寫操做的代碼會和之前的版本有修改,對於任何的寫操做都要檢查是否有陰影桶,若是有陰影桶則也要寫到陰影桶中:


SetUserInfo(info)
{
    service = GetServiceByUid(info.uid);
    service.SetUserInfo(info);
    
    if((shadow_service = GetShadowServiceByUid(info.uid)))
        shadow_service.SetUserInfo(info);
}
第二步,當server3上的bucket7的數據和server4上的shadow bucket7的數據徹底一致以後。 就能夠把server3上的bucket7配置去掉, 把server4上的shadow bucket7升級爲bucket 7。 這時的配置文件以下:


[server1]
addr = 10.0.0.2
bucket = 1,2

[server2]
addr = 10.0.0.3
bucket= 3,4

[server3]
addr = 10.0.0.4
bucket = 5,6

[server4]
addr= 10.0.0.5
bucket=7


這時的集羣系統以下圖所示:

圖9 擴容後的系統

對於合併操做也是採用相同的方法,設置陰影桶並在雙寫的同時作數據遷移。

總結

這篇文章討論了多種數據存儲集羣的實現方案。採用一致性哈希的算法能夠在必定程度上減小擴容帶來的衝擊。咱們在實踐中能夠結合實際狀況使用一致性哈希算法實現數據存儲集羣的負載均衡,簡化擴容的操做以免相應的問題和風險。

相關文章
相關標籤/搜索