先描述下基本場景:
html
系統API接口日均調用次數預計1億次,提供5臺服務器。
python須要作兩種層面的控制:算法
> 單IP、單應用每小時調用次數不超過10000次數據庫
> 單應用、單用戶、單接口每小時調用次數不超過1000次數組
要求每次對頻控系統的調用的平均響應時間在1ms內。緩存
此外,應用開發者和開放平臺所屬公司關心調用次數統計數據,如當天某應用全部接口被調用總次數、當天某應用某接口被調用次數、當天某應用用戶使用數等。服務器
根據上面,咱們能夠直接獲得系統響應度要求和計算獲得系統吞吐量要求,計算公式以下:網絡
頻控系統吞吐量(系統每秒可以處理的請求數) = 80% * 1億 / (24小時 * 60分鐘 * 60秒 * 40% * 5) = 4630tps
80%、40%是指一天中有80%的請求發生在40%的時間內,是粗略的估算值。5是服務器數量。因此獲得吞吐量要求爲4630tps。前期設計系統時必須參考這些性能指標,後期壓測系統時必須根據這些指標設計測試計劃。數據結構
總結下系統設計須要達成的目標:併發
請求的響應足夠快
能支撐4630tps
佔用的CPU、內存等硬件資源不能太誇張(隱性設計目標)
A、數據結構設計初步嘗試
計數是典型的key-value數據結構。
可能想到的最簡單最天然的方式是下面這樣的:
K(app_id, ip) => V(count, startTime, lastTime) K(app_id, uid, interface_id) => V(count, startTime, lastTime)
startTime記錄的是第一次調用的發生時刻,lastTime記錄的是最近一次調用的發生時刻,它們用來判斷是否應該重置計數值count和是否該拒絕調用。startTime和lastTime都只精確到秒級。
爲了節省內存,有必要對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獲得16位定長字符串做爲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號的小時數」計算16位MD5值,而後獲得所屬的CountingBloomFilter(若是沒有就建立),而後每次先檢查是否已達到最大插入次數,若是是則直接返回,若是不是才插入。可是咱們別忘了一點:CountingBloomFilter支持最大重複插入次數爲15,遠小於這裏的1000次和10000次。因此很遺憾,CountingBloomFilter不適合這種方案。但這是一個很好的起點,Value的數據結構雖然不能用CountingBloomFilter,但或許能夠用其餘的優化數據結構,能夠參考:
http://blog.csdn.net/hguisu/article/details/7856239
還有一篇文章標題大概是用1k內存來排序億級數組,找不到了。另外頻率控制數據結構模型還能夠參考「令牌桶算法」,這裏再也不深刻,詳情看:http://en.wikipedia.org/wiki/Token_bucket
B、進一步的數據存儲和數據結構設計
考慮到性能要求,確定須要用到Cache,這裏打算選用Redis作Cache。再根據上面的估算,數據項總共有600億,因此不可能把全部數據項所有放到Redis Cache中(假設每一個Cache項佔100個字節,估算下須要多少內存,嘿嘿)。
因此我這裏採用冷熱數據分離方案。有這麼三類數據:
冷數據存放在MySQL數據庫,按照app_id、uid進行水平Shard
不冷不熱數據採用Redis Hash結構存儲(這種內存結構更加緊湊)
熱數據放在另外的Redis庫中,而且「展開式」存儲以改善訪問性能
先簡單說下不冷不熱數據的Redis Hash結構,Key是app_id,Field是MD5(ip, interface_id, uid, 如今距離1970年1月1號的小時數),Value包括兩個計數值,即單IP、單應用已調用次數和單應用、單接口、單用戶已調用次數。這樣設計至關於把原本應該存成兩項的數據合併到了一個緩存數據項中。
熱數據的所謂「展開式」結構是指將上面兩個維度的計數分開,即存成相似下面這兩種結構:
K:MD5(app_id, ip, 如今距離1970年1月1號的小時數),V:count K:MD5(app_id, interface_id, uid, 如今距離1970年1月1號的小時數),V:count
這種方案有一個小缺陷,那就是小時區間的劃分不太符合用戶的直覺。用戶的直覺是認爲真正第一次調用的時刻纔是計時起點。爲了知足這個需求,能夠迴歸到A節的數據結構上,變成以下:
K:MD5(app_id, ip),V:count、startTime、endTime的優化數據結構 K:MD5(app_id, interface_id, uid),V:count、startTime、endTime的優化數據結構
另外,我後面仔細思考過,不冷不熱數據採用Redis Hash和熱數據展開存儲,是否真的會像我推斷的那樣展開存儲更快?
答案是不必定。由於Redis Hash存儲的話,只須要一次Redis訪問外加一些解析計算開銷,而展開存儲須要兩次Redis訪問。應該不是絕對的,需實際進行測量後才能下結論。
Redis Cache失效時間:
若是採用的數據結構是:
K:MD5(app_id, ip, 如今距離1970年1月1號的小時數),V:count K:MD5(app_id, interface_id, uid, 如今距離1970年1月1號的小時數),V:count
設置緩存失效時間爲1小時。
若是採用的數據結構是:
K:MD5(app_id, ip),V:count、startTime、endTime的優化數據結構 K:MD5(app_id, interface_id, uid),V:count、startTime、endTime的優化數據結構
那麼建議設置緩存永不失效。
冷熱數據遷移過程:
數據的冷熱一直在發生着改變,因此冷熱數據之間須要進行遷移。包括下面四種:
熱數據中符合不冷不熱數據標準的數據移動到不冷不熱數據緩存
將不冷不熱數據中符合熱數據標準的數據遷移到熱數據緩存
將不冷不熱數據中符合冷數據標準的數據遷移到MySQL數據庫
將MySQL數據庫中符合不冷不熱數據標準的數據遷移到不冷不熱數據緩存
判斷冷熱的標準能夠基於下面一種或者幾種:
天天計算一次的歷史平均每小時調用次數:單應用、單IP和單應用、單接口、單用戶
根據應用的用戶量級來區分冷熱,也就是區分熱應用、不熱不冷應用和冷應用。好比某個用戶量級超過20萬,則該應用下的接口調用數據存爲熱數據。而後用戶量級在5萬到20萬之間的應用的接口調用數據存爲不熱不冷數據。至於用戶量級低於5萬的應用的接口調用數據就存爲冷數據了。這種是我比較推薦的方案,由於簡單科學,而且只須要控制到應用粒度
基於最近50次調用的平均時間間隔來判斷數據冷熱,也就是對於每個數據項還要存儲一個它最近50次調用的平均時間間隔(能夠借鑑下文提到的滑動窗口算法)
好比我採用第二種劃分標準。天天跑一次後臺腳本將熱應用、不熱不冷應用和冷應用區分出來,而後緩存到Redis Cache中(應用數不會太多,百萬級就了不得了,因此甚至能夠存爲Local Cache),相似這樣:
app_id => 1或者2或者3 # 1表示是熱應用,2表示是不熱不冷應用,3表示是冷應用
而後API接口調用時,先根據app_id去這個Cache裏找出應用的熱度級別,而後就知道該訪問前面哪種存儲了。固然可能出現訪問未命中的狀況,在那種狀況須要依次訪問熱緩存數據 => 不冷不熱緩存數據 => 冷數據來肯定是由於數據存儲中尚未相應的Key或者是尚未及時遷移數據到正確的存儲,但那種狀況畢竟是不多的。
C、借債機制
好比限制某應用某個接口單用戶每小時調用次數不能超過1000次,那麼是否嚴格按照1000次的標準來哪?
答案是否。由於用戶調用接口的頻率可能並不均勻,好比像下面這樣:
第1小時 | 第2小時 | 第3小時 |
1200 | 700 | 950 |
若是嚴格按照1000次標準來的話,那麼第1小時內就有200次調用被拒絕。而若是採用簡單的借債機制,即如容許超出1000次往上300次,那麼在第一小時內的1200次調用都能成功,但欠下200的債,須要在第2小時償還,第2小時的調用額度就變成800了,以此類推
D、RPC框架選擇
頻率控制系統是做爲一個獨立的系統供其餘系統調用,因此須要一個高性能的RPC框架來保證能知足併發要求。選擇Thrift,而且採用TNonblockingServer非阻塞模式。
E、滑動窗口算法
我最先聽到滑動窗口算法是在大學的計算機網絡課程上,它是TCP協議用來保證數據幀順序和限制流量的一個算法。Storm中貌似也用到了滑動窗口算法來限制調用頻率。因此或許能夠遷移應用到這個系統的設計中。
滑動窗口算法能夠參考:http://blog.csdn.net/thisispan/article/details/7545785
F、服務降級
雖然調用頻率控制系統是開放平臺中的一個重要系統,但咱們也不但願在調用頻率控制系統不可用的狀況下正常的數據API調用都不可用。因此頻控系統應該增長一個服務開關,以實現服務降級。
2014年11月04日22:59更新:另外API接口調用頻率控制比較適合用Storm這種實時事件處理框架來處理。
2015年03月10日22:14更新:
數據結構能夠用位運算優化,用一個長整數同時包含count、startTime、endTime信息。
此外若是用Redis等帶緩存自動失效的系統能夠省略「如今距離1970年1月1日的小時數」字段,每次調用都只須要GET-COMPARE-SET。