摘要
我們希望伺服器能在請求流量的控制上有一定的自動控制能力;本文通過簡介令牌桶演算法和討論演算法的 redis 實現給出流量整形(traffic shaping)的示例,來介紹網路流量整形。
令牌桶演算法
令牌桶演算法(token bucket) 並不是網路流量整形中的奇技淫巧,而是非常常用的演算法,從百度百科上已經可以對它有一個概括的瞭解。對此演算法的深入讀者可自行查閱研究,這裡我通俗化的來解釋一下這個演算法。
在令牌桶演算法中,每一個訪客都擁有一個獨立的“令牌桶”,在這個“令牌桶”裡放了一些“令牌”,訪客每次來訪都會消耗“令牌桶”中的“令牌”,如果“令牌桶”空了,將會對訪客做特殊處理(如拒絕其繼續訪問以達到限流的目的)。
問題一:訪客來訪是一個持續的過程,如果最初的“令牌”數目固定,“令牌桶”中的令牌會慢慢被消耗殆盡,這樣正常的訪客也將無法訪問—-所以我們需要以一個恆定的速率來向“令牌桶”中新增一定數量的“令牌”, 這樣就可以讓訪客持續的訪問。
問題二:我們以一個恆定速率向“令牌桶”中新增“令牌”, 那麼如果訪客一直沒來訪他的“令牌桶”豈不會累積大量“令牌”麼?—-所以,我們設定“令牌桶”中“令牌”的最大數量,“令牌桶”滿了就不需要再去新增了。這解決了“令牌”累積的問題,也使它更像一個“桶”。
如此,“令牌桶演算法”中的重要的引數有:1. 給“令牌桶”新增“令牌”的速率(如果訪客以這個速率消耗令牌,將一直不會被限流); 2. “令牌桶”的容量(如果消耗令牌的速率大於新增令牌的速率,將消耗桶中的存貨,如果消耗速率過大,令牌會被消耗殆盡,訪客將被限流)。注意:一般情況下“令牌桶”最初的狀態是滿的。
Redis
作為優秀的記憶體資料庫,redis 可以幫助我們在應用層次快速響應。本文不過多贅述 redis 的優劣,你可以用 redis 做很多事情,在網路流量整形方面,它是很好的實現方案, 下面我們來解析這樣一個方案。
Show me the code
說明:這是一段 Python 程式碼,這段程式碼來自 GitHub 使用者 justinfay。為了使邏輯更清楚,我修改了程式碼的部分內容和註釋,以下是修改後的程式碼,我們用這段程式碼來看令牌桶演算法的 redis 實現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
import redis from redis import WatchError import time # 向令牌桶中新增令牌的速率 RATE = 0.1 # 令牌桶的最大容量 DEFAULT = 100 # redis key 的過期時間 TIMEOUT = 60 * 60 r = redis.Redis() def token_bucket(tokens, key): pipe = r.pipeline() while 1: try: pipe.watch('%s:available' % key) pipe.watch('%s:ts' % key) current_ts = time.time() # 獲取令牌桶中剩餘令牌 old_tokens = pipe.get('%s:available' % key) if old_tokens is None: current_tokens = DEFAULT else: old_ts = pipe.get('%s:ts' % key) # 通過時間戳計算這段時間內應該新增多少令牌,如果桶滿,令牌數取桶滿數。 current_tokens = float(old_tokens) + min( (current_ts - float(old_ts)) * RATE, DEFAULT - float(old_tokens) ) # 判斷剩餘令牌是否足夠 if 0 <= tokens <= current_tokens: current_tokens -= tokens consumes = True else: consumes = False # 以下動作為更新 redis 中key的值,並跳出迴圈返回結果。 pipe.multi() pipe.set('%s:available' % key, current_tokens) pipe.expire('%s:available' % key, TIMEOUT) pipe.set('%s:ts' % key, current_ts) pipe.expire('%s:ts' % key, TIMEOUT) pipe.execute() break except WatchError: continue finally: pipe.reset() return consumes if __name__ == "__main__": tokens = 5 key = '192.168.1.1' if token_bucket(tokens, key): print 'haz tokens' else: print 'cant haz tokens' |
需要說的幾點
1. 這段程式碼在網路流量整形策略中起到什麼作用?
對訪客的一次訪問,我們通過以上程式碼可以來判斷此次訪問是否超過了我們的限制,通過返回的判斷結果,我們將對此次訪問選擇正確的處理策略,比如你可以拒絕消耗完令牌的訪客進行訪問,從而控制他的訪問速率,從而達到網路流量整形的目的。
2. redis 在其中如何工作?
對於每個獨立的訪客,redis 會為他建立兩個 key,一個 key 儲存了剩餘令牌的數量,另外一個 key 儲存了最近一次訪問的時間戳。其中,最近一次訪問時間戳在新訪問到來時候用於計算時間間隔,從而計算在此時間間隔內應該向令牌桶中新增多少令牌,進而獲得當前令牌桶的剩餘令牌數。
3. redis pipe 起到什麼作用?
我們看到程式碼中 while 迴圈,執行了 redis pipe 中的 watch 動作,這是對 redis 事務 的使用。 這使這裡的演算法能處理併發的來訪。在 redis 中,事務執行是對 redis key 的一個加鎖的操作,一個事務沒有執行完,別的動作將無法操作這個 key ,程式碼中迴圈執行 watch 動作,就是去檢查當前 key 是否有未執行完畢的事務,只有所有事務都執行的時候才可能進入執行體,完成令牌判斷或者消耗。 —— 這樣避免了併發的訪問在 set redis key 時候的混亂。
4. 如何調參
程式碼中 RATE 和 DEFAULT 為主要引數,分別代表每秒鐘消耗令牌的速率,和令牌桶的容量。通過調整這兩個引數來控制你想要的訪問速率。
總結
這是一個實用的方式來完成網路流量整形,可以有效控制一些爆發式的流量訪問,使訪問更加平滑容易控制。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式