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 ```

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

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