先描述下基本場景: 算法
系統API接口日均調用次數預計1億次,提供5臺服務器。 數據庫
須要作兩種層面的控制: 緩存
> 單IP、單應用每小時調用次數不超過10000次 服務器
> 單應用、單用戶、單接口每小時調用次數不超過1000次 數據結構
要求每次對頻控系統的調用的響應時間在20ms內。 app
此外,應用開發者和開放平臺所屬公司關心調用次數統計數據,如當天某應用全部接口被調用總次數、當天某應用某接口被調用次數、當天某應用用戶使用數等。性能
根據上面,咱們能夠直接獲得系統響應度要求和計算獲得系統吞吐量要求,計算公式以下: 測試
1 優化 2 ui |
頻控系統吞吐量(系統每秒可以處理的請求數) = 80% * 1億 / (24小時 * 60分鐘 * 60秒 * 40% * 5) = 4630tps |
80%、40%是指一天中有80%的請求發生在40%的時間內,是粗略的估算值。5是服務器數量。因此獲得吞吐量要求爲4630tps。前期設計系統時必須參考這些性能指標,後期壓測系統時必須根據這些指標設計測試計劃。
總結下系統設計須要達成的目標:
請求的響應足夠快
能支撐4630tps
佔用的CPU、內存等硬件資源不能太誇張(隱性設計目標)
A、數據結構設計
計數是典型的key-value數據結構。
可能想到的最簡單最天然的方式是下面這樣的:
1 2 |
K(app_id, ip) => V(count, startTime, lastTime) K(app_id, uid, interface_id) => V(count, startTime, lastTime) |
startTime記錄的是第一次調用的發生時刻,lastTime記錄的是最近一次調用的發生時刻,它們用來判斷是否應該重置計數值count和是否該拒絕調用。
爲了節省內存,有必要對key和value作特殊設計,最次的方案固然是直接拼接上面各個字段。但這是很是不合理的,咱們來簡單估算下:
假設應用有10,000個,平均每一個應用的用戶數爲100,000,接口數爲50,獨立訪問IP地址爲1,000,000,那麼數據項總共爲:
10,000 * 1,000,000 + 10,000 * 100,000 * 50 = 600億
那麼若是每一個數據項節省1個字節,可以節省的總數據存儲是600G,這是很是可觀的。
對於Key,一種更優方案是先拼接Key的字符串,而後MD5獲得32位定長字符串做爲Key,Key定長的話或許對性能提高也會有必定幫助。
對於Value,count、startTime、lastTime信息不能丟失,那麼或許能夠考慮下面兩種優化方案:
無損壓縮Value字符串,好比使用Snappy字符串壓縮算法,但壓縮和解壓縮會帶來額外的CPU計算消耗,須要權衡
計數不須要太精確,因此能夠犧牲必定精確度換取空間節省。或許咱們能夠利用 CountingBloomFilter?Key須要從新設計爲:MD5(app_id, interface_id, 如今距離1970年1月1號的小時數),Value就是CountingBloomFilter數據結構了,每一個調用先根據app_id、 interface_id、如今距離1970年1月1號的小時數計算32位MD5值,而後獲得所屬的CountingBloomFilter(若是沒有就 建立),而後每次先檢查是否已達到最大插入次數,若是是則直接返回,若是不是才插入。但 是咱們別忘了一點:CountingBloomFilter支持最大重複插入次數爲15,遠小於這裏的1000次和10000次。因此很殘酷,CountingBloomFilter不適合這種方案。但這是一個很好的起點,Value的數據結構雖然不能用 CountingBloomFilter,但或許能夠用其餘的優化數據結構,請看:http://blog.csdn.net/hguisu/article/details/7856239。
另外頻率控制通常能夠採用「令牌桶算法」,這裏再也不深刻,能夠參考:
http://en.wikipedia.org/wiki/Token_bucket
B、數據存儲設計
考慮到性能要求,確定須要用到Cache,這裏打算選用Redis。再根據上面的估算,數據項總共有600億,因此不可能把全部數據項所有放到Redis Cache中(假設每一個Cache項佔100個字節,估算下須要多少內存。
因此我這裏採用冷熱數據分離方案。有這麼三類數據:
冷數據存放在MySQL數據庫,按照app_id、uid進行水平Shard
不冷不熱數據採用Hash結構壓縮存儲在Redis,具體結構下面會提到
熱數據放在另外的Redis庫中,而且「展開式」存儲以改善訪問性能
熱數據的所謂「展開式」結構是指將上面兩個維度的計數分開,即存成相似下面這兩種結構:
1 2 |
K取MD5(app_id, ip, 如今距離1970年1月1號的小時數),V取一個長整型值表示計數 K取MD5(app_id, interface_id, uid, 如今距離1970年1月1號的小時數),V取一個長整型值表示計數 |
Redis Cache失效時間:
全部Redis Cache數據的失效時間設置爲1小時到1小時1分鐘之間的某個隨機值,這樣能某種程度上避免緩存集體失效引發的「雪崩」。
冷熱數據遷移過程:
數據的冷熱一直在發生着改變,因此冷熱數據之間須要進行遷移。
第一種方案是由後臺進程按期將
熱數據中符合冷數據標準的數據移動到不冷不熱數據緩存
將不冷不熱數據中符合熱數據標準的數據遷移到熱數據緩存
將不冷不熱數據中符合冷數據標準的數據遷移到MySQL數據庫
將MySQL數據庫中符合不冷不熱數據標準的數據遷移到不冷不熱數據緩存
判斷冷熱的標準是基於天天計算一次的歷史平均每小時調用次數。
第二種方案是在調用時主動進行遷移,基於最近50次調用的平均時間間隔來判斷(也就是對於每個數據項還要存儲一個它最近50次調用的平均時間間隔),遷移過程同第一種。