日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

一文搞懂负载均衡中的一致性哈希算法

發布時間:2025/3/21 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一文搞懂负载均衡中的一致性哈希算法 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一致性哈希算法在很多領域有應用,例如分布式緩存領域的 MemCache,Redis,負載均衡領域的 Nginx,各類 RPC 框架。不同領域場景不同,需要顧及的因素也有所差異,本文主要討論在負載均衡中一致性哈希算法的設計。

在介紹一致性哈希算法之前,我將會介紹一些哈希算法,討論它們的區別和使用場景。也會給出一致性哈希算法的 Java 通用實現,可以直接引用,文末會給出 github 地址。

友情提示:閱讀本文前,最好對一致性哈希算法有所了解,例如你最好聽過一致性哈希環這個概念,我會在基本概念上縮短篇幅。

一致性哈希負載均衡介紹

負載均衡這個概念可以抽象為:從 n 個候選服務器中選擇一個進行通信的過程。負載均衡算法有多種多樣的實現方式:隨機、輪詢、最小負載優先等,其中也包括了今天的主角:一致性哈希負載均衡。一致性哈希負載均衡需要保證的是“相同的請求盡可能落到同一個服務器上”,注意這短短的一句描述,卻包含了相當大的信息量。“相同的請求” — 什么是相同的請求?一般在使用一致性哈希負載均衡時,需要指定一個 key 用于 hash 計算,可能是:

  • 請求方 IP

  • 請求服務名稱,參數列表構成的串

  • 用戶 ID

  • “盡可能” —為什么不是一定?因為服務器可能發生上下線,所以少數服務器的變化不應該影響大多數的請求。這也呼應了算法名稱中的“一致性”。

    同時,一個優秀的負載均衡算法還有一個隱性要求:流量盡可能均勻分布。

    綜上所述,我們可以概括出一致性哈希負載均衡算法的設計思路。

    • 盡可能保證每個服務器節點均勻的分攤流量

    • 盡可能保證服務器節點的上下線不影響流量的變更

    哈希算法介紹

    哈希算法是一致性哈希算法中重要的一個組成部分,你可以借助 Java 中的 inthashCode()去理解它。 說到哈希算法,你想到了什么?Jdk 中的 hashCode、SHA-1、MD5,除了這些耳熟能詳的哈希算法,還存在很多其他實現,詳見 HASH 算法一覽。可以將他們分成三代:

    • 第一代:SHA-1(1993),MD5(1992),CRC(1975),Lookup3(2006)

    • 第二代:MurmurHash(2008)

    • 第三代:CityHash, SpookyHash(2011)

    這些都可以認為是廣義上的哈希算法,你可以在 wiki 百科 中查看所有的哈希算法。當然還有一些哈希算法如:Ketama,專門為一致性哈希算法而設計。

    既然有這么多哈希算法,那必然會有人問:當我們在討論哈希算法時,我們再考慮哪些東西?我大概總結下有以下四點:

  • 實現復雜程度

  • 分布均勻程度

  • 哈希碰撞概率

  • 性能

  • 先聊聊性能,是不是性能越高就越好呢?你如果有看過我曾經的文章 《該如何設計你的 PasswordEncoder?》,應該能了解到,在設計加密器這個場景下,慢 hash 算法反而有優勢;而在負載均衡這個場景下,安全性不是需要考慮的因素,所以性能自然是越高越好。

    優秀的算法通常比較復雜,但不足以構成評價標準,有點黑貓白貓論,所以 2,3 兩點:分布均勻程度,哈希碰撞概率成了主要考慮的因素。

    我挑選了幾個值得介紹的哈希算法,重點介紹下。

  • MurmurHash 算法:高運算性能,低碰撞率,由 Austin Appleby 創建于 2008 年,現已應用到 Hadoop、libstdc++、nginx、libmemcached 等開源系統。2011 年 Appleby 被 Google 雇傭,隨后 Google 推出其變種的 CityHash 算法。官方只提供了 C 語言的實現版本。Java 界中 Redis,Memcached,Cassandra,HBase,Lucene 都在使用它。 在 Java 的實現,Guava 的 Hashing 類里有,上面提到的 Jedis,Cassandra 里都有相關的 Util 類。

  • FNV 算法:全名為 Fowler-Noll-Vo 算法,是以三位發明人 Glenn Fowler,Landon Curt Noll,Phong Vo 的名字來命名的,最早在 1991 年提出。 特點和用途:FNV 能快速 hash 大量數據并保持較小的沖突率,它的高度分散使它適用于 hash 一些非常相近的字符串,比如 URL,hostname,文件名,text,IP 地址等。

  • Ketama 算法:將它稱之為哈希算法其實不太準確,稱之為一致性哈希算法可能更為合適,其他的哈希算法有通用的一致性哈希算法實現,只不過是替換了哈希方式而已,但 Ketama 是一整套的流程,我們將在后面介紹。

  • 以上三者都是最合適的一致性哈希算法的強力爭奪者。

    一致性哈希算法實現

    一致性哈希的概念我不做贅述,簡單介紹下這個負載均衡中的一致性哈希環。首先將服務器(ip+端口號)進行哈希,映射成環上的一個節點,在請求到來時,根據指定的 hash key 同樣映射到環上,并順時針選取最近的一個服務器節點進行請求(在本圖中,使用的是 userId 作為 hash key)。

    當環上的服務器較少時,即使哈希算法選擇得當,依舊會遇到大量請求落到同一個節點的問題,為避免這樣的問題,大多數一致性哈希算法的實現度引入了虛擬節點的概念。

    在上圖中,只有兩臺物理服務器節點:11.1.121.1 和 11.1.121.2,我們通過添加后綴的方式,克隆出了另外三份節點,使得環上的節點分布的均勻。一般來說,物理節點越多,所需的虛擬節點就越少。

    介紹完了一致性哈希換,我們便可以對負載均衡進行建模了:

    public interface LoadBalancer {Server select(List<Server> servers, Invocation invocation); }

    下面直接給出通用的算法實現:

    public class ConsistentHashLoadBalancer implements LoadBalancer{private HashStrategy hashStrategy = new JdkHashCodeStrategy();private final static int VIRTUAL_NODE_SIZE = 10;private final static String VIRTUAL_NODE_SUFFIX = "&&";@Overridepublic Server select(List<Server> servers, Invocation invocation) {int invocationHashCode = hashStrategy.getHashCode(invocation.getHashKey());TreeMap<Integer, Server> ring = buildConsistentHashRing(servers);Server server = locate(ring, invocationHashCode);return server;}private Server locate(TreeMap<Integer, Server> ring, int invocationHashCode) {// 向右找到第一個 keyMap.Entry<Integer, Server> locateEntry = ring.ceilingEntry(invocationHashCode);if (locateEntry == null) {// 想象成一個環,超過尾部則取第一個 keylocateEntry = ring.firstEntry();}return locateEntry.getValue();}private TreeMap<Integer, Server> buildConsistentHashRing(List<Server> servers) {TreeMap<Integer, Server> virtualNodeRing = new TreeMap<>();for (Server server : servers) {for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {// 新增虛擬節點的方式如果有影響,也可以抽象出一個由物理節點擴展虛擬節點的類virtualNodeRing.put(hashStrategy.getHashCode(server.getUrl() + VIRTUAL_NODE_SUFFIX + i), server);}}return virtualNodeRing;}}

    對上述的程序做簡單的解讀:

    Server 是對服務器的抽象,一般是 ip+port 的形式。

    public class Server {private String url; }

    Invocation 是對請求的抽象,包含一個用于 hash 的 key。

    public class Invocation {private String hashKey; }

    使用 TreeMap 作為一致性哈希環的數據結構, ring.ceilingEntry 可以獲取環上最近的一個節點。在 buildConsistentHashRing 之中包含了構建一致性哈希環的過程,默認加入了 10 個虛擬節點。

    計算方差,標準差的公式:

    public class StatisticsUtil {//方差s^2=[(x1-x)^2 +...(xn-x)^2]/npublic static double variance(Long[] x) {int m = x.length;double sum = 0;for (int i = 0; i < m; i++) {//求和sum += x[i];}double dAve = sum / m;//求平均值double dVar = 0;for (int i = 0; i < m; i++) {//求方差dVar += (x[i] - dAve) * (x[i] - dAve);}return dVar / m;}//標準差σ=sqrt(s^2)public static double standardDeviation(Long[] x) {int m = x.length;double sum = 0;for (int i = 0; i < m; i++) {//求和sum += x[i];}double dAve = sum / m;//求平均值double dVar = 0;for (int i = 0; i < m; i++) {//求方差dVar += (x[i] - dAve) * (x[i] - dAve);}return Math.sqrt(dVar / m);}}

    其中, HashStrategy 是下文中重點討論的一個內容,他是對 hash 算法的抽象,我們將會著重對比各種 hash 算法給測評結果帶來的差異性。

    public interface HashStrategy {int getHashCode(String origin); }

    測評程序

    前面我們已經明確了一個優秀的一致性哈希算法的設計思路。這一節我們給出實際的量化指標:假設 m 次請求打到 n 個候選服務器上

    • 統計每個服務節點收到的流量,計算方差、標準差。測量流量分布均勻情況,我們可以模擬 10000 個隨機請求,打到 100 個指定服務器,測試最后個節點的方差,標準差。

    • 記錄 m 次請求落到的服務器節點,下線 20% 的服務器,重放流量,統計 m 次請求中落到跟原先相同服務器的概率。測量節點上下線的情況,我們可以模擬 10000 個隨機請求,打到 100 個指定服務器,之后下線 20 個服務器并重放流量,統計請求到相同服務器的比例。

    public class LoadBalanceTest {static String[] ips = {...}; // 100 臺隨機 ip/*** 測試分布的離散情況*/@Testpublic void testDistribution() {List<Server> servers = new ArrayList<>();for (String ip : ips) {servers.add(new Server(ip));}ConsistentHashLoadBalancer chloadBalance = new ConsistentHashLoadBalancer();// 構造 10000 隨機請求List<Invocation> invocations = new ArrayList<>();for (int i = 0; i < 10000; i++) {invocations.add(new Invocation(UUID.randomUUID().toString()));}// 統計分布AtomicLongMap<Server> atomicLongMap = AtomicLongMap.create();for (Invocation invocation : invocations) {Server selectedServer = chloadBalance.select(servers, invocation);atomicLongMap.getAndIncrement(selectedServer);}System.out.println(StatisticsUtil.standardDeviation(atomicLongMap.asMap().values().toArray(new Long[]{})));}/*** 測試節點新增刪除后的變化程度*/@Testpublic void testNodeAddAndRemove() {List<Server> servers = new ArrayList<>();for (String ip : ips) {servers.add(new Server(ip));}List<Server> serverChanged = servers.subList(0, 80);ConsistentHashLoadBalancer chloadBalance = new ConsistentHashLoadBalancer();// 構造 10000 隨機請求List<Invocation> invocations = new ArrayList<>();for (int i = 0; i < 10000; i++) {invocations.add(new Invocation(UUID.randomUUID().toString()));}int count = 0;for (Invocation invocation : invocations) {Server origin = chloadBalance.select(servers, invocation);Server changed = chloadBalance.select(serverChanged, invocation);if (origin.getUrl().equals(changed.getUrl())) count++;}System.out.println(count / 10000D);}

    不同哈希算法的實現及測評

    最簡單、經典的 hashCode 實現:

    public class JdkHashCodeStrategy implements HashStrategy {@Overridepublic int getHashCode(String origin) {return origin.hashCode();} }

    FNV132HASH 算法實現:

    public class FnvHashStrategy implements HashStrategy {private static final long FNV_32_INIT = 2166136261L;private static final int FNV_32_PRIME = 16777619;@Overridepublic int getHashCode(String origin) {final int p = FNV_32_PRIME;int hash = (int) FNV_32_INIT;for (int i = 0; i < origin.length(); i++)hash = (hash ^ origin.charAt(i)) * p;hash += hash << 13;hash ^= hash >> 7;hash += hash << 3;hash ^= hash >> 17;hash += hash << 5;hash = Math.abs(hash);return hash;} }

    CRC 算法:

    public class CRCHashStrategy implements HashStrategy {@Overridepublic int getHashCode(String origin) {CRC32 crc32 = new CRC32();crc32.update(origin.getBytes());return (int) ((crc32.getValue() >> 16) & 0x7fff & 0xffffffffL);} }

    Ketama 算法:

    public class KetamaHashStrategy implements HashStrategy {private static MessageDigest md5Digest;static {try {md5Digest = MessageDigest.getInstance("MD5");} catch (NoSuchAlgorithmException e) {throw new RuntimeException("MD5 not supported", e);}}@Overridepublic int getHashCode(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 class MurmurHashStrategy implements HashStrategy {@Overridepublic int getHashCode(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);// for big-endian version, do this first:// finish.position(8-buf.remaining());finish.put(buf).rewind();h ^= finish.getLong();h *= m;}h ^= h >>> r;h *= m;h ^= h >>> r;buf.order(byteOrder);return (int) (h & 0xffffffffL);} }

    測評結果:

    ?方差標準差不變流量比例
    JdkHashCodeStrategy29574.08171.970.6784
    CRCHashStrategy3013.0254.890.7604
    FnvHashStrategy792.0228.140.7892
    KetamaHashStrategy1147.0833.860.80
    MurmurHashStrategy634.8225.190.80

    其中方差和標準差反映了均勻情況,越低越好,可以發現 MurmurHashStrategy,KetamaHashStrategy,FnvHashStrategy 都表現的不錯,其中 MurmurHashStrategy 最為優秀。

    不變流量比例體現了服務器上下線對原有請求的影響程度,不變流量比例越高越高,可以發現 KetamaHashStrategy 和 MurmurHashStrategy 表現最為優秀。

    我并沒有對小集群,小流量進行測試,樣本偏差性較大,僅從這個常見場景來看,MurmurHashStrategy 似乎是最優的選擇。

    至于性能測試,MurmurHash 也十分的高性能,我并沒有做測試(感興趣的同學可以對幾種 strategy 用 JMH 測評一下),這里我貼一下 MurmurHash 官方的測評數據:

    OneAtATime - 354.163715 mb/sec FNV - 443.668038 mb/sec SuperFastHash - 985.335173 mb/sec lookup3 - 988.080652 mb/sec MurmurHash 1.0 - 1363.293480 mb/sec MurmurHash 2.0 - 2056.885653 mb/sec

    擴大虛擬節點可以明顯降低方差和標準差,但虛擬節點的增加會加大內存占用量以及計算量

    Ketama 一致性哈希算法實現

    Ketama 算法有其專門的配套實現方式

    public class KetamaConsistentHashLoadBalancer implements LoadBalancer {private static MessageDigest md5Digest;static {try {md5Digest = MessageDigest.getInstance("MD5");} catch (NoSuchAlgorithmException e) {throw new RuntimeException("MD5 not supported", e);}}private final static int VIRTUAL_NODE_SIZE = 12;private final static String VIRTUAL_NODE_SUFFIX = "-";@Overridepublic Server select(List<Server> servers, Invocation invocation) {long invocationHashCode = getHashCode(invocation.getHashKey());TreeMap<Long, Server> ring = buildConsistentHashRing(servers);Server server = locate(ring, invocationHashCode);return server;}private Server locate(TreeMap<Long, Server> ring, Long invocationHashCode) {// 向右找到第一個 keyMap.Entry<Long, Server> locateEntry = ring.ceilingEntry(invocationHashCode);if (locateEntry == null) {// 想象成一個環,超過尾部則取第一個 keylocateEntry = ring.firstEntry();}return locateEntry.getValue();}private TreeMap<Long, Server> buildConsistentHashRing(List<Server> servers) {TreeMap<Long, Server> virtualNodeRing = new TreeMap<>();for (Server server : servers) {for (int i = 0; i < VIRTUAL_NODE_SIZE / 4; i++) {byte[] digest = computeMd5(server.getUrl() + VIRTUAL_NODE_SUFFIX + i);for (int h = 0; h < 4; h++) {Long k = ((long) (digest[3 + h * 4] & 0xFF) << 24)| ((long) (digest[2 + h * 4] & 0xFF) << 16)| ((long) (digest[1 + h * 4] & 0xFF) << 8)| (digest[h * 4] & 0xFF);virtualNodeRing.put(k, server);}}}return virtualNodeRing;}private long getHashCode(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 rv;}private 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();}}

    稍微不同的地方便在于:Ketama 將四個節點標為一組進行了虛擬節點的設置。

    ?方差標準差不變流量比例

    KetamaConsistent

    HashLoadBalancer

    911.0830.180.7936

    實際結果并沒有太大的提升,可能和測試數據的樣本規模有關。

    總結

    優秀的哈希算法和一致性哈希算法可以幫助我們在大多數場景下應用的高性能,高穩定性,但在實際使用一致性哈希負載均衡的場景中,最好針對實際的集群規模和請求哈希方式進行壓測,力保流量均勻打到所有的機器上,這才是王道。

    不僅僅是分布式緩存,負載均衡等等有限的場景,一致性哈希算法、哈希算法,尤其是后者,是一個用處很廣泛的常見算法,了解它的經典實現是很有必要的,例如 MurmurHash,在 guava 中就有其 Java 實現,當需要高性能,分布均勻,碰撞概率小的哈希算法時,可以考慮使用它。

    總結

    以上是生活随笔為你收集整理的一文搞懂负载均衡中的一致性哈希算法的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。