18-bo‘lim
Bir vaqtda ko'p mijoz
Ketma-ket serverning muammosi, oqim va jarayon yondashuvlari, selectors bilan multiplekslash va asyncio.
Ushbu bo‘lim mundarijasi
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:
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:
| Yondashuv | Qanday ishlaydi | Narxi |
|---|---|---|
| Jarayon har mijozga | fork bilan nusxa | Eng qimmat, lekin izolyatsiya to'liq |
| Oqim har mijozga | Thread | Arzonroq, lekin xotira va kontekst almashinuvi |
| Multiplekslash | select / epoll | Bitta oqim, arzon, lekin kod murakkabroq |
| Asinxron | asyncio | Multiplekslash ustidagi qulay qobiq |
Birinchi ikkisi sodda, lekin ming mijozda ming oqim kerak bo'ladi. Keyingi ikkisi esa bitta oqimda ishlaydi.
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.
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")
--- 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:
setblocking(False)- soket endi kutmaydi, tayyor bo'lmasa darhol qaytadi.selektor.select()- "qaysi biri tayyor?" degan savolga javob beradi va faqat shu yerda kutadi.
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.
| Xavfli | Xavfsiz |
|---|---|
time.sleep(5) | Taymerni selektorga qo'shish |
| Sinxron baza so'rovi | Asinxron drayver |
| Katta faylni o'qish | Bo'laklab o'qish |
| Og'ir hisoblash | Alohida 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:
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")
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:
| Oqim | asyncio | |
|---|---|---|
| Almashish qachon | Istalgan paytda | Faqat await da |
| Poyga holati xavfi | Yuqori | Ancha past |
| Qulf kerakmi | Ko'pincha ha | Kamdan-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? #
| Holat | Tavsiya |
|---|---|
| Ko'p kutish, kam hisoblash (veb, proksi) | asyncio yoki multiplekslash |
| Og'ir hisoblash (rasm, tahlil) | Jarayonlar |
| Oddiy ichki vosita, kam mijoz | Oqimlar - eng sodda |
| Mavjud sinxron kutubxonalar ko'p | Oqimlar |
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.
Ko'p mijozga xizmat qilish - bu resurslarni ham ko'p ishlatish degani. Cheklov qo'yilmasa, server o'zini o'zi o'ldiradi.
| Resurs | Nima bo'ladi | Chora |
|---|---|---|
| Fayl deskriptorlari | "Too many open files" | Chegarani oshiring va cheklang |
| Xotira | Har ulanish bufer oladi | Bir vaqtdagi ulanish sonini cheklang |
| Yarim ochiq ulanishlar | Sekin hujum turi | Qo'l berishga vaqt chegarasi |
| Ishlanmagan navbat | Kechikish o'sadi | listen() 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.
selectorsmisolini ishga tushiring.- Mijozlar sonini beshga oshiring va
tartibni moslashtiring. - Ishlovchiga
time.sleep(1)qo'shing - nima buzildi? - Uni olib tashlab, nima uchun buzilganini yozing.
asynciomisolini ishga tushiring.- Uchinchi mijoz qo'shing va kechikishini 0.01 qiling.
await asyncio.sleepnitime.sleepga almashtiring va natijani izohlang.- Har ulanishga oqim ajratadigan versiyani yozing.
- Uchala yondashuvni jadval qilib solishtiring.
- O'z loyihangiz uchun qaysi biri mos kelishini asoslang.
Xulosa #
- Oddiy server halqasi
acceptvarecvda 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.
asyncioxuddi shu mexanizmni qulayroq yozuvda beradi.await- navbat berish nuqtasi; faqat shu yerda boshqa vazifaga o'tiladi.- Shuning uchun
asyncioda 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.
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.