自编自导一个RPC框架之《一致性哈希负载均衡算法思考》

一致性哈希负载均衡算法思考

一个好的负载均衡算法至少拥有以下特性:

  • 在分配请求到各个服务实例上,它是能够尽量均衡的(相同key会尽量落到同一服务实例,不同key尽量分布均匀)
  • 在适应新的服务实例加入或离开集群,它是动态的
  • 在应对不同场景下的需求,它是多样性的(可以配置权重、策略等)

目前,一致性哈希负载均衡算法有非常多的实现,当我们进行选择时,我们应该从那些方面进行评估后选择合适的:

  • 实现复杂程度
  • 扰动函数的设计
  • 均衡程度
  • 性能

对于作用于服务负载的算法,性能肯定是越优秀越佳,而对于实现的复杂度肯定会稍微有点复杂。对于算法实现,扰动函数的设计和均衡程度才是进行评估的重点。下面将从MurmurHash、FNV、Ketama进行分析


一致性哈希算法实现

一、设计一个一致性哈希算法需要做什么

  • 需要一个装载ip:port的容器
  • 避免数据倾斜、提高节点可用性,引入虚拟节点(物理节点基数低的情况下)
  • 设计一个低碰撞的扰动函数(用一些大佬写就很不错)
  • 每次请求都会进行扰动,然后取容器去取,向后取的第一个服务实例
// FNV132HASH
public class ConsistentHashStrategy implements LoadBalanceStrategy {

	/**
	 * 虚拟节点个数
	 */
	private static final int VIRTUAL_NODE_NUM = 500;

	/**
	 * 用于计算hash值的常量
	 */
	private static final int HASH_CONSTANT = 16777619;

	/**
	 * hash 计算hash值 MurmurHash
	 *
	 * @Date 2023/5/6 21:04
	 * @param str str
	 * @return {@link long}
	 * @author Lucky LanAn
	 * @since 1.0
	 */
	private static long hash(String str) {
		int hash = (int) 2166136261L;
		// 计算hash值
		for (int i = 0; i < str.length(); i++) {
			hash = (hash ^ str.charAt(i)) * HASH_CONSTANT;
		}
		// 扰动hash值,增加随机性
		hash += hash << 13;
		hash ^= hash >> 7;
		hash += hash << 3;
		hash ^= hash >> 17;
		hash += hash << 5;
		return hash & 0x7FFFFFFF;
	}

	public String onRefresh(String serviceKey, TreeSet<String> addressList) {
		TreeMap<Long, String> virtualNodes = new TreeMap<>();
		for (String address : addressList) {
			for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
				long hash = hash(address + i);
				virtualNodes.put(hash, address);
			}
		}
		long hash = hash(serviceKey);
		if (virtualNodes.size() == 0) {
			return null;
		}
		Map.Entry<Long, String> longStringEntry = virtualNodes.ceilingEntry(hash);
		if (longStringEntry == null) {
			longStringEntry = virtualNodes.firstEntry();
		}
		return longStringEntry.getValue();
	}

	/**
	 * route
	 *
	 * @param serviceKey serviceKey 服务名
	 * @param addressSet addressSet 服务地址 set
	 * @return {@link String}
	 * @Date 2023/5/6 16:17
	 * @author Lucky LanAn
	 * @since 1.0
	 */
	@Override
	public String route(String serviceKey, TreeSet<String> addressSet) {
		return onRefresh(serviceKey, addressSet);
	}
}

下面就只放出hash函数

// KetamaHash
private static MessageDigest md5Digest;

	static {
		try {
			md5Digest = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
	}

	public int getKetamaHashCode(String origin) {
		byte[] bKey = computeMd5(origin);
		long rv = ((long) (bKey[3] & 0xFF) << 24)
				| ((long) (bKey[2] & 0xFF) << 16)
				| ((long) (bKey[1] & 0xFF) << 8)
				| (bKey[0] & 0xFF);
		return (int) (rv & 0xffffffffL);
	}

	/**
	 * Get the md5 of the given key.
	 */
	public static byte[] computeMd5(String k) {
		MessageDigest md5;
		try {
			md5 = (MessageDigest) md5Digest.clone();
		} catch (CloneNotSupportedException e) {
			throw new RuntimeException("clone of MD5 not supported", e);
		}
		md5.update(k.getBytes());
		return md5.digest();
	}
// MurmurHash
public int getMurmurHashCode(String origin) {

		ByteBuffer buf = ByteBuffer.wrap(origin.getBytes());
		int seed = 0x1234ABCD;

		ByteOrder byteOrder = buf.order();
		buf.order(ByteOrder.LITTLE_ENDIAN);

		long m = 0xc6a4a7935bd1e995L;
		int r = 47;

		long h = seed ^ (buf.remaining() * m);

		long k;
		while (buf.remaining() >= 8) {
			k = buf.getLong();

			k *= m;
			k ^= k >>> r;
			k *= m;

			h ^= k;
			h *= m;
		}

		if (buf.remaining() > 0) {
			ByteBuffer finish = ByteBuffer.allocate(8).order(
					ByteOrder.LITTLE_ENDIAN);
			finish.put(buf).rewind();
			h ^= finish.getLong();
			h *= m;
		}
		h ^= h >>> r;
		h *= m;
		h ^= h >>> r;

		buf.order(byteOrder);
		return (int) (h & 0xffffffffL);
	}

测评

我们进行测试的时候,先构建String serviceKey和TreeSet<String> addressSet,然后每次serviceKey选出对应的address,对这个address进行计数加1,然后对每个address的计数进行离散程度计算,就大概了解数据均匀不。

public class TestLoadBalance {


	@Test
	public void TestA() {
		LoadBalanceStrategy loadBalanceStrategy = ExtensionLoader.getLoader(LoadBalanceStrategy.class).getExtension("consistent_hash");
		TreeSet<String> addressSet = StatisticsUtil.build(100);
		System.out.println("build address set success");
		TreeSet<String> request = StatisticsUtil.build(10000);
		System.out.println("build request set success");

		Map<String, Integer> result = new HashMap<>();
		for (String str : request) {
			String address = loadBalanceStrategy.route(str, addressSet);
			result.put(address, result.getOrDefault(address, 0) + 1);
		}

		Integer[] longs = result.values().toArray(new Integer[]{});

		System.out.println("Load balance result:");
		System.out.println(result);
		System.out.println("result : " + StatisticsUtil.test(longs));
	}


	static class StatisticsUtil {

		//方差s^2=[(x1-x)^2 +...(xn-x)^2]/n
		public static Map<String, Double> test(Integer[] x) {
			Map<String, Double> map = new HashMap<>(8);
			int m = x.length;
			double sum = 0, max = x[0], min = x[0];
			for (Integer val : x) {//求和
				if (val < min) {
					min = val;
				}
				if (val > max) {
					max = val;
				}
				sum += val;
			}
			double dAve = sum / m;//求平均值
			double dVar = 0;
			for (int i = 0; i < m; i++) {//求方差
				dVar += (x[i] - dAve) * (x[i] - dAve);
			}
			double v = dVar / m;
			map.put("方差s^2=[(x1-x)^2 +...(xn-x)^2]/n", v);
			map.put("标准差σ=sqrt(s^2)", Math.sqrt(v));
			map.put("max", max);
			map.put("min", min);
			return map;
		}

		public static TreeSet<String> build(int num) {
			TreeSet<String> addressSet = new TreeSet<>();
			for (int i = 0; i < num; i++) {
				addressSet.add("192.168.0." + i + ":8080");
			}
			return addressSet;
		}
	}
}
  • FNV132HASH result : {标准差σ=sqrt(s2)=11.406138698087096, min=80.0, 方差s2=[(x1-x)2 +…(xn-x)^2]/n=130.1, max=137.0}
  • KetamaHash result : {标准差σ=sqrt(s2)=9.302687783646187, min=79.0, 方差s2=[(x1-x)2 +…(xn-x)^2]/n=86.54, max=125.0}
  • MurmurHash result : {标准差σ=sqrt(s2)=9.748846085563153, min=73.0, 方差s2=[(x1-x)2 +…(xn-x)2]/n=95.04, max=128.0}
hash namesqrt(s2)s2maxmin
FNV132HASH11.406138698087096,130.1137.080.0
KetamaHash9.30268778364618786.54125.079.0
MurmurHash9.74884608556315395.04128.073.0

总结

  • MurmurHashStrategy算法在哈希效率上表现较好,适用于需要快速计算哈希值的场景,如缓存和数据存储的负载均衡。但是,它的哈希值分布不如一致性哈希算法均匀。
  • 一致性哈希算法通常用于分布式缓存、分布式文件系统等场景的负载均衡。KetamaHashStrategy是一致性哈希算法的一种实现,具有较好的哈希均匀性和可扩展性,能够支持节点的增加、删除和失效检测,具有较好的容错性。
  • FnvHashStrategy算法是一种简单的哈希算法,适用于对哈希算法效率和分布均匀性要求较低的场景。它的主要优点是计算速度快,但在一些场景下哈希冲突率较高

最终,在不追求快速计算哈希值的情况下,我选择更加均匀的KetamaHashStrategy


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝桉未与

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值