一致性哈希算法和Go语言实现

一致性哈希算法,当我第一次听到这个名字的时候,感觉特别高深。而它往往会和分布式系统相关,准确的说,是分布式缓存。

在Web服务中,缓存是介于数据库和服务端程序之间的一个东西。在网站的业务还不是很大的时候,一般不会需要这个东西,每次都可以从数据库中查询获得数据,但是随着网站的访问量增大,每次请求都访问数据库的话,数据库的压力就会非常大,导致响应变慢,于是就需要一种缓存数据库,它们的存储结构一般比较简单,而且它们的数据都是存储在内存中。典型的缓存数据库有Redis,Memcache。有了缓存之后,服务端的程序可以先从缓存中读取数据,如果发现没有数据,那么从数据库中读取,并且将它放入缓存中,局部性原理,它们下次被访问的概率会很大,因此,当再次访问该数据的时候,就可以直接从缓存中读取数据。由于磁盘和内存的性能差别是很大的,因此,命中缓存和不命中缓存的性能差距也是很大的。要增加缓存的命中概率,很显然,增加机器是一种简单有效的解决办法,但是一台机器的内存是非常有限的,因此,我们需要多台机器一起组成一个缓存集群。然后将数据分散到各个机器上,在这个分散过程中,我就需要一种hash算法。如果不加额外考虑,我们可以根据机器的台数n,给机器编号成0到n-1,使用模n的方法分散到各个机器中,根据概率的分布原理,每台机器获得的数据大致是相同的。

随着网站规模的增大,n台机器已经不足以支撑现有的数据缓存,这个时候,需要增加机器,如果还是按照取模的办法,假如现在的机器增加到n+1台,那么缓存的命中率会变成原来的1/(n+1),这个命中率实在是太低了。根据理论计算,要保证最大的负载均衡的情况下,我们增加一台机器后的最大命中率是可以达到原来的n/(n+1)的。就相当于从原来的每台机器上拿出1/(n+1)的数据,然后放到新的机器里面。根据数学归纳法可以求得在n台机器上增加m台机器,在达到最大化负载均衡的情况下,缓存命中率最高可以达到原来的n/(n+m)。那么怎样的一种哈希算法才可以使得扩容后的命令率接近这个最高理论值呢?这就是一致性哈希算法。

现在,我们要做的其实就是设计一个满足要求的哈希算法。我们的期望是这样的,当加入新的成员(缓存机器)的时候,我们可以分别从原来的机器中抽取一小部分的数据到我们新加入的机器中,这样就保证了原有机器的数据不变化。举个形象的例子,大家一起喝可乐,开始八个人,每个人一小满杯,刚好倒完。又来了两个人,这个时候,我们不可能把所有可乐倒回一个大瓶子,然后分给大家吧,最好最公平的方式就是大家都匀出一小点给新的两个空杯子。这只是个例子,不一定实际,但是道理是一样的。在这种分布式缓存系统中,我们应该怎么做呢?在量子力学理论中,能力是一份一份的,而不是连续的。受这个启发,我们可以把机器分成一小份一小份,然后每一小份负责一个哈希范围。这样一台机器就负责多个哈希范围。这些一小份一小份,我们称之为虚拟节点。再来考虑在n台机器基础上增加m台机器的情况。其实只要把原来机器每台总份数的m/(n+m)分给新的机器就可以了。至于整个哈希范围分成多少份,这个问题需要实践和经验来获得,显然,分多了的话,管理和操作的效率下降,而分少了呢,随着机器增加容易造成机器的负载不均匀。

首先,我们把整个哈希范围设想成一条线段,一般用一个无符号的32位整型来表示整个范围。在线段上,我们可以将它分成一段一段的,每一个小线段由头和尾两个坐标来表示,同时为了避免相邻两个线段交界处重复,我们可以预先定义每一个小线段都是左闭右开。然后每台机器都会管理多个小线段。当有新的机器加入的时候,我们根据加入的机器的数量,根据最大限度负载均衡原则,计算出需要从每个现有的机器中拿出多少份给新的机器。也就是说每个机器都会维护一个自己的数据结构,里面包括自己需要管理的区间的集合,而每一次机器的变动操作都会更新这个数据机构。而当要查询一个key的数据对应哪一台机器的时候,为了加快查询速度,需要建立一张倒排索引,以便快速查到对应的机器。这种方法是我自己根据一致性哈希达到的目的而想的。它可以实现一致性哈希的要求,但是明显管理起来比较麻烦,而且具体分成多少份也是一个难定夺的值。下面,对它进行简化。

上面,每一台机器管理一条线段,很明显这样子比较复杂,对于一段区间,需要维护一个包含两个值的元组。事实上,只需要维护一个点即可,我们可以定义这个点只管辖它本身以及顺时针方向直到下一个点(不包括下一个点)的区间。但是对于最后一个点和第一个点会有点麻烦,于是,把原来的线段两头相接,这样就形成了一个环。这样,每个点都可以往顺时针方向寻找,并且能够寻找到相应的点。因此,一台机器只要维护他自己的点的列表就可以了。在上面,当增加机器的时候,我们选择的是将已有的份移向新的机器,这样需要维护一个算法,保证最大负载均衡,而实际上可以在已有的环上增加点来达到分割已有机器数据的目的。例如在在线段一中加上一个点,则线段一为分割成两个线段,这样就巧妙的把数据分割问题解决了,当然这么做之后,导致总的份数不再是确定值(其实我们根本不在意总的份数),总的份数由机器数量以及每个机器占有的份数决定。每个机器占用多少份比较合理呢?有人给出的答案是150。当然这也是个经验值。现在,现在的一致性哈希算法变成了这样:每个机器有用固定个数的点,这些点分布在一个由32位无符号整形组成的环上。每个点管理本身和顺时针方向直到下一个点的区域。如何生成这些点呢?随机。使用哈希函数对机器名字进行哈希,得到一个无符号32位整型。在Golang中使用crc32.ChecksumIEEE函数生成。因此对于一个key,需要查询它对应的机器的时候,只需要查找它逆时针方向的第一个点,然后找到这个点对应的机器即可。

下面是Golang语言的一个简单实现,仅供参考。主要借鉴了这份代码的实现。其实对于节点的查询,这里使用排序后二分查询(Golang自带sort包)。复杂度为logN,但是插入和删除的复杂度是nlogN。如果使用红黑树实现,两者的复杂度都是nlogN,从算法角度来讲,使用红黑树比较合适,但是实际上一致性哈希使用最多的是查找,而对节点的改动相对较少,因此差距并不是很大。

import (
    "hash/crc32"
    "sort"
    "strconv"
    "sync"
)

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return "ConsistentError: " + e.s
}

// 定义错误类型
func ConsistentError(text string) error {
    return &errorString{text}
}

// 定义环类型
type Circle []uint32

func (c Circle) Len() int {
    return len(c)
}

func (c Circle) Less(i, j int) bool {
    return c[i] < c[j]
}

func (c Circle) Swap(i, j int) {
    c[i], c[j] = c[j], c[i]
}

type Hash func(date []byte) uint32

type Consistent struct {
    hash         Hash  // 产生uint32类型的函数
    circle       Circle  // 环
    virtualNodes int  // 虚拟节点个数,文中所说150
    virtualMap   map[uint32]string  // 点到主机的映射
    members      map[string]bool  // 主机列表
    sync.RWMutex
}

func NewConsisten() *Consistent {
    return &Consistent{
        hash:         crc32.ChecksumIEEE,
        circle:       Circle{},
        virtualNodes: 150,
        virtualMap:   make(map[uint32]string),
        members:      make(map[string]bool),
    }
}

// generate a string key for an element with an index
func (c *Consistent) eltKey(key string, idx int) string {
    return key + "|" + strconv.Itoa(idx)
}

func (c *Consistent) updateCricle() {
    c.circle = Circle{}
    for k := range c.virtualMap {
        c.circle = append(c.circle, k)
    }
    sort.Sort(c.circle)
}

func (c *Consistent) Members() []string {
    c.RLock()
    defer c.RUnlock()

    m := make([]string, len(c.members))

    var i = 0
    for k := range c.members {
        m[i] = k
        i++
    }

    return m
}

func (c *Consistent) Get(key string) string {
    hashKey := c.hash([]byte(key))
    c.RLock()
    defer c.RUnlock()

    i := c.search(hashKey)

    return c.virtualMap[c.circle[i]]
}

// search nearly vnode around key
// sort.Search uses binary search to find key
// every vnode cover its self and clockwise area
func (c *Consistent) search(key uint32) int {
    f := func(x int) bool {
        return c.circle[x] >= key
    }

    i := sort.Search(len(c.circle), f)
    i = i - 1
    if i < 0 {
        i = len(c.circle) - 1
    }
    return i
}

// this function is beautiful
func (c *Consistent) ForceSet(keys ...string) {
    mems := c.Members()
    for _, elt := range mems {
        var found = false

    FOUNDLOOP:
        for _, k := range keys {
            if k == elt {
                found = true
                break FOUNDLOOP
            }
        }
        if !found {
            c.Remove(elt)
        }
    }

    for _, k := range keys {
        c.RLock()
        _, ok := c.members[k]
        c.RUnlock()

        if !ok {
            c.Add(k)
        }
    }
}

func (c *Consistent) Add(elt string) {
    c.Lock()
    defer c.Unlock()

    if _, ok := c.members[elt]; ok {
        return
    }

    c.members[elt] = true

    for idx := 0; idx < c.virtualNodes; idx++ {
        c.virtualMap[c.hash([]byte(c.eltKey(elt, idx)))] = elt
    }

    c.updateCricle()
}

func (c *Consistent) Remove(elt string) {
    c.Lock()
    defer c.Unlock()

    if _, ok := c.members[elt]; !ok {
        return
    }

    delete(c.members, elt)

    for idx := 0; idx < c.virtualNodes; idx++ {
        delete(c.virtualMap, c.hash([]byte(c.eltKey(elt, idx))))
    }

    c.updateCricle()
}

转载:http://cloudaice.com/consistent-hash/

时间: 2024-11-06 07:39:36

一致性哈希算法和Go语言实现的相关文章

分布算法之一致性哈希算法

h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child { margin-top: 0; padding-top: 0; } a:first-child h1, a:first-child h2, a:first-child h3, a:fi

Go语言实现一致性哈希(Consistent Hashing)算法

一致性哈希可用于解决服务器均衡问题. 用Golang简单实现了下,并加入了权重.可采用合适的权重配合算法使用. package main //一致性哈希(Consistent Hashing) //author: Xiong Chuan Liang //date: 2015-2-20 import ( "fmt" "hash/crc32" "sort" "strconv" "sync" ) const DE

一致性哈希算法(用于解决服务器均衡问题)

tencent2012 笔试题附加题 问题描述:  例如手机朋友网有n个服务器,为了方便用户的访问会在服务器上缓存数据,因此用户每次访问的时候最好能保持同一台服务器.已有的做法是根据ServerIPIndex[QQNUM%n]得到请求的服务器,这种方法很方便将用户分到不同的服务器上去.但是如果一台服务器死掉了,那么n就变为了n-1,那么ServerIPIndex[QQNUM%n]与ServerIPIndex[QQNUM%(n-1)]基本上都不一样了,所以大多数用户的请求都会转到其他服务器,这样会

转(一致性哈希算法(consistent hashing))

转自:http://blog.csdn.net/cywosp/article/details/23397179 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似.一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用. 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义: 1.平衡性(Balance)

一致性哈希算法(consistent hashing)(转)

原文链接:每天进步一点点——五分钟理解一致性哈希算法(consistent hashing) 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似.一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用. 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义: 1.平衡性(Balance):平衡性是指哈希的

最短路径Dijkstra算法和Floyd算法整理、

转载自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html 最短路径—Dijkstra算法和Floyd算法 Dijkstra算法 1.定义概览 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.Dijkstra算法是很有代表性的最短路径算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹

一致性哈希算法及其在分布式系统中的应用(转)

原文:http://blog.codinglabs.org/articles/consistent-hashing.html 本文将会从实际应用场景出发,介绍一致性哈希算法(Consistent Hashing)及其在分布式系统中的应用.首先本文会描述一个在日常开发中经常会遇到的问题场景,借此介绍一致性哈希算法以及这个算法如何解决此问题:接下来会对这个算法进行相对详细的描述,并讨论一些如虚拟节点等与此算法应用相关的话题. 分布式缓存问题 假设我们有一个网站,最近发现随着流量增加,服务器压力越来越

五分钟理解一致性哈希算法(consistent hashing)

转载请说明出处:http://blog.csdn.net/cywosp/article/details/23397179 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似.一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用. 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义: 1.平衡性(Bal

MySTL: BM算法和Sunday快速字符串匹配算法

BM算法研究了很久了,说实话BM算法的资料还是比较少的,之前找了个资料看了,还是觉得有点生涩难懂,找了篇更好的和算法更好的,总算是把BM算法搞懂了. 1977年,Robert S.Boyer和J Strother Moore提出了另一种在O(n)时间复杂度内,完成字符串匹配的算法,这个算法在单模匹配上比KMP算法还要出色 PS:其BM算法在跳转优化上的确比KMP算法要好很多,能在O(N)的上界就完成匹配了,但是不是绝对的,我们讲到后面再来说这个问题. 我们知道,KMP算法之所以能那么快,是因为他