文|孫珩珂
上海交通大學
本文1987字 閱讀 10 分鐘
01 最佳化背景
此前 Dragonfly 的 P2P 下載採用靜態限流策略,相關配置項在 dfget.yaml
配置檔案中:
# 下載服務選項。
download:
# 總下載限速。
totalRateLimit: 1024Mi
# 單個任務下載限速。 perPeerRateLimit: 512Mi
其中 perPeerRateLimit
為單個任務設定流量上限, totalRateLimit
為單個節點的所有任務設定流量上限。
靜態限流策略的理想情況是: perPeerRateLimit
設定為20M , totalRateLimit
設定為 100M ,且該節點目前執行了 5 個或更多的 P2P 下載任務,這種情況下可以確保所有任務總頻寬不會超過 100M ,且頻寬會被有效利用。
這種限流策略的缺點是:若perPeerRateLimit
設定為 20M , totalRateLimit
設定為 100M ,並且當前該節點只執行了一個下載任務,那麼該任務的最大下載速度為 20M ,和最大頻寬 100M 相比,浪費了 80% 的頻寬。
因此,為了最大限度地利用頻寬,需要使用動態限流來確保任務數量少時能能充分利用總頻寬,而任務數量多時也能公平分配頻寬。最終,我們設計出一套根據上下文進行動態限流的演算法,其中上下文指各任務在過去一秒內使用的頻寬,此外,演算法還考慮到了任務數量、任務剩餘大小、任務保底頻寬等因素,效能相比原來的靜態限流演算法有顯著提升。
02 相關程式碼分析
perPeerRateLimit
配置項最終賦值給 peerTaskConductor
的pt.limiter
,由 peerTaskConductor
的 DownloadPiece()
函式里進行限速,pt.waitLimit()
進行實際限流工作,底層呼叫 Go 自帶的限流函式 WaitN()
。
TotalRateLimit
配置項則在建立 Daemon
時被賦值給 pieceManager
的pm.limiter
,在 pieceManager
的 DownloadPiece()
和 processPieceFromSource()
函式中用到的 pm.limiter
,而這兩個函式都會由 peerTaskConductor
呼叫,也就是說 P2P 下載會先進行總限速,之後再進行每個任務單獨限速。
根據以上分析,Dragonfly 進行任務限速的邏輯為,每個peer task(peerTaskConductor
)會有單獨的限速 perPeerRateLimit
,同時 pieceManager
會有 TotalRateLimit
的總限速,以此達到單任務單獨限流,同時限制所有任務總頻寬的效果。
### 03 最佳化方案
為了解決此前靜態限流演算法總頻寬利用率不佳的缺點,需要將其改進為動態限流演算法,即總頻寬限速仍恆定,但每個任務的單獨頻寬限速需要根據上下文適度、定期調整,已達到最大化利用總頻寬、同時相對公平分配頻寬的目的。
在經過數個改版後,最終我們確定了根據上下文進行限流的 sampling traffic shaper 動態限流演算法。具體方案為,每個任務的單任務限流交由 TrafficShaper
組建進行統一管理, TrafficShaper
維護當前正在執行的所有任務,並且定期(每秒)更新這些任務的頻寬。
具體來說,上下文指每個任務在上一秒使用的頻寬、每個任務的剩餘大小、任務數量、任務保底頻寬(不能低於 pieceSize
)等因素, TrafficShaper
會根據這些上下文公平地、效率最大化地為每個任務分配其下一秒的頻寬(具體分配方案詳見下一小節),實現動態限流的效果。
04 最佳化實現
定義 TrafficShaper
介面如下:
// TrafficShaper allocates bandwidth for running tasks dynamically
type TrafficShaper interface {
// Start starts the TrafficShaper
Start()
// Stop stops the TrafficShaper
Stop()
// AddTask starts managing the new task
AddTask(taskID string, ptc *peerTaskConductor)
// RemoveTask removes completed task
RemoveTask(taskID string)
// Record records task's used bandwidth
Record(taskID string, n int)
// GetBandwidth gets the total download bandwidth in the past second
GetBandwidth() int64
}
該介面有兩種實現,第一種是 samplingTrafficShaper
即基於上下文的 traffic shaper ,第二種是 plainTrafficShaper
只記錄頻寬使用情況,除此之外不做任何動態限流工作,用於和 samplingTrafficShaper
對比效能提升。
同時,將相關配置項修改為如下內容:
# 下載服務選項。
download:
# 總下載限速。
totalRateLimit: 1024Mi
# 單個任務下載限速。
perPeerRateLimit: 512Mi
# traffic shaper型別,有sampling和plain兩種可選 trafficShaperType: sampling
Traffic shaper 的具體執行邏輯為,由peerTaskManager
維護trafficShaper
,在建立peerTaskManager
時,根據配置初始化trafficShaper
,並且呼叫Start()
函式,啟動trafficShaper
,具體來說,新建time.NewTicker
,跨度為 1 秒,也即每秒trafficShaper
都會呼叫updateLimit()
函式以動態更新所有任務的頻寬限流。
updateLimit()
函式會遍歷所有執行中的任務,得出每個任務上一秒消耗的頻寬以及所有任務消耗的總頻寬,隨後根據任務上一秒使用的頻寬、任務剩餘大小等因素,按比例分配頻寬,具體來說首先根據上一秒該任務使用頻寬以及該任務剩餘大小的最大值確定下一秒該任務頻寬,接著所有任務頻寬根據總頻寬按比例縮放,得到下一秒的真實頻寬;同時需要確保每個任務的頻寬不低於該任務的 pieceSize
,以免出現持續飢餓狀態。
在 peerTaskManager
的 getOrCreatePeerTaskConductor()
函式中,若新建任務,需要頻寬,那麼呼叫 AddTask()
更新所有任務的頻寬,即按照已有任務的平均任務分配頻寬,然後再根據總頻寬上限將所有任務的頻寬等比例進行縮放;根據平均頻寬分配新任務頻寬的優勢為,避免了已經有一個任務佔滿了所有頻寬,有新任務進來時,頻寬會被壓縮到很小 **的情況;同時,不是平均分配頻寬,而是按需等比例分配,可以確保頻寬需求量大的任務仍然頻寬最多。在 peerTaskManager
的 PeerTaskDone()
函式中,任務完成,不再佔用頻寬,呼叫 RemoveTask()
按比例擴大所有任務的頻寬。
最後, peerTaskManager
停止時,呼叫 Stop
函式,停止執行 traffic shaper 。
05 最佳化結果
測試 traffic shaper 相比原有的靜態限流策略在單個任務、多個任務併發、多個任務交錯等多種情況下的效能提升,測試結果如下:
注:若不特殊註明,單任務限流為4KB/s,總限流為10KB/s
可以看到, traffic shaper 在單任務、多工不相交、單任務低頻寬等情況下相比靜態限流策略效能提升明顯,為 24%~59% 。在多個任務併發、多個任務交錯等情況下和靜態限流策略效能相當。綜上,實驗證明 sampling traffic shaper 能很好地解決任務數量較少時總頻寬被大量浪費的情況,同時在任務數量較多以及其他複雜情況時依舊能保證和靜態限流演算法持平的效果。
PR 連結(已合併):
https://github.com/dragonflyoss/Dragonfly2/pull/1654
本週推薦閱讀