Представьте: ваш API — это как популярный ночной клуб, и без надлежащего контроля толпы всё может быстро стать хаотичным. Именно здесь на помощь приходит троттлинг — это, по сути, вышибала вашего сервера, решающий, кто и когда может войти. Сегодня мы подробно рассмотрим реализацию надёжных механизмов троттлинга в Go, которые обеспечат бесперебойную работу вашего API, даже когда интернет решит устроить вечеринку на ваших конечных точках.
Великая путаница: ограничение скорости против троттлинга
Прежде чем мы закатаем рукава и начнём программировать, давайте проясним распространённое заблуждение, в которое иногда впадают даже опытные разработчики. Хотя эти термины часто используются взаимозаменяемо (и я сам был виновен в этом), на самом деле они обозначают совершенно разные вещи.
Ограничение скорости — это как наличие личного помощника у каждого пользователя. Если вы установите лимит в 100 запросов в час на пользователя, каждый человек сможет сделать свои 100 запросов независимо. Представьте, что каждому дают свою собственную пиццу — делиться не нужно.
Троттлинг, с другой стороны, больше похож на одну пиццу для всей вечеринки. Когда вы устанавливаете троттлинг на 100 запросов в час, это общая пропускная способность вашего сервера, независимо от того, сколько пользователей стучится в вашу дверь. Один прожорливый пользователь теоретически может съесть всю пиццу, оставив всех остальных голодными.
Для нашего сегодняшнего пути реализации мы сосредоточимся на троттлинге — защите вашего сервера от перегрузки совокупным спросом всех пользователей.
Выбор стратегии троттлинга: buffet алгоритмов
Как и в случае с разными способами приготовления кофе (и поверьте мне, я пробовал их все во время ночных сессий кодирования), существует несколько алгоритмов для реализации троттлинга. Давайте рассмотрим самые популярные из них:
Алгоритм «ведро токенов»
Ведро токенов — это как банка печенья (токенов), которая пополняется с постоянной скоростью. Каждому запросу нужно взять печенье, чтобы продолжить. Печенья нет? Извините, придётся подождать, пока банка снова не наполнится.
package main
import (
"fmt"
"sync"
"time"
)
type TokenBucket struct {
capacity int
tokens int
refillRate int
lastRefill time.Time
mutex sync.Mutex
}
func NewTokenBucket(capacity, refillRate int) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity,
refillRate: refillRate,
lastRefill: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mutex.Lock()
defer tb.mutex.Unlock()
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
// Add tokens based on elapsed time
tokensToAdd := int(elapsed * float64(tb.refillRate))
tb.tokens += tokensToAdd
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity
}
tb.lastRefill = now
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}
Алгоритм фиксированного окна
Этот подход похож на часовые временные интервалы в кабинете врача. Каждый час получает свежую порцию доступных приёмов, но как только они заканчиваются, вам придётся ждать следующего часа.
type FixedWindow struct {
limit int
window time.Duration
counter int
windowStart time.Time
mutex sync.Mutex
}
func NewFixedWindow(limit int, window time.Duration) *FixedWindow {
return &FixedWindow{
limit: limit,
window: window,
windowStart: time.Now(),
}
}
func (fw *FixedWindow) Allow() bool {
fw.mutex.Lock()
defer fw.mutex.Unlock()
now := time.Now()
// Check if we need to start a new window
if now.Sub(fw.windowStart) >= fw.window {
fw.counter = 0
fw.windowStart = now
}
if fw.counter < fw.limit {
fw.counter++
return true
}
return false
}
Создание готового к использованию middleware для троттлинга
Теперь, когда мы рассмотрели теорию, давайте создадим нечто, что вы сможете использовать в продакшене. Мы создадим гибкое middleware, которое работает с популярными фреймворками Go, такими как Gin.
package throttle
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
type Throttler struct {
bucket *TokenBucket
mutex sync.RWMutex
}
type TokenBucket struct {
capacity int
tokens int
refillRate int
lastRefill time.Time
tickerStop chan bool
mutex sync.Mutex
}
func NewThrottler(capacity, refillRate int) *Throttler {
bucket := &TokenBucket{
capacity: capacity,
tokens: capacity,
refillRate: refillRate,
lastRefill: time.Now(),
tickerStop: make(chan bool),
}
// Start the refill goroutine
go bucket.startRefillTicker()
return &Throttler{bucket: bucket}
}
func (tb *TokenBucket) startRefillTicker() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
tb.refill()
case <-tb.tickerStop:
return
}
}
}
func (tb *TokenBucket) refill() {
tb.mutex.Lock()
defer tb.mutex.Unlock()
tokensToAdd := tb.refillRate
tb.tokens += tokensToAdd
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity
}
}
func (tb *TokenBucket) takeToken() bool {
tb.mutex.Lock()
defer tb.mutex.Unlock()
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}
func (t *Throttler) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !t.bucket.takeToken() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
"message": "Please try again later",
"code": "THROTTLED",
})
c.Abort()
return
}
c.Next()
}
}
// Cleanup stops the refill ticker
func (t *Throttler) Cleanup() {
close(t.bucket.tickerStop)
}
Собираем всё вместе: полный пример
Давайте создадим полноценный HTTP-сервер, который демонстрирует наше middleware для троттлинга в действии:
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// Create a throttler: 10 requests capacity, refill 2 tokens per second
throttler := NewThrottler(10, 2)
defer throttler.Cleanup()
router := gin.Default()
// Apply throttling middleware globally
router.Use(throttler.Middleware())
// Health check endpoint (also throttled)
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"message": "API работает без сбоев!",
})
})
// Data endpoint
router.GET("/api/data", func(c *gin.Context) {
// Simulate some processing time
time.Sleep(100 * time.Millisecond)
c.JSON(http.StatusOK, gin.H{
"data": []string{"item1", "item2", "item3"},
"timestamp": time.Now().Unix(),
})
})
// Status endpoint to check throttling state
router.GET("/throttle/status", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"available_tokens": throttler.bucket.tokens,
"capacity": throttler.bucket.capacity,
"refill_rate": throttler.bucket.refillRate,
})
})