前言
互聯(lián)網(wǎng)公司中,絕大部分都沒有馬爸爸系列的公司那樣財(cái)大氣粗蛋济,他們即沒有強(qiáng)勁的服務(wù)器碗旅、也沒有錢去購買昂貴的海量數(shù)據(jù)庫祟辟。那他們是怎么應(yīng)對(duì)大數(shù)據(jù)量高并發(fā)的業(yè)務(wù)場(chǎng)景的呢?
這個(gè)和當(dāng)前的開源技術(shù)醇份、海量數(shù)據(jù)架構(gòu)都有著不可分割的關(guān)系僚纷。比如通過mysql拗盒、nginx等開源軟件锣咒,通過架構(gòu)和低成本的服務(wù)器搭建千萬級(jí)別的用戶訪問系統(tǒng)毅整。
怎么樣搭建一個(gè)好的系統(tǒng)架構(gòu),這個(gè)話題我們能聊上個(gè)七天七夜艇潭。這里我主要結(jié)合Redis集群來講一下一致性Hash的相關(guān)問題蹋凝。
Redis集群的使用
我們?cè)谑褂肦edis的過程中,為了保證Redis的高可用改含,我們一般會(huì)對(duì)Redis做主從復(fù)制捍壤,組成Master-Master
或者Master-Slave
的形式鞍爱,進(jìn)行數(shù)據(jù)的讀寫分離睹逃,如下圖1-1所示:
當(dāng)緩存數(shù)據(jù)量超過一定的數(shù)量時(shí)沉填,我們就要對(duì)Redis集群做分庫分表的操作拜轨。
來個(gè)栗子允青,我們有一個(gè)電商平臺(tái)颠锉,需要使用Redis存儲(chǔ)商品的圖片資源琼掠,存儲(chǔ)的格式為鍵值對(duì),key值為圖片名稱悼瓮,Value為該圖片所在的文件服務(wù)器的路徑横堡,我們需要根據(jù)文件名冠桃,查找到文件所在的文件服務(wù)器上的路徑,我們的圖片數(shù)量大概在3000w左右污茵,按照我們的規(guī)則進(jìn)行分庫葬项,規(guī)則就是隨機(jī)分配的民珍,我們以每臺(tái)服務(wù)器存500w的數(shù)量穷缤,部署12臺(tái)緩存服務(wù)器津肛,并且進(jìn)行主從復(fù)制,架構(gòu)圖如下圖1-2所示:由于我們定義的規(guī)則是隨機(jī)的秸脱,所以我們的數(shù)據(jù)有可能存儲(chǔ)在任何一組Redis中摊唇,比如我們需要查詢"product.png"的圖片巷查,由于規(guī)則的隨機(jī)性岛请,我們需要遍歷所有Redis服務(wù)器警绩,才能查詢得到肩祥。這樣的結(jié)果顯然不是我們所需要的。所以我們會(huì)想到按某一個(gè)字段值進(jìn)行Hash值岸霹、取模松申。所以我們就看看使用Hash的方式是怎么進(jìn)行的贸桶。
使用Hash的Redis集群
如果我們使用Hash的方式皇筛,每一張圖片在進(jìn)行分庫的時(shí)候都可以定位到特定的服務(wù)器,示意圖如圖1-3所示:
從上圖中旗笔,我們需要查詢的是圖product.png
蝇恶,由于我們有6臺(tái)主服務(wù)器撮弧,所以計(jì)算的公式為:hash(product.png) % 6 = 5
, 我們就可以定位到是5號(hào)主從姚糊,這們就省去了遍歷所有服務(wù)器的時(shí)間救恨,從而大大提升了性能肠槽。
使用Hash時(shí)遇到的問題
在上述hash取模的過程中署浩,我們雖然不需要對(duì)所有Redis服務(wù)器進(jìn)行遍歷而提升了性能。但是,使用Hash算法緩存時(shí)會(huì)出現(xiàn)一些問題弊攘,Redis服務(wù)器變動(dòng)時(shí)襟交,所有緩存的位置都會(huì)發(fā)生改變
捣域。
比如,現(xiàn)在我們的Redis緩存服務(wù)器增加到了8臺(tái)迹鹅,我們計(jì)算的公式從hash(product.png) % 6 = 5
變成了hash(product.png) % 8 = ?
結(jié)果肯定不是原來的5了斜棚。
再者弟蚀,6臺(tái)的服務(wù)器集群中义钉,當(dāng)某個(gè)主從群出現(xiàn)故障時(shí)断医,無法進(jìn)行緩存奏纪,那我們需要把故障機(jī)器移除序调,所以取模數(shù)又會(huì)從6變成了5发绢。我們計(jì)算的公式也會(huì)變化。
由于上面hash算法是使用取模來進(jìn)行緩存的经柴,為了規(guī)避上述情況坯认,Hash一致性算法就誕生了~~
一致性Hash算法原理
一致性Hash算法也是使用取模的方法牛哺,不過引润,上述的取模方法是對(duì)服務(wù)器的數(shù)量進(jìn)行取模痒玩,而一致性的Hash算法是對(duì)2的32方
取模。即别凹,一致性Hash算法將整個(gè)Hash空間組織成一個(gè)虛擬的圓環(huán)番川,Hash函數(shù)的值空間為0 ~ 2^32 - 1(一個(gè)32位無符號(hào)整型)
颁督,整個(gè)哈希環(huán)如下:
整個(gè)圓環(huán)以
順時(shí)針方向組織
沉御,圓環(huán)正上方的點(diǎn)代表0吠裆,0點(diǎn)右側(cè)的第一個(gè)點(diǎn)代表1烂完,以此類推试疙。第二步,我們將各個(gè)服務(wù)器使用Hash進(jìn)行一個(gè)哈希抠蚣,具體可以選擇服務(wù)器的IP或主機(jī)名作為關(guān)鍵字進(jìn)行哈希祝旷,這樣每臺(tái)服務(wù)器就確定在了哈希環(huán)的一個(gè)位置上,比如我們有三臺(tái)機(jī)器嘶窄,使用IP地址哈希后在環(huán)空間的位置如圖1-4所示:
現(xiàn)在怀跛,我們使用以下算法定位數(shù)據(jù)訪問到相應(yīng)的服務(wù)器:
將數(shù)據(jù)Key使用相同的函數(shù)Hash計(jì)算出哈希值,并確定此數(shù)據(jù)在環(huán)上的位置柄冲,從此位置沿環(huán)順時(shí)針查找,遇到的服務(wù)器就是其應(yīng)該定位到的服務(wù)器现横。
例如漓拾,現(xiàn)在有ObjectA,ObjectB长赞,ObjectC三個(gè)數(shù)據(jù)對(duì)象晦攒,經(jīng)過哈希計(jì)算后,在環(huán)空間上的位置如下:
根據(jù)一致性算法得哆,Object -> NodeA,ObjectB -> NodeB, ObjectC -> NodeC
一致性Hash算法的容錯(cuò)性和可擴(kuò)展性
現(xiàn)在哟旗,假設(shè)我們的Node C宕機(jī)了贩据,我們從圖中可以看到栋操,A、B不會(huì)受到影響饱亮,只有Object C對(duì)象被重新定位到Node A矾芙。所以我們發(fā)現(xiàn),在一致性Hash算法中近上,如果一臺(tái)服務(wù)器不可用剔宪,受影響的數(shù)據(jù)僅僅是此服務(wù)器到其環(huán)空間前一臺(tái)服務(wù)器之間的數(shù)據(jù)(這里為Node C到Node B之間的數(shù)據(jù)),其他不會(huì)受到影響壹无。如圖1-6所示:
另外一種情況,現(xiàn)在我們系統(tǒng)增加了一臺(tái)服務(wù)器Node X斗锭,如圖1-7所示:
此時(shí)對(duì)象ObjectA地淀、ObjectB沒有受到影響,只有Object C重新定位到了新的節(jié)點(diǎn)X上岖是。
如上所述:
一致性Hash算法對(duì)于節(jié)點(diǎn)的增減都只需重定位環(huán)空間中的一小部分?jǐn)?shù)據(jù)帮毁,有很好的容錯(cuò)性和可擴(kuò)展性。
數(shù)據(jù)傾斜問題
在一致性Hash算法服務(wù)節(jié)點(diǎn)太少的情況下豺撑,容易因?yàn)楣?jié)點(diǎn)分布不均勻面造成數(shù)據(jù)傾斜(被緩存的對(duì)象大部分緩存在某一臺(tái)服務(wù)器上)問題
烈疚,如圖1-8特例:
這時(shí)我們發(fā)現(xiàn)有大量數(shù)據(jù)集中在節(jié)點(diǎn)A上,而節(jié)點(diǎn)B只有少量數(shù)據(jù)聪轿。為了解決數(shù)據(jù)傾斜問題爷肝,一致性Hash算法引入了
虛擬節(jié)點(diǎn)機(jī)制
,即對(duì)每一個(gè)服務(wù)器節(jié)點(diǎn)計(jì)算多個(gè)哈希屹电,每個(gè)計(jì)算結(jié)果位置都放置一個(gè)此服務(wù)節(jié)點(diǎn)阶剑,稱為虛擬節(jié)點(diǎn)。具體操作可以為服務(wù)器IP或主機(jī)名后加入編號(hào)來實(shí)現(xiàn)危号,實(shí)現(xiàn)如圖1-9所示:
數(shù)據(jù)定位算法不變牧愁,只需要增加一步:虛擬節(jié)點(diǎn)到實(shí)際點(diǎn)的映射。
所以加入虛擬節(jié)點(diǎn)之后外莲,即使在服務(wù)節(jié)點(diǎn)很少的情況下猪半,也能做到數(shù)據(jù)的均勻分布。
具體實(shí)現(xiàn)
算法接口類
public interface IHashService {
Long hash(String key);
}
算法接口實(shí)現(xiàn)類
public class HashService implements IHashService {
/**
* MurMurHash算法,性能高,碰撞率低
*
* @param key String
* @return Long
*/
public Long hash(String key) {
ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
int seed = 0x1234ABCD;
ByteOrder byteOrder = buf.order();
buf.order(ByteOrder.LITTLE_ENDIAN);
long m = 0xc6a4a7935bd1e995L;
int r = 47;
long h = seed ^ (buf.remaining() * m);
long k;
while (buf.remaining() >= 8) {
k = buf.getLong();
k *= m;
k ^= k >>> r;
k *= m;
h ^= k;
h *= m;
}
if (buf.remaining() > 0) {
ByteBuffer finish = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
finish.put(buf).rewind();
h ^= finish.getLong();
h *= m;
}
h ^= h >>> r;
h *= m;
h ^= h >>> r;
buf.order(byteOrder);
return h;
}
}
模擬機(jī)器節(jié)點(diǎn)
public class Node<T> {
private String ip;
private String name;
public Node(String ip, String name) {
this.ip = ip;
this.name = name;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 使用IP當(dāng)做hash的Key
*
* @return String
*/
@Override
public String toString() {
return ip;
}
}
一致性Hash操作
public class ConsistentHash<T> {
// Hash函數(shù)接口
private final IHashService iHashService;
// 每個(gè)機(jī)器節(jié)點(diǎn)關(guān)聯(lián)的虛擬節(jié)點(diǎn)數(shù)量
private final int numberOfReplicas;
// 環(huán)形虛擬節(jié)點(diǎn)
private final SortedMap<Long, T> circle = new TreeMap<Long, T>();
public ConsistentHash(IHashService iHashService, int numberOfReplicas, Collection<T> nodes) {
this.iHashService = iHashService;
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes) {
add(node);
}
}
/**
* 增加真實(shí)機(jī)器節(jié)點(diǎn)
*
* @param node T
*/
public void add(T node) {
for (int i = 0; i < this.numberOfReplicas; i++) {
circle.put(this.iHashService.hash(node.toString() + i), node);
}
}
/**
* 刪除真實(shí)機(jī)器節(jié)點(diǎn)
*
* @param node T
*/
public void remove(T node) {
for (int i = 0; i < this.numberOfReplicas; i++) {
circle.remove(this.iHashService.hash(node.toString() + i));
}
}
public T get(String key) {
if (circle.isEmpty()) return null;
long hash = iHashService.hash(key);
// 沿環(huán)的順時(shí)針找到一個(gè)虛擬節(jié)點(diǎn)
if (!circle.containsKey(hash)) {
SortedMap<Long, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
}
測(cè)試類
public class TestHashCircle {
// 機(jī)器節(jié)點(diǎn)IP前綴
private static final String IP_PREFIX = "192.168.0.";
public static void main(String[] args) {
// 每臺(tái)真實(shí)機(jī)器節(jié)點(diǎn)上保存的記錄條數(shù)
Map<String, Integer> map = new HashMap<String, Integer>();
// 真實(shí)機(jī)器節(jié)點(diǎn), 模擬10臺(tái)
List<Node<String>> nodes = new ArrayList<Node<String>>();
for (int i = 1; i <= 10; i++) {
map.put(IP_PREFIX + i, 0); // 初始化記錄
Node<String> node = new Node<String>(IP_PREFIX + i, "node" + i);
nodes.add(node);
}
IHashService iHashService = new HashService();
// 每臺(tái)真實(shí)機(jī)器引入100個(gè)虛擬節(jié)點(diǎn)
ConsistentHash<Node<String>> consistentHash = new ConsistentHash<Node<String>>(iHashService, 500, nodes);
// 將5000條記錄盡可能均勻的存儲(chǔ)到10臺(tái)機(jī)器節(jié)點(diǎn)上
for (int i = 0; i < 5000; i++) {
// 產(chǎn)生隨機(jī)一個(gè)字符串當(dāng)做一條記錄偷线,可以是其它更復(fù)雜的業(yè)務(wù)對(duì)象,比如隨機(jī)字符串相當(dāng)于對(duì)象的業(yè)務(wù)唯一標(biāo)識(shí)
String data = UUID.randomUUID().toString() + i;
// 通過記錄找到真實(shí)機(jī)器節(jié)點(diǎn)
Node<String> node = consistentHash.get(data);
// 再這里可以能過其它工具將記錄存儲(chǔ)真實(shí)機(jī)器節(jié)點(diǎn)上磨确,比如MemoryCache等
// ...
// 每臺(tái)真實(shí)機(jī)器節(jié)點(diǎn)上保存的記錄條數(shù)加1
map.put(node.getIp(), map.get(node.getIp()) + 1);
}
// 打印每臺(tái)真實(shí)機(jī)器節(jié)點(diǎn)保存的記錄條數(shù)
for (int i = 1; i <= 10; i++) {
System.out.println(IP_PREFIX + i + "節(jié)點(diǎn)記錄條數(shù):" + map.get(IP_PREFIX + i));
}
}
}
運(yùn)行結(jié)果如下:
每臺(tái)機(jī)器映射的虛擬節(jié)點(diǎn)越多,則分布的越均勻~~~
感興趣的同學(xué)可以拷貝上面的代碼運(yùn)行嘗試一下声邦。