Maqolalar 7 daqiqa 82 ko‘rish
Rate limiting: API'ni bo‘g‘ib qo‘yishdan qanday saqlash mumkin
Ochiq API'dan kimdir albatta haddan ortiq foydalanadi — bilib yoki bilmay. Token bucket g‘oyasini tushunamiz, Redis bilan ishlaydigan cheklovni yozamiz va uni terminaldan o‘zimiz sinab ko‘ramiz.
Mundarija
API'ni internetga chiqargan har bir odam ertami-kechmi bitta manzarani ko‘radi: kimdir bitta endpointga daqiqada minglab so‘rov yubora boshlaydi. Ba'zan bu yomon niyatli hujum — masalan login sahifasida parol terib ko‘rish. Ba'zan esa oddiy xato: kimningdir mobil ilovasi sikl ichida qolib ketgan. Natija ikkalasida ham bir xil: bitta mijoz butun serverni band qilib, qolganlarga xizmat qolmaydi.
Rate limiting shu muammoni yechadi. U har bir mijozga ma'lum vaqt ichida nechta so‘rov yuborishga ruxsat berishni belgilaydi. Bu g‘oyani tushunib, keyin ishlaydigan versiyasini yozamiz va o‘zimiz sinab ko‘ramiz.
Token bucket: chelak va tokenlar#
Eng ko‘p ishlatiladigan usulni tasavvur qilish oson. Har bir mijozning bitta chelagi bor va u tokenlar bilan to‘ldirilgan. Har so‘rov chelakdan bitta token oladi. Chelak bo‘shab qolsa, keyingi so‘rov rad etiladi. Tokenlar esa vaqt o‘tgani sari asta-sekin qayta to‘ldirilib turadi.
Bu usulning yaxshi tomoni shundaki, u qisqa vaqtli portlashlarga chidaydi: foydalanuvchi bir zumda o‘nta so‘rov yuborsa ham (chelak to‘la bo‘lsa), o‘tkazadi, lekin uzoq muddat bosim o‘tkaza olmaydi. Amalda bu tabiiy his qilinadi — odamlar ba'zan tez-tez bosadi, lekin uzluksiz emas.
Biz esa boshqa, undan ham soddaroq usuldan boshlaymiz: qat'iy oyna (fixed window). Uning mantiqi bir gapga sig‘adi — "shu 60 soniya ichida nechta so‘rov keldi" deb sanab boramiz, chegaradan oshsa rad etamiz, 60 soniya tugagach hisobni noldan boshlaymiz. Bu olti qatorda yoziladi, Redis bilan juda yaxshi tushadi va g‘oyani ko‘rsatish uchun ideal. Uning o‘ziga xos bir kamchiligi bor, lekin avval yozib, keyin o‘sha kamchilikni o‘z ko‘zimiz bilan ko‘ramiz.
Redis bilan yozamiz#
Sinash uchun Redis kerak. Eng tez yo‘li — Docker bilan bitta konteyner ko‘tarish:
docker run -d --name redis-sinov -p 6379:6379 redis:7
uv add redis
Endi sodda cheklovni yozamiz. limit.py fayliga saqlang:
# limit.py
import redis
r = redis.Redis(decode_responses=True)
def is_allowed(user_id, limit=5, window=60):
key = f"rate:{user_id}"
current = r.incr(key) # qiymatni bittaga oshiradi, bo'lmasa 0'dan boshlaydi
if current == 1:
# bu birinchi so'rov — demak oynani hozir boshlaymiz
r.expire(key, window)
return current <= limit
Bu yerda hiyla incr da. Redis'ning incr buyrug‘i qiymatni bittaga
oshiradi va agar kalit umuman bo‘lmasa, uni noldan boshlaydi. Birinchi
so‘rovda qiymat 1 bo‘ladi va biz o‘sha paytda kalitga muddat qo‘yamiz —
masalan 60 soniya. Shu 60 soniya ichida nechta so‘rov kelsa, incr sanab
boradi. Chegaradan oshsa, funksiya False qaytaradi. Muddat tugagach, Redis
kalitni o‘zi o‘chiradi va hisob yana noldan boshlanadi.
O‘zimiz sinab ko‘ramiz#
Faylning oxiriga kichik tekshiruv qo‘shamiz:
for i in range(1, 8):
natija = is_allowed("olim", limit=5, window=60)
print(f"{i}-so'rov: {'o‘tdi' if natija else 'RAD ETILDI'}")
Ishga tushiring:
uv run python limit.py
Natija shunday bo‘ladi:
1-so'rov: o‘tdi
2-so'rov: o‘tdi
3-so'rov: o‘tdi
4-so'rov: o‘tdi
5-so'rov: o‘tdi
6-so'rov: RAD ETILDI
7-so'rov: RAD ETILDI
Beshtadan keyin eshik yopildi. Bir daqiqa kutib qayta ishga tushirsangiz, hisob yangilanganini ko‘rasiz — chunki Redis kalitning muddati tugab, uni o‘chirgan.
Ikki qatorning orasidagi tuzoq#
Kod ishlaydi, lekin unda bitta nozik joy bor va uni ko‘rsatib qo‘yay,
chunki u kamdan-kam, lekin juda yomon tarzda otiladi. incr va expire —
Redis'ga ikkita alohida murojaat. Agar birinchisi bajarilib, ikkinchisi
bajarilmay qolsa (jarayon o‘sha lahzada o‘ldi, tarmoq uzildi, Redis
qayta ishga tushdi), kalit muddatsiz qolib ketadi:
normal holat, kalit TTL: 60 soniya
expire bajarilmasa, TTL: -1 (-1 = muddat yo'q)
-1 degani — bu kalit hech qachon o‘chmaydi. Ya'ni o‘sha foydalanuvchi
beshta so‘rovdan keyin butunlay va abadiy bloklanadi; hech qanday
kutish yordam bermaydi, chunki kutadigan muddatning o‘zi yo‘q. Buni
sinovda ham ko‘rdim: kalitni qo‘lda muddatsiz qoldirsangiz, foydalanuvchi
beshinchi so‘rovda qotib qoladi.
Tuzatish ham qisqa — ikkala buyruqni bitta pipeline'da yuborib, muddatni har safar "agar hali qo‘yilmagan bo‘lsa" shartida qo‘yamiz:
def is_allowed(user_id, limit=5, window=60):
key = f"rate:{user_id}"
pipe = r.pipeline()
pipe.incr(key)
# nx=True: muddat faqat hali qo'yilmagan bo'lsa qo'yiladi
pipe.expire(key, window, nx=True)
current, _ = pipe.execute()
return current <= limit
Endi ikkala buyruq Redis'ga bitta paketda ketadi, va agar baribir birorta muddat yo‘qolsa, keyingi so‘rov uni o‘zi qo‘yib yuboradi:
uzilishdan keyin TTL: -1
keyingi so'rovdan keyin TTL: 60 -> o'zini tuzatdi
Qat'iy oynaning kamchiligi#
Endi yuqorida va'da qilgan kamchilikka kelamiz. Sinash uchun oynani qisqartiramiz — 60 soniya o‘rniga 4 soniya, chegara o‘sha-o‘sha 5 ta. Birinchi so‘rovni yuboramiz, oynaning tugashiga 0,3 soniya qolganda yana to‘rttasini, keyin oyna yangilanishini kutib yana beshtasini:
oyna oxirida: 5 ta so'rov o'tdi
oyna boshida: 5 ta so'rov o'tdi
=> 0.4 soniya ichida jami 10 ta so'rov o'tdi,
holbuki chegara 4 soniyaga 5 ta edi.
Chegarani ikki barobar oshib ketdik, ustiga oynaning o‘ndan bir qismida. Sabab shundaki, qat'iy oyna vaqtni bo‘laklarga bo‘lib sanaydi va bo‘laklarning chegarasida nima bo‘layotganini ko‘rmaydi: bir bo‘lakning oxiri bilan ikkinchisining boshi qo‘shilib, ikki barobar yuk hosil qiladi. Login sahifasi uchun bu jiddiy — hujumchi shu naqshni bilib, chegarangizni doimiy ravishda ikkilantirib turadi.
Boshida aytgan token bucket'da bunday muammo yo‘q, chunki u vaqtni bo‘laklarga bo‘lmaydi — tokenlar uzluksiz, tomchilab to‘lib boradi. Lekin uni to‘g‘ri yozish bir necha qatordan ko‘proq ish oladi, va bu bizni oxirgi bo‘limga olib keladi.
Rad etganda mijozga to‘g‘ri javob bering#
Cheklov ishga tushganda mijozni shunchaki "xato" bilan qaytarmaslik kerak.
To‘g‘ri javob — 429 Too Many Requests holati va qachon qayta urinish
mumkinligini aytadigan sarlavha:
if not is_allowed(user_id):
return JsonResponse(
{"error": "Juda ko‘p so‘rov. Biroz kuting."},
status=429,
headers={"Retry-After": "60"}, # "60 soniyadan keyin qayta uring"
)
Retry-After sarlavhasi muhim: yaxshi yozilgan mijozlar uni o‘qiydi va
aynan o‘shancha kutib qayta uradi, ko‘r-ko‘rona bosishda davom etmaydi.
Faqat IP bo‘yicha cheklash yetarli emas
Cheklovni faqat IP manzil bo‘yicha qursangiz, bitta muammoga duch kelasiz: bitta internet-provayder yoki ofis tarmog‘i ortida yuzlab odam bir xil IP bilan chiqadi. Ulardan biri chegaraga yetsa, qolgan hammasi ham bloklanadi. Foydalanuvchi tizimga kirgan bo‘lsa, uni ID bo‘yicha cheklang; kirmaganini esa IP bilan cheklaysiz, lekin login va parol tiklash kabi sezgir joylarga alohida, qattiqroq chegara qo‘yasiz.
Production'da o‘zingiz yozmang#
Yuqoridagi kod g‘oyani tushunish uchun juda foydali, lekin haqiqiy loyihada
tayyor va sinovdan o‘tgan yechim ishlatgan ma'qul. Nginx darajasida
limit_req bor va u so‘rov ilovagacha yetib bormasdanoq cheklaydi. Django
uchun django-ratelimit degan kutubxona bor va u view'larga bitta dekorator
bilan chegara qo‘yish imkonini beradi. Ikkalasi ham yuqoridagi bilan bir xil
tamoyilda ishlaydi, faqat yuqorida ko‘rganimiz kabi chekka holatlar —
oyna chegarasi, yo‘qolgan muddat, bir vaqtda kelgan so‘rovlar, bir necha
serverga taqsimlangan hisob — allaqachon puxta o‘ylab qo‘yilgan.
Rate limiting — oz kod evaziga katta himoya. Har bir ommaviy endpoint uchun, ayniqsa login va ro‘yxatdan o‘tish sahifalari uchun u deyarli majburiy.