写点什么

架构师训练营第 1 期 -Week5 - 课后练习

用户头像
鲁小鲁
关注
发布于: 2020 年 10 月 25 日
架构师训练营第 1 期 -Week5 - 课后练习

作业


  1. 用你熟悉的编程语言实现一致性 hash 算法。

  2. 编写测试用例测试这个算法,测试 100 万 KV 数据,10 个服务器节点的情况下,计算这些 KV 数据在服务器上分布数量的标准差,以评估算法的存储负载不均衡性。


基本思路

1. 应该有 3 个对象: 节点对象 、 Hash 环对象、一致性算法对象

2. 节点对象有服务器地址、其的虚拟节点切片等属性,在构造对象时,自动生成虚拟节点

3. Hash 环对象有虚拟节点对象的切片,提供增加虚拟节点等属性、获取一个 Hash Code 顺时针最近的虚拟节点等方法

4. 一致性算法对象组合 Hash 环、节点对象。提供按 Key 保存数据,按 Key 获取数据,添加节点、获取标准差等方法


使用 Golang 实现

节点对象

保存服务器相关信息、节点对应的虚拟节点

package consistency_hash
var PointUsed = make(map[uint32]bool, 4096) // 记录已经使用的虚拟节点
type Node struct { Host string // 服务器 IP + PORT Code uint32 VirtualNumber uint VirtualCodeList []uint32}
func NewNode(host string, virtualNumber uint) *Node { node := &Node{ Host: host, VirtualNumber:virtualNumber, } node.setVirtualNode() // 设置虚拟节点
return node}
// 设置虚拟节点func (n *Node) setVirtualNode() { n.VirtualCodeList = GetHashCodeList(n.VirtualNumber)}
复制代码


Hash 环对象

具有获取 key 顺时针最近的虚拟节点、保存虚拟节点等方法

package consistency_hash
import ( "sort")
const ( RingMin = 0 RingMax = 4294967295)
// hash环type Ring struct { min uint max uint PointList []uint32}
func NewRing() *Ring { ring := &Ring{ min: RingMin, max: RingMax, PointList: make([]uint32,0, 4096), }
return ring}
// 获得顺时针虚拟节点func (r *Ring) GetNearestVirtualNode(hashCode uint32) uint32 { var nearestPoint uint32 for _, point := range r.PointList{ if hashCode > point { continue }
nearestPoint = point break }
// 如果key的hash code大于所有虚拟节点,则放在第一个虚拟节点 if nearestPoint == 0 { nearestPoint = r.PointList[0] }
return nearestPoint}

// 设置环上的虚拟节点func (r *Ring) SetPointList(node *Node) { r.PointList = append(r.PointList, node.VirtualCodeList...)
// 重新排序 sort.Slice(r.PointList, func(i, j int) bool { return r.PointList[i] < r.PointList[j] })}

复制代码


一致性 Hash 算法对象

具有添加节点、保存缓存、获取缓存、计算标准差等方法


package consistency_hash
import ( "fmt" "math")
type ConsistencyHash struct { ring *Ring NodeNumber float64 NodeList []*Node PointNodeMap map[uint32]*Node NodeKeyCountMap map[*Node]float64}

func NewConsistencyHash() *ConsistencyHash { consistencyHash := &ConsistencyHash{ NodeNumber: 0, ring: NewRing(), NodeList: make([]*Node,0,16), PointNodeMap: make(map[uint32]*Node, 4096), NodeKeyCountMap: make(map[*Node]float64,16), }
return consistencyHash}
// 保存缓存到服务器上。这里只是key对应节点保存的key的数量加1func (c *ConsistencyHash) Set(key string) { node := c.GetNode(key) c.NodeKeyCountMap[node]++}
// 按key读取缓存,这里只是返回节点的hostfunc (c *ConsistencyHash) Get(key string) string { node := c.GetNode(key)
return node.Host}
// 添加节点func (c *ConsistencyHash) AddNode(node *Node) { c.NodeList = append(c.NodeList,node) c.NodeKeyCountMap[node] = 0 c.NodeNumber++
for _,point := range node.VirtualCodeList{ c.PointNodeMap[point] = node }
c.ring.SetPointList(node)}
// 获得物理节点func (c *ConsistencyHash) GetNode(key string) *Node { hashCode := GetHashCode(key) nearestPoint := c.ring.GetNearestVirtualNode(hashCode)
return c.PointNodeMap[nearestPoint]}
// 获取标准差func (c *ConsistencyHash) GetSigma(){
var sum float64 = 0 for _, value := range c.NodeKeyCountMap{ sum += value }
mean := sum / c.NodeNumber fmt.Printf("平均值:%0.2f", mean)
var variance float64 for _, v := range c.NodeKeyCountMap { variance += math.Pow(v - mean, 2) }
σ := math.Sqrt(variance / c.NodeNumber) fmt.Printf("标准差:%0.3f\n", σ)}
复制代码


工具函数

有 3 个函数:获取指定长度个的 Hash code、获取一个字符串的 Hash code、获取随机字符串。

package consistency_hash
import ( "fmt" "hash/crc32" "math/rand" "time")
// 全局的随机种子var r *rand.Rand
func init() { r = rand.New(rand.NewSource(time.Now().Unix()))}
// 生成指定长度的hash code(虚拟节点)func GetHashCodeList(number uint) []uint32 { var index uint = 0 codeMap := make(map[uint32]bool,number) codeList := make([]uint32,0,number) for { if index >= number { break }
key := GetRandom(10) code := GetHashCode(key)
// 如果虚拟节点以及被使用,则继续取下一个 if ok:= PointUsed[code]; ok { continue }
if ok := codeMap[code]; ok { continue }
if code < RingMin || code > RingMax { fmt.Printf("异常的code:%d\n", code) continue }
index++ codeMap[code] = true codeList = append(codeList, code) }
return codeList}
// 获取key的hash codefunc GetHashCode(key string) uint32 {
hashCode:=crc32.ChecksumIEEE([]byte(key))
return hashCode}
//生成随机字符串func GetRandom(size int) string { str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" result := make([]byte, 0, size) for i := 0; i < size; i++ { result = append(result, str[r.Intn(len(str))]) }
return string(result)}
复制代码


测试用例

package consistency_hash
import ( "fmt" "testing")

func TestConsistencyHash(t *testing.T) { nodeNum := 10 // 10个服务器 nodePrefix := "10.35.5.%d:8888" var virtualNumber uint = 150 // 每个节点 150个虚拟节点
consistencyHash := NewConsistencyHash() // 10个服务器节点 for i := 0; i < nodeNum; i++ { key := fmt.Sprintf(nodePrefix, i) consistencyHash.AddNode(NewNode(key, virtualNumber)) }
// 生成100万个key,通过一致性算法放到集群内 keyNum := 1000000 for i:=0;i<keyNum;i++ { consistencyHash.Set(GetRandom(10)) }
// 求标准差 consistencyHash.GetSigma() t.Log("集群各个服务器内key的数量:\n") for node,count := range consistencyHash.NodeKeyCountMap{ t.Log(node.Host, ": ",count) }}
复制代码


平均值:100000.00 标准差:4516.950


单节点的虚拟节点为 150 时,标准差少的时候为 2000,一般没有超过 1 万。


据说不同 Hash code 的算法对标准差会有影响,所以一致性 hash 算法要根据实际情况选择合适的 Hash code 算法。

用户头像

鲁小鲁

关注

博学而笃志,切问而近思。 2018.09.29 加入

Go开发

评论

发布
暂无评论
架构师训练营第 1 期 -Week5 - 课后练习