18-bo‘lim

Bir vaqtda ko'p mijoz

Ketma-ket serverning muammosi, oqim va jarayon yondashuvlari, selectors bilan multiplekslash va asyncio.

🕑 8 daqiqa o‘qish 📄 835 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo: accept va recv kutadi
  2. selectors bilan multiplekslash
  3. asyncio - o'sha g'oya, qulayroq yozuv
  4. Qaysi birini tanlash?
  5. Xulosa

Shu paytgacha yozgan serverlarimiz bitta mijozga xizmat qilardi. Ikkinchisi navbatda kutib turishi kerak edi.

Haqiqiy serverda esa minglab mijoz bir vaqtda ishlaydi.

Muammo: accept va recv kutadi #

Oddiy server halqasi shunday:

Python
while True:
    ulanish = tinglovchi.accept()   # bu yerda KUTADI
    malumot = ulanish.recv(1024)    # bu yerda ham KUTADI
    ulanish.sendall(javob)
    ulanish.close()

Agar birinchi mijoz sekin bo'lsa - masalan so'rov yuborishdan oldin o'n soniya o'ylansa - qolgan hamma o'sha o'n soniya kutadi.

Bu muammoning to'rt xil yechimi bor:

YondashuvQanday ishlaydiNarxi
Jarayon har mijozgafork bilan nusxaEng qimmat, lekin izolyatsiya to'liq
Oqim har mijozgaThreadArzonroq, lekin xotira va kontekst almashinuvi
Multiplekslashselect / epollBitta oqim, arzon, lekin kod murakkabroq
AsinxronasyncioMultiplekslash ustidagi qulay qobiq

Birinchi ikkisi sodda, lekin ming mijozda ming oqim kerak bo'ladi. Keyingi ikkisi esa bitta oqimda ishlaydi.

C10K muammosi

1999-yilda Den Kegel "C10K" degan maqola yozdi: bitta server o'n ming ulanishni bir vaqtda ko'tara oladimi?

O'sha paytda javob "yo'q" edi - har ulanish uchun jarayon yoki oqim kerak bo'lardi.

Yechim epoll (Linux) va kqueue (BSD) kabi samarali multiplekslash mexanizmlari bo'ldi. Bugun bitta server million ulanishni ushlab tura oladi.

nginx, Redis va Node.js - hammasi shu yondashuvga qurilgan.

selectors bilan multiplekslash #

G'oya: bitta oqim bir nechta soketni kuzatadi va qaysi biri tayyor bo'lsa, o'shani ishlaydi.

Python
import selectors
import socket
import threading

PORT = 9960
selektor = selectors.DefaultSelector()

tinglovchi = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tinglovchi.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tinglovchi.bind(("127.0.0.1", PORT))
tinglovchi.listen(8)
tinglovchi.setblocking(False)
selektor.register(tinglovchi, selectors.EVENT_READ, data=None)

nomlar = {}
qolgan = {"xabar": 6}


def server():
    while qolgan["xabar"] > 0:
        for kalit, _ in selektor.select(timeout=5):
            s = kalit.fileobj
            if kalit.data is None:
                ulanish, _ = s.accept()
                ulanish.setblocking(False)
                nomlar[ulanish] = f"mijoz-{len(nomlar) + 1}"
                selektor.register(ulanish, selectors.EVENT_READ,
                                  data=nomlar[ulanish])
                continue

            malumot = s.recv(1024)
            if not malumot:
                selektor.unregister(s)
                s.close()
                continue
            if malumot == b"salom":
                print(f"  {kalit.data} ulandi")
            else:
                qolgan["xabar"] -= 1
                print(f"  {kalit.data}: {malumot.decode()}")
            s.sendall(b"qabul qilindi")


oqim = threading.Thread(target=server, daemon=True)
oqim.start()

print("--- uchta mijoz ulanadi")
mijozlar = []
for i in range(3):
    m = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    m.connect(("127.0.0.1", PORT))
    m.sendall(b"salom")
    m.recv(64)          # server javob berguncha kutamiz
    mijozlar.append(m)

print("--- endi ular ARALASH tartibda yozadi")
tartib = [2, 0, 1, 2, 1, 0]
for n, i in enumerate(tartib, 1):
    mijozlar[i].sendall(f"xabar {n}".encode())
    mijozlar[i].recv(64)

oqim.join()
for m in mijozlar:
    m.close()
selektor.close()
tinglovchi.close()

print()
print("bitta oqim, bitta jarayon - lekin uchala mijoz ham xizmat oldi")
Natija
--- uchta mijoz ulanadi
  mijoz-1 ulandi
  mijoz-2 ulandi
  mijoz-3 ulandi
--- endi ular ARALASH tartibda yozadi
  mijoz-3: xabar 1
  mijoz-1: xabar 2
  mijoz-2: xabar 3
  mijoz-3: xabar 4
  mijoz-2: xabar 5
  mijoz-1: xabar 6

bitta oqim, bitta jarayon - lekin uchala mijoz ham xizmat oldi

Ikki muhim nuqta:

  1. setblocking(False) - soket endi kutmaydi, tayyor bo'lmasa darhol qaytadi.
  2. selektor.select() - "qaysi biri tayyor?" degan savolga javob beradi va faqat shu yerda kutadi.
Bu yondashuvda bitta qoida bor

Ishlovchi kod hech qachon uzoq kutmasligi kerak.

Bitta oqim hamma mijozga xizmat qiladi. Agar bitta so'rovni ishlash paytida fayl o'qish yoki bazaga so'rov bloklab qo'ysa, butun server to'xtaydi.

XavfliXavfsiz
time.sleep(5)Taymerni selektorga qo'shish
Sinxron baza so'roviAsinxron drayver
Katta faylni o'qishBo'laklab o'qish
Og'ir hisoblashAlohida jarayonga berish

Node.js dasturchilari buni "event loop ni bloklamang" deb aytadi - bu aynan shu qoida.

asyncio - o'sha g'oya, qulayroq yozuv #

selectors bilan yozilgan kod tez, lekin o'qish qiyin: mantiq halqa ichida tarqalib ketadi.

asyncio xuddi shu mexanizmni ishlatadi, lekin kodni ketma-ket yozilgandek ko'rsatadi:

Python
import asyncio

PORT = 9961
hisob = {"mijoz": 0}


async def ishlovchi(oqi, yoz):
    hisob["mijoz"] += 1
    nom = f"mijoz-{hisob['mijoz']}"
    print(f"  {nom} ulandi")
    while True:
        malumot = await oqi.read(100)
        if not malumot:
            break
        print(f"  {nom}: {malumot.decode()}")
        yoz.write(b"qabul qilindi")
        await yoz.drain()
    yoz.close()


async def mijoz(nom, xabarlar, kechikish):
    oqi, yoz = await asyncio.open_connection("127.0.0.1", PORT)
    for x in xabarlar:
        await asyncio.sleep(kechikish)
        yoz.write(x.encode())
        await yoz.drain()
        await oqi.read(100)
    yoz.close()
    await yoz.wait_closed()


async def asosiy():
    server = await asyncio.start_server(ishlovchi, "127.0.0.1", PORT)
    async with server:
        # Kechikishlar har xil - lekin ular BIR-BIRINI KUTMAYDI
        await asyncio.gather(
            mijoz("a", ["a-1", "a-2"], 0.03),
            mijoz("b", ["b-1", "b-2"], 0.05),
        )


asyncio.run(asosiy())
print()
print("await - boshqa vazifaga navbat berish nuqtasi")
print("oqim ham, jarayon ham qo'shilmadi")
Natija
  mijoz-1 ulandi
  mijoz-2 ulandi
  mijoz-1: a-1
  mijoz-2: b-1
  mijoz-1: a-2
  mijoz-2: b-2

await - boshqa vazifaga navbat berish nuqtasi
oqim ham, jarayon ham qo'shilmadi

Ikki mijoz turli tezlikda ishladi va bir-birini kutmadi. Kod esa oddiy while halqasiga o'xshaydi.

await nima qiladi?

await - bu "men shu yerda kutaman, boshqa vazifalar ishlayversin" degan belgi.

Hodisa halqasi shu nuqtada boshqa tayyor vazifaga o'tadi va kutilgan narsa tayyor bo'lganda qaytib keladi.

Muhim farq:

Oqimasyncio
Almashish qachonIstalgan paytdaFaqat await da
Poyga holati xavfiYuqoriAncha past
Qulf kerakmiKo'pincha haKamdan-kam

Ikkinchi qator katta afzallik: await bo'lmagan joyda kodingiz uzilmaydi, shuning uchun ko'p klassik sinxronizatsiya muammolari umuman paydo bo'lmaydi.

Qaysi birini tanlash? #

HolatTavsiya
Ko'p kutish, kam hisoblash (veb, proksi)asyncio yoki multiplekslash
Og'ir hisoblash (rasm, tahlil)Jarayonlar
Oddiy ichki vosita, kam mijozOqimlar - eng sodda
Mavjud sinxron kutubxonalar ko'pOqimlar

Oxirgi qator amalda ko'p hal qiladi: asyncio ning butun foydasi kutubxonalar ham asinxron bo'lgandagina ochiladi. Bitta sinxron baza drayveri butun hodisa halqasini to'xtatishi mumkin.

Ulanishlar soni cheksiz emas

Ko'p mijozga xizmat qilish - bu resurslarni ham ko'p ishlatish degani. Cheklov qo'yilmasa, server o'zini o'zi o'ldiradi.

ResursNima bo'ladiChora
Fayl deskriptorlari"Too many open files"Chegarani oshiring va cheklang
XotiraHar ulanish bufer oladiBir vaqtdagi ulanish sonini cheklang
Yarim ochiq ulanishlarSekin hujum turiQo'l berishga vaqt chegarasi
Ishlanmagan navbatKechikish o'sadilisten() navbatini cheklang

Har ulanish uchun kutish vaqti qo'yish majburiy: aks holda bitta jim mijoz resursni cheksiz ushlab turadi.

Misollarimizda settimeout aynan shuning uchun ishlatilgan.

Amaliy topshiriq
  1. selectors misolini ishga tushiring.
  2. Mijozlar sonini beshga oshiring va tartib ni moslashtiring.
  3. Ishlovchiga time.sleep(1) qo'shing - nima buzildi?
  4. Uni olib tashlab, nima uchun buzilganini yozing.
  5. asyncio misolini ishga tushiring.
  6. Uchinchi mijoz qo'shing va kechikishini 0.01 qiling.
  7. await asyncio.sleep ni time.sleep ga almashtiring va natijani izohlang.
  8. Har ulanishga oqim ajratadigan versiyani yozing.
  9. Uchala yondashuvni jadval qilib solishtiring.
  10. O'z loyihangiz uchun qaysi biri mos kelishini asoslang.

Xulosa #

  • Oddiy server halqasi accept va recv da kutadi - qolganlar navbatda turadi.
  • To'rt yechim bor: jarayon, oqim, multiplekslash va asinxron.
  • Jarayon va oqim sodda, lekin mingta mijozda qimmatga tushadi.
  • Multiplekslash: bitta oqim ko'p soketni kuzatadi va tayyorini ishlaydi.
  • Buning uchun soket setblocking(False) bo'lishi kerak.
  • Bu yondashuvda ishlovchi kod hech qachon bloklamasligi shart.
  • asyncio xuddi shu mexanizmni qulayroq yozuvda beradi.
  • await - navbat berish nuqtasi; faqat shu yerda boshqa vazifaga o'tiladi.
  • Shuning uchun asyncio da poyga holatlari oqimlardagidan kamroq.
  • Har ulanishga kutish vaqti va umumiy ulanishlar soniga chegara qo'ying.

Keyingi bo'limda tarmoq ilovalarining kattaroq tuzilmasini - proksi, balanslovchi va CDN ni ko'ramiz.

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.