張炎潑先生於2016年加入白山雲科技,主要負責Object Storage Service研發、資料跨機房分布和修複問題解決等工作。以實現100PB級資料存放區為目標,其帶領團隊完成全網分布儲存系統的設計、實現與部署工作,將資料“冷”“熱”分離,使冷資料成本壓縮至1.2倍冗餘度。
張炎潑先生2006年至2015年,曾就職於新浪,負責Cross-IDC PB級雲端儲存體服務的架構設計、協作流程制定、代碼規範和實施標準制定及大部分功能實現等工作,支援新浪微博、微盤、視頻、SAE、音樂、軟體下載等新浪內部儲存等業務;2015年至2016年,於美團擔任進階技術專家,設計了跨機房的百PBObject Storage Service解決方案:設計和實現高並發和高可靠的多副本複製策略,最佳化Erasure Code降低90%IO開銷。
軟體開發中,一個hash表相當於把n個key隨機放入到b個bucket中,以實現n個資料在b個單位空間的儲存。
我們發現hash表中存在一些有趣現象:
hash表中key的分布規律
當hash表中key和bucket數量一樣時(n/b=1):
直觀的展示了當n=b=20時,hash表裡每個bucket中key的數量(按照key的數量對bucket做排序):
往往我們對hash表的第一感覺是:如果將key隨機放入所有的bucket,bucket中key的數量較為均勻,每個bucket裡key數量的期望是1。
而實際上,bucket裡key的分布在n較小時非常不均勻;當n增大時,才會逐漸趨於平均。
key的數量對3類bucket數量的影響
下表表示當b不變,n增大時,n/b的值如何影響3類bucket的數量佔比(衝突率即含有多於1個key的bucket佔比):
更直觀一點,我們用來展示空bucket率和衝突率隨n/b值的變化趨勢:
key數量對bucket均勻程度的影響
上面幾組數字是當n/b較小時有意義的參考值,但隨n/b逐漸增大,空bucket與1個key的bucket數量幾乎為0,絕大多數bucket含有多個key。
當n/b超過1(1個bucket允許儲存多個key), 我們主要觀察的對象就轉變成bucket裡key數量的分布規律。
下表表示當n/b較大,每個bucket裡key的數量趨於均勻時,不均勻的程度是多少。
為了描述這種不均勻的程度,我們使用bucket中key數量的最大值和最小值之間的比例((most-fewest)/most)來表示。
下表列出了b=100時,隨n增大,key的分布情況。
可以看出,隨著bucket裡key平均數量的增加,其分布的不均勻程度也逐漸降低。
和空bucket或1個key的bucket的佔比不同n/b,均勻程度不僅取決於n/b的值,也受b值的影響,後面會提到。未使用統計中常用的均方差法去描述key分布的不均勻程度,是因為軟體開發過程中,更多時候要考慮最壞情況下所需準備的記憶體等資源。
Load Factor:n/b<0.75
hash表中常用一個概念 load factor α=n/b,來描述hash表的特徵。
通常,基於記憶體儲存的hash表,它的 n/b ≤0.75。這樣設定,既可節省空間的,也可以保持key的衝突率相對較低,低衝突率意味著低頻率的hash重定位,hash表的插入會更快。
線性探測是一個經常被使用的解決插入時hash衝突的演算法,它在1個bucket出現衝突時,按照逐步增加的步長順序向後查看這個bucket後面的bucket,直到找到1個空bucket。因此它對hash的衝突非常敏感。
在n/b=0.75 這個情境中,如果不使用線性探測(譬如使用bucket內的鏈表來儲存多個key),大約有47% 的bucket是空的;如果使用線性探測,這47%的bucket中,大約一半的bucket會被線性探測填充。
在很多記憶體hash表的實現中,選擇n/b=<=0.75作為hash表的容量上限,不僅是考慮到衝突率隨n/b增大而增大,更重要的是線性探測的效率會隨著n/b的增大而迅速降低。
hash表特性小貼士:
Load Factor:n/b>1
另外一種hash表的實現,專門用來儲存比較多的key,當 n/b>1n/b1.0時,線性探測失效(沒有足夠的bucket儲存每個key)。這時1個bucket裡不僅儲存1個key,一般在一個bucket內用chaining,將所有落在這個bucket的key用鏈表串連起來,來解決衝突時多個key的儲存。
鏈表只在n/b不是很大時適用。因為鏈表的尋找需要O(n)的時間開銷,對於非常大的n/b,有時會用tree替代鏈表來管理bucket內的key。
n/b值較大的使用情境之一是:將一個網站的使用者隨機分配到多個不同的web-server上,這時每個web-server可以服務多個使用者。多數情況下,我們都希望這種分配能儘可能均勻,從而有效利用每個web-server資源。
這就要求我們關注hash的均勻程度。因此,接下來要討論的是,假定hash函數完全隨機的,均勻程度根據n和b如何變化。
n/b 越大,key的分布越均勻
當 n/b 足夠大時,空bucket率趨近於0,且每個bucket中key的數量趨於平均。每個bucket中key數量的期望是:
avg=n/b
定義一個bucket平均key的數量是100%:bucket中key的數量剛好是n/b,分別類比了 b=20,n/b分別為 10、100、1000時,bucket中key的數量分布。
可以看出,當 n/b 增大時,bucket中key數量的最大值與最小值差距在逐漸縮小。下表列出了隨b和n/b增大,key分布的均勻程度的變化:
結論:
計算
上述大部分結果來自於程式類比,現在我們來解決從數學上如何計算這些數值。
每類bucket的數量
空bucket數量
對於1個key, 它不在某個特定的bucket的機率是 (b−1)/b
所有key都不在某個特定的bucket的機率是( (b−1)/b)n
已知:
空bucket率是:
空bucket數量為:
有1個key的bucket數量
n個key中,每個key有1/b的機率落到某個特定的bucket裡,其他key以1-(1/b)的機率不落在這個bucket裡,因此,對某個特定的bucket,剛好有1個key的機率是:
剛好有1個key的bucket數量為:
多個key的bucket
剩下即為含多個key的bucket數量:
key在bucket中分布的均勻程度
類似的,1個bucket中剛好有i個key的機率是:n個key中任選i個,並都以1/b的機率落在這個bucket裡,其他n-i個key都以1-1/b的機率不落在這個bucket裡,即:
這就是著名的二項式分布。
我們可通過二項式分布估計bucket中key數量的最大值與最小值。
通過常態分佈來近似
當 n, b 都很大時,二項式分布可以用常態分佈來近似估計key分布的均勻性:
p=1/b,1個bucket中剛好有i個key的機率為:
1個bucket中key數量不多於x的機率是:
所以,所有不多於x個key的bucket數量是:
bucket中key數量的最小值,可以這樣估算: 如果不多於x個key的bucket數量是1,那麼這唯一1個bucket就是最少key的bucket。我們只要找到1個最小的x,讓包含不多於x個key的bucket總數為1, 這個x就是bucket中key數量的最小值。
計算key數量的最小值x
一個bucket裡包含不多於x個key的機率是:
Φ(x) 是常態分佈的累計分布函數,當x-μ趨近於0時,可以使用以下方式來近似:
這個函數的計算較難,但只是要找到x,我們可以在[0~μ]的範圍內逆向遍曆x,以找到一個x 使得包含不多於x個key的bucket期望數量是1。
x可以認為這個x就是bucket裡key數量的最小值,而這個hash表中,不均勻的程度可以用key數量最大值與最小值的差異來描述: 因為常態分佈是對稱的,所以key數量的最大值可以用 μ + (μ-x) 來表示。最終,bucket中key數量最大值與最小值的比例就是:
(μ是均值n/b)
程式類比
以下python指令碼類比了key在bucket中分布的情況,同時可以作為對比,驗證上述計算結果。
import sysimport mathimport timeimport hashlibdef normal_pdf(x, mu, sigma): x = float(x) mu = float(mu) m = 1.0 / math.sqrt( 2 * math.pi ) / sigma n = math.exp(-(x-mu)**2 / (2*sigma*sigma))return m * ndef normal_cdf(x, mu, sigma): # integral(-oo,x) x = float(x) mu = float(mu) sigma = float(sigma) # to standard form x = (x - mu) / sigma s = x v = x for i in range(1, 100): v = v * x * x / (2*i+1) s += v return 0.5 + s/(2*math.pi)**0.5 * math.e ** (-x*x/2)def difference(nbucket, nkey): nbucket, nkey= int(nbucket), int(nkey) # binomial distribution approximation by normal distribution # find the bucket with minimal keys. # # the probability that a bucket has exactly i keys is: # # probability density function # normal_pdf(i, mu, sigma) # # the probability that a bucket has 0 ~ i keys is: # # cumulative distribution function # normal_cdf(i, mu, sigma) # # if the probability that a bucket has 0 ~ i keys is greater than 1/nbucket, we # say there will be a bucket in hash table has: # (i_0*p_0 + i_1*p_1 + ...)/(p_0 + p_1 + ..) keys. p = 1.0 / nbucket mu = nkey * p sigma = math.sqrt(nkey * p * (1-p)) target = 1.0 / nbucket minimal = mu while True: xx = normal_cdf(minimal, mu, sigma) if abs(xx-target) < target/10: break minimal -= 1 return minimal, (mu-minimal) * 2 / (mu + (mu - minimal))def difference_simulation(nbucket, nkey): t = str(time.time()) nbucket, nkey= int(nbucket), int(nkey) buckets = [0] * nbucket for i in range(nkey): hsh = hashlib.sha1(t + str(i)).digest() buckets[hash(hsh) % nbucket] += 1 buckets.sort() nmin, mmax = buckets[0], buckets[-1] return nmin, float(mmax - nmin) / mmaxif __name__ == "__main__": nbucket, nkey= sys.argv[1:] minimal, rate = difference(nbucket, nkey) print 'by normal distribution:' print ' min_bucket:', minimal print ' difference:', rate minimal, rate = difference_simulation(nbucket, nkey) print 'by simulation:' print ' min_bucket:', minimal print ' difference:', rate