Rate limiting (Redis)
Rate limiting — ma’lum vaqt oralig‘ida klient yuborishi mumkin bo‘lgan so‘rovlar sonini cheklash. Masalan, "bitta foydalanuvchi daqiqasiga 60 tadan ko‘p so‘rov yubora olmaydi".
Nima uchun kerak?
- Hujumlardan himoya. Parolni tanlab topish, ma’lumotlarni ommaviy yig‘ib olish (scraping) va serverni so‘rovlar bilan to‘ldirib tashlash (DoS).
- Resurslarni adolatli taqsimlash. Bitta klientning xato yozilgan skripti boshqa barcha foydalanuvchilar uchun serverni sekinlashtirmasligi kerak.
- Xarajatlarni nazorat qilish. Har bir so‘rov tashqi pullik API’ga, masalan SMS yuborish yoki AI modeliga murojaat qilsa, cheklovsiz endpoint katta hisob-kitobga olib kelishi mumkin.
- Tarif rejalari. Bepul tarifda soatiga 100 so‘rov, pullik tarifda 10 000 so‘rov.
Svetoforni tasavvur qiling. U yo‘ldagi mashinalar sonini cheklamaydi, lekin chorrahaga bir vaqtda qancha mashina kirishini boshqaradi. Natijada tirbandlik va avariyalar kamayadi.
Algoritmlar
Fixed window (qat’iy oyna)
Vaqt teng oraliqlarga bo‘linadi, masalan har bir daqiqa. Har bir oraliqda so‘rovlar sanaladi:
10:00:00–10:00:59 → hisoblagich: 0, 1, 2 ... 60 → 61-so‘rov rad etiladi
10:01:00 → hisoblagich nolga tushadi
- ✅ juda sodda: bitta hisoblagich va muddat;
- ❌ oyna chegarasida "portlash" bo‘lishi mumkin: 10:00:59 da 60 ta va 10:01:00 da yana 60 ta so‘rov. Natijada bir soniya ichida 120 ta so‘rov o‘tadi.
Sliding window (siljuvchi oyna)
Har bir so‘rovda oxirgi 60 soniya ichidagi so‘rovlar sanaladi. Chegaradagi portlash muammosi yo‘q, lekin har bir so‘rov vaqtini saqlash kerak bo‘ladi. Amalda ko‘pincha taxminiy variant ishlatiladi: joriy va oldingi oyna hisoblagichlari vaznli o‘rtacha sifatida qo‘shiladi.
Token bucket (tokenlar chelagi)
Chelakda ko‘pi bilan N ta token bo‘ladi. Har bir so‘rov bitta token oladi. Tokenlar doimiy tezlikda to‘ldirilib turadi, masalan soniyasiga bitta. Chelak bo‘sh bo‘lsa, so‘rov rad etiladi.
Sig‘im: 10, to‘ldirish: 1 token/soniya
Klient 10 ta so‘rovni birdaniga yubora oladi (burst)
keyin soniyasiga 1 tadan
- ✅ qisqa muddatli portlashlarga ruxsat beradi, lekin o‘rtacha tezlikni cheklaydi. Bu real foydalanuvchilar xatti-harakatiga yaqin: sahifa ochilganda bir nechta so‘rov birdaniga ketadi, keyin tanaffus bo‘ladi;
- Go’da
golang.org/x/time/ratepaketi aynan shu algoritmni amalga oshiradi.
Kalitni tanlash
Rate limiter nimani sanashini kalit belgilaydi:
| Kalit | Qachon |
|---|---|
| Foydalanuvchi ID | Tizimga kirgan foydalanuvchilar uchun. Eng aniq variant |
| API kaliti | Serverlararo integratsiyalar va tariflar uchun |
| IP manzil | Tizimga kirmagan so‘rovlar uchun: login, ro‘yxatdan o‘tish |
| Endpoint + kalit | Qimmat endpointlar uchun alohida chegara: POST /sms:user:42 |
IP bo‘yicha cheklashda ehtiyot bo‘ling: bitta IP ortida butun ofis, universitet yoki mobil operatorning minglab foydalanuvchisi bo‘lishi mumkin (NAT). Shu sababli IP chegarasi foydalanuvchi chegarasidan kengroq qo‘yiladi.
1. Xotirada: x/time/rate
Server bitta nusxada ishlasa, eng oddiy yechim — xotiradagi token bucket:
type memoryLimiter struct {
mu sync.Mutex
limiters map[string]*rate.Limiter
r rate.Limit
burst int
}
func (m *memoryLimiter) Allow(key string) bool {
m.mu.Lock()
lim, ok := m.limiters[key]
if !ok {
lim = rate.NewLimiter(m.r, m.burst)
m.limiters[key] = lim
}
m.mu.Unlock()
return lim.Allow()
}
// Foydalanish: soniyasiga 1 ta, birdaniga 10 tagacha
lim := &memoryLimiter{limiters: map[string]*rate.Limiter{}, r: rate.Every(time.Second), burst: 10}
Bu yechimning ikki kamchiligi bor:
- Map cheksiz o‘sadi. Uzoq vaqt ishlatilmagan kalitlarni vaqti-vaqti bilan o‘chirib turish kerak. Brute-force himoya darsidagi
cleanupfunksiyasi kabi. - Bir nechta server nusxasi. Load balancer ortida 4 ta nusxa bo‘lsa, har birida alohida chegara bo‘ladi. Natijada klient haqiqiy chegaradan 4 barobar ko‘p so‘rov yubora oladi.
Ikkinchi muammo hisoblagichlarni umumiy omborda saqlash orqali hal qilinadi. Bunday ombor sifatida ko‘pincha Redis ishlatiladi.
2. Redis bilan taqsimlangan rate limiter
Redis — ma’lumotlarni xotirada saqlaydigan juda tez kalit-qiymat bazasi. U hisoblagichlar uchun ideal: INCR buyrug‘i qiymatni atomar oshiradi, EXPIRE esa kalitga yashash muddatini beradi.
Redis’ni Docker’da ishga tushiramiz:
Go uchun rasmiy klient:
Fixed window: Lua skript bilan
Oddiy yondashuv ikkita buyruqdan iborat:
Lekin ular orasida dastur o‘chib qolsa, kalit muddatsiz qoladi va foydalanuvchi abadiy bloklanadi. Ikki buyruqni atomar bajarish uchun Redis’da Lua skript ishlatiladi. Redis skriptni boshqa buyruqlar bilan aralashtirmasdan, bir butun holda bajaradi:
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return {current, redis.call("TTL", KEYS[1])}
Skript hisoblagichni oshiradi va birinchi so‘rovda kalitga muddat beradi. U joriy qiymatni va oyna tugashigacha qolgan soniyalarni qaytaradi.
Go kodi
var fixedWindow = redis.NewScript(`
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return {current, redis.call("TTL", KEYS[1])}
`)
type RedisLimiter struct {
rdb *redis.Client
limit int
window time.Duration
}
type Result struct {
Allowed bool
Limit int
Remaining int
Reset time.Duration // oyna tugashigacha
}
func (l *RedisLimiter) Allow(ctx context.Context, key string) (Result, error) {
windowStart := time.Now().Unix() / int64(l.window.Seconds())
redisKey := fmt.Sprintf("ratelimit:%s:%d", key, windowStart)
vals, err := fixedWindow.Run(ctx, l.rdb, []string{redisKey}, int(l.window.Seconds())).Int64Slice()
if err != nil {
return Result{}, err
}
count, ttl := int(vals[0]), time.Duration(vals[1])*time.Second
return Result{
Allowed: count <= l.limit,
Limit: l.limit,
Remaining: max(l.limit-count, 0),
Reset: ttl,
}, nil
}
redis.NewScriptskriptni bir marta yuklaydi. Keyingi chaqiruvlarda faqat uning SHA xeshi yuboriladi (EVALSHA), bu esa tarmoq trafigini kamaytiradi.- Kalitga oyna raqami qo‘shilgan. Har bir yangi oyna o‘z hisoblagichiga ega bo‘ladi, eskilari esa
EXPIREtufayli o‘zi o‘chadi.
Middleware
func RateLimit(l *RedisLimiter, keyFunc func(*http.Request) string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
res, err := l.Allow(r.Context(), keyFunc(r))
if err != nil {
// Redis ishlamayapti: so‘rovni o‘tkazib yuboramiz (fail open)
slog.Error("rate limiter", "err", err)
next.ServeHTTP(w, r)
return
}
w.Header().Set("RateLimit-Limit", strconv.Itoa(res.Limit))
w.Header().Set("RateLimit-Remaining", strconv.Itoa(res.Remaining))
w.Header().Set("RateLimit-Reset", strconv.Itoa(int(res.Reset.Seconds())))
if !res.Allowed {
w.Header().Set("Retry-After", strconv.Itoa(int(res.Reset.Seconds())))
http.Error(w, `{"error":{"code":"rate_limited","message":"so‘rovlar juda ko‘p"}}`,
http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
Javob sarlavhalari klientga o‘z holatini bilish imkonini beradi:
| Sarlavha | Ma’nosi |
|---|---|
RateLimit-Limit |
Oynadagi umumiy chegara |
RateLimit-Remaining |
Qolgan so‘rovlar soni |
RateLimit-Reset |
Oyna tugashigacha qolgan soniyalar |
Retry-After |
429 javobida: qancha kutish kerak |
RateLimit-* sarlavhalari IETF’da standartlashtirilmoqda. Ko‘p API’lar esa ularning X-RateLimit-* ko‘rinishidagi eski variantidan foydalanadi. Qaysi birini tanlasangiz ham, API hujjatida yozib qo‘ying.
Redis ishlamay qolsa nima bo‘ladi?
Bu muhim arxitektura qarori:
- Fail open — Redis ishlamasa, so‘rovlar cheklovsiz o‘tkaziladi. Servis ishlashda davom etadi, lekin vaqtincha himoyasiz qoladi.
- Fail closed — Redis ishlamasa, barcha so‘rovlar rad etiladi. Himoya saqlanadi, lekin servis to‘xtab qoladi.
Umumiy API chegaralari uchun odatda fail open tanlanadi: rate limiter ishlamay qolgani uchun butun servis to‘xtamasligi kerak. Login yoki SMS yuborish kabi nozik endpointlar uchun esa fail closed yoki xotiradagi zaxira limiterga o‘tish ma’qulroq.
To‘liq misol
| main.go | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
Sinab ko‘ramiz. POST /smsga ketma-ket 5 ta so‘rov yuboramiz:
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w "%{http_code} " -X POST http://localhost:8080/sms
done; echo
Oxirgi javobning sarlavhalarini ko‘ramiz:
HTTP/1.1 429 Too Many Requests
Ratelimit-Limit: 3
Ratelimit-Remaining: 0
Ratelimit-Reset: 41
Retry-After: 41
Redis ichida nima borligini ham ko‘rish mumkin:
Har bir limiter va har bir oyna uchun alohida kalit bor. Ular oyna tugagach o‘zi o‘chadi.
Server bir nechta nusxada ishga tushirilsa ham, barcha nusxalar bitta Redis’dan foydalanadi. Chegara esa umumiy bo‘lib qoladi.
Endi Redis’ni to‘xtatib ko‘ramiz:
Server logida:
Fail open strategiyasi ishladi: Redis ishlamasa ham API javob berishda davom etdi. Redis’ga murojaat uchun 100 ms timeout qo‘yilgan. Klient ulanishga bir necha marta urinib ko‘rdi, lekin 100 ms tugagach, so‘rov kutishni to‘xtatdi. Shu sababli Redis javob bermay qolsa ham API so‘rovlari osilib qolmaydi.
Tayyor kutubxonalar
github.com/go-redis/redis_rate— go-redis jamoasining kutubxonasi. U GCRA algoritmini amalga oshiradi, bu esa token bucket’ning taqsimlangan varianti. Chegara chetidagi portlash muammosi yo‘q.golang.org/x/time/rate— xotirada ishlaydigan token bucket.- API gateway’lar, masalan nginx (
limit_req), Envoy va Kong, so‘rovlar ilovaga yetib kelmasidan oldin ham cheklash imkonini beradi.
Xulosa
- Rate limiting hujumlardan himoya qiladi, resurslarni adolatli taqsimlaydi va xarajatlarni nazorat qiladi.
- Asosiy algoritmlar: fixed window (sodda), sliding window (aniqroq) va token bucket (portlashga ruxsat beradi).
- Kalit sifatida foydalanuvchi ID, API kaliti yoki IP ishlatiladi. Qimmat endpointlar uchun alohida chegara qo‘yiladi.
- Bir nechta server nusxasi uchun hisoblagichlar Redis’da saqlanadi. Atomarlik Lua skript bilan ta’minlanadi.
- Klientga
429,Retry-AftervaRateLimit-*sarlavhalari qaytariladi. - Redis ishlamay qolganda qanday yo‘l tutish — fail open yoki fail closed — oldindan hal qilinadi.
Keyingi darsda brauzer xavfsizlik sarlavhalari va boshqa domenlardan kirishni boshqaradigan CORS bilan ishlashni ko‘ramiz.