分布式數據緩存中的一致性哈希算法

2020-09-03 程式設計師歷小冰

一致性哈希算法在分布式緩存領域的 MemCached,負載均衡領域的 Nginx 以及各類 RPC 框架中都有廣泛的應用,它主要是為了解決傳統哈希函數添加哈希表槽位數後要將關鍵字重新映射的問題。

本文會介紹一致性哈希算法的原理及其實現,並給出其不同哈希函數實現的性能數據對比,探討Redis 集群的數據分片實現等,文末會給出實現的具體 github 地址。

Memcached 與客戶端分布式緩存

Memcached 是一個高性能的分布式緩存系統,然而服務端沒有分布式功能,各個伺服器不會相互通信。它的分布式實現依賴於客戶端的程序庫,這也是 Memcached 的一大特點。比如第三方的 spymemcached 客戶端就基於一致性哈希算法實現了其分布式緩存的功能。

其具體步驟如下:

  • 向 Memcached 添加數據,首先客戶端的算法根據 key 值計算出該 key 對應的伺服器。
  • 伺服器選定後,保存緩存數據。
  • 獲取數據時,對於相同的 key ,客戶端的算法可以定位到相同的伺服器,從而獲取數據。

在這個過程中,客戶端的算法首先要保證緩存的數據儘量均勻地分布在各個伺服器上,其次是當個別伺服器下線或者上線時,會出現數據遷移,應該儘量減少需要遷移的數據量。

客戶端算法是客戶端分布式緩存性能優劣的關鍵。

普通的哈希表算法一般都是計算出哈希值後,通過取餘操作將 key 值映射到不同的伺服器上,但是當伺服器數量發生變化時,取餘操作的除數發生變化,所有 key 所映射的伺服器幾乎都會改變,這對分布式緩存系統來說是不可以接收的。

一致性哈希算法能儘可能減少了伺服器數量變化所導致的緩存遷移。

哈希算法

首先,一致性哈希算法依賴於普通的哈希算法。大多數同學對哈希算法的理解可能都停留在 JDK 的 hashCode 函數上。其實哈希算法有很多種實現,它們在不同方面都各有優劣,針對不同的場景可以使用不同的哈希算法實現。

下面,我們會介紹一下幾款比較常見的哈希算法,並且了解一下它們在分布均勻程度,哈希碰撞概率和性能等方面的優劣。

MD5 算法:全稱為 Message-Digest Algorithm 5,用於確保信息傳輸完整一致。是計算機廣泛使用的雜湊算法之一,主流程式語言普遍已有 MD5 實現。MD5 的作用是把大容量信息壓縮成一種保密的格式(就是把一個任意長度的字節串變換成定長的16進位數字串)。常見的文件完整性校驗就是使用 MD5。

CRC 算法:全稱為 CyclicRedundancyCheck,中文名稱為循環冗餘校驗。它是一類重要的,編碼和解碼方法簡單,檢錯和糾錯能力強的哈希算法,在通信領域廣泛地用於實現差錯控制。

MurmurHash 算法:高運算性能,低碰撞率,由 Austin Appleby 創建於 2008 年,現已應用到 Hadoop、libstdc++、nginx、libmemcached 等開源系統。Java 界中 Redis,Memcached,Cassandra,HBase,Lucene和Guava 都在使用它。

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

Ketama 算法:一致性哈希算法的實現之一,其他的哈希算法有通用的一致性哈希算法實現,只不過是替換了哈希映射函數而已,但 Ketama 是一整套的流程,我們將在後面介紹。

一致性哈希算法

下面,我們以分布式緩存場景為例,分析一下一致性哈希算法環的原理。

首先將緩存伺服器( ip + 埠號)進行哈希,映射成環上的一個節點,計算出緩存數據 key 值的 hash key,同樣映射到環上,並順時針選取最近的一個伺服器節點作為該緩存應該存儲的伺服器。具體實現見後續的章節。

比如說,當存在 A,B,C,D 四個緩存伺服器時,它們及其 key 值為1的緩存數據在一致性哈希環上的位置如下圖所示,根據順時針取最近一個伺服器節點的規則,該緩存數據應該存儲在伺服器 B 上。

當要存儲一個 key 值為4的緩存數據時,它在一致性哈希環上的位置如下所示,所以它應該存儲在伺服器 C 上。

類似的,key 值為5,6的數據應該存在服務 D 上,key 值為7,8的數據應該存儲在服務 A 上。

此時,伺服器 B 宕機下線,伺服器 B 中存儲的緩存數據要進行遷移,但由於一致性哈希環的存在,只需要遷移key 值為1的數據,其他的數據的存儲伺服器不會發生變化。這也是一致性哈希算法比取餘映射算法出色的地方。

由於伺服器 B 下線,key 值為1的數據順時針最近的伺服器是 C ,所以數據存遷移到伺服器 C 上。

現實情況下,伺服器在一致性哈希環上的位置不可能分布的這麼均勻,導致了每個節點實際佔據環上的區間大小不一。

這種情況下,可以增加虛節點來解決。通過增加虛節點,使得每個節點在環上所「管轄」的區域更加均勻。這樣就既保證了在節點變化時,儘可能小的影響數據分布的變化,而同時又保證了數據分布的均勻。

具體實現

下面我們實現 Memcached 分布式緩存場景下的一致性哈希算法,並給出具體的測試性能數據。該實現借鑑了 kiritomoe 博文中的實現和 spymemcached 客戶端代碼。具體實現請看我的github,地址為 https://github.com/ztelur/consistent-hash-algorithm。

NodeLocator 是分布式緩存場景下一致性哈希算法的抽象,它有一個 getPrimary 函數,接收一個緩存數據的 key 值,輸出存儲該緩存數據的伺服器實例。

public interface NodeLocator { MemcachedNode getPrimary(String k);}

下面是通用的一致性哈希算法的實現,它使用 TreeMap 作為一致性哈希環的數據結構,其 ceilingEntry 函數可以獲取環上最近的一個節點。 buildConsistentHashRing 函數中包含了構建一致性哈希環的過程,默認加入了 12 個虛擬節點。

public class ConsistentHashNodeLocator implements NodeLocator { private final static int VIRTUAL_NODE_SIZE = 12; private final static String VIRTUAL_NODE_SUFFIX = "-"; private volatile TreeMap<Long, MemcachedNode> hashRing; private final HashAlgorithm hashAlg; public ConsistentHashNodeLocator(List<MemcachedNode> nodes, HashAlgorithm hashAlg) { this.hashAlg = hashAlg; this.hashRing = buildConsistentHashRing(hashAlg, nodes); } @Override public MemcachedNode getPrimary(String k) { long hash = hashAlg.hash(k); return getNodeForKey(hashRing, hash); } private MemcachedNode getNodeForKey(TreeMap<Long, MemcachedNode> hashRing, long hash) { /* 向右找到第一個key */ Map.Entry<Long, MemcachedNode> locatedNode = hashRing.ceilingEntry(hash); /* 想像成為一個環,超出尾部取出第一個 */ if (locatedNode == null) { locatedNode = hashRing.firstEntry(); } return locatedNode.getValue(); } private TreeMap<Long, MemcachedNode> buildConsistentHashRing(HashAlgorithm hashAlgorithm, List<MemcachedNode> nodes) { TreeMap<Long, MemcachedNode> virtualNodeRing = new TreeMap<>(); for (MemcachedNode node : nodes) { for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) { // 新增虛擬節點的方式如果有影響,也可以抽象出一個由物理節點擴展虛擬節點的類 virtualNodeRing.put(hashAlgorithm.hash(node.getSocketAddress().toString() + VIRTUAL_NODE_SUFFIX + i), node); } } return virtualNodeRing; }}

在 getPrimary 函數中,首先使用 HashAlgorithm 計算出 key 值對應的哈希值,然後調用 getNodeForKey 函數從 TreeMap 中獲取對應的最近的伺服器節點實例。

HashAlgorithm 是對哈希算法的抽象,一致性哈希算法可以使用各種普通的哈希算法,比如說 CRC ,MurmurHash 和 FNV 等。下面,我們將會對比各種哈希算法給該實現帶來的性能差異性。

性能測試

測試數據是評價一個算法好壞的最為真實有效的方法,量化的思維模式一定要有,這也是程式設計師進階的法寶之一。我們以下面四個量化的指標對基於不同哈希函數的一致性哈希算法進行評測。

  • 統計每個伺服器節點存儲的緩存數量,計算方差和標準差。測量緩存分布均勻情況,我們可以模擬 50000個緩存數據,分配到100 個伺服器,測試最後個節點存儲緩存數據量的方差和標準差。
  • 隨機下線10%的伺服器,重新分配緩存,統計緩存遷移比率。測量節點上下線的情況,我們可以模擬 50000 個緩存數據,分配到100 個指定伺服器,之後隨機下線 10 個伺服器並重新分配這50000個數據,統計緩存分配到不同伺服器的比例,也就是遷移比率。
  • 使用JMH對不同哈希算法的執行效率進行對比。

具體評測算法如下。

public class NodeLocatorTest { /** * 測試分布的離散情況 */ @Test public void testDistribution() { List<MemcachedNode> servers = new ArrayList<>(); for (String ip : ips) { servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080))); } // 使用不同的DefaultHashAlgorithm進行測試,得出不同的數據 NodeLocator nodeLocator = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.NATIVE_HASH); // 構造 50000 隨機請求 List<String> keys = new ArrayList<>(); for (int i = 0; i < 50000; i++) { keys.add(UUID.randomUUID().toString()); } // 統計分布 AtomicLongMap<MemcachedNode> atomicLongMap = AtomicLongMap.create(); for (MemcachedNode server : servers) { atomicLongMap.put(server, 0); } for (String key : keys) { MemcachedNode node = nodeLocator.getPrimary(key); atomicLongMap.getAndIncrement(node); } System.out.println(StatisticsUtil.variance(atomicLongMap.asMap().values().toArray(new Long[]{}))); System.out.println(StatisticsUtil.standardDeviation(atomicLongMap.asMap().values().toArray(new Long[]{}))); } /** * 測試節點新增刪除後的變化程度 */ @Test public void testNodeAddAndRemove() { List<MemcachedNode> servers = new ArrayList<>(); for (String ip : ips) { servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080))); } //隨機下線10個伺服器, 先shuffle,然後選擇0到90,簡單模仿隨機計算。 Collections.shuffle(servers); List<MemcachedNode> serverChanged = servers.subList(0, 90); NodeLocator loadBalance = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.NATIVE_HASH); NodeLocator changedLoadBalance = new ConsistentHashNodeLocator(serverChanged, DefaultHashAlgorithm.NATIVE_HASH); // 構造 50000 隨機請求 List<String> keys = new ArrayList<>(); for (int i = 0; i < 50000; i++) { keys.add(UUID.randomUUID().toString()); } int count = 0; for (String invocation : keys) { MemcachedNode origin = loadBalance.getPrimary(invocation); MemcachedNode changed = changedLoadBalance.getPrimary(invocation); // 統計發生變化的數值 if (!origin.getSocketAddress().equals(changed.getSocketAddress())) count++; } System.out.println(count / 50000D); } static String[] ips = {...};}

JMH的測試腳本如下所示。

@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MICROSECONDS)@State(Scope.Thread)public class JMHBenchmark { private NodeLocator nodeLocator; private List<String> keys; @Benchmark public void test() { for (String key : keys) { MemcachedNode node = nodeLocator.getPrimary(key); } } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JMHBenchmark.class.getSimpleName()) .forks(1) .warmupIterations(5) .measurementIterations(5) .build(); new Runner(opt).run(); } @Setup public void prepare() { List<MemcachedNode> servers = new ArrayList<>(); for (String ip : ips) { servers.add(new MemcachedNode(new InetSocketAddress(ip, 8080))); } nodeLocator = new ConsistentHashNodeLocator(servers, DefaultHashAlgorithm.MURMUR_HASH); // 構造 50000 隨機請求 keys = new ArrayList<>(); for (int i = 0; i < 50000; i++) { keys.add(UUID.randomUUID().toString()); } } @TearDown public void shutdown() { } static String[] ips = {...};}

分別測試了 JDK 哈希算法,FNV132 算法,CRC 算法,MurmurHash 算法和Ketama 算法,分別對應 DefaultHashAlgorithm 的 NATIVE_HASH, FNV1_32_HASH, CRC_HASH, MURMUR_HASH 和 KETAMA_HASH 。具體數據如下所示。

虛擬槽分區

有些文章說,Redis 集群並沒有使用一致性哈希算法,而是使用虛擬槽分區算法。但是外網(地址見文末)上都說 Redis 使用的虛擬槽分區只是一致性哈希算法的變種,虛擬槽可以允許 Redis 動態擴容。

或許只有去了解一下Redis的源碼才能對這個問題作出準確的回答。請了解的同學積極留言解答,謝謝。

個人微信公眾號: remcarpediem

個人博客地址:

github 地址: https://github.com/ztelur/consistent-hash-algorithm

redis分布式討論的地址: https://www.reddit.com/r/redis/comments/4yztxi/whichoneisbetterhashslotor_consistent/

參考

  • https://jistol.github.io/software%20engineering/2018/07/07/consistent-hashing-sample/
  • https://mp.weixin.qq.com/s/oe3EPu5DxB0bWheBImMsHg

相關焦點

  • 5分鐘理解一致性哈希算法
    前言一致性哈希算法(Consistent Hashing)在分布式系統的應用還是十分廣泛的,本文儘量結合業務場景快速講解一致性哈希算法的應用及與其相關的話題。1 分布式緩存隨著業務的擴展,流量的劇增,單體項目逐漸劃分為分布式系統。對於經常使用的數據,我們可以使用Redis作為緩存機制,減少數據層的壓力。
  • 圖解一致性哈希算法,全網(小區區域網)最通俗易懂
    ,在後端面試和開發中會遇到「一致性哈希」,那麼什麼是一致性哈希呢?緩存系統負載均衡在分布式集群緩存的負載均衡實現中,比如 memcached 緩存集群,需要把緩存數據的 key 利用哈希函數散列,這樣緩存數據能夠均勻分布到各個分布式存儲節點上,要實現這樣的負載均衡一般可以用哈希算法來實現。
  • 10分鐘了解一致性哈希算法,全網(小區區域網)(建議收藏)
    很多同學應該都知道什麼是哈希函數,在後端面試和開發中會遇到「一致性哈希」,那麼什麼是一致性哈希呢?名字聽起來很厲害的樣子,其實原理並不複雜,這篇文章帶你徹底搞懂一致性哈希!進入主題前,先來一場緊張刺激的模擬面試吧。模擬面試面試官:看你簡歷上寫參與了一個大型項目,用到了分布式緩存集群,那你說說你們是怎麼做緩存負載均衡?
  • 數據分布方式一哈希與一致性哈希詳解
    首先,我們來看一下數據分布設計的原則。數據分布設計原則是分布式存儲系統設計的基本原則,指導了哈希和一致性哈希方法的選擇和應用。數據分布設計原則實,這裡的數據分布,主要就是數據分片。相信你還記得,我在第24篇文章中與你分享分布式存儲系統的導購時,已經和你提到數據分片技術,它解決了確定數據位置的問題,並著重與你講述了按照數據特徵進行劃分的分片方法。今天,我主要與你講解按照數據範圍,採用哈希一致性哈希等對數據劃分的方法。假設,現在有上百G數據需要進行分布式存儲,也就是要存儲到不同的節點上。
  • 職場|程式設計師必須知道的一致性哈希路由算法
    基於以上的考慮,現在企業存儲紛紛採用分布式存儲,將數據存儲在多個配置相同的伺服器上,組成分布式集群(省錢,擴容方便,風險小)。那麼,存儲數據從一個節點變為多個分布式節點的時候,有什麼路由算法能保證同一數據的讀寫都能在一個節點上呢?
  • 簡明講解一致性哈希算法
    簡明講解一致性哈希算法哈希算法#如果我們用(用戶id)%伺服器機器數這樣的方法來分配伺服器。雖然我們能保證數據的均勻性,但穩定性差,比如我們增加一個節點,會導致大量的映射失效。所以哈希算法只適用於節點數比較固定的情況,並不能很好的應對節點的變化。一致性哈希算法#這個時候一致性算法就來了,你看這個哈希環它是又大又圓,用它來降低映射關係大量失效的可能性剛剛好。任何一條線段都有無數個點,這個大家應該沒什麼意見吧?所以理論上這個哈希圈是能存儲無限多的東西的。
  • 什麼是一致性哈希算法
    ---數據分片與路由當數據量很大時,通過改善單機硬體資源的縱向擴充方式來存儲數據變得越來越不適用,而通過增加機器數目來獲得水平橫向擴展的方式則越來越流行。因此,就有個問題,如何將這些海量的數據分配到各個機器中?數據分布到各個機器存儲之後,又如何進行查找?這裡主要記錄一致性Hash算法如何將數據分配到各個機器中去。
  • 一口氣講透一致性哈希(Hash),助力「碼農變身」
    如今雲計算、大數據、物聯網、AI的興起,使得分布系統得到了前所未有的廣泛應用,然而由於分布式系統具有極高的複雜度,帶來了許多難題,一致性哈希就是為了解決分布式難題之一應運而生的,本篇主要圖示講解一致性哈希的原理及其應用,助力碼農變身。
  • 大數據和分布式入門:主流分布式緩存組件
    在大數據處理當中,核心指導思想始終是分布式,基於分布式思想,我們有了Hadoop等開源技術框架,能夠以更低的成本完成企業大數據系統平臺搭建,支持業務進展。今天大數據和分布式入門,我們主要來聊聊主流的大數據分布式緩存組件。
  • 分布式協議與算法,你了解多少?
    一致性Hash算法一致性Hash算法是為了解決Hash算法的遷移成本,以一個10節點的集群為例,如果向集群中添加節點時,如果使用了哈希 算法,需要遷移高達90.91%的數據,使用一致哈希的話,只需要遷移 6.48% 的數據。
  • 談了千百遍的緩存數據一致性問題
    當我們的系統引入緩存組件之後,性能得到了大幅度提升,但是隨之而來的是代碼需要引入一定的複雜度,比如緩存的更新策略,寫入策略,過期策略等,而其中最可能導致程式設計師加班的莫過於緩存和資料庫的一致性問題了,既:緩存中的數據和資料庫中的數據不一致。
  • GitHub上都在找的分布式核心筆記終於來了
    分布式作為現在作為Java開發必知必會的技術,同時分布式技術也屬於面試中的必問題,那麼我們就需要十分明白分布式,今天就為大家整理了一份Java分布式核心原理筆記,GitHub上人人都在找的分布式核心技術筆記終於終於免費開源了!
  • 來自字節跳動的一道題:一致性Hash
    緣起我有一個圖片存取服務,為了快速獲取圖片,我架起了3臺緩存伺服器,用簡單的Hash映射決定圖片存儲在哪臺緩存上。是否有這樣的算法,解決分布式緩存中,解決簡單Hash隨緩存伺服器伸縮,造成大面積緩存失效的問題
  • 阿里P7聯與京東T6出版:深度解構分布式緩存技術原理,實踐及電商
    哈希取模一致性哈希路由表數據拆分第三章目前市面上已經有很多開源的緩存框架,比如Redis、Memcached非持久化的Tair可以看成是-一個分布式緩存。持久化的Tair將數據存放於磁碟中。在最新版本的Tair項目中實現了以下4種存儲引擎。
  • Redis Sharding 集群跟一致性哈希有什麼瓜葛?
    如果使用一致性哈希算法的話(我這裡畫了個概念圖方便理解),算法會針對每個物理節點(Node)虛擬出多個虛擬節點(這裡簡稱Virtual Node或者VNode),這樣在整個哈希環空間內就有多個互相交錯的虛擬節點,那數據分布得更均衡了而避免對於某臺伺服器的讀寫壓力,一般來說虛擬節點越多數據分布得越均勻(曾經在某個文章看過壓測數據,當虛擬節點數達到
  • 分布式一致性算法,你確定不了解一下?
    分布式事務也可以被定義為一種嵌套型的事務,同時也就具有了ACID事務的特性。CAP理論Consistency(一致性):數據一致更新,所有數據變動都是同步的(強一致性)。堅持事務ACID(原子性、一致性、隔離性和持久性)的傳統資料庫以及對結果一致性非常敏感的應用通常會做出這樣的選擇。AP系統(放棄C):這裡所說的放棄一致性,並不是完全放棄數據一致性,而**是放棄數據的強一致性,而保留數據的最終一致性。**如果即要求系統高可用又要求分區容錯,那麼就要放棄一致性了。
  • 分布式一致性算法,你確定不了解一下
    集中式與分布式集中式分布式分布式事務一致性協議2PC:Two-Phase Commit二階段提交協議3PC:Three-phase Commit 三階段提交協議Paxos算法RAFT算法總結集中式與分布式集中式就是將所有的業務都部署在一個中心主機
  • hash一致性算法
    一致性hash算法是,1097麻省理工提出的分布式hashDHT實現算法,極倔internet的熱點問題 平衡性 hash結果儘可能的分布到所有的緩存中去,緩衝空間利用率最高單調性 保持已有的緩存能映射到對應的位置,新加入的緩存能加入新的位置不會映射到舊的位置分散性 儘量降低分散性的緩存不一致情況發生負載 負載被粉絲降低負荷
  • 聊聊分布式系統的數據一致性
    進入公司以來,先後參與了分布式資料庫、分布式文件系統、NOS對象存儲雲服務等多個大型存儲系統的開發工作。保證各個數據副本間的一致性,是評價存儲系統優劣的核心指標,也是貫穿整個開發過程中的討論重點。以前都是碰到問題見招拆招,看一些分布式系統中偏理論的研究時也是覺得雲裡霧裡,所以也想趁這次機會好好梳理一下,整理出的內容也希望能夠對大家以後的開發工作起到幫助。數據多副本間的一致性存儲系統是千差萬別的,可以拿來存放視頻這種動輒幾個G的大文件,也可以存放幾KB的KV鍵值對數據,還可能是MySQL這種關係型的資料庫。
  • 分布式理論(五) - 一致性算法Paxos
    該算法在很多大廠都得到了工程實踐,比如阿里的 OceanBase 的 分布式資料庫,底層就是使用的 Paxos 算法。再比如 Google 的 chubby 分布式鎖 也是用的這個算法。可見該算法在分布式系統中的地位,甚至於,Paxos 就是 分布式一致性 的代名詞。