Dragonfly 中 P2P 傳輸協議最佳化

SOFAStack發表於2022-11-23

圖片

文|孫珩珂

上海交通大學

本文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

本週推薦閱讀

Dragonfly 基於 P2P 的檔案和映象分發系統

深入 HTTP/3(2)|不那麼 Boring 的 SSL

Go 程式碼城市上雲——KusionStack 實踐

MOSN 反向通道詳解

相關文章