做者簡介:python
張炎潑(XP)web
白山雲科技合夥人兼研發副總裁,綽號XP。
張炎潑先生於2016年加入白山雲科技,主要負責對象存儲研發、數據跨機房分佈和修復問題解決等工做。以實現100PB級數據存儲爲目標,其帶領團隊完成全網分佈存儲系統的設計、實現與部署工做,將數據「冷」「熱」分離,使冷數據成本壓縮至1.2倍冗餘度。算法
張炎潑先生2006年至2015年,曾就任於新浪,負責Cross-IDC PB級雲存儲服務的架構設計、協做流程制定、代碼規範和實施標準制定及大部分功能實現等工做,支持新浪微博、微盤、視頻、SAE、音樂、軟件下載等新浪內部存儲等業務;2015年至2016年,於美團擔任高級技術專家,設計了跨機房的百PB對象存儲解決方案:設計和實現高併發和高可靠的多副本複製策略,優化Erasure Code下降90%IO開銷。數據結構
軟件開發中,一個hash表至關於把n個key隨機放入到b個bucket中,以實現n個數據在b個單位空間的存儲。
咱們發現hash表中存在一些有趣現象:架構
下圖直觀的展現了當n=b=20時,hash表裏每一個bucket中key的數量(按照key的數量對bucket作排序):併發
每每咱們對hash表的第一感受是:若是將key隨機放入全部的bucket,bucket中key的數量較爲均勻,每一個bucket裏key數量的指望是1。app
而實際上,bucket裏key的分佈在n較小時很是不均勻;當n增大時,纔會逐漸趨於平均。函數
下表表示當b不變,n增大時,n/b的值如何影響3類bucket的數量佔比(衝突率即含有多於1個key的bucket佔比):
高併發
更直觀一點,咱們用下圖來展現空bucket率和衝突率隨n/b值的變化趨勢:優化
上面幾組數字是當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的值,也受b值的影響,後面會提到。
未使用統計中經常使用的均方差法去描述key分佈的不均勻程度,是由於軟件開發過程當中,更多時候要考慮最壞狀況下所需準備的內存等資源。
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表特性小貼士:
hash表經過空間浪費換取了訪問速度的提高;磁盤的空間浪費是沒法忍受的,但對內存的少量浪費是可接受的;
hash表只適合隨機訪問快的存儲介質。硬盤上的數據存儲更多使用btree或其餘有序的數據結構。
另一種hash表的實現,專門用來存儲比較多的key,當 n/b>1時,線性探測失效(沒有足夠的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 足夠大時,空bucket率趨近於0,且每一個bucket中key的數量趨於平均。每一個bucket中key數量的指望是:
**avg=n/b**
定義一個bucket平均key的數量是100%:bucket中key的數量恰好是n/b,下圖分別模擬了 b=20,n/b分別爲 十、100、1000時,bucket中key的數量分佈。
能夠看出,當 n/b 增大時,bucket中key數量的最大值與最小值差距在逐漸縮小。下表列出了隨b和n/b增大,key分佈的均勻程度的變化:
結論:
上述大部分結果來自於程序模擬,如今咱們來解決從數學上如何計算這些數值。
空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數量:
相似的,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 sys
import math
import time
import hashlib
def 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 * n
def 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) / mmax
if 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