Asosiy qismga o'tish

Serverga chiqarish: Compose, qayta ishga tushirish va teskari proksi

30-bob 10 kod bloki 1917 so'z

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

"Serverda ishlamayapti"

Rasm tayyor (28-bob), tezligi o'lchangan (29-bob). Endi uni serverga chiqarish kerak.

Birinchi urinish odatda shunday bo'ladi: SSH bilan serverga kirasiz va qo'lda yozasiz:

bash1 qator
docker run -d -p 8000:8000 mlops-servis:v1

Ishlaydi. Xursandsiz.

Uch kun o'tadi. Server qayta yuklanadi (yangilanish, elektr, nima bo'lsa ham). Ertalab sayt ishlamayapti.

Nega? Chunki docker run bir martalik buyruq. Server o'chsa, konteyner o'chadi va hech kim uni qaytadan ko'tarmaydi.

Ikkinchi muammo ham bor: o'sha buyruq faqat sizning boshingizda. Qaysi portlar, qaysi o'zgaruvchilar, qaysi hajmlar edi? Uch oydan keyin eslay olmaysiz.

Buyruq emas, ta'rif

Yechim — buyruqni faylga aylantirish.

Bu 2-bobdagi retsept g'oyasining aynan o'zi: boshda saqlangan bilim yo'qoladi, yozilgani qoladi.

docker compose aynan shu ishni qiladi: siz nima kerakligini yozasiz, u esa buyruqni o'zi tuzadi.

shu faylni yaratingcompose.yaml

yaml21 qator
services:
  servis:
    image: mlops-servis:v1
    container_name: mlops-servis
    restart: unless-stopped          # <-- eng muhim qator
    ports:
      - "18110:8000"
    environment:
      MODEL_YOLI: /app/model.onnx
      BROKER_CHEGARASI: "0.5"
    healthcheck:
      test: ["CMD", "python3", "-c",
             "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 10s
    deploy:
      resources:
        limits:
          memory: 512M

To'rtta qator alohida e'tiborga loyiq.

restart: unless-stopped — yuqoridagi muammoning yechimi. Server qayta yuklansa, Docker konteynerni o'zi ko'taradi. "unless-stopped" degani: siz o'zingiz to'xtatgan bo'lsangiz, ko'tarmaydi.

healthcheck — 28-bobdagi tekshiruv, endi Compose darajasida.

memory: 512M — chegara. Konteyner buzilib xotirani yeb ketsa, u o'zi o'ladi va qayta ko'tariladi. Chegara bo'lmasa, u butun serverni yiqitadi — shu jumladan boshqa xizmatlarni.

environment — 24-bobdagi sozlama qoidasi: qiymatlar kodda emas, tashqarida.

`docker kill` qayta ishga tushirishni **ishga solmaydi**

Men bu demoni avval docker kill bilan yozdim va u ishlamadi: konteyner o'ldi va o'sha yerda qoldi, RestartCount 0 bo'lib turdi.

Sabab mantiqiy: Docker uchun docker kill va docker stop — bu sizning qaroringiz. Siz konteynerni to'xtatdingiz, demak u to'xtab tursin. Siz ta'mirlayotgan bo'lishingiz mumkin.

Qayta ishga tushirish siyosati faqat o'z-o'zidan o'lgan konteynerga tegishli: dastur qulasa, xotira tugasa, jarayon xato bilan chiqsa.

Shuning uchun yuqoridagi kodda docker exec ... kill 1 ishlatiladi — u konteyner ichidagi asosiy jarayonni o'ldiradi, ya'ni haqiqiy qulashga o'xshaydi.

Sinovda buni bilmaslik chalg'itadi: "siyosat yozdim, lekin ishlamayapti" deb o'ylaysiz.

`restart: always` va `unless-stopped` farqi

Ikkalasi ham konteynerni qayta ko'taradi, lekin bitta muhim farq bor.

always — siz docker stop qilsangiz ham, Docker demoni qayta ishga tushganda konteyner yana ko'tariladi. Ta'mirlash paytida bu juda bezovta qiladi.

unless-stopped — siz o'zingiz to'xtatgan bo'lsangiz, tinch qoldiradi.

Ishlab chiqarish uchun deyarli har doim unless-stopped.

Ko'taramiz

bash24 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 tayyor')
"
docker images mlops-servis:v1 -q | grep -q . || docker build -q -f Dockerfile.servis -t mlops-servis:v1 . > /dev/null 2>&1
docker compose -f compose.yaml down > /dev/null 2>&1
docker compose -f compose.yaml up -d > /dev/null 2>&1
for i in $(seq 1 40); do
  [ "$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:18110/healthz 2>/dev/null)" = "200" ] && { echo "servis ${i} urinishda tayyor"; break; }
  sleep 0.5
done
docker compose -f compose.yaml ps --format '{{.Name}}  {{.Status}}' 2>/dev/null
Chiqish (qaytish kodi 0, 4542 ms)
model tayyor
servis 3 urinishda tayyor
mlops-servis  Up 1 second (health: starting)

Diqqat: buyruq docker run ... uzun qatori emas, shunchaki docker compose up -d. Qolgan hamma narsa faylda yozilgan va u git da saqlanadi.

Qayta ishga tushirishni sinaymiz

Aytilgan gap yetarli emas — tekshiramiz. Konteyner ichidagi jarayonni ataylab o'ldiramiz, ya'ni qulashni taqlid qilamiz.

bash9 qator
echo "qulashdan oldin:"
docker inspect mlops-servis --format '  holat: {{.State.Status}}  qayta ishga tushish: {{.RestartCount}}'
docker exec mlops-servis kill 1 > /dev/null 2>&1
sleep 8
echo "qulagandan keyin:"
docker inspect mlops-servis --format '  holat: {{.State.Status}}  qayta ishga tushish: {{.RestartCount}}'
for i in $(seq 1 30); do curl -s http://127.0.0.1:18110/healthz > /dev/null 2>&1 && break; sleep 1; done
echo -n "  servis javob beryaptimi: "; curl -s http://127.0.0.1:18110/healthz
echo ""
Chiqish (qaytish kodi 0, 8125 ms)
qulashdan oldin:
  holat: running  qayta ishga tushish: 0
qulagandan keyin:
  holat: running  qayta ishga tushish: 0
  servis javob beryaptimi: {"holat":"tirik"}

Natijaga qarang: konteyner o'lmadi ham. RestartCount 0 bo'lib qoldi, holat running.

Sabab qiziq va uni bilish kerak: konteyner ichida PID 1 signallarni e'tiborsiz qoldiradi. Bu Linux yadrosining qoidasi — 1-raqamli jarayon maxsus himoyalangan, hatto kill -9 ham unga ta'sir qilmaydi.

Ya'ni qulashni ichkaridan taqlid qilib bo'lmaydi.

Demak qayta ishga tushirish siyosatini sinash uchun haqiqiy sabab kerak:

Sabab Siyosat ishlaydimi
Server qayta yuklandi Ha
Docker demoni qayta ishga tushdi Ha
Dastur ichida istisno → jarayon chiqdi Ha
Xotira chegarasidan oshdi (OOM) Ha
docker stop / docker kill Yo'q — bu sizning qaroringiz
Konteyner ichidan kill 1 Yo'q — PID 1 himoyalangan

RestartCount raqami esa keyinchalik muhim bo'ladi: agar u tez o'sib borsa, demak konteyner qayta-qayta yiqilmoqda va sabab ichkarida. 34-bobdagi monitoring aynan shunga qaraydi.

Oldiga proksi qo'yamiz

Hozir servis 18110-portda turibdi. Haqiqiy saytda esa foydalanuvchi 80/443 portga keladi va https://model.rostuy.uz deb yozadi.

O'rtada teskari proksi (reverse proxy) turadi. U:

  • 443-portda turadi va TLS ni hal qiladi;
  • so'rovni ichkaridagi servisga uzatadi;
  • bir nechta servisni bitta domen ostida birlashtiradi;
  • so'rov sonini cheklaydi (rate limit).

shu faylni yaratingnginx.conf

python30 qator
events {}

http {
  # 29-bobdagi yuk sinovi: cheklovsiz server navbatga to'ladi
  limit_req_zone $binary_remote_addr zone=bashorat:10m rate=30r/s;

  upstream model {
    server servis:8000;
  }

  server {
    listen 80;

    # sog'liq tekshiruvi - cheklovsiz
    location /healthz {
      proxy_pass http://model;
    }

    location / {
      limit_req zone=bashorat burst=20 nodelay;
      limit_req_status 429;

      proxy_pass http://model;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_connect_timeout 2s;
      proxy_read_timeout 10s;
    }
  }
}

shu faylni yaratingcompose-proksi.yaml

yaml19 qator
services:
  servis:
    image: mlops-servis:v1
    restart: unless-stopped
    environment:
      MODEL_YOLI: /app/model.onnx
    expose:
      - "8000"                       # faqat ichki tarmoqqa

  proksi:
    image: nginx:1.27-alpine
    container_name: mlops-proksi
    restart: unless-stopped
    ports:
      - "18120:80"                   # tashqariga FAQAT proksi chiqadi
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - servis

Eng muhim o'zgarishni sezdingizmi? Servisda endi ports yo'q, faqat expose bor.

Ya'ni servis tashqaridan umuman ko'rinmaydi. Unga faqat proksi orqali kirish mumkin. Bu xavfsizlikning asosiy qoidasi: tashqariga faqat kerakli narsa chiqsin.

bash16 qator
docker compose -f compose.yaml down > /dev/null 2>&1
docker compose -f compose-proksi.yaml up -d > /dev/null 2>&1
for i in $(seq 1 40); do
  [ "$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:18120/healthz 2>/dev/null)" = "200" ] && { echo "proksi ${i} urinishda tayyor"; break; }
  sleep 0.5
done
echo -n "proksi orqali healthz : "; curl -s http://127.0.0.1:18120/healthz
echo ""
echo -n "proksi orqali bashorat: "
curl -s -X POST http://127.0.0.1:18120/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 -n "servisga TO'G'RIDAN kirish: "
curl -s -m 3 -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:18110/healthz || echo "ULANIB BO'LMADI (kutilgani)"
Chiqish (qaytish kodi 0, 3143 ms)
proksi 2 urinishda tayyor
proksi orqali healthz : {"holat":"tirik"}
proksi orqali bashorat: {"broker_ehtimoli":0.9179,"broker":true,"chegara":0.5,"ms":3.2}
servisga TO'G'RIDAN kirish: HTTP 000
ULANIB BO'LMADI (kutilgani)

Oxirgi qatorga qarang: 18110-port endi javob bermaydi. Servis faqat proksi orqali ishlaydi.

Cheklov ishlaydimi

29-bobda ko'rdik: yuk oshsa navbat paydo bo'ladi va p99 qulaydi. Proksidagi limit_req shuning oldini oladi — u ortiqcha so'rovni serverga qo'ymaydi.

python29 qator
python3 -c "
import urllib.request, urllib.error, json
from concurrent.futures import ThreadPoolExecutor
import collections

ELON = {'xonalar': 2, 'maydon': 55, 'qavat': 3, 'jami_qavat': 9, 'narx_mln': 4.5,
        'tavsif_uzunligi': 300, 'rasmlar_soni': 8, 'telefon_takrorlanishi': 4,
        'kunlik_elonlar': 2, 'egasi_tasdiqlangan': 0}
xom = json.dumps(ELON).encode()

def sorov(_):
    r = urllib.request.Request('http://127.0.0.1:18120/bashorat', data=xom,
                               headers={'Content-Type': 'application/json'})
    try:
        with urllib.request.urlopen(r, timeout=10) as j:
            j.read(); return 200
    except urllib.error.HTTPError as e:
        return e.code
    except Exception:
        return 0

with ThreadPoolExecutor(max_workers=60) as ex:
    kodlar = collections.Counter(ex.map(sorov, range(400)))

print('400 ta tez sorov natijasi:')
for k, n in sorted(kodlar.items()):
    nom = {200: 'OK', 429: 'juda kop sorov (cheklov ishladi)', 0: 'ulanmadi'}.get(k, 'boshqa')
    print(f'  HTTP {k}: {n:>4}  {nom}')
"
Chiqish (qaytish kodi 0, 204 ms)
400 ta tez sorov natijasi:
  HTTP 200:   25  OK
  HTTP 429:  375  juda kop sorov (cheklov ishladi)

429 chiqdi — demak cheklov ishlayapti.

Bu yaxshi xabar: server yiqilmadi. U ortiqcha so'rovga "hozir emas" dedi va o'z ishini davom ettirdi.

Cheklovsiz servis — o'zini o'zi yiqitadi

29-bobdagi jadvalni eslang: 50 parallel so'rovda p99 120 ms ga chiqdi. 500 parallel bo'lsa nima bo'ladi?

Server "yo'q" demaydi — u hammasini qabul qiladi va hammasi navbatda turadi. Xotira to'ladi, javob vaqti o'sadi, oxirida hamma so'rov timeout bo'ladi.

Ya'ni hech kim xizmat olmaydi. 90% so'rovni rad etib, 10% ga to'g'ri xizmat qilish ancha yaxshi.

Bu limit_req ning butun ma'nosi: yomon kunda qisman ishlash, umuman ishlamaslikdan afzal.

Haqiqiy serverda nima o'zgaradi

Bu bobda hammasi mahalliy ishladi. Haqiqiy VPS da farqlar quyidagicha:

python9 qator
import pandas as pd
show(pd.DataFrame([
    {"Narsa": "Port", "Bu yerda": "18120", "Haqiqiy serverda": "80 va 443"},
    {"Narsa": "TLS", "Bu yerda": "Yo'q", "Haqiqiy serverda": "Let's Encrypt (certbot)"},
    {"Narsa": "Domen", "Bu yerda": "127.0.0.1", "Haqiqiy serverda": "model.domeningiz.uz"},
    {"Narsa": "Rasm", "Bu yerda": "Mahalliy qurilgan", "Haqiqiy serverda": "Registry dan (31-bob)"},
    {"Narsa": "Yangilash", "Bu yerda": "Qo'lda", "Haqiqiy serverda": "CI/CD (31, 32-bob)"},
    {"Narsa": "Kirish", "Bu yerda": "Ochiq", "Haqiqiy serverda": "Faqat kalit bilan SSH"},
]))
NarsaBu yerdaHaqiqiy serverda
Port1812080 va 443
TLSYo'qLet's Encrypt (certbot)
Domen127.0.0.1model.domeningiz.uz
RasmMahalliy qurilganRegistry dan (31-bob)
YangilashQo'ldaCI/CD (31, 32-bob)
KirishOchiqFaqat kalit bilan SSH

Serverni himoyalashning eng kam to'plami

Model servisi chiqarishdan oldin:

  1. SSH parol bilan emas, kalit bilan. PasswordAuthentication no.
  2. Faqat 22, 80, 443 ochiq. Qolgani ufw deny. Ayniqsa ma'lumotlar bazasi porti hech qachon tashqariga chiqmasin.
  3. Konteyner root emas (28-bob).
  4. Xotira chegarasi — bitta xizmat butun serverni yiqitmasin.
  5. Avtomatik xavfsizlik yangilanishlari yoqilgan.

Beshtasi ham bir martalik ish, lekin ular eng ko'p uchraydigan hujumlarni to'sadi.

Tozalash

bash4 qator
docker compose -f compose-proksi.yaml down > /dev/null 2>&1
docker compose -f compose.yaml down > /dev/null 2>&1
echo "compose xizmatlari to'xtatildi"
docker ps --format '{{.Names}}' | grep -v k3d
Chiqish (qaytish kodi 0, 2272 ms)
compose xizmatlari to'xtatildi
novavivo-postgres
novavivo-redis
stratix-med-frontend-1

Xulosa

Tushuncha Asosiy fikr
docker run muammosi Bir martalik buyruq; server o'chsa qaytmaydi
Compose fayli Buyruq emas — ta'rif, git da saqlanadi
unless-stopped Qayta ko'taradi, lekin siz to'xtatsangiz tinch qoldiradi
Xotira chegarasi Bitta xizmat butun serverni yiqitmasin
RestartCount Tez o'ssa — konteyner qayta-qayta yiqilmoqda
Teskari proksi TLS, bir nechta xizmat, cheklov bitta joyda
expose va ports Servis tashqariga umuman chiqmasin
limit_req Yomon kunda qisman ishlash — umuman ishlamaslikdan afzal
429 Rad etish — yiqilishdan yaxshiroq

Talaba ishi

  1. restart: unless-stopped ni olib tashlang va konteynerni o'ldiring. Nima bo'ldi?
  2. Xotira chegarasini 50M qiling. Servis ko'tariladimi? docker inspect da OOMKilled nima deydi?
  3. rate=30r/s ni 5r/s qiling va sinovni qayta o'tkazing. Nechta 429 chiqdi?
  4. Proksiga ikkinchi servis qo'shing (masalan /v2/ yo'lida) va ikkalasini bitta port ostida ishlating.
  5. proxy_read_timeout ni 1s qiling va to'plamli so'rov yuboring. Qanday xato chiqadi va u kimning aybi?

Bu bobni bepul o'qidingiz.

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

Telegramda yozish Narxlar