写点什么

架构师训练营第五周作业

用户头像
四夕晖
关注
发布于: 2020 年 10 月 25 日

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

public class ConsistentHash<T> {    /**     * Hash计算对象,用于自定义hash算法     */    HashFunc hashFunc;    /**     * 复制的节点个数     */    private final int numberOfReplicas;    /**     * 一致性Hash环     */    private final SortedMap<Long, T> circle = new TreeMap<>();
/** * 构造,使用Java默认的Hash算法 * @param numberOfReplicas 复制的节点个数,增加每个节点的复制节点有利于负载均衡 * @param nodes 节点对象 */ public ConsistentHash(int numberOfReplicas, Map<T, Integer> nodes) { this.numberOfReplicas = numberOfReplicas; this.hashFunc = key -> {// return fnv1HashingAlg(key.toString()); return md5HashingAlg(key.toString()); }; //初始化节点 for (T node : nodes.keySet()) { add(node); } }
/** * 构造 * @param hashFunc hash算法对象 * @param numberOfReplicas 复制的节点个数,增加每个节点的复制节点有利于负载均衡 * @param nodes 节点对象 */ public ConsistentHash(HashFunc hashFunc, int numberOfReplicas, Collection<T> nodes) { this.numberOfReplicas = numberOfReplicas; this.hashFunc = hashFunc; //初始化节点 for (T node : nodes) { add(node); } }
/** * 增加节点<br> * 每增加一个节点,就会在闭环上增加给定复制节点数<br> * 例如复制节点数是2,则每调用此方法一次,增加两个虚拟节点,这两个节点指向同一Node * 由于hash算法会调用node的toString方法,故按照toString去重 * * @param node 节点对象 */ public void add(T node) {// circle.put(hashFunc.hash(node.toString()), node); for (int i = 0; i <= numberOfReplicas; i++) { circle.put(hashFunc.hash(node.toString() + i), node); } }
/** * 移除节点的同时移除相应的虚拟节点 * * @param node 节点对象 */ public void remove(T node) { for (int i = 0; i < numberOfReplicas; i++) { circle.remove(hashFunc.hash(node.toString() + i)); } }
/** * 获得一个最近的顺时针节点 * * @param key 为给定键取Hash,取得顺时针方向上最近的一个虚拟节点对应的实际节点 * @return 节点对象 */ public T get(Object key) { if (circle.isEmpty()) { return null; } long hash = hashFunc.hash(key); if (!circle.containsKey(hash)) { //返回此映射的部分视图,其键大于等于 hash SortedMap<Long, T> tailMap = circle.tailMap(hash); hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); } //正好命中 return circle.get(hash); }
/** * 使用MD5算法 * @param key * @return */ private static long md5HashingAlg(String key) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); md5.reset(); md5.update(key.getBytes()); byte[] bKey = md5.digest(); long res = ((long) (bKey[3] & 0xFF) << 24) | ((long) (bKey[2] & 0xFF) << 16) | ((long) (bKey[1] & 0xFF) << 8)| (long) (bKey[0] & 0xFF); return res; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return 0L; }
/** * 使用FNV1hash算法 * @param key * @return */ private static long fnv1HashingAlg(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; return hash; }
/** * Hash算法对象,用于自定义hash算法 */ public interface HashFunc { public Long hash(Object key); }}
复制代码


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


public class Test extends TestCase {
private ConsistentHash<String> consistentHashing; private static Map<String, Integer> nodes = new HashMap<>();
public Test(String testName) { super(testName); for (int i = 0; i < 10; i++) { nodes.put("192.168.0." + (i + 1), i ); } }
public static TestSuite suite() { return new TestSuite(Test.class); }
public void testHash() { //无虚拟节点 goHash(0); //10虚拟节点 goHash(10); goHash(100); goHash(1000); goHash(10000); goHash(100000); }
public void goHash(int virNUm) { consistentHashing = new ConsistentHash<>(virNUm, nodes);
int[] counters = new int[nodes.size()]; for (int i = 0; i < 1000000; i++) { String node = UUID.randomUUID().toString(); String serverIp = consistentHashing.get(node); counters[nodes.get(serverIp)]++; }
for (int i = 0; i < counters.length; i++) { System.out.println("路由到结点[192.168.0." + (i + 1) + "] " + counters[i] + "次"); } double dsv = MathUtils.standardDiviation(counters); System.out.println(virNUm + "虚拟节点标准差:" + dsv);
assertTrue(true); }}
复制代码

测试结果:

路由到结点[192.168.0.1] 53290次路由到结点[192.168.0.2] 18649次路由到结点[192.168.0.3] 254214次路由到结点[192.168.0.4] 24436次路由到结点[192.168.0.5] 205070次路由到结点[192.168.0.6] 66066次路由到结点[192.168.0.7] 42079次路由到结点[192.168.0.8] 272557次路由到结点[192.168.0.9] 10908次路由到结点[192.168.0.10] 52731次0虚拟节点标准差:14654.29507004687
路由到结点[192.168.0.1] 82794次路由到结点[192.168.0.2] 78577次路由到结点[192.168.0.3] 105079次路由到结点[192.168.0.4] 119982次路由到结点[192.168.0.5] 111965次路由到结点[192.168.0.6] 131697次路由到结点[192.168.0.7] 130103次路由到结点[192.168.0.8] 60297次路由到结点[192.168.0.9] 126026次路由到结点[192.168.0.10] 53480次10虚拟节点标准差:14654.29507004687
路由到结点[192.168.0.1] 86344次路由到结点[192.168.0.2] 104864次路由到结点[192.168.0.3] 97530次路由到结点[192.168.0.4] 98138次路由到结点[192.168.0.5] 95254次路由到结点[192.168.0.6] 104830次路由到结点[192.168.0.7] 115565次路由到结点[192.168.0.8] 86401次路由到结点[192.168.0.9] 114282次路由到结点[192.168.0.10] 96792次100虚拟节点标准差:9523.838511860646
路由到结点[192.168.0.1] 97853次路由到结点[192.168.0.2] 100106次路由到结点[192.168.0.3] 98878次路由到结点[192.168.0.4] 96620次路由到结点[192.168.0.5] 102272次路由到结点[192.168.0.6] 98587次路由到结点[192.168.0.7] 101002次路由到结点[192.168.0.8] 98077次路由到结点[192.168.0.9] 103105次路由到结点[192.168.0.10] 103500次1000虚拟节点标准差:2259.5495126241426路由到结点[192.168.0.1] 100166次路由到结点[192.168.0.2] 99953次路由到结点[192.168.0.3] 99424次路由到结点[192.168.0.4] 98537次路由到结点[192.168.0.5] 101740次路由到结点[192.168.0.6] 100498次路由到结点[192.168.0.7] 100824次路由到结点[192.168.0.8] 100985次路由到结点[192.168.0.9] 99095次路由到结点[192.168.0.10] 98778次10000虚拟节点标准差:986.8647323721726路由到结点[192.168.0.1] 99652次路由到结点[192.168.0.2] 100449次路由到结点[192.168.0.3] 100534次路由到结点[192.168.0.4] 99458次路由到结点[192.168.0.5] 100133次路由到结点[192.168.0.6] 99463次路由到结点[192.168.0.7] 100392次路由到结点[192.168.0.8] 99981次路由到结点[192.168.0.9] 100235次路由到结点[192.168.0.10] 99703次100000虚拟节点标准差:387.9613382799889
复制代码

虚拟节点达到 10 个时标准差只有 387,是虚拟节点越多越好?越多越稳定?


用户头像

四夕晖

关注

还未添加个人签名 2018.01.16 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营第五周作业