1
架构师训练营第 1 期 -Week5 - 课后练习
发布于: 2020 年 10 月 25 日
作业
用你熟悉的编程语言实现一致性 hash 算法。
编写测试用例测试这个算法,测试 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的数量加1
func (c *ConsistencyHash) Set(key string) {
node := c.GetNode(key)
c.NodeKeyCountMap[node]++
}
// 按key读取缓存,这里只是返回节点的host
func (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 code
func 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 算法。
划线
评论
复制
发布于: 2020 年 10 月 25 日阅读数: 53
鲁小鲁
关注
博学而笃志,切问而近思。 2018.09.29 加入
Go开发
评论