海量數據解決思路之Hash算法

一、概述

   本文將粗略講述一下Hash算法的概念特性,裏邊會結合分佈式系統負載均衡實例對Hash的一致性做深入探討。另外,探討一下Hash算法在海量數據處理方案中的通用性。最後,從源代碼出發,具體分析一下Hash算法在MapReduce框架的中的應用。

二、Hash算法

   Hash可以通過散列函數將任意長度的輸入變成固定長度的輸出,也可以將不同的輸入映射成爲相同的相同的輸出,而且這些輸出範圍也是可控制的,所以起到了很好的壓縮映射和等價映射功能。這些特性被應用到了信息安全領域中加密算法,其中等價映射這一特性在海量數據解決方案中起到相當大的作用,特別是在整個MapReduce框架中,下面章節會對這二方面詳細說。話說,Hash爲什麼會有這種壓縮映射和等價映射功能,主要是因爲Hash函數在實現上都使用到了取模。下面看看幾種常用的Hash函數:


·直接取餘法:f(x):= x mod maxM ; maxM一般是不太接近 2^t 的一個質數。

·乘法取整法:f(x):=trunc((x/maxX)*maxlongit) mod maxM,主要用於實數。

·平方取中法:f(x):=(x*x div 1000 ) mod 1000000); 平方後取中間的,每位包含信息比較多。

三、Hash算法在海量數據處理方案中的應用

單機處理海量數據的大體主流思想是和MapReduce框架一樣,都是採取分而治之的方法,將海量數據切分爲若干小份來進行處理,並且在處理的過程中要兼顧內存的使用情況和處理併發量情況。而更加仔細的處理流程大體上分爲幾步(對大多數情況都使用,其中少部分情況要根據你自己的實際情況和其他解決方法做比較採用最符合實際的方法):

  • 第一步:分而治之。

   採用Hash取模進行等價映射。採用這種方法可以將巨大的文件進行等價分割(注意:符合一定規律的數據要被分割到同一個小文件)變成若干個小文件再進行處理。這個方法針對數據量巨大,內存受到限制時十分有效。

  • 第二步:利用hashMap在內存中進行統計。

   我們通過Hash映射將大文件分割爲小文件後,就可以採用HashMap這樣的存儲結構來對小文件中的關注項進行頻率統計。具體的做法是將要進行統計的Item作爲HashMap的key,此Item出現的次數作爲value。

  • 第三步:在上一步進行統計完畢之後根據場景需求往往需要對存儲在HashMap中的數據根據出現的次數來進行排序。其中排序我們可以採用堆排序、快速排序、歸併排序等方法。

   現在我們來看看具體的例子:

【例子1】海量日誌數據,提取出某日訪問百度次數最多的那個IP

   思路:當看到這樣的業務場景,我們腦子裏應該立馬會想到這些海量網關日誌數據量有多大?這些IP有多少中組合情況,最大情況下佔多少存儲空間?解決這樣的問題前我們最重要的先要知道數據的規模,這樣才能從大體上制定解決方案。所以現在假設這些這些網關日誌量有3T。下面大體按照我們上面的步驟來對解決此場景進行分析:

(1)首先,從這些海量數據中過濾出指定一天訪問百度的用戶IP,並逐個寫到一個大文件中。

(2)採用“分而治之”的思想用Hash映射將大文件進行分割降低數據規模。按照IP地址的Hash(IP)%1024值,把海量IP日誌分別存儲到1024個小文件中,其中Hash函數得出值爲分割後小文件的編號。

(3)逐個讀小文件,對於每一個小文件構建一個IP爲key,出現次數爲value的HashMap。對於怎麼利用HashMap記錄IP出現的次數這個比較簡單,因爲我們可以通過程序讀小文件將IP放到HashMap中key的之後可以先判斷此IP是否已經存在如果不存在直接放進去,其出現次數記錄爲1,如果此IP已經存儲則過得其對應的value值也就是出現的次數然後加1就ok。最後,按照IP出現的次數採用排序算法對HashMap中的數據進行排序,同時記錄當前出現次數最多的那個IP地址;

(4)走到這步,我們可以得到1024個小文件中出現次數最多的IP了,再採用常規的排序算法找出總體上出現次數最多的IP就ok了。


這個我們需要特別地明確知道一下幾點內容:

第一:我們通過Hash函數:Hash(IP)%1024將大文件映射分割爲了1024個小文件,那麼這1024個小文件的大小是否均勻?另外,我們採用HashMap來進行IP頻率的統計,內存消耗是否合適?

  • 首先是第一個問題,被分割的小文件的大小的均勻程度是取決於我們使用怎麼樣的Hash函數,對本場景而言就是:Hash(IP)%1024。設計良好的Hash函數可以減少衝突,使數據均勻的分割到1024個小文件中。但是儘管數據映射到了另外一些不同的位置,但數據還是原來的數據,只是代替和表示這些原始數據的形式發生了變化而已。

  • 另外,看看第二個問題:用HashMap統計IP出現頻率的內存使用情況。

要想知道HashMap在統計IP出現的頻率,那麼我們必須對IP組合的情況有所瞭解。32Bit的IP最多可以有2^32種的組合方式,也就是說去所有IP最多佔4G存儲空間。在此場景中,我們已經根據IP的hash值將大文件分割出了1024個小文件,也就是說這4G的IP已經被分散到了1024個文件中。那麼在Hash函數設計合理最perfect的情況下針對每個小文件的HashMap佔的內存大小最多爲4G/1024+存儲IP對應的次數所佔的空間,所以內存絕對夠用。

第二:Hash取模是一種等價映射,換句話說通過映射分割之後相同的元素只會分到同一個小文件中去的。就本場景而言,相同的IP通過Hash函數後只會被分割到這1024個小文件中的其中一個文件。


【例子2】給定a、b兩個文件,各存放50億個url,每個url各佔64字節,內存限制是4G,讓你找出a、b文件共同的url?

   思路:還是老一套,先Hash映射降低數據規模,然後統計排序。

 具體做法:

(1)分析現有數據的規模。

   按照每個url64字節來算,每個文件有50億個url,那麼每個文件大小爲5G*64=320G。320G遠遠超出內存限定的4G,所以不能將其全部加載到內存中來進行處理,需要採用分而治之的方法進行處理。

(2)Hash映射分割文件。逐行讀取文件a,採用hash函數:Hash(url)%1000將url分割到1000個小文件中,文件即爲f1_1,f1_2,f1_3,...,f1_1000。那麼理想情況下每個小文件的大小大約爲300m左右。再以相同的方法對大文件b進行相同的操作再得到1000個小文件,記爲:f2_1,f2_2,f2_3,...,f2_1000。

   經過一番折騰後我們將大文件進行了分割並且將相同url都分割到了這2組小文件中下標相同的兩個文件中,其實我們可以將這2組文件看成一個整體:f1_1&f2_1,f1_2&,f2_2,f1_3&f2_3,...,f1_1000&f2_1000。那麼我們就可以將問題轉化成爲求這1000對小文件中相同的url就可以了。接下來,求每對小文件中的相同url,首先將每對對小文件中較小的那個的url放到HashSet結構中,然後遍歷對應這對小文件中的另一個文件,看其是否存纔剛剛構建的HashSet中,如果存在說明是一樣的url,將這url直接存到結果文件就ok了。


【例子3】有10個文件,每個文件1G,每個文件的每一行存放的都是用戶的query,每個文件的query都可能重複。要求你按照query的頻度排序。

【例子4】有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。

像例子3和例子4這些場景都可以用我們的一貫老招數解決:先Hash映射降低數據規模,然後統計加載到內存,最後排序。具體做法可以參考上面2個例子。


四、Hash算法在MapReduce框架中的應用

    Hash算法在分佈式計算框架MapReduce中起着核心作用。先來看看下面整個mapreduce的運行流程,首先是原始數據經過切片進入到map函數中,經過map函數的數據會在整個環形緩衝區裏邊進行第一次排序,接着map的輸出結果會根據key值(默認情況是這樣,另外可以自定義)進行Hash映射將數據量龐大的map輸出分割爲N份(N爲reduce數目)來實現數據的並行處理,這就是Partition階段,另外MapReduce框架中Partition的實現方式往往能夠決定數據的傾斜度,所以在處理數據前最好要對數據的分佈情況有所瞭解。

wKiom1Nub-iSfMjWAAV3-6bga5I401.jpg

接下來從MapReudce的源碼角度來研究一下Partition的實現原理:

   其Partition的實現主要有:HashPartitioner、BinaryPartitioner、KeyFieldBasedPartitioner、TotalOrderPartitioner這幾種,其中HashPartitioner是默認的。首先來看看HashPartitioner的核心實現:

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.mapreduce.lib.partition;
import org.apache.hadoop.mapreduce.Partitioner;
/** Partition keys by their {@link Object#hashCode()}. */
public class HashPartitioner<K, V> extends Partitioner<K, V> {
  /** Use {@link Object#hashCode()} to partition. */
  public int getPartition(K key, V value,
                          int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }
}

我們看到第25行,在這裏我們有看到了可愛的Hash取模映射方法,這樣做的原因大家看到這裏都應該已經瞭然於心了。另外,TotalOrderPartitioner、BinaryPartitioner等幾種Partitioner的實現都是基於Hash取模映射方法,只是他們爲了實現自己自定義的功能而添加了一些邏輯,例如其中的TotalOrderPartitioner可以實現全排序功能。其他幾個Partition的源代碼這裏就不貼了,有興趣的可以自己看看。


五、Hash算法的一致性

本部分爲本文最後一部分,之所以要介紹這一部分的內容主要是從Hash算法的完整性出發的,這部分的內容和海量數據的解決方案關係不大,主要是用於分佈式緩存設計方面。由於關於這部分的內容已經有一些大拿們做了很深入的研究並且講解地相當完美,小弟這裏就直接引用了。所以本部分引用sparkliang的blog。

consistent hashing算法早在1997年就在論文Consistent hashing and random trees中被提出,目前在cache系統中應用越來越廣泛;

1 基本場景

比如你有N個cache服務器(後面簡稱cache),那麼如何將一個對象object映射到N個cache上呢,你很可能會採用類似下面的通用方法計算object的hash值,然後均勻的映射到到N個cache;

hash(object)%N

一切都運行正常,再考慮如下的兩種情況;

1 一個cache服務器m down掉了(在實際應用中必須要考慮這種情況),這樣所有映射到cache m的對象都會失效,怎麼辦,需要把cache m從cache中移除,這時候cache是N-1臺,映射公式變成了hash(object)%(N-1);

2 由於訪問加重,需要添加cache,這時候cache是N+1臺,映射公式變成了hash(object)%(N+1);

1和2意味着什麼?這意味着突然之間幾乎所有的cache都失效了。對於服務器而言,這是一場災難,洪水般的訪問都會直接衝向後臺服務器;

再來考慮第三個問題,由於硬件能力越來越強,你可能想讓後面添加的節點多做點活,顯然上面的hash算法也做不到。

有什麼方法可以改變這個狀況呢,這就是consistent hashing...

2 hash 算法和單調性

Hash算法的一個衡量指標是單調性(Monotonicity),定義如下:

  單調性是指如果已經有一些內容通過哈希分派到了相應的緩衝中,又有新的緩衝加入到系統中。哈希的結果應能夠保證原有已分配的內容可以被映射到新的緩衝中去,而不會被映射到舊的緩衝集合中的其他緩衝區。

容易看到,上面的簡單hash算法hash(object)%N難以滿足單調性要求。

3 consistent hashing 算法的原理

consistent hashing是一種hash算法,簡單的說,在移除/添加一個cache時,它能夠儘可能小的改變已存在key映射關係,儘可能的滿足單調性的要求。

下面就來按照5個步驟簡單講講consistent hashing算法的基本原理。

3.1 環形hash 空間

考慮通常的hash算法都是將value映射到一個32爲的key值,也即是0~2^32-1次方的數值空間;我們可以將這個空間想象成一個首(0)尾(2^32-1)相接的圓環,如下面圖1所示的那樣。

circle space

圖1環形hash空間

3.2 把對象映射到hash 空間

接下來考慮4個對象object1~object4,通過hash函數計算出的hash值key在環上的分佈如圖2所示。

hash(object1) = key1;

… …

hash(object4) = key4;

object

圖2 4個對象的key值分佈

3.3 把cache 映射到hash 空間

Consistent hashing的基本思想就是將對象和cache都映射到同一個hash數值空間中,並且使用相同的hash算法。

假設當前有A,B和C共3臺cache,那麼其映射結果將如圖3所示,他們在hash空間中,以對應的hash值排列。

hash(cache A) = key A;

… …

hash(cache C) = key C;

cache

圖3 cache和對象的key值分佈


說到這裏,順便提一下cache的hash計算,一般的方法可以使用cache機器的IP地址或者機器名作爲hash輸入。

3.4 把對象映射到cache

現在cache和對象都已經通過同一個hash算法映射到hash數值空間中了,接下來要考慮的就是如何將對象映射到cache上面了。

在這個環形空間中,如果沿着順時針方向從對象的key值出發,直到遇見一個cache,那麼就將該對象存儲在這個cache上,因爲對象和cache的hash值是固定的,因此這個cache必然是唯一和確定的。這樣不就找到了對象和cache的映射方法了嗎?!

依然繼續上面的例子(參見圖3),那麼根據上面的方法,對象object1將被存儲到cache A上;object2和object3對應到cache C;object4對應到cache B;

3.5 考察cache 的變動

前面講過,通過hash然後求餘的方法帶來的最大問題就在於不能滿足單調性,當cache有所變動時,cache會失效,進而對後臺服務器造成巨大的衝擊,現在就來分析分析consistent hashing算法。

3.5.1 移除cache

考慮假設cache B掛掉了,根據上面講到的映射方法,這時受影響的將僅是那些沿cache B逆時針遍歷直到下一個cache(cache C)之間的對象,也即是本來映射到cache B上的那些對象。

因此這裏僅需要變動對象object4,將其重新映射到cache C上即可;參見圖4。

remove

圖4 Cache B被移除後的cache映射

3.5.2 添加cache

再考慮添加一臺新的cache D的情況,假設在這個環形hash空間中,cache D被映射在對象object2和object3之間。這時受影響的將僅是那些沿cache D逆時針遍歷直到下一個cache(cache B)之間的對象(它們是也本來映射到cache C上對象的一部分),將這些對象重新映射到cache D上即可。


因此這裏僅需要變動對象object2,將其重新映射到cache D上;參見圖5。

add

圖5 添加cache D後的映射關係

4 虛擬節點

考量Hash算法的另一個指標是平衡性(Balance),定義如下:

平衡性

  平衡性是指哈希的結果能夠儘可能分佈到所有的緩衝中去,這樣可以使得所有的緩衝空間都得到利用。

hash算法並不是保證絕對的平衡,如果cache較少的話,對象並不能被均勻的映射到cache上,比如在上面的例子中,僅部署cache A和cache C的情況下,在4個對象中,cache A僅存儲了object1,而cache C則存儲了object2、object3和object4;分佈是很不均衡的。

爲了解決這種情況,consistent hashing引入了“虛擬節點”的概念,它可以如下定義:

“虛擬節點”(virtual node)是實際節點在hash空間的複製品(replica),一實際個節點對應了若干個“虛擬節點”,這個對應個數也成爲“複製個數”,“虛擬節點”在hash空間中以hash值排列。

仍以僅部署cache A和cache C的情況爲例,在圖4中我們已經看到,cache分佈並不均勻。現在我們引入虛擬節點,並設置“複製個數”爲2,這就意味着一共會存在4個“虛擬節點”,cache A1, cache A2代表了cache A;cache C1, cache C2代表了cache C;假設一種比較理想的情況,參見圖6。

virtual nodes

圖6 引入“虛擬節點”後的映射關係


此時,對象到“虛擬節點”的映射關係爲:

objec1->cache A2;objec2->cache A1;objec3->cache C1;objec4->cache C2;

因此對象object1和object2都被映射到了cache A上,而object3和object4映射到了cache C上;平衡性有了很大提高。

引入“虛擬節點”後,映射關係就從{對象->節點}轉換到了{對象->虛擬節點}。查詢物體所在cache時的映射關係如圖7所示。

map

圖7 查詢對象所在cache


“虛擬節點”的hash計算可以採用對應節點的IP地址加數字後綴的方式。例如假設cache A的IP地址爲202.168.14.241。

引入“虛擬節點”前,計算cache A的hash值:

Hash(“202.168.14.241”);

引入“虛擬節點”後,計算“虛擬節”點cache A1和cache A2的hash值:

Hash(“202.168.14.241#1”);  // cache A1

Hash(“202.168.14.241#2”);  // cache A2



參考文獻:

http://blog.csdn.net/v_july_v/article/details/7382693

文章第五部分來自:http://blog.csdn.net/sparkliang/article/details/5279393


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章