【第五周】命题作业——实现一致性 hash 算法
用你熟悉的编程语言实现一致性 hash 算法。
编写测试用例测试这个算法,测试 100 万 KV 数据,10 个服务器节点的情况下,计算这些 KV 数据在服务器上分布数量的标准差,以评估算法的存储负载不均衡性。
答:
一致性hash的核心要点是:如何将大量的未知Key的hash值均匀的恒定的散落在整个hash环上。
一致性hash环示意图
要素:Hash环、2^32、节点、Key,伸缩
一、代码编写
1、Hash环位置计算方法,这是一致性hash最重要的方法。
该方法用来保证所有虚拟节点和大量key值是否能均匀的恒定的散列在hash环上。
/** * 通过Hash计算获得其在Hash环上的值 * @param key * @return */ private int getValueOnRingByHash(String key){ final int p = 16777619; int hash = (int)2166136261L; for (int i = 0; i < key.length(); i++) hash = (hash ^ key.charAt(i)) * p; hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5; if (hash < 0) hash = Math.abs(hash); return hash; }
注:该方法跟网上的一样,是网上拷贝的,其他的自己写
2、通过Key值获取物理节点的方法,该方法是一致性hash的核心思想之一。
采用的是通过Key值位置顺时针查找这样一种方式,找到的第一个虚拟节点就是Key值对应的节点。
当且仅当该虚拟节点被移除,或者虚拟节点和Key值位置之间出现新节点时,该Key与节点的一致性被破坏。
/** * 先获得Hash环上的值,然后找到大于自己最近的虚拟节点,返回实体节点 * @return */ private T getFirstHigherNodeOnRing(String key){ int ringValue = getValueOnRingByHash(key); Map.Entry<Integer, T> higherEntry = ((TreeMap<Integer, T>) vNodes).higherEntry(ringValue); if(higherEntry==null){ higherEntry = ((TreeMap<Integer, T>) vNodes).firstEntry(); } return higherEntry.getValue(); }
3、给物理节点创建虚拟节点的方法。
给每个物理节点创建足够数量的虚拟节点,以使物理节点对应的Hash环空间占比尽量平衡。
/** * 给物理节点创建N个虚拟节点 * @param key 物理节点用于计算Hash环上虚拟节点值的KEY,比如 {IP}:{PORT} * @param val 物理节点对象 */ public void appendNode(String key, T val){ int count = 0; while (count<vNodeQty){ count++; vNodes.put(getValueOnRingByHash(key+"#VN"+count), val); } }
4、完整的一致性Hash类
使用泛型定义物理节点对象,便于应对各种场景。
package xyz.hs.geek.training.week4;import java.util.*;/** * @author huangsui * Created on 2020/7/2 */public class ConsistentHashing<T> { /* 每个物理节点在Hash环上创建的虚拟节点数 */ private final int vNodeQty; /* Hash环上创建的所有虚拟节点,key为Hash环上的位置值(即hash值),value为物理节点对象 */ private final SortedMap<Integer, T> vNodes = new TreeMap<>(); public ConsistentHashing() { this.vNodeQty = 160; } public ConsistentHashing(int vNodeQty) { this.vNodeQty = vNodeQty; } /** * 添加物理节点 * @param nodes,Map的key为物理节点用于hash计算的值,value是物理节点对象 */ public void appendNodes(Map<String, T> nodes){ for (Map.Entry<String, T> entry: nodes.entrySet()){ appendNode(entry.getKey(), entry.getValue()); } } /** * 添加物理节点 * @param nodes,节点Key和节点对象都使用同一字符串 */ public void appendNodes(String[] nodes){ for (String node: nodes){ appendNode(node, (T)node); } } /** * 给物理节点创建vNodeQty个虚拟节点 * @param key 物理节点用于计算Hash环上虚拟节点值的KEY,比如 {IP}:{PORT} * @param val 物理节点对象 */ public void appendNode(String key, T val){ int count = 0; while (count<vNodeQty){ count++; vNodes.put(getValueOnRingByHash(key+"#VN"+count), val); } } /** * 删除该物理节点的所有虚拟节点 * @param key 物理节点用于计算Hash环上虚拟节点位置的KEY */ public void removeNode(String key){ int count = 0; while (count<vNodeQty){ count++; vNodes.remove(getValueOnRingByHash(key+"#VN"+count)); } } /** * 将Key进行一致性Hash计算,返回其对应的物理节点对象 * @param key * @return 物理节点对象 */ public T doHashing(String key){ return this.getFirstHigherNodeOnRing(key); } /** * 先获得Hash环上的值,然后找到大于自己最近的虚拟节点,返回实体节点 * @return */ private T getFirstHigherNodeOnRing(String key){ int ringValue = getValueOnRingByHash(key); Map.Entry<Integer, T> higherEntry = ((TreeMap<Integer, T>) vNodes).higherEntry(ringValue); if(higherEntry==null){ higherEntry = ((TreeMap<Integer, T>) vNodes).firstEntry(); } return higherEntry.getValue(); } /** * 通过Hash计算获得其在Hash环上的值(位置) * @param key * @return */ private int getValueOnRingByHash(String key){ final int p = 16777619; int hash = (int)2166136261L; for (int i = 0; i < key.length(); i++) hash = (hash ^ key.charAt(i)) * p; hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5; if (hash < 0) hash = Math.abs(hash); return hash; }}
二、标准差测算
1、测算代码
十台主机,100万数据,进行标准差测算
package xyz.hs.geek.training;import org.junit.Test;import xyz.hs.geek.training.week4.ConsistentHashing;import java.util.Collection;import java.util.HashMap;import java.util.Map;/** * @author huangsui * Created on 2020/7/5 */public class ConsistentHashingTests { /** * 测试标准差 */ @Test public void testStD(){ // 十台主机 String[] nodes = new String[]{ "192.168.1.10:10", "192.168.1.10:11", "192.168.1.10:12", "192.168.1.10:13", "192.168.1.10:14", "192.168.1.10:15", "192.168.1.10:16", "192.168.1.10:17", "192.168.1.10:18", "192.168.1.10:19"}; ConsistentHashing<String> consistentHashing = new ConsistentHashing(210); consistentHashing.appendNodes(nodes); // 100W数据 Map<String, Integer> stats = new HashMap<>(); for (String node: nodes){ stats.put(node, 0); } for (int i=0;i<1000000;i++){ String key = "8.8.8.8:"+i; String node = consistentHashing.doHashing(key); stats.put(node, stats.get(node)+1); } Collection<Integer> collection = stats.values(); Integer[] arr = new Integer[collection.size()]; arr = collection.toArray(arr); System.out.println(std(arr)); } private double std(Integer[] arr){ int len = arr.length; double sum = 0; for (int i = 0; i < len; i++) { sum += arr[i]; } double avg = sum / len; double std = 0; for (int i = 0; i < len; i++) { std += (arr[i] - avg) * (arr[i] - avg); } return Math.sqrt(std / len); }}
2、标准差输出
输出值:3873
测算过程中,对物理节点的样本进行了调整,发现其对标准差还是有一定影响的。样本的变化直接导致的是物理节点在Hash环上的位置占比发生变化,而能平衡这个变化的就是虚拟节点数了。之后通过调整虚拟节点数,标准差回落到了更小的区间。
所以,并没有一个恒定的最优的虚拟节点数,还是需要基于样本数据进行测算,才能得到一个合适的虚拟节点数。
三尾鱼
还未添加个人签名 2018.07.10 加入
还未添加个人简介
评论