Maqolalar 7 daqiqa 88 ko‘rish
Xatoni ham keshlab qo‘yish: shu saytda bo‘lgan real tuzoq
Saytdagi GitHub repolar soni bir kun ichida eskirib qotib qoldi — lokalda esa hammasi joyida. Sababi men bilib turib qazigan chuqur edi: xato natijani ham muvaffaqiyat bilan bir xil muddatga keshlab qo‘yganman. Tuzatdim, keyin raqam o‘zi bilan o‘zi kelishmay qoldi, keyin sayt «0 ta repo» deb e'lon qildi. Uch bosqichli tashxis.
Mundarija
Bir kuni shu saytning o‘zida qiziq nosozlik yuz berdi. Saytda GitHub'dagi ochiq repolarim soni ko‘rsatiladi va u jonli olinadi — o‘sha paytda bu raqam bosh sahifada turardi. Production'da esa u eskirgan zaxira raqamni ko‘rsatib qotib qoldi, holbuki lokal kompyuterimda hammasi to‘g‘ri ishlardi. Xato loglari ham yo‘q, kod ham bexato ko‘rinadi — shunchaki raqam "muzlab" qolgan. Sabab esa men o‘zim, bilib turib qazib qo‘ygan chuqur bo‘lib chiqdi: men xato natijani ham muvaffaqiyat bilan bir xil muddatga keshlagan ekanman.
Kod qanday edi#
def repo_count() -> int:
cached = cache.get(CACHE_KEY)
if cached is not None:
return cached
value = fetch_from_github() # xato yoki timeout bo'lsa None qaytadi
if value is None:
value = FALLBACK # zaxira qiymat
cache.set(CACHE_KEY, value, 60 * 60 * 12) # 12 soatga yozamiz
return value
Bir qarashda mantiq to‘g‘ri ko‘rinadi: GitHub javob bermasa zaxira qiymatni
olamiz, keshga yozamiz va o‘n ikki soat tinch yashaymiz. Lekin bu yerda bitta
e'tibordan chetda qolgan narsa bor. Oxirgi cache.set qatori muvaffaqiyatli
javobni ham, zaxira qiymatni ham bir xil o‘n ikki soatga yozadi.
Nima yuz berdi#
Server qayta ishga tushganda birinchi so‘rov GitHub'ga uch soniyalik timeout
bilan ketdi. Aynan o‘sha lahzada tarmoq bir zum sekinroq edi, so‘rov o‘tmadi
va funksiya None qaytardi. Demak zaxira qiymat keshga tushdi — va o‘n ikki
soatga tushdi. Endi natija ravshan: keyingi o‘n ikki soat davomida hech kim
GitHub'ga qayta so‘rov yubormaydi, chunki kesh "to‘la" turibdi. Bitta omadsiz
soniya butun kunni eskirgan raqam bilan qoldirdi.
Bu holatning o‘z nomi bor: negative caching, ya'ni salbiy natijani — xato, bo‘sh javob yoki 404 ni — keshlab qo‘yish. O‘z-o‘zidan bu yomon narsa emas, hatto DNS ham shunday qiladi. Xato mening kodimda edi: men salbiy natijani ijobiy natija muddatiga keshlaganman.
To‘g‘ri yechim#
Yechim ham xuddi muammo kabi oddiy — ikkala holatga alohida muddat berish:
TTL = 60 * 60 * 12 # muvaffaqiyat: 12 soat
FAILURE_TTL = 60 * 15 # xatolik: atigi 15 daqiqa, keyin qayta urinamiz
def repo_count() -> int:
cached = cache.get(CACHE_KEY)
if cached is not None:
return cached
value = fetch_from_github()
if value is None:
cache.set(CACHE_KEY, FALLBACK, FAILURE_TTL)
return FALLBACK
cache.set(CACHE_KEY, value, TTL)
return value
Endi omadsiz so‘rov ko‘pi bilan o‘n besh daqiqa yashaydi. Foydalanuvchi zaxira qiymatni qisqa vaqt ko‘radi, keyin tizim o‘zini o‘zi tuzatadi va yana jonli raqamga qaytadi.
Shu bilan tugadi deb o‘ylagandim. Aslida bu hikoyaning faqat birinchi qismi ekan.
Ertasiga: raqam o‘zi bilan o‘zi kelishmay qoldi#
Tuzatishdan keyin yangi g‘alatilik boshlandi. Sahifani yangilayman — 60 ta repo; yana yangilayman — 93 ta; yana bosaman — yana 60. Bitta sayt, bitta lahza, ikki xil javob.
Sabab keshning o‘zida edi. Men cache.set deb yozganimda Django qayerga
yozayotganini o‘ylab ko‘rmagan ekanman. Django'ning standart keshi —
LocMemCache, ya'ni jarayonning o‘z xotirasi. Production'da esa gunicorn
bir nechta worker bilan ishlaydi, demak har worker'da alohida, bir-biridan
bexabar kesh bor. So‘rovga qaysi worker javob bersa, o‘sha worker'ning keshi
ko‘rinadi: biri GitHub'dan jonli sonni olib ulgurgan, ikkinchisi zaxira
qiymat bilan o‘tirgan.
Lokalda esa runserver bitta jarayon — shuning uchun bu nosozlikni lokalda
umuman ko‘rib bo‘lmasdi. Yechim sifatida keshni hamma worker o‘qiy oladigan
joyga, ya'ni fayl tizimiga ko‘chirdim:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": str(BASE_DIR / "var" / "cache"),
}
}
Va eng achchig‘i: zaxira qiymatning o‘zi#
Uchinchi zarba esa eng uyaltirgani bo‘ldi. Zaxira qiymat .env dan olinardi
va uning standart qiymati — nol:
GITHUB_REPO_COUNT_FALLBACK = env.int("GITHUB_REPO_COUNT_FALLBACK", default=0)
Ya'ni GitHub bitta marta so‘rov chegarasiga urilsa, sayt "0 ta ochiq repo" deb e'lon qilardi. O‘n besh daqiqalik tuzatishim zararni qisqartirgan, lekin yo‘qotmagan edi — chunki men hali ham noto‘g‘ri javobni keshlayotgan edim, shunchaki qisqaroq muddatga.
To‘g‘ri javob boshqacha bo‘lib chiqdi: xato natijani umuman keshlamaslik. Xato bo‘lsa oxirgi haqiqiy son o‘z joyida qolaveradi va o‘sha ko‘rsatiladi. Bir kunlik eski, lekin haqiqiy raqam — yangi, lekin yolg‘on nolga har doim afzal:
def repo_count() -> int:
state = _read_state() # oxirgi haqiqiy son, diskda
known = state.get("count")
fresh = known is not None and time.time() - state.get("fetched_at", 0) < _TTL
if not fresh:
fetched = _fetch()
# Nolga tushib ketish deyarli har doim xato alomati (chegara, tarmoq,
# akkaunt nomi noto'g'ri) — bunday javob eski sonni siqib chiqarmaydi.
if fetched and (known is None or fetched > 0):
_write_state(fetched)
known = fetched
return known if known is not None else settings.GITHUB_REPO_COUNT_FALLBACK
Diqqat qiling: bu yerda _write_state faqat haqiqiy son kelganda chaqiriladi.
Zaxira qiymat hech qachon keshga yozilmaydi — u faqat "hali hech qachon
muvaffaqiyatli so‘rov bo‘lmagan" holat uchun qoldi. Salbiy natija uchun
muddat tanlash muammosi shu bilan butunlay yo‘qoldi: keshlanmagan narsaning
muddati ham bo‘lmaydi.
Bu tuzoqdan chiqqan qoidalar#
Bu kichik nosozlikdan bir necha amaliy xulosa qoldi. Eng asosiysi — uni men ikki bosqichda o‘rgandim: muvaffaqiyat va xatolik hech qachon bir xil muddatga keshlanmasligi kerak, xatolik uchun qisqa, muvaffaqiyat uchun uzun muddat. Buni tushunganimdan keyin esa yana bir qadam qoldi: iloji bo‘lsa xatolikni umuman keshlamaslik va oxirgi haqiqiy qiymatni ushlab turish.
Ikkinchisi: zaxira qiymat ham javobning bir turi, demak u ham haqiqatga o‘xshashi kerak. Menikida u nol edi — texnik jihatdan "qiymat yo‘q" degani, foydalanuvchi uchun esa "bu odamning bitta ham ochiq repozitoriysi yo‘q" degani. Zaxira qiymat — vaqtinchalik yamoq, doimiy holat emas, va u ko‘rinishda uzoq qolsa, foydalanuvchi uchun asta-sekin "haqiqat"ga aylanadi.
Uchinchisi: kesh qayerda yashayotganini bilish kerak. cache.set bir qatorga
o‘xshaydi, lekin uning ortida jarayon xotirasi, fayl yoki Redis turishi
mumkin — va bu tanlov bir necha worker'li production'da to‘g‘ridan-to‘g‘ri
ko‘rinadigan farq beradi. To‘rtinchisi: tashqi xizmatga qo‘yiladigan
timeout'ni juda qisqa qilmaslik kerak, chunki uch soniya tashqi API uchun
kam va bitta sekin javob butun keshni zaharlashi mumkin.
Bunday nosozlikning ayyorligi shundaki, uni topish qiyin: kod ishlayapti, xato loglari yo‘q, faqat bitta raqam qotib qolgan yoki sakrab turibdi. Shuning uchun endi men keshga yozadigan har bir joyda o‘zimga ikkita savol beraman: bu yerga xato qiymat tushib qolsa, u qachon tuzaladi? Va — uni bu yerga umuman qo‘yish shartmi? O‘sha ikki savol endi mening har bir kod ko‘rigimda turadi.