18-bo‘lim
Ishlash va yuklama testlari
Tezlikni test bilan qo'riqlash, vaqt o'lchashning beqarorligi, so'rovlar sonini sanash orqali N+1 muammosini tutish va oddiy yuklama testi.
Ushbu bo‘lim mundarijasi
Kod to'g'ri ishlashi mumkin, lekin foydalanuvchi uchun juda sekin bo'lishi mumkin. Bu ham xato - faqat boshqa turdagi.
Muammo shundaki, tezlikni testlash ko'rinishidan oson, amalda esa tuzoqqa to'la.
Eng ko'p qilinadigan xato #
Birinchi o'ylanadigan narsa - vaqtni o'lchab, chegaraga solishtirish:
def test_tez_ishlaydi():
boshlanish = time.perf_counter()
hisobla(katta_malumot)
assert time.perf_counter() - boshlanish < 0.1 # yomon
Bu test beqaror bo'lishga mahkum:
| Sabab | Natija |
|---|---|
| CI serveri sizning noutbukdan sekin | Test CI da yiqiladi |
| Boshqa jarayon protsessorni band qildi | Tasodifan yiqiladi |
| Konteyner resurslari cheklangan | Doim yiqiladi |
| Kesh isigan yoki sovuq | Har xil natija |
| Yangi, tezroq kompyuter | Test hech qachon yiqilmaydi - foydasiz |
Oxirgi qator ayniqsa muhim: chegara juda bo'sh qo'yilsa, test sekinlashuvni umuman sezmaydi.
Yaxshiroq yo'l: amallarni sanash #
Vaqt o'rniga bajarilgan ishni o'lchash mumkin - va bu o'lchov har kompyuterda bir xil chiqadi.
Eng foydali misol - ma'lumotlar bazasiga yuboriladigan so'rovlar soni:
import sqlite3
SXEMA = """
CREATE TABLE mijoz (id INTEGER PRIMARY KEY, ism TEXT NOT NULL);
CREATE TABLE buyurtma (
id INTEGER PRIMARY KEY,
mijoz_id INTEGER NOT NULL REFERENCES mijoz(id),
summa INTEGER NOT NULL
);
"""
def baza_yarat(ulanish, mijozlar_soni=50):
ulanish.executescript(SXEMA)
for i in range(1, mijozlar_soni + 1):
ulanish.execute("INSERT INTO mijoz VALUES (?, ?)", (i, f"Mijoz {i}"))
ulanish.execute(
"INSERT INTO buyurtma (mijoz_id, summa) VALUES (?, ?)",
(i, i * 1000),
)
def royxat_sekin(ulanish):
"""Har buyurtma uchun mijozni alohida so'raydi - N+1 muammosi."""
buyurtmalar = ulanish.execute("SELECT id, mijoz_id, summa FROM buyurtma").fetchall()
natija = []
for buyurtma_id, mijoz_id, summa in buyurtmalar:
qator = ulanish.execute(
"SELECT ism FROM mijoz WHERE id = ?", (mijoz_id,)
).fetchone()
natija.append({"id": buyurtma_id, "mijoz": qator[0], "summa": summa})
return natija
def royxat_tez(ulanish):
"""Bitta so'rov bilan hammasini oladi."""
qatorlar = ulanish.execute(
"SELECT b.id, m.ism, b.summa "
"FROM buyurtma b JOIN mijoz m ON m.id = b.mijoz_id"
).fetchall()
return [{"id": i, "mijoz": ism, "summa": s} for i, ism, s in qatorlar]
Ikkala funksiya ham bir xil natija qaytaradi. Farq faqat so'rovlar sonida - va uni sanash mumkin:
import sqlite3
import pytest
from magazin import baza_yarat
class SorovHisoblagich:
"""Ulanishdan o'tgan SQL so'rovlarini sanaydi."""
def __init__(self, ulanish):
self.sorovlar = []
ulanish.set_trace_callback(self.sorovlar.append)
@property
def soni(self):
return len(self.sorovlar)
@pytest.fixture
def ulanish():
aloqa = sqlite3.connect(":memory:")
baza_yarat(aloqa)
yield aloqa
aloqa.close()
@pytest.fixture
def hisoblagich(ulanish):
return SorovHisoblagich(ulanish)
from magazin import royxat_sekin, royxat_tez
def test_ikkala_usul_bir_xil_natija(ulanish):
assert royxat_sekin(ulanish) == royxat_tez(ulanish)
def test_sekin_usul_har_qator_uchun_sorov_yuboradi(ulanish, hisoblagich):
royxat_sekin(ulanish)
assert hisoblagich.soni == 51
def test_tez_usul_bitta_sorov_yuboradi(ulanish, hisoblagich):
royxat_tez(ulanish)
assert hisoblagich.soni == 1
pytest -q -p no:randomly test_sorovlar.py
... [100%]
3 passed in 0.01s
Ikkinchi testdagi 51 soni - N+1 muammosining ta'rifi: bitta so'rov
ro'yxat uchun, ellikta har qator uchun.
Endi kimdir royxat_tez ichiga tasodifan sikl qo'shsa, uchinchi test
darhol yiqiladi - va u sizning kompyuteringizda ham, CI da ham,
uch yildan keyin ham bir xil ishlaydi.
Shu naqshni boshqa resurslarga ham qo'llash mumkin:
| Nimani sanash | Qanday |
|---|---|
| SQL so'rovlari | set_trace_callback yoki ORM hisoblagichi |
| HTTP so'rovlari | Dublyor qo'yib, chaqiruvlarni sanash |
| Fayl ochishlar | monkeypatch bilan open ni o'rash |
| Yaratilgan obyektlar | Konstruktorda hisoblagich |
| Jurnal yozuvlari | caplog fixture'i |
Bu testlar beqaror emas, chunki ular vaqtni emas, ishni o'lchaydi.
Vaqt o'lchash kerak bo'lganda #
Ba'zan o'sish tartibini ko'rish kerak bo'ladi. Unda mutlaq vaqtni emas, nisbatni o'lchang:
def bormi(elementlar, izlanayotgan):
"""Bir xil kod, lekin tezligi ma'lumot turiga bog'liq.
Ro'yxat berilsa - O(n): har element ko'rib chiqiladi.
To'plam berilsa - O(1): xesh bo'yicha to'g'ridan-to'g'ri topiladi.
"""
return izlanayotgan in elementlar
import time
from qidiruv import bormi
def olcha(malumot, marta=200):
boshlanish = time.perf_counter()
for _ in range(marta):
bormi(malumot, -1) # hech qachon topilmaydi - eng yomon holat
return time.perf_counter() - boshlanish
royxat = list(range(200_000))
toplam = set(royxat)
royxat_vaqti = olcha(royxat)
toplam_vaqti = olcha(toplam)
print(f"ro'yxatda qidirish: ~{royxat_vaqti:.4f} soniya")
print(f"to'plamda qidirish: ~{toplam_vaqti:.6f} soniya")
print(f"farq: ~{royxat_vaqti / toplam_vaqti:.0f} baravar")
python3 olcham.py
ro'yxatda qidirish: ~0.2999 soniya
to'plamda qidirish: ~0.000012 soniya
farq: ~24182 baravar
Farq juda katta - lekin aniq son sizning kompyuteringizda boshqacha bo'ladi. Shuning uchun testda aniq sonni emas, tartibni tekshiramiz:
import time
from qidiruv import bormi
def olcha(hajm, marta=100):
malumot = list(range(hajm))
boshlanish = time.perf_counter()
for _ in range(marta):
bormi(malumot, -1)
return time.perf_counter() - boshlanish
def test_royxatda_qidirish_chiziqli_osadi():
kichik = olcha(50_000)
katta = olcha(100_000)
nisbat = katta / kichik
# ikki barobar ko'p element - taxminan ikki barobar ko'p vaqt.
# chegaralar keng: o'lchov shovqini kvadratik o'sishdan kichik.
assert 1.2 < nisbat < 4.0
pytest -q -p no:randomly test_osish.py
. [100%]
1 passed in 0.11s
Chegaralar keng qo'yilgan (1.2 dan 4.0 gacha), chunki o'lchovda
shovqin bo'ladi. Shunga qaramay bunday test CI da vaqti-vaqti bilan
yiqilishi mumkin.
Shuning uchun uni asosiy quvurdan tashqarida ishlatish odatiy amaliyot: alohida belgi qo'yib, faqat tunda yoki qo'lda ishga tushiriladi.
Agar sizga barqaror natija kerak bo'lsa, vaqtni emas, amallarni sanang - yuqoridagi so'rov hisoblagichi kabi.
Oddiy yuklama testi #
Yuklama testi boshqa savolga javob beradi: tizim bir vaqtda ko'p so'rov kelganda ushlab turadimi?
13-bo'limdagi serverni qayta ishlatamiz - unga Flask kerak:
pip install flask
import sys
from flask import Flask
app = Flask(__name__)
@app.get("/salom")
def salom():
return {"xabar": "assalomu alaykum"}
if __name__ == "__main__":
app.run(port=int(sys.argv[1]), threaded=True)
import socket
import subprocess
import sys
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor
import pytest
SOROVLAR = 200
OQIMLAR = 8
def bosh_port():
with socket.socket() as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
@pytest.fixture(scope="module")
def manzil():
port = bosh_port()
jarayon = subprocess.Popen(
[sys.executable, "veb.py", str(port)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
tugash = time.monotonic() + 15
while time.monotonic() < tugash:
try:
with socket.create_connection(("127.0.0.1", port), timeout=0.2):
break
except OSError:
time.sleep(0.05)
try:
yield f"http://127.0.0.1:{port}"
finally:
jarayon.terminate()
jarayon.wait(timeout=5)
def test_barcha_sorovlar_muvaffaqiyatli(manzil):
def sorov(_):
with urllib.request.urlopen(manzil + "/salom", timeout=10) as javob:
return javob.status
boshlanish = time.perf_counter()
with ThreadPoolExecutor(max_workers=OQIMLAR) as bajaruvchi:
kodlar = list(bajaruvchi.map(sorov, range(SOROVLAR)))
davomiylik = time.perf_counter() - boshlanish
print(f"\n{SOROVLAR} so'rov, {OQIMLAR} oqim: ~{davomiylik:.2f} soniya")
print(f"soniyasiga: ~{SOROVLAR / davomiylik:.0f} so'rov")
assert kodlar == [200] * SOROVLAR
pytest -q -s -p no:randomly test_yuklama.py
200 so'rov, 8 oqim: ~0.23 soniya
soniyasiga: ~853 so'rov
.
1 passed in 0.43s
E'tibor bering: test tezlikni tasdiqlamaydi. U faqat ikki
narsani tekshiradi - hamma so'rov muvaffaqiyatli qaytdimi va necha
vaqt ketdi. Tezlik soni chop etiladi, lekin assert qilinmaydi.
Bu ataylab shunday: son sizning kompyuteringizda boshqacha bo'ladi, xatolar esa har joyda bir xil.
Yuqoridagi misol tushuntirish uchun yetarli, lekin jiddiy ish uchun tayyor vositalar ishlatiladi:
| Vosita | Xususiyati |
|---|---|
| Locust | Python da yoziladi, veb interfeysi bor |
| k6 | JavaScript da yoziladi, CI uchun qulay |
Apache Bench (ab) | Bitta buyruq, tez tekshiruv uchun |
| wrk | Juda yuqori yuklama bera oladi |
Ular foydalanuvchilar sonini asta-sekin oshirish, javob vaqtining taqsimotini (masalan 95-foizli) ko'rsatish va hisobot chizish kabi ishlarni o'zi bajaradi.
Eng muhim maslahat: yuklama testini ishlab chiqarishga o'xshash muhitda o'tkazing. Noutbukdagi natija server haqida deyarli hech narsa demaydi.
Sekin joyni topish #
Test sekinlashuvni ko'rsatdi - endi qayerda ekanini bilish kerak. Buning uchun profillovchi ishlatiladi:
python3 -m cProfile -s cumtime olcham.py
Chiqishdagi ustunlar:
| Ustun | Ma'nosi |
|---|---|
ncalls | Funksiya necha marta chaqirilgan |
tottime | Funksiyaning o'zida ketgan vaqt |
cumtime | Funksiya va u chaqirgan hamma narsaga ketgan vaqt |
percall | Bitta chaqiruvga to'g'ri keladigan vaqt |
-s cumtime ro'yxatni umumiy vaqt bo'yicha saralaydi, shuning uchun
eng ko'p vaqt yeyayotgan funksiya yuqorida turadi. Sonlar har ishga
tushirishda biroz farq qiladi - muhimi tartib.
magazin.pyni yozib, ikkala usulning bir xil natija berishini tekshiring.- So'rov hisoblagichini yozib,
royxat_sekinnechta so'rov yuborishini sanang. - Mijozlar sonini 100 ga oshiring - hisoblagich nima ko'rsatadi?
royxat_tezichiga ataylab sikl qo'shing va testning yiqilishini ko'ring.olcham.pyni ishga tushirib, o'z kompyuteringizdagi farqni yozib oling.test_osish.pyni ishga tushiring va nisbat qancha chiqqanini ko'ring.- Chegarani
1.8dan2.2gacha toraytiring - test barqaror qoladimi? - Yuklama testini ishga tushirib, soniyasiga necha so'rov chiqqanini yozing.
OQIMLARni 1 ga tushiring - natija qanday o'zgardi?cProfilebilan o'z loyihangizdagi sekin joyni toping.
Xulosa #
- Sekinlik ham xato - faqat boshqa turdagi, va uni ham test bilan qo'riqlash mumkin.
- Mutlaq vaqtni tekshiradigan test beqaror bo'lishga mahkum: CI, konteyner va kesh natijani o'zgartiradi.
- Chegara bo'sh qo'yilsa, test sekinlashuvni umuman sezmaydi - ya'ni foydasiz bo'ladi.
- Eng barqaror usul - vaqtni emas, bajarilgan ishni sanash.
- SQL so'rovlarini
set_trace_callbackbilan sanash mumkin; bu o'lchov har kompyuterda bir xil chiqadi. - N+1 muammosi: ro'yxat uchun bitta so'rov va har qator uchun yana bittadan.
- Bizning misolimizda sekin usul 51, tez usul esa 1 so'rov yubordi.
- Xuddi shu naqshni HTTP chaqiruvlari, fayl ochishlar va jurnal yozuvlariga ham qo'llash mumkin.
- Vaqt o'lchash kerak bo'lsa, mutlaq qiymatni emas, o'sish tartibini tekshiring.
- Bunday testda chegaralar keng bo'lishi kerak va ularni alohida belgi bilan asosiy quvurdan chiqarish ma'qul.
- Yuklama testi boshqa savolga javob beradi: bir vaqtda ko'p so'rov kelganda tizim ushlab turadimi.
- Yuklama testida tezlik sonini chop eting, lekin unga
assertqo'ymang - xatolarga qo'ying. - Jiddiy yuklama testi uchun Locust, k6,
abvawrkkabi vositalar bor. - Yuklama testini ishlab chiqarishga o'xshash muhitda o'tkazing; noutbukdagi natija server haqida hech narsa demaydi.
- Sekin joyni topish uchun
python3 -m cProfile -s cumtimeishlatiladi.
Keyingi bo'limda hamma narsani birlashtiramiz: testlarni CI quvuriga ulaymiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.