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.

🕑 14 daqiqa o‘qish 📄 949 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Eng ko'p qilinadigan xato
  2. Yaxshiroq yo'l: amallarni sanash
  3. Vaqt o'lchash kerak bo'lganda
  4. Oddiy yuklama testi
  5. Sekin joyni topish
  6. Xulosa

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:

Python
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:

SababNatija
CI serveri sizning noutbukdan sekinTest CI da yiqiladi
Boshqa jarayon protsessorni band qildiTasodifan yiqiladi
Konteyner resurslari cheklanganDoim yiqiladi
Kesh isigan yoki sovuqHar xil natija
Yangi, tezroq kompyuterTest 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:

Python
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:

Python
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)
Python
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
Terminal
pytest -q -p no:randomly test_sorovlar.py
Natija
...                                                              [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.

Bu testning eng qimmatli tomoni

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 sanashQanday
SQL so'rovlariset_trace_callback yoki ORM hisoblagichi
HTTP so'rovlariDublyor qo'yib, chaqiruvlarni sanash
Fayl ochishlarmonkeypatch bilan open ni o'rash
Yaratilgan obyektlarKonstruktorda hisoblagich
Jurnal yozuvlaricaplog 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:

Python
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
Python
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")
Terminal
python3 olcham.py
Natija
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:

Python
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
Terminal
pytest -q -p no:randomly test_osish.py
Natija
.                                                                [100%]
1 passed in 0.11s
Bunday test ham to'liq barqaror emas

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.

Tezlikni qo'riqlashning uch usuli Amallarni sanash So'rovlar, chaqiruvlar, fayl ochishlar soni + butunlay barqaror Har quvurda ishlatilsin O'sish tartibi Hajm ikki barobar oshsa, vaqt necha barobar oshdi +/- shovqinli Alohida belgi bilan Mutlaq vaqt "0.1 soniyadan tez bo'lishi kerak" - beqaror Kamdan-kam to'g'ri keladi Mutlaq vaqt faqat haqiqiy talab bo'lganda: "javob 200 ms dan kechikmasin" kabi Unda ham chegarani real muhitda o'lchab, keng qo'ying
Barqarorlik chapdan o'ngga kamayadi

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:

Terminal
pip install flask
Python
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)
Python
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
Terminal
pytest -q -s -p no:randomly test_yuklama.py
Natija

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.

Haqiqiy yuklama testi uchun maxsus vositalar bor

Yuqoridagi misol tushuntirish uchun yetarli, lekin jiddiy ish uchun tayyor vositalar ishlatiladi:

VositaXususiyati
LocustPython da yoziladi, veb interfeysi bor
k6JavaScript da yoziladi, CI uchun qulay
Apache Bench (ab)Bitta buyruq, tez tekshiruv uchun
wrkJuda 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:

Terminal
python3 -m cProfile -s cumtime olcham.py

Chiqishdagi ustunlar:

UstunMa'nosi
ncallsFunksiya necha marta chaqirilgan
tottimeFunksiyaning o'zida ketgan vaqt
cumtimeFunksiya va u chaqirgan hamma narsaga ketgan vaqt
percallBitta 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.

Amaliy topshiriq
  1. magazin.py ni yozib, ikkala usulning bir xil natija berishini tekshiring.
  2. So'rov hisoblagichini yozib, royxat_sekin nechta so'rov yuborishini sanang.
  3. Mijozlar sonini 100 ga oshiring - hisoblagich nima ko'rsatadi?
  4. royxat_tez ichiga ataylab sikl qo'shing va testning yiqilishini ko'ring.
  5. olcham.py ni ishga tushirib, o'z kompyuteringizdagi farqni yozib oling.
  6. test_osish.py ni ishga tushiring va nisbat qancha chiqqanini ko'ring.
  7. Chegarani 1.8 dan 2.2 gacha toraytiring - test barqaror qoladimi?
  8. Yuklama testini ishga tushirib, soniyasiga necha so'rov chiqqanini yozing.
  9. OQIMLAR ni 1 ga tushiring - natija qanday o'zgardi?
  10. cProfile bilan 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_callback bilan 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 assert qo'ymang - xatolarga qo'ying.
  • Jiddiy yuklama testi uchun Locust, k6, ab va wrk kabi 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 cumtime ishlatiladi.

Keyingi bo'limda hamma narsani birlashtiramiz: testlarni CI quvuriga ulaymiz.

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.