一、集群方案與分區
1、一致性hash分區
一致性哈希分區(Distributed Hash Table)實現思路是為系統中每個節點分配一個token,范圍一般在0~232,這些token構成一個哈希環。數據讀寫執行節點查找操作時,先根據key計算hash值,然后順時針找到第一個大于等于該哈希值的token節點。
這種方式相比節點取余最大的好處在于加入和刪除節點只影響哈希環中相鄰的節點,對其他節點無影響。
為了保證數據和負載的均衡,通過虛擬槽分區,巧妙地使用了哈希空間,使用分散度良好的哈希函數把所有數據映射到一個固定范圍的整數集合中,整數定義為槽(slot)。
1、客戶端分片
把分片的邏輯放在Redis客戶端實現,通過Redis客戶端預先定義好的路由規則,把對Key的訪問轉發到不同的Redis實例中,最后把返回結果匯集。
如ShardedJedisPool。
http://shift-alt-ctrl.iteye.com/blog/1885959
#node構建過程(redis.clients.util.Sharded):
//shards列表為客戶端提供了所有redis-server配置信息,包括:ip,port,weight,name
//其中weight為權重,將直接決定“虛擬節點”的“比例”(密度),權重越高,在存儲是被hash命中的概率越高
//--其上存儲的數據越多。
//其中name為“節點名稱”,jedis使用name作為“節點hash值”的一個計算參數。
//---
//一致性hash算法,要求每個“虛擬節點”必須具備“hash值”,每個實際的server可以有多個“虛擬節點”(API級別)
//其中虛擬節點的個數= “邏輯區間長度” * weight,每個server的“虛擬節點”將會以“hash”的方式分布在全局區域中
//全局區域總長為2^32.每個“虛擬節點”以hash值的方式映射在全局區域中。
// 環形:0-->vnode1(:1230)-->vnode2(:2800)-->vnode3(400000)---2^32-->0
//所有的“虛擬節點”將按照其”節點hash“順序排列(正序/反序均可),因此相鄰兩個“虛擬節點”之間必有hash值差,
//那么此差值,即為前一個(或者后一個,根據實現而定)“虛擬節點”所負載的數據hash值區間。
//比如hash值為“2000”的數據將會被vnode1所接受。
//---
private void initialize(List<S> shards) {
nodes = new TreeMap<Long, S>();//虛擬節點,采取TreeMap存儲:排序,二叉樹
for (int i = 0; i != shards.size(); ++i) {
final S shardInfo = shards.get(i);
if (shardInfo.getName() == null)
//當沒有設置“name”是,將“SHARD-NODE”作為“虛擬節點”hash值計算的參數
//"邏輯區間步長"為160,為什么呢??
//最終多個server的“虛擬節點”將會交錯布局,不一定非常均勻。
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
}
else
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);
}
resources.put(shardInfo, shardInfo.createResource());
}
}
#node選擇方式:
public R getShard(String key) {
return resources.get(getShardInfo(key));
}
//here:
public S getShardInfo(byte[] key) {
//獲取>=key的“虛擬節點”的列表
SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
//如果不存在“虛擬節點”,則將返回首節點。
if (tail.size() == 0) {
return nodes.get(nodes.firstKey());
}
//如果存在,則返回符合(>=key)條件的“虛擬節點”的第一個節點
return tail.get(tail.firstKey());
}
2、Twemproxy
Twemproxy是由Twitter開源的Redis代理,其基本原理是:Redis客戶端把請求發送到Twemproxy,Twemproxy根據路由規則發送到正確的Redis實例,最后Twemproxy把結果匯集返回給客戶端。(Twemproxy通過lvs做負載均衡及高可用)
Twemproxy通過引入一個代理層,將多個Redis實例進行統一管理,使Redis客戶端只需要在Twemproxy上進行操作,而不需要關心后面有多少個Redis實例,從而實現了Redis集群。
缺點:由于Redis客戶端的每個請求都經過Twemproxy代理才能到達Redis服務器,這個過程中會產生性能損失。最大的問題,Twemproxy無法平滑地增加Redis實例(可以做到自動剔除)。
3、codis
Codis Proxy:Redis客戶端連接到Redis實例的代理,實現了Redis的協議,Redis客戶端連接到Codis Proxy進行各種操作。Codis Proxy是無狀態的,可以用Keepalived等負載均衡軟件部署多個Codis Proxy實現高可用。
CodisRedis:Codis項目維護的Redis分支,添加了slot和原子的數據遷移命令。Codis上層的 Codis Proxy和Codisconfig只有與這個版本的Redis通信才能正常運行。
Codisconfig:Codis管理工具。可以執行添加刪除CodisRedis節點、添加刪除Codis Proxy、數據遷移等操作。另外,Codisconfig自帶了HTTP server,里面集成了一個管理界面,方便運維人員觀察Codis集群的狀態和進行相關的操作,極大提高了運維的方便性,彌補了Twemproxy的缺點。
ZooKeeper:Codis依賴于ZooKeeper存儲數據路由表的信息和Codis Proxy節點的元信息。另外,Codisconfig發起的命令都會通過ZooKeeper同步到CodisProxy的節點。
Codis最大的優勢在于支持平滑增加(減少)Redis Server Group(Redis實例),能安全、透明地遷移數據,這也是Codis 有別于Twemproxy等靜態分布式 Redis 解決方案的地方。Codis增加了Redis Server Group后,就牽涉到slot的遷移問題。
4、redis cluster
一個Redis實例具備了“數據存儲”和“路由重定向”,完全去中心化的設計。這帶來的好處是部署非常簡單,直接部署Redis就行,不像Codis有那么多的組件和依賴。
但需要客戶端支持,如果對協議進行了較大的修改,對應的Redis客戶端也需要升級。造成Redis 3.0集群在業界并沒有被大規模使用。
5、redis cluster使用注意
1)key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key執行批量操作。對于映射為不同slot值的key由于執行mget、mget等操作可能存在于多個節點上因此不被支持。
2)key事務操作支持有限。同理只支持多key在同一節點上的事務操作,當多個key分布在不同的節點上時無法使用事務功能。
3)key作為數據分區的最小粒度,因此不能將一個大的鍵值對象如hash、list等映射到不同的節點。
4)不支持多數據庫空間。單機下的Redis可以支持16個數據庫,集群模式下只能使用一個數據庫空間,即db0。
5)復制結構只支持一層,從節點只能復制主節點,不支持嵌套樹狀復制結構。
二、redis cluster搭建與節點通訊
1、redis cluster搭建
1)準備節點
#節點端口
port 6379
# 開啟集群模式
cluster-enabled yes
# 節點超時時間,單位毫秒
cluster-node-timeout 15000
# 集群內部配置文件
cluster-config-file "nodes-6379.conf"
當集群內節點信息發生變化,如添加節點、節點下線、故障轉移等。節點會自動保存集群狀態到配置文件中。需要注意的是,Redis自動維護集群配置文件,不要手動修改,防止節點重啟時產生集群信息錯亂。
#cat data/nodes-6379.conf
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 //節點ID
myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
節點ID不同于運行ID。節點ID在集群初始化時只創建一次,節點重啟時會加載集群配置文件進行重用,而Redis的運行ID每次重啟都會變化。
2)節點握手
節點握手是指一批運行在集群模式下的節點通過Gossip協議彼此通信,達到感知對方的過程。
只需要在集群內任意節點上執行cluster meet命令加入新節點,握手狀態會通過消息在集群內傳播,這樣其他節點會自動發現新節點并發起握手流程。
3)分配槽
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0...5461}
redis-trib.rb是采用Ruby實現的Redis集群管理工具。內部通過Cluster相關命令幫我們簡化集群創建、檢查、槽遷移和均衡等常見運維操作。
三、請求路由與客戶端
Redis集群對客戶端通信協議做了比較大的修改,為了追求性能最大化,并沒有采用代理的方式而是采用客戶端直連節點的方式。
在集群模式下,Redis接收任何鍵相關命令時首先計算鍵對應的槽,再根據槽找出所對應的節點,如果節點是自身,則處理鍵命令;否則回復MOVED重定向錯誤,通知客戶端請求正確的節點。這個過程稱為MOVED重定向。
#計算槽節點
def key_hash_slot(key):
int keylen = key.length();
for (s = 0; s < keylen; s++){
if (key[s] == '{'){
break;
}
}
if (s == keylen) return crc16(key,keylen) & 16383;
for (e = s+1; e < keylen; e++):
if (key[e] == '}') break;
if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;
return crc16(key+s+1,e-s-1) & 16383; /* 使用{和}之間的有效部分計算槽,{hash_tag} */
#查找槽節點
def execute_or_redirect(key):
int slot = key_hash_slot(key);
ClusterNode node = slots[slot];
if(node == clusterState.myself):
return executeCommand(key);
else:
return '(error) MOVED {slot} {node.ip}:{node.port}';
mget等命令優化批量調用時,鍵列表必須具有相同的slot,否則會報錯。這時可以利用hash_tag讓不同的鍵具有相同的slot達到優化的目的。
Pipeline同樣可以受益于hash_tag,由于Pipeline只能向一個節點批量發送執行命令,而相同slot必然會對應到唯一的節點,降低了集群使用Pipeline的門檻。
Jedis客戶端命令執行流程
1)計算slot并根據slots緩存獲取目標節點連接,發送命令。
2)如果出現連接錯誤,使用隨機連接重新執行鍵命令,每次命令重試對redi-rections參數減1。
3)捕獲到MOVED重定向錯誤,使用cluster slots命令更新slots緩存(renewSlotCache方法)。
4)重復執行1)~3)步,直到命令執行成功,或者當redi-rections<=0時拋出JedisClusterMaxRedirectionsException異常。
#JedisClusterCommand的runWithRetries方法(jedis2.8.1)
private T runWithRetries(byte[] key, int redirections, boolean tryRandomNode, boolean asking) {
if (redirections <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
}
Jedis connection = null;
try {
if (asking) {
// TODO: Pipeline asking with the original command to make it
// faster....
connection = askConnection.get();
connection.asking();
// if asking success, reset asking flag
asking = false;
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
return execute(connection);
} catch (JedisConnectionException jce) {
if (tryRandomNode) {
// maybe all connection is down
throw jce;
}
// release current connection before recursion
releaseConnection(connection);
connection = null;
// retry with random connection
return runWithRetries(key, redirections - 1, true, asking);
} catch (JedisRedirectionException jre) {
// if MOVED redirection occurred,
if (jre instanceof JedisMovedDataException) {
// it rebuilds cluster's slot cache
// recommended by Redis cluster specification
this.connectionHandler.renewSlotCache(connection);
}
// release current connection before recursion or renewing
releaseConnection(connection);
connection = null;
if (jre instanceof JedisAskDataException) {
asking = true;
askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));
} else if (jre instanceof JedisMovedDataException) {
} else {
throw new JedisClusterException(jre);
}
return runWithRetries(key, redirections - 1, false, asking);
} finally {
releaseConnection(connection);
}
}
#renewSlotCache
public void renewSlotCache(Jedis jedis) {
try {
cache.discoverClusterSlots(jedis);
} catch (JedisConnectionException e) {
renewSlotCache();
}
}
#discoverClusterSlots
public void discoverClusterSlots(Jedis jedis) {
w.lock();
try {
this.slots.clear();
List<Object> slots = jedis.clusterSlots();
for (Object slotInfoObj : slots) {
List<Object> slotInfo = (List<Object>) slotInfoObj;
if (slotInfo.size() <= 2) {
continue;
}
List<Integer> slotNums = getAssignedSlotArray(slotInfo);
// hostInfos
List<Object> hostInfos = (List<Object>) slotInfo.get(2);
if (hostInfos.size() <= 0) {
continue;
}
// at this time, we just use master, discard slave information
HostAndPort targetNode = generateHostAndPort(hostInfos);
setNodeIfNotExist(targetNode);
assignSlotsToNode(slotNums, targetNode);
}
} finally {
w.unlock();
}
}
#assignSlotsToNode
public void assignSlotsToNode(List<Integer> targetSlots, HostAndPort targetNode) {
w.lock();
try {
JedisPool targetPool = nodes.get(getNodeKey(targetNode));
if (targetPool == null) {
setNodeIfNotExist(targetNode);
targetPool = nodes.get(getNodeKey(targetNode));
}
for (Integer slot : targetSlots) {
slots.put(slot, targetPool);
}
} finally {
w.unlock();
}
}
四、故障轉移與集群運維
主觀下線
1)節點a發送ping消息給節點b,如果通信正常將接收到pong消息,節點a更新最近一次與節點b的通信時間。
2)如果節點a與節點b通信出現問題則斷開連接,下次會進行重連。如果一直通信失敗,則節點a記錄的與節點b最后通信時間將無法更新。
3)節點a內的定時任務檢測到與節點b最后通信時間超高cluster-node-timeout時,更新本地對節點b的狀態為主觀下線(pfail)。
客觀下線
1)當消息體內含有其他節點的pfail狀態會判斷發送節點的狀態,如果發送節點是主節點則對報告的pfail狀態處理,從節點則忽略。
2)找到pfail對應的節點結構,更新clusterNode內部下線報告鏈表。
3)根據更新后的下線報告鏈表告嘗試進行客觀下線。
嘗試客觀下線
1)首先統計有效的下線報告數量,如果小于集群內持有槽的主節點總數的一半則退出。
2)當下線報告大于槽主節點數量一半時,標記對應故障節點為客觀下線狀態。
3)向集群廣播一條fail消息,通知所有的節點將故障節點標記為客觀下線,fail消息的消息體只包含故障節點的ID。
集群中使用管道
http://www.bubuko.com/infodetail-1106789.html
https://blog.csdn.net/youaremoon/article/details/51751991