最重要的一點忘了寫了:一致性哈希算法爲啥能在節點變更的時候只有少量key遷移是因爲sortkeys列表其實就是一個哈希環,客戶端的哈希值和存量的節點哈希值在有序的sortkeys列表中的相對位置沒有變,變的是下線節點前面的哈希到再前面一個之間的值所以變更率爲:1-n/m
open-falcon中transfer會爲judge和graph生成兩個一致性哈希環
func initNodeRings() {
cfg := g.Config()
JudgeNodeRing = rings.NewConsistentHashNodesRing(int32(cfg.Judge.Replicas), cutils.KeysOfMap(cfg.Judge.Cluster))
GraphNodeRing = rings.NewConsistentHashNodesRing(int32(cfg.Graph.Replicas), cutils.KeysOfMap(cfg.Graph.Cluster))
}
哈希環的目的是爲了給每個上報上來的counter:endpoint+metric+tag 算一致性哈希打到不同後端judge 和graph實例中.返回來查找時也這樣幹.典型的分佈式緩存思想,這是falcon承受高併發的基礎。一致性哈希普遍存在 lvs nginx 等lb應用場景中。Nginx的負載均衡 - 一致性哈希 (Consistent Hash)
哈希Hash,就是把任意長度的輸入,通過散列算法,變換成固定長度的輸出,該輸出就是散列值。
不定長輸入-->哈希函數-->定長的散列值
1.哈希算法的本質是對原數據的有損壓縮
2.哈希運算包括 加法Hash、 位運算Hash、乘法Hash、除法Hash、查表Hash、混合Hash
3.散列值的固定長度是將輸入分成固定長度位,依次進行hash運算,然後用不通方法迭代.位不足補全
4.哈希表的查找: 集合中拿出一個元素作對比,不一致再縮小範圍查找,而哈希的查找是根據key值直接計算出這個元素在集合中的位置,近乎O(1)時間複雜度
5.哈希的抗碰撞能力:對於任意兩個不同的數據塊,其hash值相同的可能性極小:對於一個給定的數據塊,找到和它hash值相同的數據塊極爲困難。
6.抗篡改能力:對於一個數據塊,哪怕只改動其一個比特位,其hash值的改動也會非常大。
下面來看下一致性哈希算法
當有節點變更(增加或減少時)只有少量key 需要reblance到新的節點。
良好的一致性哈希算法應該滿足:
平衡性:是指哈希的結果儘量均分到所有節點中
單調性:
分散性:由不同終端哈希的結果不一致,好的一致性哈希算法應避免這個
負載:不同的終端可能將相同的內容映射到不同的節點
1.一致性哈希算法需要的數據結構爲 一個map 一個排序後的哈希key列表
2.生成哈希環的過程爲: 爲每個節點通過散列算法(md5 crc32)生成 key,更新map,添加key列表
3.查找過程:根據要存儲的 字符串算出key2 然後通過二分查找法找到比key2大一點的那個key1的索引,根據key1去map中拿到對應的節點
4.引入虛擬節點是爲了解決數據傾斜的問題:一致性哈希算法在服務節點太少時,容易因爲節點分部不均勻而造成數據傾斜問題
5.虛擬節點做法就是生成多個(一般30+)個hashkey 對應同一個節點:這就好比你去淘寶搜一樣商品,看了一家店後又看到賣同樣同樣東西的另一家店,賣家給你提供了個店鋪的列表跟你說這幾家店鋪都是我的。殊途同歸的感覺
面具體看一致性哈希代碼 源碼-->
falcon中用的哈希環源碼
https://github.com/toolkits/consistent
1.先看下數據結構 :記住這個map 和 sortkeys,因爲這兩個是核心
type HashKey uint32
type HashKeyOrder []HashKey
type HashRing struct {
ring map[HashKey]string //哈希環中的map
sortedKeys []HashKey //哈希key列表
nodes []string //節點列表
weights map[string]int
}
再看下falcon中的 是不是發現差不多
// Consistent holds the information about the members of the consistent hash circle.
type Consistent struct {
circle map[uint32]string
members map[string]bool
sortedHashes uints
NumberOfReplicas int
count int64
scratch [64]byte
sync.RWMutex
}
2.再來看下生成哈希環的過程:
首先初始化下結構體,然後調用一個生成環的函數
func New(nodes []string) *HashRing {
hashRing := &HashRing{
ring: make(map[HashKey]string),
sortedKeys: make([]HashKey, 0),
nodes: nodes,
weights: make(map[string]int),
}
//生成哈希環
hashRing.generateCircle()
return hashRing
}
看下這裏的邏輯: 1.循環所有虛擬節點,根據node生成 hashkey 分別塞入map 和sortkeys中
func (h *HashRing) generateCircle() {
totalWeight := 0
//這段關於權重的可以不看
for _, node := range h.nodes {
if weight, ok := h.weights[node]; ok {
totalWeight += weight
} else {
totalWeight += 1
h.weights[node] = 1
}
}
for _, node := range h.nodes {
weight := h.weights[node]
// 三個節點且權重都是1時 factor=40 factor是爲了增加虛擬節點
factor := math.Floor(float64(40*len(h.nodes)*weight) / float64(totalWeight))
for j := 0; j < int(factor); j++ {
//nodekey : 'node01-00' 'node01-01' 'node01-02'
nodeKey := fmt.Sprintf("%s-%d", node, j)
//bKey : [236 120 185 49 156 84 249 99 169 176 131 185 148 230 91 141]
bKey := hashDigest(nodeKey)
for i := 0; i < 3; i++ {
//key:3261919718
//key:2087224356
//key:2167064686
key := hashVal(bKey[i*4 : i*4+4])
fmt.Printf("Akey:%v\n",key)
//把h.ring這個map 塞了3*factor=120 個 value爲這個node的key
h.ring[key] = node
//列表添加操作
h.sortedKeys = append(h.sortedKeys, key)
}
}
}
//h.sortedKeys ring.keys() 就是[31575610 64842500 65702829 80981415 ...]
sort.Sort(HashKeyOrder(h.sortedKeys))
}
看下這裏的hashDigest:就是生成MD5 []byte
func hashDigest(key string) [md5.Size]byte {
return md5.Sum([]byte(key))
}
falcon用的是crc32.ChecksumIEEE
func (c *Consistent) hashKey(key string) uint32 {
if len(key) < 64 {
var scratch [64]byte
copy(scratch[:], key)
return crc32.ChecksumIEEE(scratch[:len(key)])
}
return crc32.ChecksumIEEE([]byte(key))
}
看下這裏的hashval:將生成的md5 byte每四位進行位移+或操作作爲hashkey
func hashVal(bKey []byte) HashKey {
//位移加或操作
return ((HashKey(bKey[3]) << 24) |
(HashKey(bKey[2]) << 16) |
(HashKey(bKey[1]) << 8) |
(HashKey(bKey[0])))
}
看到這裏我們心裏就有譜了:爲每個節點算出3*40=120個uint32的數字作爲key塞入map和hashkey列表中 最後將hashkey列表排序,爲最後的二分查找做準備
3.最後我們看下查找的過程:
查找的過程就是先根據 key生成哈希key 通過sortkeys列表二分查找找到這個key在列表中的索引,根據索引拿到hashkey 再去map get出對應的節點
func (h *HashRing) GetNode(stringKey string) (node string, ok bool) {
//首先要獲取這個key 在sortedKeys列表中的索引
pos, ok := h.GetNodePos(stringKey)
if !ok {
return "", false
}
return h.ring[h.sortedKeys[pos]], true
}
func (h *HashRing) GetNodePos(stringKey string) (pos int, ok bool) {
if len(h.ring) == 0 {
return 0, false
}
// key 爲hashkey 2880865363
key := h.GenKey(stringKey)
nodes := h.sortedKeys
/*
這裏獲取hashkey在h.sortedKeys中的索引採用的是二分查找法
sort.Search 的第二個參數很有意思,是一個返回bool的方法
*/
pos = sort.Search(len(nodes), func(i int) bool { return nodes[i] > key })
if pos == len(nodes) {
// Wrap the search, should return first node
return 0, true
} else {
return pos, true
}
}
讓我們來看下查找這裏:使用的
/*
這裏獲取hashkey在h.sortedKeys中的索引採用的是二分查找法
sort.Search 的第二個參數很有意思,是一個返回bool的方法
*/
pos = sort.Search(len(nodes), func(i int) bool { return nodes[i] > key })
讓我們來看下源碼中Search的說明:連我這麼渣的英文都看出來了這是二分查找法:
過程就是根據算出的 key1 和這個有序列表做二分查找找到大於key1的最小的key
>>1就是除以2的一次方 就是減半了
// Search uses binary search to find and return the smallest index i
// in [0, n) at which f(i) is true, assuming that on the range [0, n),
// f(i) == true implies f(i+1) == true. That is, Search requires that
// f is false for some (possibly empty) prefix of the input range [0, n)
// and then true for the (possibly empty) remainder; Search returns
// the first true index. If there is no such index, Search returns n.
// (Note that the "not found" return value is not -1 as in, for instance,
// strings.Index.)
// Search calls f(i) only for i in the range [0, n).
func Search(n int, f func(int) bool) int {
// Define f(-1) == false and f(n) == true.
// Invariant: f(i-1) == false, f(j) == true.
i, j := 0, n
for i < j {
h := int(uint(i+j) >> 1) // avoid overflow when computing h
// i ≤ h < j
if !f(h) {
i = h + 1 // preserves f(i-1) == false
} else {
j = h // preserves f(j) == true
}
}
// i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
return i
}
來擼個python的二分查找
def bin_search(data_set,val):
#low 和high代表下標 最小下標,最大下標
low=0
high=len(data_set)-1
while low <=high:# 只有當low小於High的時候證明中間有數
mid=(low+high)//2
print "low:%d,mid:%d,high:%d" % (low,mid, high)
if data_set[mid]==val:
return mid #返回他的下標
elif data_set[mid]>val:
high=mid-1
else:
low=mid+1
return # return null證明沒有找到
data_set = list(range(100))
print(bin_search(data_set, 34))
下面我們來測試下這個一致性哈希算法 在節點變化時key的遷移情況
func RingInit(server_arr []string) *hashring.HashRing{
return hashring.New(server_arr)
}
func PengzhuangCeshi(){
servers1 :=[]string{
"192.168.0.241:11212",
"192.168.0.242:11212",
"192.168.0.243:11212",
"192.168.0.244:11212",
"192.168.0.245:11212",
}
servers2 :=[]string{
"192.168.0.241:11212",
"192.168.0.242:11212",
"192.168.0.243:11212",
"192.168.0.244:11212",
}
r1 := RingInit(servers1)
r2 := RingInit(servers2)
test_num :=10000000
client_ip := "10.10.10.10"
migr_num :=0
for i:=0;i<test_num;i++{
key :=fmt.Sprintf("%s_%v",client_ip,i)
choose_server1,_ := r1.GetNode(key)
choose_server2,_ := r2.GetNode(key)
if choose_server1 !=choose_server2{
migr_num+=1
}
}
fmt.Println("migr_num",migr_num)
fmt.Printf("migr_rate %.3f", float32(migr_num)/float32(test_num))
}
func main(){
PengzhuangCeshi()
//Test()
}
test_num :=10000000
4/5變化
migr_num 1839416
migr_rate 0.184
5/2變化
migr_num 5737265
migr_rate 0.574
3/2變化
migr_num 3072919
migr_rate 0.307
4/3變化
migr_num 2491462
migr_rate 0.249
如果一臺服務器不可用,則受影響的數據僅僅是此服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它不會受到影響。
我們推測遷移率爲 rate = 1- m/n if m<n ???
最後廢話少說 擼個python的一致性哈希環
#coding:utf-8
import md5
class ConsistentHashRing(object):
def __init__(self,nodes,replicas=3):
self.replicas = replicas
self.ring = {}
self.sort_keys = []
if nodes:
for node in nodes:
self.add_nodes(node)
def add_nodes(self,node):
for i in xrange(self.replicas):
key='%s_%d'%(node,i)
hashkey = self.gen_key(key)
#print hashkey
self.ring[hashkey] = node
self.sort_keys.append(hashkey)
self.sort_keys.sort()
def gen_key(self,key):
m = md5.new()
m.update(key)
return long(m.hexdigest(), 16)
def get_node(self,data_key):
return self.get_node_pos(data_key)[0]
def get_node_pos(self,data_key):
key = self.gen_key(data_key)
nodes = self.sort_keys
for i in xrange(0,len(nodes)):
node = nodes[i]
if key <= node:
return self.ring[node],i
return self.ring[nodes[0]],0
if __name__ == '__main__':
nodes=["node-1","node-2","node-3"]
Ring = ConsistentHashRing(nodes)
for i in xrange(10000):
print Ring.get_node("key1-%d"%i)