一致性哈希算法
1 hash算法在RPC中的应用和缺陷
【背景】:我们有一个社交网站,需要使用Redis存储图片资源,存储的格式为键值对,其中,key为图片名称,value为该图片所在文件服务器路径,我们需要根据文件名查找该文件所在文件服务器上的路径以实现访问并传递给用户,数据量大概200w左右,规则就是随机分配,我们可以部署8台服务器,为保证Redis的高可用,通常我们会做组成4组master-slave的形式,从而实现主机挂掉时秒级切换为备机,使得业务不受影响。
【结果】:由于规则随机,一条数据可能存储在任何一组Redis中,因此对于任意一张图片,我们不确定具体是在哪一个Redis服务器上,因此我们最多需要对所有服务器进行遍历才能查询到,这显然不是我们期望的结果。
【改进】:采用简单的取模的Hash方式(hash(图片名称)%N),每一张图片在进行分库时都可以定位到特定的服务器。因此对于任意一张图片既可在存储时定位出其要存储的服务器位置,也可在访问时直接定位其位置,如果在指定服务器上没有该图片,说明必然没有缓存,此时应直接请求后端服务器。从而大大的提高性能。
【缺陷】:简单取模的hash方式对于服务器数量的变化十分敏感,具体体现在,当服务器中某一组出现故障无法提供服务或者增加一组服务缓存器提供服务时,取模的hash算法的分母发生变化,则由图片名计算出来的服务器组的位置必然发生变化,从而导致大量的缓存在同一时间失效,造成了缓存的雪崩,此时会有大量的请求直接打到后端服务器,后端服务器很有可能因承受不住巨大的压力从而被压垮。
为了避免因服务器变动引起的缓存雪崩问题,一致性哈希算法应用而生。
2 一致性哈希算法
【数据与服务器映射规则定义】:
- 求出服务器的哈希值,并将其映射到0~2^32的圆上
hash(服务器名或IP) % 2^32
- 采用同样的方式求出存储数据的key的哈希值,并映射到相同的圆上
hash(key) % 2^32
- 从数据映射的位置开始顺时针查找,将数据保存到找到的第一个服务器上,如果超过2^32仍然找不到服务器,则将数据保存到第一个服务器上。
当在上述状态图中添加一台服务器时,余数式分布式算法势必会因为模的变动导致缓存失效,但 一致性哈希算法中,只有增加服务器地点顺时针方向的第一台服务器上的键会受到影响,将本应打到该服务器上的部分或全部数据打到本服务器上。如下图所示:
对于服务器组故障的分析同理。
综上可知,一致性哈希算法能够保证当系统的节点发生变化时仍然能够很好的对外提供良好的服务,并且只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
3 数据倾斜
假设只有两个节点A和B,而所有数据都分布在A的逆时针方向,那么所有的数据都会打到A上,相当于B服务器并没有起到作用,这就是一致性哈希算法在使用过程中的因节点分布不均匀导致的数据倾斜的问题。为了解决这一问题,一致性哈希算法引入虚拟节点机制,即对每一个服务器节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点,具体的做法可以在服务器ip或主机名后增加编号来实现,从而保证服务器节点在哈希环上均匀分布,从而降低了大量数据只打到其中一台服务器上的概率。
4 一致性哈希算法的实现
实现的思路比较简单,通过TreeMap建立圆环,并使用默认升序作为排序规则,在确定服务器的过程中每次截取比当前key值大的区间以获取距离最近的服务器位置,若区间内不存在服务器则取第一个服务器。Demo如下所示:
class ConsistantHash { static class Node { private int nodeNum; public Node(int num) { this.nodeNum = num; } @Override public String toString() { return String.valueOf(nodeNum); } } /** * 圆环(用treeMap的主要原因是可以排序) */ private TreeMap<Long, Node> circle = new TreeMap<Long, Node>(); /** * 添加节点到圆环中 * */ public void addNode(Node node) { Long nodeKey = md5(node.toString()); System.out.println(node.toString() + " md5:" + nodeKey); circle.put(nodeKey, node); } public static void main(String[] args) { ConsistantHash h = new ConsistantHash(); /** * 添加节点服务器 * */ h.addNode(new Node(10)); h.addNode(new Node(30)); h.addNode(new Node(60)); /** * 设定节点,确定其服务器位置 * */ for (int i = 0; i < 50; i++) { System.out.println(i + "--->服务器节点" + h.getNode("" + i)); } } /** * 设置节点服务器分配规则 * */ public Node getNode(String key) { /** * treemap 转成 排序好的map * */ Long keyMd5 = md5(key); System.out.println("key is:" + key + ", keyMd5 is:" + keyMd5); /** * 截取key大于等于keyMd5的所有元素, 因为正常情况下 md5(key) =< md5(node) * */ SortedMap<Long, Node> sortedMap = circle.tailMap(keyMd5); Long k; /** * 没命中则选择第一个节点并直接返回 * */ if (sortedMap.isEmpty()) { System.out.println("not hit and choose the first node"); k = (circle.firstKey()); } else { /** * 命中则选择最近的一个节点 * */ k = (sortedMap.firstKey()); } Node node = circle.get(k); System.out.println(key + "(" + keyMd5 + ") --->" + node.toString() + "(" + md5(node.toString()) + ")"); return node; } /** * 确定加密方式 * */ private long md5(String key) { Long nodeKey = 0L; int numOfTen = key.toCharArray().length - 1; for (char it : key.toCharArray()) { if (numOfTen > 0) { for (int i = 0; i < numOfTen; i++) { nodeKey = nodeKey + Long.valueOf(it + "") * 10; } } else { nodeKey = nodeKey + Long.valueOf(it + ""); } numOfTen--; } return nodeKey; } }
参考:https://www.cnblogs.com/study-everyday/p/8629100.html
参考:https://www.cnblogs.com/lpfuture/p/5796398.html