講解分佈式數據存儲的核心算法,數據分佈的算法html
hash算法 -> 一致性hash算法(memcached) -> redis cluster,hash slot算法node
1、概述web
一、咱們的memcache客戶端(這裏我看的spymemcache的源碼),使用了一致性hash算法ketama進行數據存儲節點的選擇。與常規的hash算法思路不一樣,只是對咱們要存儲數據的key進行hash計算,分配到不一樣節點存儲。一致性hash算法是對咱們要存儲數據的服務器進行hash計算,進而確認每一個key的存儲位置。redis
二、常規hash算法的應用以及其弊端算法
最常規的方式莫過於hash取模的方式。好比集羣中可用機器適量爲N,那麼key值爲K的的數據請求很簡單的應該路由到hash(K) mod N對應的機器。的確,這種結構是簡單的,也是實用的。可是在一些高速發展的web系統中,這樣的解決方案仍有些缺陷。隨着系統訪問壓力的增加,緩存系統不得不經過增長機器節點的方式提升集羣的相應速度和數據承載量。增長機器意味着按照hash取模的方式,在增長機器節點的這一時刻,大量的緩存命不中,緩存數據須要從新創建,甚至是進行總體的緩存數據遷移,瞬間會給DB帶來極高的系統負載,設置致使DB服務器宕機。api
三、設計分佈式cache系統時,一致性hash算法能夠幫咱們解決哪些問題?數組
分佈式緩存設計核心點:在設計分佈式cache系統的時候,咱們須要讓key的分佈均衡,而且在增長cache server後,cache的遷移作到最少。緩存
這裏提到的一致性hash算法ketama的作法是:選擇具體的機器節點不在只依賴須要緩存數據的key的hash自己了,而是機器節點自己也進行了hash運算。ruby
2、一致性哈希算法情景描述(轉載)服務器
如假設某哈希函數H的值空間爲0-2^32-1(即哈希值是一個32位無符號整形),整個哈希空間環以下:
下一步將各個服務器使用Hash進行一個哈希,具體能夠選擇服務器的ip或主機名做爲關鍵字進行哈希,這樣每臺機器就能肯定其在哈希環上的位置,這裏假設將上文中四臺服務器使用ip地址哈希後在環空間的位置以下:
接下來使用以下算法定位數據訪問到相應服務器:將數據key使用相同的函數Hash計算出哈希值,並肯定此數據在環上的位置,今後位置沿環順時針「行走」,第一臺遇到的服務器就是其應該定位到的服務器。
例如咱們有Object A、Object B、Object C、Object D四個數據對象,通過哈希計算後,在環空間上的位置以下:
根據一致性哈希算法,數據A會被定爲到Node A上,B被定爲到Node B上,C被定爲到Node C上,D被定爲到Node D上。
下面分析一致性哈希算法的容錯性和可擴展性。現假設Node C不幸宕機,能夠看到此時對象A、B、D不會受到影響,只有C對象被重定位到Node D。通常的,在一致性哈希算法中,若是一臺服務器不可用,則受影響的數據僅僅是此服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它不會受到影響。
下面考慮另一種狀況,若是在系統中增長一臺服務器Node X,以下圖所示:
此時對象Object A、B、D不受影響,只有對象C須要重定位到新的Node X 。通常的,在一致性哈希算法中,若是增長一臺服務器,則受影響的數據僅僅是新服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它數據也不會受到影響。
綜上所述,一致性哈希算法對於節點的增減都只需重定位環空間中的一小部分數據,具備較好的容錯性和可擴展性。
Consistent Hashing最大限度地抑制了hash鍵的從新分佈。另外要取得比較好的負載均衡的效果,每每在服務器數量比較少的時候須要增長虛擬節點來保證服務器能均勻的分佈在圓環上。由於使用通常的hash方法,服務器的映射地點的分佈很是不均勻。使用虛擬節點的思想,爲每一個物理節點(服務器)在圓上分配100~200個點。這樣就能抑制分佈不均勻,最大限度地減少服務器增減時的緩存從新分佈。用戶數據映射在虛擬節點上,就表示用戶數據真正存儲位置是在該虛擬節點表明的實際物理服務器上。
下面有一個圖描述了須要爲每臺物理服務器增長的虛擬節點。
圖三
x軸表示的是須要爲每臺物理服務器擴展的虛擬節點倍數(scale),y軸是實際物理服務器數,能夠看出,當物理服務器的數量很小時,須要更大的虛擬節點,反之則須要更少的節點,從圖上能夠看出,在物理服務器有10臺時,差很少須要爲每臺服務器增長100~200個虛擬節點才能達到真正的負載均衡。
3、以spymemcache源碼來演示虛擬節點應用
一、上邊描述的一致性Hash算法有個潛在的問題是:
(1)、將節點hash後會不均勻地分佈在環上,這樣大量key在尋找節點時,會存在key命中各個節點的機率差異較大,沒法實現有效的負載均衡。
(2)、若有三個節點Node1,Node2,Node3,分佈在環上時三個節點挨的很近,落在環上的key尋找節點時,大量key順時針老是分配給Node2,而其它兩個節點被找到的機率都會很小。
二、這種問題的解決方案能夠有:
改善Hash算法,均勻分配各節點到環上;[引文]使用虛擬節點的思想,爲每一個物理節點(服務器)在圓上分配100~200個點。這樣就能抑制分佈不均勻,最大限度地減少服務器增減時的緩存從新分佈。用戶數據映射在虛擬節點上,就表示用戶數據真正存儲位置是在該虛擬節點表明的實際物理服務器上。
在查看Spy Memcached client時,發現它採用一種稱爲Ketama的Hash算法,以虛擬節點的思想,解決Memcached的分佈式問題。
三、源碼說明
該client採用TreeMap存儲全部節點,模擬一個環形的邏輯關係。在這個環中,節點以前是存在順序關係的,因此TreeMap的key必須實現Comparator接口。
那節點是怎樣放入這個環中的呢?
protected void setKetamaNodes(List<MemcachedNode> nodes) { TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>(); int numReps= config.getNodeRepetitions(); for(MemcachedNode node : nodes) { // Ketama does some special work with md5 where it reuses chunks. if(hashAlg == HashAlgorithm.KETAMA_HASH) { for(int i=0; i<numReps / 4; i++) { byte[] digest=HashAlgorithm.computeMd5(config.getKeyForNode(node, i)); for(int h=0;h<4;h++) { Long k = ((long)(digest[3+h*4]&0xFF) << 24) | ((long)(digest[2+h*4]&0xFF) << 16) | ((long)(digest[1+h*4]&0xFF) << 8) | (digest[h*4]&0xFF); newNodeMap.put(k, node); getLogger().debug("Adding node %s in position %d", node, k); } } } else { for(int i=0; i<numReps; i++) { newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node); } } } assert newNodeMap.size() == numReps * nodes.size(); ketamaNodes = newNodeMap;
上面的流程大概能夠這樣概括:四個虛擬結點爲一組,以getKeyForNode方法獲得這組虛擬節點的name,Md5編碼後,每一個虛擬結點對應Md5碼16個字節中的4個,組成一個long型數值,作爲這個虛擬結點在環中的唯一key。第10行k爲何是Long型的呢?就是由於Long型實現了Comparator接口。
處理完正式結點在環上的分佈後,能夠開始key在環上尋找節點的遊戲了。
對於每一個key仍是得完成上面的步驟:計算出Md5,根據Md5的字節數組,經過Kemata Hash算法獲得key在這個環中的位置。
MemcachedNode getNodeForKey(long hash) { final MemcachedNode rv; if(!ketamaNodes.containsKey(hash)) { // Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5 // in a lot of places, so I'm doing this myself. SortedMap<Long, MemcachedNode> tailMap=getKetamaNodes().tailMap(hash); if(tailMap.isEmpty()) { hash=getKetamaNodes().firstKey(); } else { hash=tailMap.firstKey(); } } rv=getKetamaNodes().get(hash); return rv; }
上邊代碼的實現就是在環上順時針查找,沒找到就去的第一個,而後就知道對應的物理節點了。
4、應用場景分析
一、memcache的add方法:經過一致性hash算法確認當前客戶端對應的cacheserver的hash值以及要存儲數據key的hash進行對應,確認cacheserver,獲取connection進行數據存儲
二、memcache的get方法:經過一致性hash算法確認當前客戶端對應的cacheserver的hash值以及要提取數據的hash值,進而確認存儲的cacheserver,獲取connection進行數據提取
5、總結
一、一致性hash算法只是幫咱們減小cache集羣中的機器數量增減的時候,cache的數據能進行最少重建。只要cache集羣的server數量有變化,必然產生數據命中的問題
二、對於數據的分佈均衡問題,經過虛擬節點的思想來達到均衡分配。固然,咱們cache server節點越少就越須要虛擬節點這個方式來均衡負載。
三、咱們的cache客戶端根本不會維護一個map來記錄每一個key存儲在哪裏,都是經過key的hash和cacheserver(也許ip能夠做爲參數)的hash計算當前的key應該存儲在哪一個節點上。
四、當咱們的cache節點崩潰了。咱們一定丟失部分cache數據,而且要根據活着的cache server和key進行新的一致性匹配計算。有可能對部分沒有丟失的數據也要作重建...
五、至於正常到達數據存儲節點,如何找到key對應的數據,那就是cache server自己的內部算法實現了,此處不作描述。
前言
對於以前所講的master+slave進行讀寫分離同時經過sentinel集羣保障高可用的架構,對於通常的數據量系統已經足夠。可是對於數據量龐大的T級別的數據,單master可能就沒法知足橫向擴展的場景。
因此redis cluster支持多master+slave架構,支持讀寫分離和主備切換,多個master支持分片hash slot分佈式存儲數據
一、redis cluster介紹
redis cluster
(1)自動將數據進行分片,每一個master上放一部分數據
(2)提供內置的高可用支持,部分master不可用時,仍是能夠繼續工做的
在redis cluster架構下,每一個redis要放開兩個端口號,好比一個是6379,另一個就是加10000的端口號,好比16379
16379端口號是用來進行節點間通訊的,也就是cluster bus的東西,集羣總線。cluster bus的通訊,用來進行故障檢測,配置更新,故障轉移受權
cluster bus用了另一種二進制的協議,主要用於節點間進行高效的數據交換,佔用更少的網絡帶寬和處理時間
二、最老土的hash算法和弊端(大量緩存重建)
三、一致性hash算法(自動緩存遷移)+虛擬節點(自動負載均衡)
四、redis cluster的hash slot算法
redis cluster有固定的16384個hash slot,對每一個key計算CRC16值,而後對16384取模,能夠獲取key對應的hash slot
redis cluster中每一個master都會持有部分slot,好比有3個master,那麼可能每一個master持有5000多個hash slot
hash slot讓node的增長和移除很簡單,增長一個master,就將其餘master的hash slot移動部分過去,減小一個master,就將它的hash slot移動到其餘master上去
移動hash slot的成本是很是低的
客戶端的api,能夠對指定的數據,讓他們走同一個hash slot,經過hash tag來實現
redis cluster的重要配置
1 cluster-enabled <yes/no> 2 3 cluster-config-file <filename>:指定一個文件,供cluster模式下的redis實例將集羣狀態保存在起來,包括集羣中其餘機器的信息,好比節點的上線和下線,故障轉移,這些不是咱們去維護,提供一個文件地址,讓redis本身去維護 4 5 cluster-node-timeout <milliseconds>:節點存活超時時長,超過必定時長,認爲節點宕機,master宕機的話就會觸發主備切換,slave宕機就不會提供服務
在3臺機器上啓動6個redis實例
對於redis cluster集羣,要求至少3個master,從而可以組成一個健壯的分佈式的集羣,每一個master都建議至少給一個slave,3個master,3個slave,因此建議在正式環境下,可以部署6臺機器去搭建redis cluster集羣,最少的狀況是有3臺機器,此時須要master和對應的slave再也不同一臺機器
咱們模擬7001-7006端口號來部署6個redis節點,每臺機器部署兩個節點:
1 mkdir -p /etc/redis-cluster 存放cluster-config-file信息 2 mkdir -p /var/log/redis 存放redis的日誌信息 3 mkdir -p /var/redis/7001 存放redis的持久化文件 4 5 配置文件中的改動: 6 7 port 7001 8 cluster-enabled yes -- 啓用 cluster 集羣 9 cluster-config-file /etc/redis-cluster/node-7001.conf 10 cluster-node-timeout 15000 11 daemonize yes 12 pidfile /var/run/redis_7001.pid 13 dir /var/redis/7001 14 logfile /var/log/redis/7001.log 15 bind 192.168.1.199 16 appendonly yes
17 #必需要關掉 slaveof 配置
在對應的每臺機器下的/etc/init.d中,放2個對應端口號的啓動腳本,分別爲: redis_7001, redis_7002…須要注意的是每一個啓動腳本內,都必定要修改對應的端口號
咱們須要安裝官方提供的redis-trib.rb來完成集羣的管理:
1 yum install -y ruby 2 yum install -y rubygems 3 gem install redis
第三步你的ruby版本過低會報錯
請跳轉 https://www.cnblogs.com/PatrickLiu/p/8454579.html
或者 http://www.javashuo.com/article/p-cbdmvsiu-ko.html
將redis-trib.rb配置到環境變量:
cp /usr/local/redis-3.2.8/src/redis-trib.rb /usr/local/bin
執行以下命令:
redis-trib.rb create --replicas 1 192.168.1.199:7001 192.168.1.107:7002 192.168.1.104:7003 192.168.1.104:7004 192.168.1.105:7005 192.168.1.105:7006
replicas表示每一個master對應的slave節點數量,後續爲全部節點服務地址。成功執行後會自動將全部節點配置成集羣架構模式,會自動有如下特色:
讀寫分離、master-slave高可用主備切換、橫向master數據分片
能夠經過如下命令來覈查:
redis-trib.rb check 192.168.1.199:7001
以前咱們所搭建的一主多從架構模式,是爲來水平擴展,可是基於redis cluster自己的master就能夠進行橫向擴展,因此咱們使用redis cluster架構模式,讀寫都在master便可,對應的slave節點主要是進行熱備切換。而且,redis cluster默認是沒有開啓在slave節點上的讀操做,須要執行readonly命令來開啓,此外,jedis也會將請求都發送至master,須要從新封裝或者修改源碼來達到基於redis cluster的讀寫分離實現,因此也沒有必要在redis cluster進行讀寫分離。