Asosiy qismga o'tish

Model konteyneri: modelni rasm ichiga solamizmi?

28-bob 11 kod bloki 1817 so'z

Bu bobdagi kod shu fayllar bilan ishlaydio'z kompyuteringizda ishga tushiring

Ko'chish kuni

Yangi uyga ko'chyapsiz. Oldingizda quti va narsalar.

Bitta savol bor va u ko'ringanidan muhimroq: televizorni qutiga solasizmi, yoki alohida olib borasizmi?

Qutiga solsangiz — hammasi bir joyda, bir marta ko'tarasiz, hech narsa yo'qolmaydi. Lekin quti og'ir bo'ladi va televizor almashtirmoqchi bo'lsangiz, butun qutini qayta yig'asiz.

Alohida olib borsangiz — quti yengil, televizorni istalgan payt almashtirasiz. Lekin endi ikkita narsani kuzatish kerak, va biri yetib bormasa, uy to'liq bo'lmaydi.

27-bobda servis yozdik. Endi uni ko'chirish kerak — ishlab chiqarish serveriga. Va savol aynan o'sha: model faylini rasm ichiga solamizmi?

Ikki yo'l, ikki xil og'riq

Javob "albatta soling" emas. Ikkalasi ham amalda ishlatiladi.

python13 qator
import pandas as pd
show(pd.DataFrame([
    {"": "Rasm hajmi", "Model ICHIDA": "Katta (model + kod)", "Model TASHQARIDA": "Kichik"},
    {"": "Ishga tushish", "Model ICHIDA": "Tez — hammasi joyida",
     "Model TASHQARIDA": "Sekin — yuklab olish kerak"},
    {"": "Model almashtirish", "Model ICHIDA": "Yangi rasm qurish kerak",
     "Model TASHQARIDA": "Faqat qayta ishga tushirish"},
    {"": "Takrorlanish", "Model ICHIDA": "To'liq — rasm = hamma narsa",
     "Model TASHQARIDA": "Qisman — registrga bog'liq"},
    {"": "Qaytarish (rollback)", "Model ICHIDA": "Eski rasmga qaytasiz",
     "Model TASHQARIDA": "Alias ko'chirasiz (23-bob)"},
    {"": "Internetsiz ishlaydimi", "Model ICHIDA": "Ha", "Model TASHQARIDA": "Yo'q"},
]))
Model ICHIDAModel TASHQARIDA
Rasm hajmiKatta (model + kod)Kichik
Ishga tushishTez — hammasi joyidaSekin — yuklab olish kerak
Model almashtirishYangi rasm qurish kerakFaqat qayta ishga tushirish
TakrorlanishTo'liq — rasm = hamma narsaQisman — registrga bog'liq
Qaytarish (rollback)Eski rasmga qaytasizAlias ko'chirasiz (23-bob)
Internetsiz ishlaydimiHaYo'q

Jadvalga qarang va eng muhim qatorni toping: takrorlanish.

Model rasm ichida bo'lsa, rasm butun haqiqatni o'z ichiga oladi. Olti oydan keyin mlops-servis:v3 ni ishga tushirsangiz, siz aynan o'sha kodni va aynan o'sha modelni olasiz. 2-bobdagi takrorlanish shu yerda yakuniga yetadi.

Model tashqarida bo'lsa, rasm faqat yarmi. Ikkinchi yarmi registrda, va u o'zgargan bo'lishi mumkin.

Qaysi birini tanlash

Amaliy qoida:

  • Model kichik (< 500 MB) va kamdan-kam o'zgaradi → ichiga soling. Sodda, ishonchli, takrorlanadigan.
  • Model katta (bir necha GB) yoki tez-tez yangilanadi → tashqarida qoldiring.

Bizning model 14 MB. Demak ichiga solamiz — va shu bobda buni qilamiz. Lekin ikkinchi yo'lni ham ko'rsatamiz, chunki katta modellarda boshqa tanlov yo'q.

Tayyorgarlik

27-bobdagi servis va 26-bobdagi ONNX modeli kerak. Ular yo'q bo'lsa qayta yaratamiz.

python19 qator
python3 -c "
import sys, pathlib; sys.path.insert(0, 'src')
if not pathlib.Path('model.onnx').exists():
    import pandas as pd
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import train_test_split
    from quvur.xususiyat import XUSUSIYATLAR, tayyorla
    from quvur.format import onnx_ga
    d = pd.read_csv('../datasets/ijara.csv')
    x, y = tayyorla(d), d['broker']
    Xtr, _, ytr, _ = train_test_split(x, y, test_size=0.25, stratify=y, random_state=42)
    m = RandomForestClassifier(n_estimators=150, min_samples_leaf=3,
                               random_state=42, n_jobs=1).fit(Xtr, ytr)
    onnx_ga(m, len(XUSUSIYATLAR), 'model.onnx')
    print('model.onnx yaratildi')
else:
    print('model.onnx bor')
print('hajmi:', round(pathlib.Path('model.onnx').stat().st_size/1024/1024, 1), 'MB')
"
Chiqish (qaytish kodi 0, 2865 ms)
model.onnx yaratildi
hajmi: 6.8 MB

Servis uchun Dockerfile

7-bobdagi Dockerfile o'qitish uchun edi — unda pandas, scikit-learn, hamma narsa bor.

Servis uchun bu ortiqcha. Servis model o'qitmaydi, u faqat bashorat qiladi.

shu faylni yaratingtalablar-servis.txt

python5 qator
fastapi==0.115.6
uvicorn==0.34.0
onnxruntime==1.23.2
numpy==2.2.1
pandas==2.2.3

shu faylni yaratingDockerfile.servis

python29 qator
# 1-BOSQICH: bog'liqliklarni yig'amiz
FROM python:3.11-slim AS quruvchi

WORKDIR /qurish
COPY talablar-servis.txt .
RUN pip install --no-cache-dir --target=/paketlar -r talablar-servis.txt

# 2-BOSQICH: faqat kerakligini ko'chiramiz
FROM python:3.11-slim

# xavfsizlik: root emas, oddiy foydalanuvchi
RUN useradd --create-home --uid 10001 servis

WORKDIR /app
COPY --from=quruvchi /paketlar /paketlar
ENV PYTHONPATH=/paketlar:/app/src

# kod va model (eng ko'p o'zgaradigani oxirida - 7-bob)
COPY src/ /app/src/
COPY model.onnx /app/model.onnx

USER servis
ENV MODEL_YOLI=/app/model.onnx

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
  CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')"

CMD ["python3", "-m", "uvicorn", "xizmat.servis:app", "--host", "0.0.0.0", "--port", "8000"]

Bu Dockerfile da to'rtta qaror bor va har biri sabab bilan:

Ikki bosqich (multi-stage). pip install o'z ortidan kesh va quruvchi asboblar qoldiradi. Ikkinchi bosqich faqat natijani ko'chiradi, axlatni emas.

USER servis. Konteyner ichida root bo'lish shart emas. Agar servisda zaiflik topilsa, buzg'unchi root emas, oddiy foydalanuvchi huquqlarini oladi. Bu ko'p vaqt e'tibordan chetda qoladi — standart holda konteyner root bo'lib ishlaydi.

HEALTHCHECK. Docker endi konteyner tirikmi emas, sog'mi ekanini biladi. 27-bobdagi /healthz shu yerda ishlaydi.

Tartib. COPY src/ va COPY model.onnx eng oxirida — 7-bobdagi qatlam keshi qoidasi.

ONNX "ko'chma", lekin versiya chegarasida

Bu yerda men tuzoqqa tushdim va konteyner ishga tushmadi.

Sabab: model opset 22 bilan saqlangan (mening mashinamdagi onnxruntime 1.23), lekin talablar-servis.txt da onnxruntime==1.20.1 yozilgan edi. 1.20 esa faqat opset 21 gacha tushunadi.

Xato xabari aniq emas: Load model from /app/model.onnx failed: ... Fail

ONNX "har joyda ishlaydi" deyiladi va bu asosan to'g'ri. Lekin fayl ichida opset raqami bor, va uni o'qiydigan kutubxona o'sha raqamni qo'llab-quvvatlashi kerak.

Qoida: modelni yaratgan va o'qiydigan onnxruntime versiyalarini bir xil qiling. Tekshirish:

python1 qator
python3 -c "import onnx; m=onnx.load('model.onnx'); print(m.opset_import)"

`EXPOSE` port ochmaydi

Ko'pchilik EXPOSE 8000 portni ochadi deb o'ylaydi. Ochmaydi.

U shunchaki hujjat: "bu rasm 8000-portda ishlaydi". Portni haqiqatan ochish uchun docker run -p kerak.

Xuddi shunday HEALTHCHECK ham konteynerni qayta ishga tushirmaydi — u faqat holatni belgilaydi. Qayta ishga tushirish Docker Compose yoki Kubernetes ning ishi.

Rasmni quramiz

bash7 qator
t0=$SECONDS
docker build -q -f Dockerfile.servis -t mlops-servis:v1 . > /dev/null 2>&1
echo "qurish vaqti: $((SECONDS - t0)) s"
docker images mlops-servis:v1 --format '{{.Repository}}:{{.Tag}}  {{.Size}}'
echo ""
echo "taqqoslash uchun (7-bobdagi o'qitish rasmi):"
docker images --format '{{.Repository}}:{{.Tag}}  {{.Size}}' | grep -E "mlops-kitob" | head -2
Chiqish (qaytish kodi 0, 42586 ms)
qurish vaqti: 42 s
mlops-servis:v1  618MB

taqqoslash uchun (7-bobdagi o'qitish rasmi):
mlops-kitob:yaxshi  665MB

Ikki raqamni solishtiring. Servis rasmi o'qitish rasmidan kichikroq, lekin farq kutilganchalik katta emas.

Nega? Chunki scikit-learn ni olib tashladik, lekin pandas va numpy qoldi — ular xususiyat tayyorlash uchun kerak (3-bob: bitta tayyorlash kodi). Ular esa o'zi og'ir.

Haqiqiy kichraytirish uchun pandas ni ham olib tashlash kerak bo'lardi — ya'ni xususiyatlarni toza numpy bilan hisoblash. Bu mumkin, lekin 3-bobdagi "bitta tayyorlash kodi" qoidasini buzadi: o'qitish pandas ishlatadi, xizmat esa boshqa kod ishlatadi.

Mana haqiqiy savdo: kichik rasm yoki bitta tayyorlash kodi. Ikkinchisi muhimroq, shuning uchun biz hajmni qabul qilamiz.

Ishga tushiramiz

bash11 qator
docker rm -f kitob-servis > /dev/null 2>&1
docker run -d --name kitob-servis -p 18100:8000 mlops-servis:v1 > /dev/null 2>&1
for i in $(seq 1 40); do
  curl -s http://127.0.0.1:18100/healthz > /dev/null 2>&1 && { echo "servis ${i} soniyada tayyor"; break; }
  sleep 0.5
done
echo -n "healthz : "; curl -s http://127.0.0.1:18100/healthz
echo ""
echo -n "ready   : "; curl -s http://127.0.0.1:18100/ready
echo ""
echo -n "kim bo'lib ishlayapti: "; docker exec kitob-servis whoami
Chiqish (qaytish kodi 0, 2847 ms)
servis 3 soniyada tayyor
healthz : {"holat":"tirik"}
ready   : {"holat":"tayyor","sorovlar":0,"chegara":0.5}
kim bo'lib ishlayapti: servis

Oxirgi qatorga e'tibor bering: konteyner servis foydalanuvchisi bo'lib ishlayapti, root emas. Bitta USER qatori shuni berdi.

Bashorat so'raymiz

bash15 qator
echo "=== EGA ==="
curl -s -X POST http://127.0.0.1:18100/bashorat -H "Content-Type: application/json" -d '{
  "xonalar": 2, "maydon": 55, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.5,
  "tavsif_uzunligi": 600, "rasmlar_soni": 15, "telefon_takrorlanishi": 1,
  "kunlik_elonlar": 1, "egasi_tasdiqlangan": 1}'
echo ""
echo "=== BROKER ==="
curl -s -X POST http://127.0.0.1:18100/bashorat -H "Content-Type: application/json" -d '{
  "xonalar": 2, "maydon": 55, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.2,
  "tavsif_uzunligi": 60, "rasmlar_soni": 2, "telefon_takrorlanishi": 18,
  "kunlik_elonlar": 9, "egasi_tasdiqlangan": 0}'
echo ""
echo "=== YOMON KIRISH ==="
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://127.0.0.1:18100/bashorat \
  -H "Content-Type: application/json" -d '{"xonalar": 2, "maydon": -5}'
Chiqish (qaytish kodi 0, 19 ms)
=== EGA ===
{"broker_ehtimoli":0.0563,"broker":false,"chegara":0.5,"ms":3.15}
=== BROKER ===
{"broker_ehtimoli":0.9179,"broker":true,"chegara":0.5,"ms":1.75}
=== YOMON KIRISH ===
HTTP 422

27-bobdagi hamma xulq saqlandi: to'g'ri bashorat, 422 yomon kirishda. Faqat endi u ko'chiriladigan rasm ichida.

Docker sog'liqni ko'radimi

bash6 qator
sleep 12
docker inspect kitob-servis --format 'holat: {{.State.Status}}  sogliq: {{.State.Health.Status}}' 2>/dev/null \
  || docker inspect kitob-servis --format 'holat: {{.State.Status}}'
echo ""
echo "oxirgi tekshiruv:"
docker inspect kitob-servis --format '{{range .State.Health.Log}}{{.ExitCode}} {{end}}' 2>/dev/null | head -1
Chiqish (qaytish kodi 0, 12039 ms)
holat: running  sogliq: healthy

oxirgi tekshiruv:
0

healthy — demak HEALTHCHECK ishlayapti va Docker servisning ichki holatini biladi.

Bu 30-bobda muhim bo'ladi: yuk muvozanatlagich shu belgiga qarab so'rov yuboradi yoki yubormaydi.

Ikkinchi yo'l: model tashqarida

Katta modellar uchun boshqa tanlov yo'q. Rasm modelsiz quriladi, model esa ishga tushganda yuklanadi.

bash12 qator
docker rm -f kitob-servis-t > /dev/null 2>&1
docker run -d --name kitob-servis-t -p 18101:8000 \
  -v "$(pwd)/model.onnx:/modellar/model.onnx:ro" \
  -e MODEL_YOLI=/modellar/model.onnx \
  mlops-servis:v1 > /dev/null 2>&1
for i in $(seq 1 40); do
  curl -s http://127.0.0.1:18101/ready > /dev/null 2>&1 && break
  sleep 0.5
done
echo -n "tashqi model bilan ready: "; curl -s http://127.0.0.1:18101/ready
echo ""
echo "diqqat: rasm O'SHA rasm, faqat MODEL_YOLI boshqa"
Chiqish (qaytish kodi 0, 1801 ms)
tashqi model bilan ready: {"holat":"tayyor","sorovlar":0,"chegara":0.5}
diqqat: rasm O'SHA rasm, faqat MODEL_YOLI boshqa

Bir xil rasm, boshqa model. Ya'ni bitta rasm ko'p model bilan ishlay oladi — 23-bobdagi registr bilan birga bu kuchli naqsh bo'ladi.

Model tashqarida bo'lsa, rasm yolg'on gapiradi

mlops-servis:v1 nomi endi to'liq haqiqat emas. U kodni belgilaydi, lekin modelni yo'q.

Bir xil v1 rasmi ikki serverda boshqa-boshqa model bilan ishlashi mumkin va buni tashqaridan bilib bo'lmaydi.

Shuning uchun model tashqarida bo'lganda, servis o'zi haqida gapirishi kerak: /ready javobiga model versiyasini qo'shing (23-bobdagi alias va versiya raqami). Aks holda "qaysi model ishlayapti?" degan savol yana javobsiz qoladi — 23-bobdagi model_final_v2_YANGI.pkl papkasiga qaytamiz.

Tozalash

bash2 qator
docker rm -f kitob-servis kitob-servis-t > /dev/null 2>&1 && echo "konteynerlar o'chirildi"
docker ps --format '{{.Names}}' | grep -v k3d
Chiqish (qaytish kodi 0, 1039 ms)
konteynerlar o'chirildi
novavivo-postgres
novavivo-redis
stratix-med-frontend-1

Rasm (mlops-servis:v1) saqlanadi — u 29 va 30-boblarda kerak.

Xulosa

Tushuncha Asosiy fikr
Asosiy savol Model rasm ichidami yoki tashqarida
Model ichida To'liq takrorlanish; rasm = butun haqiqat
Model tashqarida Kichik rasm, tez almashtirish; lekin yarim haqiqat
Tanlov mezoni < 500 MB va kam o'zgarsa — ichiga
Ko'p bosqichli qurish Quruvchi asboblar yakuniy rasmga tushmaydi
USER Root bo'lib ishlamang — zaiflik narxini kamaytiradi
EXPOSE Port ochmaydi, faqat hujjat
HEALTHCHECK Docker ichki holatni biladi (30-bobda kerak)
ONNX foydasi Tezlik (26-bob) va kichik rasm
Tashqi model xavfi Rasm nomi modelni belgilamaydi — versiyani /ready ga yozing

Talaba ishi

  1. python:3.11-slim o'rniga python:3.11 ishlating va hajmni solishtiring. Farq qancha?
  2. USER servis qatorini olib tashlang va whoami ni qayta ishga tushiring. Nima o'zgardi va nega bu xavfli?
  3. HEALTHCHECK ni ataylab buzing (noto'g'ri port). Konteyner holati qanday bo'ladi? docker ps da nima ko'rinadi?
  4. /ready javobiga model versiyasini qo'shing. Tashqi model bilan ishga tushirib tekshiring.
  5. Ikki bosqichni bitta qilib yozing (FROM bitta) va hajmni solishtiring.

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar