---
title: "Docker ve Kubernetes ile GPU görüntü analitiği servislerini ölçeklemek"
author: "Abdulaziz Akyol"
author_url: https://www.abdulazizakyol.com/hakkimda/
url: https://www.abdulazizakyol.com/blog/docker-ve-kubernetes-ile-gpu-goruntu-analitigi-servislerini-olceklemek/
language: tr
published: 2026-09-25
categories: ["Yazılım geliştirme", "Görüntü işleme"]
tags: ["Docker", "Kubernetes", "NVIDIA GPU Operator", "k3s", "Prometheus", "DCGM", "GPU paylaşımı"]
translation: https://www.abdulazizakyol.com/en/blog/scaling-gpu-video-analytics-services-with-docker-and-kubernetes/
description: "Kamera başına GPU analitiği servisini Docker ve Kubernetes'te çalıştırmak: NVIDIA Container Toolkit, GPU Operator, time-slicing/MIG, k3s, izleme, güncelleme."
---

# Docker ve Kubernetes ile GPU görüntü analitiği servislerini ölçeklemek

Yazar: [Abdulaziz Akyol](https://www.abdulazizakyol.com/hakkimda/) · 2026-09-25

## Öne çıkanlar

- Docker'da GPU erişimi için hostta NVIDIA sürücüsü ve NVIDIA Container Toolkit gerekir; toolkit, nvidia-ctk runtime configure komutuyla Docker'a tanıtılır.
- Kubernetes'te GPU yalnızca limits altında ve tam sayı olarak istenir; varsayılan durumda bir pod bir GPU'yu tek başına alır.
- Time-slicing tek GPU'yu birden fazla poda paylaştırır ama kopyalar arasında bellek ve hata yalıtımı yoktur; MIG ise destekleyen GPU'larda donanım düzeyinde yalıtılmış bölümler sunar.
- Kamera başına çalışan analitik podlarında Recreate güncelleme stratejisi, aynı kamerayı iki podun aynı anda sayıp çift olay üretmesini önler.
- İzleme iki katmanlıdır: GPU için DCGM Exporter metrikleri, uygulama için kare hızı, çıkarım süresi ve son kare zamanı gibi servis metrikleri.

GPU görüntü analitiği servislerini ölçeklemek, kamera başına çalışan çıkarım süreçlerini konteynere koyup GPU erişimlerini, sağlıklarını ve güncellemelerini tekrarlanabilir biçimde yönetmek demektir. Tek sunucuda Docker Compose, çok sunuculu ya da çok lokasyonlu kurulumda Kubernetes bu işin iki ölçeği.

Bu yazıda [Python ve YOLO ile gerçek zamanlı tespit](https://www.abdulazizakyol.com/blog/python-ve-yolo-ile-gercek-zamanli-nesne-tespiti-rtsp-kameradan-olaya/) yazısındaki betiği servis haline getiriyorum. Modeli TensorRT motoruna derleme tarafı [ONNX, TensorRT ve INT8](https://www.abdulazizakyol.com/blog/edge-ai-model-hizlandirma-onnx-tensorrt-int8/) yazısında; burada o motoru sahaya dağıtmakla ilgileniyoruz.

## Mimari karar: kamera başına bir süreç

Her kamera için ayrı bir konteyner (Kubernetes'te ayrı bir pod) çalıştırıyorum. Takip durumu kameraya özgü olduğu için süreçleri ayırmak kod tarafını sadeleştiriyor; bir kameranın akış sorunu ötekileri etkilemiyor; güncelleme de kamera kamera yapılabiliyor. Bedeli GPU belleği: her süreç modeli ayrıca yüklüyor.

| Yaklaşım               | Artı                                                | Eksi                                                |
| ---------------------- | --------------------------------------------------- | --------------------------------------------------- |
| Kamera başına süreç    | Hata yalıtımı, basit kod, kamera bazında güncelleme | Her süreç modeli ayrıca yükler                      |
| Çok kameralı tek süreç | Toplu (batch) çıkarım, daha az bellek               | Bir hata tüm kameraları durdurur, kod karmaşıklaşır |

Kamera sayısı GPU belleğini zorlamaya başlayınca çok kameralı sürece geçmek mantıklı; o noktaya kadar kamera başına süreç işletmesi kolay olan tercih.

## Adım 1: Docker'da GPU: NVIDIA Container Toolkit

Hostta NVIDIA sürücüsü kurulu olmalı; CUDA kütüphaneleri imajın içinden gelir. NVIDIA Container Toolkit, sürücüyü konteynere bağlayan katman. Paket deposunu NVIDIA'nın kurulum rehberine göre ekledikten sonra:

```bash
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
sudo docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi   # GPU görünüyor mu?
```

Son komut `nvidia-smi` tablosunu basıyorsa konteynerler GPU'yu görüyor demektir.

## Adım 2: Çok aşamalı Dockerfile

İlk aşama bağımlılıkları bir sanal ortama kuruyor, ikinci aşama yalnızca bu ortamı ve uygulama dosyalarını alıyor. pip önbelleği ve derleme artıkları son imaja girmiyor.

```text
# Sürümleri sabitleyin; motoru üreten ve çalıştıran TensorRT sürümü aynı olmalı.
ultralytics==8.4.163
tensorrt-cu13==11.3.0.99      # PyPI'daki torch 2.14 tekerleği CUDA 13 kütüphanelerini çekiyor
nvidia-modelopt[onnx]==0.47.0 # TensorRT 11'de FP16/INT8 dışa aktarımı ModelOpt ile yapılıyor
paho-mqtt==2.1.0
prometheus-client==0.26.0
```

```dockerfile
# syntax=docker/dockerfile:1

# --- 1. aşama: bağımlılıkları sanal ortama kur ---------------------------------
FROM python:3.12-slim-bookworm AS build
ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt

# --- 2. aşama: yalnızca çalışma zamanı ------------------------------------------
FROM python:3.12-slim-bookworm AS runtime
# OpenCV'nin (opencv-python) ihtiyaç duyduğu sistem kütüphaneleri
RUN apt-get update \
 && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 \
 && rm -rf /var/lib/apt/lists/*
COPY --from=build /opt/venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH \
    PYTHONUNBUFFERED=1 \
    YOLO_CONFIG_DIR=/tmp/ultralytics \
    NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
RUN useradd --uid 10001 --no-create-home app
WORKDIR /app
COPY --chown=app:app rtsp_yolo_events.py observability.py ./
# Model imaja gömülmez: TensorRT motoru GPU'ya ve TensorRT sürümüne özgüdür, /app/models'e bağlanır.
USER 10001
EXPOSE 9108
# Ana döngü kalp atışı dosyasını 30 sn'dir güncellemediyse konteyner sağlıksız sayılır
HEALTHCHECK --interval=15s --timeout=3s --start-period=120s --retries=3 \
  CMD ["python", "-c", "import os,sys,time; sys.exit(0 if time.time()-os.path.getmtime('/tmp/heartbeat')<30 else 1)"]
ENTRYPOINT ["python", "rtsp_yolo_events.py"]
```

Birkaç karar:

- **Model imaja gömülmüyor.** TensorRT motoru derlendiği GPU'ya ve TensorRT sürümüne özgü; aynı imaj farklı GPU'lu sahalarda çalışacaksa motor her sahada üretilip `/app/models` altına bağlanıyor.
- **Sürümler sabit.** Motoru üreten ve çalıştıran TensorRT sürümü aynı olmalı; `latest` etiketi ve sabitlenmemiş paketler bu eşleşmeyi bozar.
- **`NVIDIA_DRIVER_CAPABILITIES`** içindeki `video`, konteynerin donanım video kod çözücüsüne erişmesi için gerekli; `compute` CUDA, `utility` ise `nvidia-smi` içindir.
- **ModelOpt imajın içinde.** Ultralytics, TensorRT 11'de ModelOpt'u ilk kullanımda kendisi kurmaya çalışıyor; kök olmayan kullanıcıyla çalışan bir konteynerde bu kurulum yapılamayacağı için paketi baştan ekliyorum.
- **Kök olmayan kullanıcı** ve Ultralytics'in ayar dosyası için yazılabilir `YOLO_CONFIG_DIR`.

Sağlık kontrolü ve Prometheus metrikleri için servise küçük bir modül ekliyorum:

```python
"""Analitik servisine Prometheus metrikleri ve kalp atışı dosyası ekler (prometheus-client)."""
from __future__ import annotations

import os
import time
from pathlib import Path

from prometheus_client import Counter, Gauge, Histogram, start_http_server

CAMERA = os.getenv("CAMERA_ID", "cam-07")
HEARTBEAT = Path(os.getenv("HEARTBEAT_FILE", "/tmp/heartbeat"))

FRAMES = Counter("va_frames_processed_total", "İşlenen kare sayısı", ["camera"])
EVENTS = Counter("va_events_total", "Üretilen olay sayısı", ["camera", "type"])
RECONNECTS = Counter("va_rtsp_reconnects_total", "RTSP yeniden bağlanma sayısı", ["camera"])
INFER = Histogram("va_inference_seconds", "Kare başına tespit+takip süresi", ["camera"],
                  buckets=(0.005, 0.01, 0.02, 0.04, 0.08, 0.16, 0.32))
LAST_FRAME = Gauge("va_last_frame_timestamp_seconds", "Son işlenen karenin zamanı", ["camera"])

_last_beat = 0.0

def start(port: int = 9108) -> None:
    start_http_server(port)  # /metrics

def heartbeat() -> None:
    """Ana döngünün her turunda çağırın; dosyaya en fazla saniyede bir yazar.
    Kamera kapalıyken de döngü dönüyorsa süreç sağlıklıdır: yeniden başlatmak kamerayı düzeltmez."""
    global _last_beat
    now = time.time()
    if now - _last_beat >= 1.0:
        HEARTBEAT.write_text(str(int(now)))
        _last_beat = now

def frame_done(seconds: float) -> None:
    """Her işlenen karede çağırın."""
    FRAMES.labels(CAMERA).inc()
    INFER.labels(CAMERA).observe(seconds)
    LAST_FRAME.labels(CAMERA).set(time.time())
```

Betiğe entegrasyonu birkaç satır: `main()` başında `obs.start(9108)`, ana döngünün her turunda `obs.heartbeat()`, `model.track` çağrısının süresini ölçüp `obs.frame_done(süre)`, olay üretilince `obs.EVENTS.labels(CAMERA_ID, tip).inc()`, okuyucu yeniden bağlandığında `obs.RECONNECTS.labels(CAMERA_ID).inc()`.

Kalp atışının neden kare işlendiğinde değil döngünün her turunda yazıldığı önemli. Kamera kapandığında süreç sağlıklıdır, sorun kameradadır; kalp atışı kareye bağlı olsaydı konteyner her kamera arızasında yeniden başlatılır, bu da kamerayı düzeltmezdi. Kubernetes belgesi de liveness kontrolünün dış bağımlılıklara bağlanmasının zincirleme yeniden başlatmalara yol açabileceği konusunda uyarıyor. Kamera kesintisi metrikten alarm olarak izleniyor.

## Adım 3: docker compose ile kamera başına servis

```yaml
# Kamera başına bir servis; ortak ayarlar YAML çapası (x-analytics) ile tek yerde.
x-analytics: &analytics
  image: registry.example.com/cx/video-analytics:1.4.2
  restart: unless-stopped
  env_file: common.env        # SITE, MODEL_PATH, MQTT_HOST, MQTT_CA
  volumes:
    - ./certs/ca.crt:/etc/cx/ca.crt:ro
    - ./models:/app/models:ro   # bu makinede üretilmiş best.engine
  deploy:
    resources:
      reservations:
        devices:
          - driver: nvidia
            device_ids: ["0"]
            capabilities: [gpu]
  logging:
    driver: json-file
    options: { max-size: "10m", max-file: "3" }

services:
  cam-01:
    <<: *analytics
    environment:
      CAMERA_ID: cam-01
      RTSP_URL: ${CAM01_RTSP_URL}
      MQTT_USER: cam-01
      MQTT_PASS: ${CAM01_MQTT_PASS}
      PROCESS_FPS: "8"
  cam-02:
    <<: *analytics
    environment:
      CAMERA_ID: cam-02
      RTSP_URL: ${CAM02_RTSP_URL}
      MQTT_USER: cam-02
      MQTT_PASS: ${CAM02_MQTT_PASS}
      PROCESS_FPS: "15"
```

Ortak ayarlar YAML çapasıyla (`x-analytics`) tek yerde; her kamera yalnızca kendi kimliğini, akış adresini ve MQTT bilgisini ekliyor. Parolalar `common.env`'e değil, Compose'un değişken yerine koyma için okuduğu `.env` dosyasına giriyor; böylece bir kameranın akış adresi öteki konteynerin ortamına düşmüyor. `.env` sürüm kontrolüne girmemeli.

Docker belgesine göre `capabilities` alanı zorunlu, `count` ile `device_ids` ise birlikte kullanılamıyor. Docker, aynı GPU'yu paylaşan konteynerler arasında bellek sınırı koymaz; her konteyner GPU belleğinin tamamını kullanabilir, bu yüzden kamera sayısını bellek ölçümüyle belirleyin.

Motoru sahadaki makinede aynı imajla bir kez üretip başlatmak:

```bash
docker run --rm --gpus all --user "$(id -u):$(id -g)" -v "$PWD/models:/app/models" \
  --entrypoint yolo registry.example.com/cx/video-analytics:1.4.2 \
  export model=/app/models/best.pt format=engine imgsz=640 quantize=16 nms=False
docker compose up -d
docker compose ps        # STATUS sütununda (healthy) görünmeli
```

## Adım 4: Kubernetes'te GPU: device plugin ve GPU Operator

Kubernetes GPU'yu tanımaz; düğümdeki GPU'ları `nvidia.com/gpu` kaynağı olarak duyuran NVIDIA device plugin gerekir. NVIDIA GPU Operator bunu ve sürücü, container toolkit, GPU Feature Discovery, DCGM Exporter ve MIG Manager bileşenlerini tek Helm kurulumuyla getiriyor:

```bash
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia && helm repo update
helm install --wait --generate-name -n gpu-operator --create-namespace \
  nvidia/gpu-operator --version=v26.7.1
# Sürücü ve toolkit hostta zaten kuruluysa: --set driver.enabled=false --set toolkit.enabled=false
```

Kubernetes belgesindeki kurallar: GPU yalnızca `limits` altında istenir, `requests` verilirse `limits`'e eşit olmalıdır ve kesirli GPU istenemez. Kamera başına Deployment:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: va-cam-07
  namespace: video-analytics
  labels: { app: video-analytics, camera: cam-07 }
spec:
  replicas: 1
  strategy:
    type: Recreate            # aynı kamerayı iki pod aynı anda saymasın
  selector:
    matchLabels: { app: video-analytics, camera: cam-07 }
  template:
    metadata:
      labels: { app: video-analytics, camera: cam-07 }
    spec:
      # runtimeClassName: nvidia   # k3s + ayrı kurulan device plugin'de gerekli (aşağıya bakın)
      nodeSelector:
        nvidia.com/gpu.present: "true"
      terminationGracePeriodSeconds: 30
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
      containers:
        - name: analytics
          image: registry.example.com/cx/video-analytics:1.4.2
          env:
            - { name: CAMERA_ID, value: cam-07 }
            - { name: SITE, value: gebze-01 }
            - { name: PROCESS_FPS, value: "8" }
            - { name: MODEL_PATH, value: /app/models/best.engine }
            - { name: MQTT_HOST, value: mosquitto.iot.svc }
            - { name: MQTT_CA, value: /etc/cx/ca.crt }
            - { name: MQTT_USER, value: cam-07 }
            - name: RTSP_URL
              valueFrom: { secretKeyRef: { name: cam-07-rtsp, key: url } }
            - name: MQTT_PASS
              valueFrom: { secretKeyRef: { name: mqtt-cam-07, key: password } }
          ports:
            - { name: metrics, containerPort: 9108 }
          resources:
            requests: { cpu: "1", memory: 1Gi }
            limits:
              memory: 3Gi
              nvidia.com/gpu: 1       # GPU yalnızca limits'te; tam sayı olmalı
          startupProbe:               # model yükleme ve TensorRT hazırlığı için 5 dakikaya kadar süre
            exec:
              command: ["python", "-c", "import os,sys,time; sys.exit(0 if time.time()-os.path.getmtime('/tmp/heartbeat')<30 else 1)"]
            periodSeconds: 10
            failureThreshold: 30
          livenessProbe:              # ana döngü 30 sn'dir dönmüyorsa (takıldıysa) yeniden başlat
            exec:
              command: ["python", "-c", "import os,sys,time; sys.exit(0 if time.time()-os.path.getmtime('/tmp/heartbeat')<30 else 1)"]
            periodSeconds: 15
            failureThreshold: 4
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
          volumeMounts:
            - { name: models, mountPath: /app/models, readOnly: true }
            - { name: ca, mountPath: /etc/cx, readOnly: true }
            - { name: tmp, mountPath: /tmp }
      volumes:
        - name: models              # motor bu düğümde, bu GPU için üretildi
          hostPath: { path: /opt/cx/models, type: Directory }
        - name: ca
          secret:
            secretName: mqtt-ca
            items: [{ key: ca.crt, path: ca.crt }]
        - name: tmp
          emptyDir: {}
```

Gizli değerler manifeste yazılmıyor:

```bash
kubectl create namespace video-analytics
kubectl -n video-analytics create secret generic cam-07-rtsp --from-literal=url='rtsp://<kullanici>:<parola>@10.0.0.21:554/stream2'
kubectl -n video-analytics create secret generic mqtt-cam-07 --from-literal=password='<parola>'
kubectl -n video-analytics create secret generic mqtt-ca --from-file=ca.crt=./certs/ca.crt
```

Manifestteki kararlar:

- **`strategy: Recreate`.** Varsayılan RollingUpdate, yeni pod hazır olana kadar eskisini çalıştırır: aynı kamerayı iki pod sayar ve çift olay üretir. Üstelik boşta GPU yoksa yeni pod `Pending` kalır ve güncelleme takılır. Recreate birkaç saniyelik boşluğu kabul edip bu iki sorunu ortadan kaldırıyor.
- **CPU limiti yok, bellek limiti var.** CPU limiti, kod çözme iş parçacıklarını kısıp gecikme yaratabilir; benim tercihim CPU'da yalnızca istek (request), bellekte ise sınır.
- **`nodeSelector`**, GPU Feature Discovery'nin koyduğu `nvidia.com/gpu.present` etiketini kullanıyor.
- **`startupProbe`**, modelin yüklenmesine ve TensorRT hazırlığına 5 dakikaya kadar süre tanıyor; başarılı olana kadar liveness devreye girmiyor.
- **`readOnlyRootFilesystem`** ile imaj salt okunur; yazılabilir tek yer `/tmp`.
- GPU Operator'ün güncel sürümlerinde CDI (Container Device Interface) varsayılan olarak açık ve Operator artık `nvidia` runtime'ını varsayılan yapmıyor; bu yüzden örnekte `runtimeClassName` yorum satırında. k3s'te durum farklı (aşağıda).

## Adım 5: Tek GPU'yu paylaşmak: time-slicing, MPS, MIG

Varsayılan durumda bir pod bir GPU'yu tek başına alır. On iki kamera podu için on iki GPU gerekmemesi için paylaşım gerekiyor.

**Time-slicing**, GPU'yu belirli sayıda kopya olarak duyurur:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: time-slicing-config-all
  namespace: gpu-operator
data:
  any: |-
    version: v1
    flags:
      migStrategy: none
    sharing:
      timeSlicing:
        resources:
          - name: nvidia.com/gpu
            replicas: 4
```

```bash
kubectl create -n gpu-operator -f time-slicing-config.yaml
kubectl patch clusterpolicies.nvidia.com/cluster-policy -n gpu-operator --type merge \
  -p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config-all", "default": "any"}}}}'
```

Bu yapılandırmayla her fiziksel GPU dört `nvidia.com/gpu` olarak görünür. NVIDIA belgesinin açık uyarıları: kopyalar arasında bellek ve hata yalıtımı yok; birden fazla kopya istemek orantılı işlem gücü garanti etmez; time-slicing açıkken DCGM Exporter metrikleri konteynerlerle ilişkilendiremiyor. Bir pod GPU belleğini doldurursa aynı GPU'daki diğerleri de etkilenir.

**MPS**, device plugin'de deneysel olarak destekleniyor; her istemcinin belleğini eşit bir payla ve işlem kapasitesini de belirli bir sınırla kısıtlıyor, MIG etkin cihazlarda çalışmıyor.

**MIG (Multi-Instance GPU)**, destekleyen GPU'ları donanım düzeyinde bellek ve hata yalıtımı olan bölümlere ayırıyor. GPU Operator'de `mig.strategy` (`single` ya da `mixed`) seçilip düğüme `nvidia.com/mig.config` etiketiyle profil veriliyor; podlar `nvidia.com/mig-1g.10gb` gibi kaynaklar istiyor. Yalnızca MIG destekleyen GPU'larda var.

| Yöntem         | Bellek yalıtımı | Hata yalıtımı | Nerede                  |
| -------------- | --------------- | ------------- | ----------------------- |
| Time-slicing   | Yok             | Yok           | Her NVIDIA GPU          |
| MPS (deneysel) | Eşit pay sınırı | Sınırlı       | MIG kapalı GPU'lar      |
| MIG            | Var             | Var           | MIG destekleyen GPU'lar |

Yeni bir seçenek: GPU Operator 26.7.1 sürüm notlarına göre R615 ve üstü sürücüyle `NVIDIA_GPU_MEMORY_REQUEST` ve `NVIDIA_GPU_MEMORY_LIMIT` ortam değişkenleri (MiB) konteyner başına yumuşak ve sert CUDA bellek sınırı koyabiliyor. Time-slicing'in bellek yalıtımı eksiğini kapatabilecek bir özellik; henüz yeni, kendi sürücü ve sürüm kombinasyonunuzda test etmeden güvenmeyin.

## Adım 6: Sahada hafif Kubernetes: k3s

Mağaza ya da fabrikadaki tek GPU sunucusunda tam bir Kubernetes kümesi ağır kalır. k3s, tek ikili dosya halinde gelen, tam uyumlu bir Kubernetes dağıtımı; çok lokasyonda aynı manifestleri ve aynı geri alma yöntemini kullanmayı sağlıyor. Tek sunuculu, tek lokasyonlu bir kurulumda ise Compose çoğu zaman yeterli.

k3s belgesine göre sıra şöyle: hosta NVIDIA sürücüsü ve container runtime kurulur, k3s kurulur (ya da yeniden başlatılır); k3s runtime'ı kendisi bulup containerd yapılandırmasına ekler ve `nvidia` RuntimeClass'ını hazır getirir.

```bash
curl -sfL https://get.k3s.io | sh -
sudo grep nvidia /var/lib/rancher/k3s/agent/etc/containerd/config.toml   # runtime bulundu mu?
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin && helm repo update
helm upgrade -i nvdp nvdp/nvidia-device-plugin -n nvidia-device-plugin --create-namespace \
  --set runtimeClassName=nvidia --set-file config.map.config=dp-time-slicing.yaml
```

`dp-time-slicing.yaml`, yukarıdaki ConfigMap'in `any` anahtarındaki içerikle aynı. Varsayılan runtime değiştirilmediyse podların `runtimeClassName: nvidia` taşıması gerekiyor; Deployment'taki yorum satırını açın. Dar hatlı sahalarda imaj çekmek de bir konu: k3s, `/etc/rancher/k3s/registries.yaml` ile yerel bir ayna (mirror) kayıt defterinden çekecek şekilde yapılandırılabiliyor.

## Adım 7: İzleme: Prometheus ve DCGM Exporter

İzleme iki katmanlı. GPU katmanını DCGM Exporter veriyor; GPU Operator ile varsayılan olarak geliyor, tek başına kurulduğunda 9400 portunda `/metrics` yayınlıyor. Uygulama katmanını servisin kendi metrikleri veriyor. Prometheus Operator kullanıyorsanız servis metrikleri için:

```yaml
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: video-analytics
  namespace: video-analytics
spec:
  selector:
    matchLabels: { app: video-analytics }
  podMetricsEndpoints:
    - port: metrics
      interval: 30s
```

Alarm ve panel için kullandığım sorgular:

```text
# Kameradan 60 sn'dir kare işlenmiyor (kamera, ağ ya da akış sorunu)
time() - va_last_frame_timestamp_seconds > 60
# Kamera başına işlenen kare hızı
rate(va_frames_processed_total[5m])
# Kamera başına p95 çıkarım süresi
histogram_quantile(0.95, sum by (le, camera) (rate(va_inference_seconds_bucket[5m])))
# GPU bellek doluluğu (yaklaşık)
DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE)
# Donanım kod çözücü kullanımı ve son XID hatası
DCGM_FI_DEV_DEC_UTIL
DCGM_FI_DEV_XID_ERRORS > 0
```

DCGM Exporter'ın varsayılan sayaç listesinde GPU kullanımı, bellek, sıcaklık, güç, kodlayıcı/kod çözücü kullanımı ve XID hataları var. Kamera analitiğinde kod çözücü kullanımını mutlaka izleyin: benim deneyimimde kamera sayısı arttığında darboğaz sık sık çıkarımda değil, kod çözmede çıkıyor.

## Adım 8: Güncelleme stratejisi

- **Değişmez etiketler.** `1.4.2` gibi sürüm etiketleri kullanın, `latest` kullanmayın; bir etiketi başka bir imaja taşımayın.
- **İmaj ve model ayrı sürümlenir.** Motor dosyasını tarihle adlandırın (`best-2026-09-25.engine`), `MODEL_PATH` ile seçin. Geri almak eski dosyayı göstermek kadar kolay olur.
- **Kamera kamera kanarya.** Önce tek bir kameranın Deployment'ını güncelleyin, işlenen kare hızını, p95 süreyi ve olay sayılarını bir süre izleyin, sonra ötekilere geçin.
- **Motoru hedefte yeniden üretin.** Sürücü, TensorRT ya da GPU değiştiğinde motoru o donanımda yeniden derlemek sürüm hattının parçası olmalı.
- **Geri alma komutu hazır olsun:**

```bash
kubectl -n video-analytics set image deployment/va-cam-07 analytics=registry.example.com/cx/video-analytics:1.4.3
kubectl -n video-analytics rollout status deployment/va-cam-07
kubectl -n video-analytics rollout undo deployment/va-cam-07     # sorun çıkarsa
```

Sürücü ve GPU Operator güncellemelerini düğümü boşaltarak (drain) ve bakım penceresinde yapın; kameralar düğümde tek olduğu için bu, o lokasyonda analitiğin kısa süre durması demek. MQTT tarafında olayların güvenli taşınması için [MQTT ile güvenli IoT](https://www.abdulazizakyol.com/blog/mqtt-ile-guvenli-iot-python-tls-ve-erisim-kontrolu/) yazısına bakabilirsiniz.

## Kontrol listesi

- [ ] Hostta sürücü ve NVIDIA Container Toolkit kurulu; `docker run --gpus all ... nvidia-smi` çalışıyor.
- [ ] İmaj çok aşamalı, kök olmayan kullanıcıyla çalışıyor, sürümler sabit; model imajın dışında.
- [ ] TensorRT motoru hedef makinede, imajdaki TensorRT sürümüyle üretildi.
- [ ] Parolalar ve akış adresleri Secret ya da `.env` içinde, sürüm kontrolünde değil.
- [ ] GPU yalnızca `limits` altında isteniyor; bellek sınırı ve CPU isteği tanımlı.
- [ ] Kamera podlarında `Recreate` stratejisi; çift sayım yok.
- [ ] GPU paylaşım yöntemi (time-slicing, MPS, MIG) yalıtım ihtiyacına göre seçildi; bellek ölçüldü.
- [ ] Liveness kontrolü ana döngüye bağlı, kamera kesintisi alarmla izleniyor.
- [ ] DCGM Exporter ve servis metrikleri Prometheus'ta; kod çözücü kullanımı panelde.
- [ ] Güncelleme önce tek kamerada deneniyor; geri alma komutu ve model dosyası geçmişi hazır.

## Sık sorulan sorular

### Docker konteynerinde GPU nasıl kullanılır?

Hosta NVIDIA sürücüsü ve NVIDIA Container Toolkit kurulur, sudo nvidia-ctk runtime configure --runtime=docker ile Docker yapılandırılıp yeniden başlatılır. Ardından docker run --gpus all ile ya da Compose'da deploy.resources.reservations.devices altında driver: nvidia ve capabilities: [gpu] tanımlanarak konteynere GPU verilir.

### Kubernetes'te GPU nasıl istenir?

Düğümde NVIDIA device plugin (tek başına ya da GPU Operator ile) çalışmalıdır. Pod, resources.limits altında nvidia.com/gpu: 1 ister; GPU yalnızca limits'te belirtilir, requests verilirse limits'e eşit olmalıdır ve kesirli değer kabul edilmez.

### Tek GPU birden fazla pod arasında nasıl paylaşılır?

Üç yol var: time-slicing ile GPU'yu belirli sayıda kopya olarak duyurmak (bellek ve hata yalıtımı yok), deneysel MPS desteğiyle belleği eşit paylara bölmek ya da MIG destekleyen GPU'ları donanım düzeyinde yalıtılmış bölümlere ayırmak.

### Sahadaki tek sunucuda Kubernetes kullanmak mantıklı mı?

Tek sunucuda Docker Compose çoğu zaman yeterlidir. Çok lokasyonda aynı dağıtımı merkezden yönetmek, sağlık kontrolü ve geri almayı standartlaştırmak gerekiyorsa k3s gibi hafif bir dağıtım tek düğümde de işe yarar.

### GPU kullanımı Prometheus ile nasıl izlenir?

NVIDIA DCGM Exporter, GPU kullanımı, bellek, sıcaklık, güç, kod çözücü kullanımı ve XID hataları gibi metrikleri varsayılan olarak 9400 portunda /metrics adresinden yayınlar. GPU Operator kurulumlarında DCGM Exporter varsayılan olarak gelir.

## Kaynaklar

1. [NVIDIA Container Toolkit — Installation Guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
2. [Docker Docs — GPU support in Docker Compose](https://docs.docker.com/compose/how-tos/gpu-support/)
3. [Kubernetes — Schedule GPUs](https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/)
4. [NVIDIA GPU Operator — Time-Slicing GPUs in Kubernetes](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html)
5. [K3s — Advanced Options (NVIDIA Container Runtime)](https://docs.k3s.io/advanced)
6. [NVIDIA DCGM — Install DCGM Exporter](https://docs.nvidia.com/datacenter/dcgm/latest/installation/install-dcgm-exporter.html)

---

Abdulaziz Akyol, CX Teknoloji'nin (yapay zekâ, görüntü işleme ve IoT) kurucusudur. Asıl sürüm: https://www.abdulazizakyol.com/blog/docker-ve-kubernetes-ile-gpu-goruntu-analitigi-servislerini-olceklemek/
