分布式應(yīng)用與一致性哈希

場景

最近要在應(yīng)用中做一層內(nèi)存緩存來加快系統(tǒng)的響應(yīng)匕荸。但是由于數(shù)據(jù)量較大,單臺Server無法Load全量數(shù)據(jù)篱竭,所以考慮使用哈希的方式進行分片存儲在Server的內(nèi)存中坑傅,接入消息隊列進行內(nèi)存數(shù)據(jù)的淘汰脚牍。

原系統(tǒng)調(diào)用關(guān)系簡化圖如下:

系統(tǒng)調(diào)用圖

無論應(yīng)用服務(wù)一[下面簡稱服務(wù)一]是以何種方式(HTTP/RPC)去請求應(yīng)用服務(wù)二[下面簡稱服務(wù)二].都會經(jīng)過一層負載均衡.而現(xiàn)在我們要做內(nèi)存緩存,實際上就是去自己實現(xiàn)一套負載均衡算法來實現(xiàn)應(yīng)用一對特定的請求key,去訪問特定的應(yīng)用二的機器.

在這里我們忽略跨機房,權(quán)重等問題.只考慮選定目標(biāo)主機的策略,即哈希函數(shù).在分布式緩存中,考量一個哈希函數(shù)是否優(yōu)秀,通常可以用三個指標(biāo)來確定:

  1. 平衡性(Balance):緩存機器使用率越高越好
  2. 單調(diào)性(Monotonicity):上線/下線機器對緩存命中率的影響越小越好
  3. 分散性(Spread):一個緩存key,存在于機器中的數(shù)量越少越好.(>=1)

接下來,我們看一下常見的兩種哈希函數(shù).在這個場景下的表現(xiàn).

簡單哈希

假定有十臺Server,我們給每臺Server編號0~9.通過對請求key進行哈希,隨后余10,得到對應(yīng)的數(shù),即為對應(yīng)Server的編號.抽象成公式:

H = hash(key) % 10

這種方式的哈希函數(shù),在平衡性(十臺server全部用上),分散性(每個key對應(yīng)一臺機器)上都很優(yōu)秀,但是在單調(diào)性上,卻表現(xiàn)不佳.下面通過一個demo程序,來看一下.

public class Demo {

    public static void main(String[] args) {
        testAfterAddOneServerHitRate();
    }

    public static void testAfterAddOneServerHitRate() {
        List<String> serverList = Lists.newArrayList("192.168.1.1", 
                "192.168.1.2", "192.168.1.3",
                "192.168.1.4", "192.168.1.5", "192.168.1.6", "192.168.1.7",
                "192.168.1.8", "192.168.1.9", "192.168.1.10");
        // serverList多了一臺之后.
        serverList.add("192.168.1.11");
        Map<String, Integer> serverHitCount = new HashMap<>(10);
        Map<String, Integer> serverMissCount = new HashMap<>(10);
        //假定哈希空間為0~2^31-1
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String targetServerBefore = serverList.get(i % 10);
            String targetServerNow = serverList.get(i % 11);
            if (targetServerNow.equals(targetServerBefore)) {
                plusOneToMapKey(serverHitCount, targetServerNow);
            } else if (!"192.168.1.11".equals(targetServerNow)) {
                plusOneToMapKey(serverMissCount, targetServerNow);
            }
        }

        System.out.println(serverHitCount);
        System.out.println(serverMissCount);
    }

    private static void plusOneToMapKey(Map<String, Integer> map, String key) {
        if (map.containsKey(key)) {
            map.put(key, map.get(key) + 1);
        } else {
            map.put(key, 1);
        }
    }
}

執(zhí)行結(jié)果為:

server hit miss rate
192.168.1.1 19522579 175703208 0.1
192.168.1.2 19522579 175703207 0.1
192.168.1.3 19522579 175703207 0.1
192.168.1.4 19522579 175703207 0.1
192.168.1.5 19522579 175703207 0.1
192.168.1.6 19522579 175703207 0.1
192.168.1.7 19522579 175703207 0.1
192.168.1.8 19522579 175703207 0.1
192.168.1.9 19522579 175703207 0.1
192.168.1.10 19522579 175703207 0.1

當(dāng)新增一臺機器后,原server0~server9上的90%的緩存已經(jīng)不能路由到原來緩存這個數(shù)據(jù)的機器.所以這種取模的哈希算法,雖然簡單,但是在分布式系統(tǒng)中,上線下線機器是在所難免的.在這種情況下緩存的大量失效是無法接收的.

一致性哈希

一致性哈希算法設(shè)計思路:

  1. 將整個哈希值空間組織成一個虛擬的圓環(huán).假定某哈希函數(shù)的值空間為0~2^31-1,Java中int類型的正值范圍.整個哈希環(huán)如下.


  2. 我們將服務(wù)器的列表,通過哈希函數(shù),哈希到圓環(huán)上.


  3. 我們將緩存key,也同樣映射到緩存空間


命中規(guī)則

緩存key,順時針移動,找到的第一臺服務(wù)器,就是該緩存key對應(yīng)緩存存在的機器.

指標(biāo)分析

  1. 平衡性: 由于在一個環(huán)上,所有的機器都會存儲一定量的緩存數(shù)據(jù)
  2. 單調(diào)性: 當(dāng)有Server02下線的時候,只有Server01和Server02中間的緩存數(shù)據(jù)會遷移到Server03,而其他緩存數(shù)據(jù)并不會沒任何移動.單調(diào)性良好.
  3. 分散性: 每個緩存key都會存儲在一臺機器中.

存在問題

當(dāng)我們的服務(wù)器較少的時候,可能會這種情況:



可能大量的緩存,會命中到Server02.因為Server01~02之間的空隙很大.這樣實際上三臺機器的負載就會很不均勻.至于有多不均勻,我們可以運行一段Demo程序來看看結(jié)果.

全部采用實節(jié)點的Demo

public class Demo {

    public static void main(String[] args) {
        pureServerList();
    }

    public static void pureServerList(){
        Hash hash = new MurmurHash();
        List<String> serverList = Lists.newArrayList("192.168.1.1", "192.168.1.2", "102.168.1.3");
        TreeMap<Integer, String> hashCircle = new TreeMap<>();
        serverList.forEach(item -> hashCircle.put(hash.hash(item) & 0x7fffffff, item));
        Map<String, Integer> counter = Maps.newHashMap();
        for (int i = 0; i < Integer.MAX_VALUE ; i++) {
            Integer serverHash = hashCircle.ceilingKey(i);
            if (serverHash == null) {
                serverHash = hashCircle.firstKey();
            }
            String server = (hashCircle.get(serverHash));
            if (counter.containsKey(server)) {
                counter.put(server, counter.get(server) + 1);
            } else {
                counter.put(server, 1);
            }
        }
        System.out.println(counter);
    }
}

代碼說明:

  1. 由于要找到比指定哈希值大的第一個key,則使用TreeMap(紅黑樹)這種結(jié)構(gòu),實現(xiàn)簡單,并且效率良好.
  2. 對哈希值進行了 & 0x7fffffff 操作,是因為哈希值可能為負.而我們的哈希空間,設(shè)定為大于等于0

程序運行結(jié)果:

server count
192.168.1.1 26553108
102.168.1.3 1057185279
192.168.1.2 1063745260

由結(jié)果我們可以看到三臺機器,實際上有一臺能映射到的機會非常少.為了解決這個問題,我們可以通過引入虛擬節(jié)點來進行解決伍玖。

虛擬節(jié)點的引入

虛擬節(jié)點.故名思議.我們可以為我們的每個服務(wù)器模擬多個節(jié)點,使得更多的機器可以落在哈希環(huán)上.通過這種方式來讓我們的key,可以落的均勻.最簡單的辦法.就是在ip后面添加#00,#01這種方式.如下圖.

下面我們再寫一段程序,看看引入虛擬節(jié)點后的key的分布情況.

public class Demo {

    public static void main(String[] args) {
        virtualServerList();
    }

    public static void virtualServerList(){
        Hash hash = new MurmurHash();
        List<String> serverList = Lists.newArrayList("192.168.1.1", "192.168.1.2", "102.168.1.3");
        List<String> vServerList = Lists.newArrayList();
        for (String str : serverList) {
            for (int i = 0; i < 1024; i++) {
                vServerList.add(((str + "#" + i)));
            }
        }
        TreeMap<Integer, String> hashCircle = new TreeMap<>();
        vServerList.forEach(item -> hashCircle.put(hash.hash(item) & 0x7fffffff, item));
        Map<String, Integer> counter = Maps.newHashMap();
        for (int i = 0; i < Integer.MAX_VALUE ; i++) {
            Integer serverHash = hashCircle.ceilingKey(i);
            if (serverHash == null) {
                serverHash = hashCircle.firstKey();
            }
            String server = (hashCircle.get(serverHash)).split("#")[0];
            if (counter.containsKey(server)) {
                counter.put(server, counter.get(server) + 1);
            } else {
                counter.put(server, 1);
            }
        }
        System.out.println(counter);
    }
}

代碼說明:

  1. 每個節(jié)點,分成1024個虛擬節(jié)點

程序運行結(jié)果:

server count
192.168.1.1 735336735
102.168.1.3 708566329
192.168.1.2 703580583

通過使用虛擬節(jié)點這種方式,我們的key已經(jīng)可以均勻的落在各臺機器上了.

總結(jié)

目前一致性哈希基本成為了分布式系統(tǒng)組件的標(biāo)準(zhǔn)配置剿吻,例如Memcached的各種客戶端都提供內(nèi)置的一致性哈希支持,值得我們學(xué)習(xí)與使用.

參考資料

Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web
一致性哈希算法及其在分布式系統(tǒng)中的應(yīng)用
每天進步一點點——五分鐘理解一致性哈希算法(consistent hashing)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末窍箍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子丽旅,更是在濱河造成了極大的恐慌椰棘,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件魔招,死亡現(xiàn)場離奇詭異晰搀,居然都是意外死亡五辽,警方通過查閱死者的電腦和手機办斑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乡翅,你說我怎么就攤上這事鳞疲。” “怎么了蠕蚜?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵尚洽,是天一觀的道長。 經(jīng)常有香客問我靶累,道長腺毫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任挣柬,我火速辦了婚禮潮酒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘邪蛔。我一直安慰自己急黎,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布侧到。 她就那樣靜靜地躺著勃教,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匠抗。 梳的紋絲不亂的頭發(fā)上故源,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音汞贸,去河邊找鬼心软。 笑死,一個胖子當(dāng)著我的面吹牛著蛙,可吹牛的內(nèi)容都是我干的删铃。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼踏堡,長吁一口氣:“原來是場噩夢啊……” “哼猎唁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起顷蟆,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤诫隅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后帐偎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逐纬,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年削樊,在試婚紗的時候發(fā)現(xiàn)自己被綠了豁生。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兔毒。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖甸箱,靈堂內(nèi)的尸體忽然破棺而出育叁,到底是詐尸還是另有隱情,我是刑警寧澤芍殖,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布豪嗽,位于F島的核電站,受9級特大地震影響豌骏,放射性物質(zhì)發(fā)生泄漏龟梦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一窃躲、第九天 我趴在偏房一處隱蔽的房頂上張望变秦。 院中可真熱鬧,春花似錦框舔、人聲如沸蹦玫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽樱溉。三九已至,卻和暖如春纬凤,著一層夾襖步出監(jiān)牢的瞬間福贞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工停士, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留挖帘,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓恋技,卻偏偏與公主長得像拇舀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蜻底,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容