優化背景
此前 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% 的帶寬。
因此,為了最大限度地利用帶寬,需要使用動態限流來確保任務數量少時能能充分利用總帶寬,而任務數量多時也能公平分配帶寬。最終,我們設計出一套根據上下文進行動態限流的算法,其中上下文指各任務在過去一秒內使用的帶寬,此外,算法還考慮到了任務數量、任務剩余大小、任務保底帶寬等因素,性能相比原來的靜態限流算法有顯著提升。
相關代碼分析
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
?的總限速,以此達到單任務單獨限流,同時限制所有任務總帶寬的效果。
優化方案
為了解決此前靜態限流算法總帶寬利用率不佳的缺點,需要將其改進為動態限流算法,即總帶寬限速仍恒定,但每個任務的單獨帶寬限速需要根據上下文適度、定期調整,已達到最大化利用總帶寬、同時相對公平分配帶寬的目的。
在經過數個改版后,最終我們確定了根據上下文進行限流的 sampling traffic shaper 動態限流算法。具體方案為,每個任務的單任務限流交由?TrafficShaper
?組建進行統一管理,?TrafficShaper
?維護當前正在運行的所有任務,并且定期(每秒)更新這些任務的帶寬。
具體來說,上下文指每個任務在上一秒使用的帶寬、每個任務的剩余大小、任務數量、任務保底帶寬(不能低于?pieceSize
?)等因素,?TrafficShaper
?會根據這些上下文公平地、效率最大化地為每個任務分配其下一秒的帶寬(具體分配方案詳見下一小節),實現動態限流的效果。
優化實現
定義?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 。
優化結果
測試 traffic shaper 相比原有的靜態限流策略在單個任務、多個任務并發、多個任務交錯等多種情況下的性能提升,測試結果如下:
注:若不特殊注明,單任務限流為4KB/s,總限流為10KB/s
可以看到, traffic shaper 在單任務、多任務不相交、單任務低帶寬等情況下相比靜態限流策略性能提升明顯,為 24%~59% 。在多個任務并發、多個任務交錯等情況下和靜態限流策略性能相當。綜上,實驗證明 sampling traffic shaper 能很好地解決任務數量較少時總帶寬被大量浪費的情況,同時在任務數量較多以及其他復雜情況時依舊能保證和靜態限流算法持平的效果。
PR 鏈接(已合并): github.com/dragonflyos…
以上就是Dragonfly P2P 傳輸協議優化代碼解析的詳細內容,更多關于Dragonfly P2P 傳輸協議的資料請關注其它相關文章!