Swift中的HTTP(十一) 受限 Throttling

語言: CN / TW / HK

HTTP簡介

HTTP基礎結構

HTTP請求體

HTTP 載入請求

HTTP 模擬測試

HTTP 鏈式載入器

HTTP 動態修改請求

HTTP 請求選項

HTTP 重置

HTTP 取消

HTTP 限流

HTTP 重試

HTTP 基礎鑑權

HTTP 自動鑑權設定

HTTP 自動鑑權

HTTP 複合載入器

HTTP 頭腦風暴

HTTP 總結

我曾經開發過一個應用程式,該應用程式使用Timer定期 ping 帶有狀態更新的伺服器。 在應用程式的一個構建中,我們注意到狀態伺服器開始遇到 CPU 峰值,最終導致它無法處理更多請求。

經過調查,我們發現對應用程式邏輯的一些簡單更改導致我們不小心使伺服器的請求過多。 計時器 A 將被設定為根據特定條件傳送狀態更新。 計時器將啟動,更新將被髮送。 引入的錯誤是計時器沒有正確失效並且會保持執行。 當滿足另一個條件時,我們將建立一個新的計時器 (B) 來發送狀態更新。 除了現在我們同時執行 AB,所以他們都會嘗試傳送請求。 然後,當請求開始超時時,完成處理程式將設定另一個計時器重試,這對兩個計時器都會發生,導致 CD。一個計時器變成兩個,變成四個,變成八個,然後是 16,然後 32…我們的伺服器以指數級增長的請求數量猛增。

立即修復是更新伺服器以立即拒絕所有傳入請求。 然後我們為應用程式釋出了一個緊急錯誤修復程式,以修復計時器失效行為。

但是……首先讓應用程式阻止這種情況發生不是很好嗎? 我們可以做到這一點,而且會非常簡單。

受限請求

讓我們看看我們的 HTTPLoader 介面來提醒我們自己的 API 契約:

``` open class HTTPLoader {

/// Load an HTTPTask that will eventually call its completion handler
func load(task: HTTPTask)

/// Reset the loader to its initial configuration
func reset(with group: DispatchGroup)

} `` 請記住,load(task:)方法不承諾何時執行任務。 載入程式可能會收到一個 HTTPTask 並立即開始執行它,或者它可能會等待幾秒鐘、幾分鐘、幾小時或幾年。 對於API` 的客戶端,沒有關於執行時間的承諾。

節流載入程式可以利用這一點。 當它收到 HTTPTask 時,它可以檢視是否允許繼續載入它。 如果是,它可以愉快地將任務傳遞給鏈中的下一個載入器。 如果不允許載入它,它可以將它放在任務列表中以便稍後執行。

我們的整體介面看起來像這樣: ``` public class Throttle: HTTPLoader { public var maximumNumberOfRequests = UInt.max

private var executingRequests = [UUID: HTTPTask]()
private var pendingRequests = [HTTPTask]()

public override func load(task: HTTPTask) {
    if UInt(executingRequests.count) < maximumNumberOfRequests {
        startTask(task)
    } else {
        pendingRequests.append(task)
    }
}

private func startTask(_ task: HTTPTask) {
    let id = task.id
    executingRequests[id] = task
    task.addCompletionHandler {
        self.executingRequests[id] = nil
        self.startNextTasksIfAble()
    }
    super.load(task: task)
}

private func startNextTasksIfAble() {
    while UInt(executingRequests.count) < maximumNumberOfRequests && pendingRequests.count > 0 {
        // we have capacity for another request, and more requests to start
        let next = pendingRequests.removeFirst()
        startTask(next)
    }
}

} ```

這個(不完整的)實現為我們提供了節流如何工作的基本概念。 當我們收到請求時,我們會檢查當前有多少任務在執行。 如果它小於我們允許的最大值,那麼我們可以開始執行這個請求。 如果我們達到(或超出)我們的限制,我們會將任務放入一個“待定”請求陣列中,以表明它需要等待載入。

載入任務會將其新增到當前正在執行的任務列表中,很像我們上次建立的 Autocancel 載入程式。 當它完成時,它會從列表中刪除。 此外,當請求完成時,載入程式會檢查是否有任務等待載入,以及是否允許執行它們。 如果是,則它將它們從陣列中拉出並開始執行它們。

這個簡單的實現有幾個缺點:

  • 它不是執行緒安全的。 請求可以從任何執行緒載入,我們正在修改載入器內部的很多狀態,但沒有確保我們對它有獨佔訪問權。 我們需要一種同步型別(例如 NSLock 或 DispatchQueue)來確保我們正確地更新狀態。

  • 我們無法對被取消的任務做出反應。 如果一個任務在它仍然掛起時被取消,那麼我們可能應該把它從陣列中拉出來並呼叫它的完成處理程式。 幸運的是,新增這個非常簡單:

``` let id = task.id task.addCancelHandler { if let index = self.pendingRequests.firstIndex(where: { $0.id === id }) { let thisTask = self.pendingRequests.remove(at: index)

     let error = HTTPError(.cancelled, ...)
     thisTask.complete(.failure(error))
 }

} pendingRequests.append(task) ``` - 我們缺少我們的 reset() 邏輯。 從概念上講,這將類似於我們上次的 Autocancel 載入器:當我們重置時,我們將每個待處理任務和正在執行的任務加入 DispatchGroup。 當每個人完成後,他們分別離開小組。 我們可以在每個任務上呼叫 cancel(),但理想情況下,我們在鏈中有一個 Autocancel 載入器,它已經為我們做了這件事。

不受限制的請求

雖然我們有能力限制請求是件好事,但可能存在我們永遠不想限制的請求。 這是新增另一個請求選項的好機會:

``` public enum ThrottleOption: HTTPRequestOption { public static var defaultOptionValue: ThrottleOption { .always }

case always
case never

}

extension HTTPRequest {
public var throttle: ThrottleOption { get { self[option: ThrottleOption.self] } set { self[option: ThrottleOption.self] = newValue } } } ```

回想一下,請求選項允許我們新增每個請求的行為。 因此,預設情況下,請求始終受到限制,但我們可以指示單個請求從不受到限制。 剩下的就是在我們的載入器中尋找它:

public override func load(task: HTTPTask) { if task.request.throttle == .never { super.load(task: task) return } ... }

有了這個,我們現在有了一個載入程式,我們可以將其插入我們的鏈中以限制同時傳出的網路請求的數量。 另請注意,我們已將 maximumNumberOfRequests 宣告為公共變數,這意味著我們可以動態更新此值。 例如,我們的應用程式可能會下載一些配置設定以指示允許載入請求的速度。

``` // A networking chain that: // - prevents you from resetting while a reset command is in progress // - automatically cancels in-flight requests when asked to reset // - limits the number of simultaneous requests to a maximum number // - updates requests with missing server information with default or per-request server environment information // - executes all of this on a URLSession

let chain = resetGuard --> autocancel --> throttle --> applyEnvironment --> ... --> urlSessionLoader ```

如果我們有這樣的東西,我們可以遠端“關閉”我們的行為不端的應用程式,而不必爭先恐後地釋出應用程式更新,並且我們可以保持我們的伺服器正常執行。

在下一篇文章中,我們將研究如何在請求失敗時自動重試請求。