Categories
程式開發

Redis Sharding集群跟一致性哈希有什麼瓜葛?


一、前言

最近在所負責的某些系統上遇到了一些Redis相關的問題,剛好在朋友圈聊到Cluster和Sharding這方面的東西,發現有些地方比較模糊,考慮到之前也整理了關於Sentinel集群模式,趁著有點力氣整理一下Sharding的一些相關資料。

Cluster模式後面有時間再補充吧。

二、Redis sharding集群

1、概念及優劣:

客戶端分片技術,即客戶端自己計算每個key應該要放到哪個Redis實例,其主要原理還是使用哈希算法對某個Key進行哈希後映射存儲到對應的Redis實例上。 (這裡提前透露簡單的哈希算法可以是通過簡單的取餘,但是這種辦法有致命的弱點,稍後再講。)

此方法的好處是1)降低服務器集群的複雜度,且2)每個實例都是獨立沒有關聯的。缺點就是1)擴容問題,目前客戶端實現在支持動態增刪實例方面沒有解決方案;2)單點故障:即如果某個分片宕機後,那個分片的數據就不能提供服務,每個實例本身的高可用需要自己想辦法。

針對以上兩個問題,一般有以下的解決方案:

單點故障:一般的做法是通過一主N從的哨兵模式實現該分片的自動故障轉移,具體的方案原理及搭建請參考我的另外一篇文章:Redis哨兵(Sentinel)模式” 擴容問題:一般只能通過重啟的方法進行擴容,但這種辦法在鍵值對數據遷移方面加大了運維側的難度,且應用層需要做配置改動去支持新的實例。還有另外一種辦法,Redis作者推薦可以使用一種PreSharding的辦法,這裡暫不做介紹,後面再補充。

2、數據傾斜問題

後面會提到Jedis的用法(因為Jedis就是客戶端分片的其中一種實現方案),這裡也先簡單介紹一下一致性哈希可以解決的問題吧。但在開始介紹之前一定要接受一個前提,一致性哈希算法可以做到:

均衡性:不同對象經哈希後的哈希值在哈希環內(即hash family)能保證盡量均勻分佈確保均衡落入到不同節點;單調性:對於部分已經分配到具體節點的鍵值對,即使有新節點加入也能保證該部分鍵值對要么映射在原節點要么映射到新節點(即不會映射到其他舊有節點上);分散性:暫時還不是特別懂,翻譯不了負載性:暫時還不是特別懂,翻譯不了

具體上述4種特性的英文描述可以在下面的原始論文中的第6頁可以找到,這裡就不做展示了。

另:1、關於緩存系統對於該算法運用的相關論文有興趣的朋友可以參考哥倫比亞大學網站的資料: http://www.cs.columbia.edu/~asherman/papers/cachePaper.pdf“2、關於算法原始論文有興趣的可以參考普林斯頓大學資料:https://www.cs.princeton.edu/courses/archive/fall09/cos518/papers/chash.pdf

如果Redis平時放些用戶session、參數這類偏靜態數據問題不大,如果我們是存儲千萬或億級級別的交易數據、埋點數據(且需要頻發讀寫)的話,在Redis Sharding的方案中,例如我們上例因為資源成本的問題我不可能配幾十或百台實例,因此只配了兩個實例,但是兩個實例會帶來什麼樣的問題?對,也會因為物理實例節點(這裡簡稱Node)的偏少而帶來數據傾斜的問題。怎麼辦呢?如果使用一致性哈希算法的話(我這裡畫了個概念圖方便理解),算法會針對每個物理節點(Node)虛擬出多個虛擬節點(這裡簡稱Virtual Node或者VNode),這樣在整個哈希環空間內就有多個互相交錯的虛擬節點,那數據分佈得更均衡了而避免對於某台服務器的讀寫壓力,一般來說虛擬節點越多數據分佈得約均勻(曾經在某個文章看過壓測數據,當虛擬節點數達到1000級別的時候,每個節點存儲的數據基本上接近“平均數”)。

具體請看下圖。在沒有虛擬的情況下,差不多所有數據(K1/K2/K3/K4/K5)全在Node1,只有K6在Node2;在使用了一致性哈希算法後(虛擬化後),K1/K3/K5實際上在Node1對應的那些虛擬節點中,K2/K4/K6則在Node2裡,這樣數據就均衡的分佈在兩個物理節點。

Redis Sharding集群跟一致性哈希有什麼瓜葛? 1

實際上,大家可以看看一下Jedis的源碼,在Node的初始化的時候Jedis會自動將每個Node虛擬出160個VNode,這樣的話上例中的2個實例實際上就有2*160=320個虛擬節點,而且可以從上圖類推出來,這320個節點是縱橫交錯的而非順序排列。另外,具體鍵值對(K,V)存儲的時候Jedis怎麼選擇對應的虛擬節點的話,大家有興趣可以看看相應源碼,這裡不做具體展示。

Redis Sharding集群跟一致性哈希有什麼瓜葛? 2

3、數據丟失問題

使用了一致性哈希算法後還有一個好處,就是在沒有足夠時間做數據遷移的前提下,動態擴容或突發宕機時候導致數據丟失或者打到後面數據庫的流量減少,最大程度避免雪崩的發生,是高容錯性的一個體現。

以下圖的哈希環為例,在沒有插入Node3節點前,如果我們通過客戶端去讀取這幾個K1/K2/K3的鍵值對的時候會根據算法自動識別是存儲在Node1並可以獲取到;但是如果在服務器壓力暴增情況下臨時新增一台服務器的話(潛台詞就是沒有做數據遷移,因為一旦上量了遷移也需要很多時間),客戶端再去獲取K1/K2/K3的時候,通過算法計算認為它們應該存儲在Node3的,但實際Node3是肯定沒有啦(因為沒做數據遷移),所以對客戶端來說50%的數據是憑空消失了( 被黃圈圈起來部分,這裡思考一下是不是跟該算法的單調性有點吻合,細品一下),這是何等悲壯啊,等著跑路吧兄dei。但是,如果是有良心的程序猿,在系統設計時候會做好容錯性設計的(不知道在讀的你有沒有做到),一般在應用層邏輯上會兜底,就是允許把請求穿透到服務端通過查庫獲取並同步更新到最新的Node3上,那下次再有請求過來的時候直接從Node3獲取即可而不需要打到數據庫。

針對上面提到的問題,

首先,如果採用了一致性哈希後,因為上面提到虛擬化節點很多,這樣的話呢就算發生上面情況,所影響的數據范圍也是相對較小的,起碼不是整個物理節點的數據都得查庫;其次,如果我們在應用層做了相應兜底處理(即穿透到數據庫獲取並同步到Redis節點),因為相應的數據范圍較小,因此打到服務端的流量壓力也沒有那麼大。

Redis Sharding集群跟一致性哈希有什麼瓜葛? 3

4、應用

目前客戶端Jedis能夠支持Redis Sharding,即ShardedJedis 以及結合緩存池的ShardedJedisPool組合使用。而且,Jedis的Redis Sharding實現是採用一致性哈希算法(具體請參考以上第二點)。具體客戶端使用方法請參考以下(整個工程為springboot工程 )。

pom.xml

redis.clients
jedis
2.8.0

application.properties

#redis sharding instance config
redis_client_timeout=500
redis_one_host=192.168.32.101
redis_one_port=6379
redis_one_password=123
redis_two_host=192.168.32.102
redis_two_port=6380
redis_two_password=123

RedisConfiguration.java

@Configuration
public class RedisConfiguration {

//redis one host
@Value("${redis_one_host}")
private String redisOneHost;

//redis one port
@Value("${redis_one_port}")
private int redisOnePort;

//redis one password
@Value("${redis_one_password}")
private String redisOnePassword;

//redis two host
@Value("${redis_one_host}")
private String redisTwoHost;

//redis two port
@Value("${redis_one_port}")
private int redisTwoPort;

//redis two password
@Value("${redis_two_password}")
private String redisTwoPassword;

//redis client timeout
@Value("${redis_client_timeout}")
private int redisClientTimeout;

@Bean(name="redisPool")
public ShardedJedisPool createRedisPool() throws Exception {

//设置连接池的相关配置
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(5);
poolConfig.setMaxIdle(2);
poolConfig.setMaxWaitMillis(5000);
poolConfig.setTestOnBorrow(false);
poolConfig.setTestOnReturn(false);

//设置Redis信息
JedisShardInfo shardInfo1 = new JedisShardInfo(redisOneHost,redisOnePort, redisClientTimeout);
shardInfo1.setPassword(redisOnePassword);
JedisShardInfo shardInfo2 = new JedisShardInfo(redisTwoHost, redisTwoPort, redisClientTimeout);
shardInfo2.setPassword(redisTwoPassword);

//初始化ShardedJedisPool
List infoList = Arrays.asList(shardInfo1, shardInfo2);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);

return jedisPool;

}

public static void main(String[] args){

ShardedJedis shardedJedis = null;

try{

RedisConfiguration redisConfiguration = new RedisConfiguration();
ShardedJedisPool shardedJedisPool = redisConfiguration.createRedisPool();
shardedJedis = shardedJedisPool.getResource();

shardedJedis.set("CSDN", "56");
shardedJedis.set("InfoQ","44");
shardedJedis.set("CNBlog","13");
shardedJedis.set("SegmentFault","22");

Client client1 = shardedJedis.getShard("CSDN").getClient();
Client client2 = shardedJedis.getShard("InfoQ").getClient();
Client client3 = shardedJedis.getShard("CNBlog").getClient();
Client client4 = shardedJedis.getShard("SegmentFault").getClient();

System.out.println("CSDN 位于实例:" + client1.getHost() + "|" + client1.getPort());
System.out.println("InfoQ 位于实例:" + client1.getHost() + "|" + client1.getPort());
System.out.println("CNBlog 位于实例:" + client1.getHost() + "|" + client1.getPort());
System.out.println("SegmentFault 位于实例:" + client1.getHost() + "|" + client1.getPort());

}catch(Exception e){
e.printStackTrace();
}finally {
shardedJedis.close();
}
}
}

運行後打印出來的日誌看,這幾個值是存放到不同的Redis實例中,但是在客戶端使用的時候究竟如何分配到不同的分片具體有Jedis實現的一致性哈希算法所決定的;當然,它所支持的默認算法是64位的MURMUR_HASH算法,另外也支持MD5哈希算法。

“C:Program FilesJavajdk1.8.0_102binjava”…CSDN 位於實例:192.168.32.101|6379InfoQ 位於實例:192.168.32.102|6380CNBlog 位於實例:192.168.32.101|6379SegmentFault 位於實例:192.168 .32.102|6380

三、後話

區別於Redis Sharding這種輕量方案,Redis Cluster是Redis官方於Redis 3.0發布後推出的一種服務端分片的解決方案,它解決了多Redis實例下的協同問題。這裡的協同包含數據自動分片、不同哈希槽(slot)的故障自動轉移、新節點擴容等功能。你看,數據分片以前是靠客戶端自己解決的,哈希槽故障自動轉移以前是靠額外的哨兵機制解決的,現在官方搞個整體解決方案以幫助客戶端輕量化以便客戶端能夠更聚焦於業務邏輯的開發工作。

我的理解是Redis Cluster是一個去中心化的集群解決方案,集群中的每個節點都是平等的(因為每個節點都知道整個集群其他節點的信息,如IP、端口、狀態等,每個節點間都是相互通過長鏈保持通訊的),它跟Sentinel的本質上的差異也在於這裡。一個去中心化模式,另一個集中式的主從模式。

具體cluster如何做數據自動分片、故障自動轉移、節點擴縮容等細節,後面通過另外一篇博客中整理再發佈吧,今天先到這裡。

四、參考

https://www.cs.princeton.edu/courses/archive/fall09/cos518/papers/chash.pdfhttp://www.cs.columbia.edu/~asherman/papers/cachePaper.pdf