Ваш API под ударом: почему rate limiting и throttling — must have для Swift-разработчика
Представьте: вы запустили крутое приложение на Swift, пользователи в восторге, серверы работают идеально. Но внезапно — коллапс. Кто-то запустил скрипт, который долбит ваш API тысячами запросов в секунду. Или просто один активный пользователь спамит кнопкой «Обновить» быстрее, чем вы успеваете обработать данные. Результат — тормоза, падение сервера и негативные отзывы.
Знакомо? Тогда эта статья для вас. Мы разберем rate limiting и throttling в Swift — техники, которые спасут ваш бэкенд и сделают приложение стабильным. Вы узнаете, как реализовать ограничение запросов в Swift на стороне клиента, используя современные подходы с async/await, и какие паттерны действительно работают.
Rate limiting vs Throttling: в чем разница?
Прежде чем писать код, давайте разберемся с терминами. Многие путают эти понятия, хотя они решают разные задачи.
Rate limiting (ограничение скорости)
Это жесткий лимит: «Не больше N запросов за период». Если лимит превышен — запрос отклоняется. Сервер обычно возвращает 429 Too Many Requests. Пример: вы разрешаете не более 100 запросов в минуту с одного IP.
Throttling (дросселирование)
Это более мягкий механизм: запросы не блокируются, а ставятся в очередь и выполняются с заданной частотой. Представьте, что ваш API может обрабатывать только 5 запросов в секунду. Throttling выстроит их в очередь и отправит с нужной скоростью, не перегружая сервер.
В Swift оба подхода реализуются по-разному. Давайте рассмотрим лучшие практики.
Практические реализации rate limiting в Swift
1. Простой таймерный throttling (GCD)
Самый простой способ ограничить запросы в Swift — использовать DispatchQueue и таймер. Вот пример, который гарантирует, что запросы будут отправляться не чаще одного раза в секунду:
import Foundation
final class RequestThrottler {
private let queue = DispatchQueue(label: "com.app.throttle")
private var lastRequestTime: Date = .distantPast
private let minimumInterval: TimeInterval
init(interval: TimeInterval = 1.0) {
self.minimumInterval = interval
}
func performThrottledRequest(block: @escaping () -> Void) {
queue.sync {
let now = Date()
if now.timeIntervalSince(lastRequestTime) >= minimumInterval {
lastRequestTime = now
DispatchQueue.main.async {
block()
}
} else {
print("Запрос отклонен: слишком часто")
}
}
}
}
// Использование
let throttler = RequestThrottler(interval: 2.0)
throttler.performThrottledRequest {
// Ваш API-запрос
print("Запрос выполнен")
}
Этот подход прост, но не идеален: он просто отбрасывает «лишние» запросы. Для многих сценариев этого достаточно, но если вам нужно сохранить порядок и гарантировать выполнение — читайте дальше.
2. Rate limiter с очередью (async/await)
С появлением Swift async/await мы получили элегантный способ реализации swift rate limiter с поддержкой очереди. Вот как это выглядит:
import Foundation
actor RateLimiter {
private let maxRequests: Int
private let perSecond: Double
private var requestTimestamps: [Date] = []
private var pendingTasks: [CheckedContinuation] = []
private var isProcessing = false
init(maxRequests: Int, perSecond: Double) {
self.maxRequests = maxRequests
self.perSecond = perSecond
}
func waitForSlot() async throws {
try await withCheckedThrowingContinuation { continuation in
pendingTasks.append(continuation)
if !isProcessing {
processQueue()
}
}
}
private func processQueue() {
isProcessing = true
Task {
while !pendingTasks.isEmpty {
let now = Date()
// Удаляем старые записи (старше 1 секунды)
requestTimestamps = requestTimestamps.filter {
now.timeIntervalSince($0) < 1.0
}
if requestTimestamps.count < maxRequests {
// Можно выполнять запрос
let continuation = pendingTasks.removeFirst()
requestTimestamps.append(now)
continuation.resume()
} else {
// Нужно подождать
let sleepTime = 1.0 / perSecond
try? await Task.sleep(nanoseconds: UInt64(sleepTime * 1_000_000_000))
}
}
isProcessing = false
}
}
}
// Использование
let limiter = RateLimiter(maxRequests: 5, perSecond: 1.0)
func makeAPICall() async throws {
try await limiter.waitForSlot()
// Ваш API-запрос
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com")!)
print("Получено \(data.count) байт")
}
// Тест: 10 запросов подряд
Task {
for i in 0..<10 {
Task {
try? await makeAPICall()
print("Запрос \(i) завершен")
}
}
}
Этот swift rate limiter использует actor для потокобезопасности и очередь с ожиданием. Он не отбрасывает запросы, а ставит их в буфер и выполняет с заданной скоростью. Идеально для работы с API, где важно не потерять ни один запрос.
3. Token Bucket алгоритм (продвинутый уровень)
Token bucket в Swift — один из самых популярных алгоритмов rate limiting. Представьте ведро, в которое каждую секунду падает N токенов. Каждый запрос забирает один токен. Если токенов нет — запрос отклоняется или ждет.
import Foundation
final class TokenBucket {
private let capacity: Int
private let refillRate: Int // токенов в секунду
private var tokens: Int
private var lastRefillTime: Date
private let queue = DispatchQueue(label: "com.tokenbucket")
init(capacity: Int, refillRate: Int) {
self.capacity = capacity
self.refillRate = refillRate
self.tokens = capacity
self.lastRefillTime = Date()
}
func tryConsume() -> Bool {
queue.sync {
refill()
if tokens > 0 {
tokens -= 1
return true
}
return false
}
}
private func refill() {
let now = Date()
let elapsed = now.timeIntervalSince(lastRefillTime)
let tokensToAdd = Int(elapsed) * refillRate
if tokensToAdd > 0 {
tokens = min(capacity, tokens + tokensToAdd)
lastRefillTime = now
}
}
}
// Использование
let bucket = TokenBucket(capacity: 10, refillRate: 2) // 10 токенов, пополнение 2/сек
func apiCallWithToken() {
if bucket.tryConsume() {
print("Запрос выполнен")
// Ваш код
} else {
print("Лимит превышен, подождите")
// Можно показать UI или поставить в очередь
}
}
for _ in 0..<15 {
apiCallWithToken()
Thread.sleep(forTimeInterval: 0.1)
}
Token bucket Swift отлично подходит для ситуаций, когда у вас есть пиковые нагрузки, но в среднем трафик стабильный. Он «сглаживает» всплески, позволяя накопить ток