Xây dựng hệ thống ML Production với Docker, FastAPI và Kubernetes

1. Từ Jupyter Notebook đến Production: Hành trình vượt chướng ngại vật

Chuyển đổi mô hình machine learning từ môi trường phát triển sang production là một bước ngoặt đầy thách thức. Không giống như việc chạy thử nghiệm trên dataset sạch sẽ với GPU local, production đòi hỏi khả năng xử lý hàng triệu request mỗi ngày, chịu đựng dữ liệu bẩn, và duy trì hoạt động 24/7 ngay cả khi các dịch vụ phụ thuộc gặp sự cố.

Điểm mấu chốt: triển khai mô hình chỉ là khởi đầu của hành trình vận hành. Bài viết này tập trung vào các chiến lược engineering thực tiễn giúp mô hình trở thành "chiến binh" production thực thụ—chịu được áp lực traffic, tự phát hiện lỗi, và graceful degradation khi cần thiết.

2. Thay đổi tư duy: Từ script một lần sang service liên tục

2.1 Vấn đề của mindset notebook

Notebook ẩn chứa ba giả định nguy hiểm: dữ liệu tĩnh và hoàn hảo, tài nguyên vô hạn, và thực thi không trạng thái. Production phá vỡ tất cả: dữ liệu luôn chảy, tài nguyên chia sẻ, và lỗi request không thể "chạy lại".

Giải pháp là tái cấu trúc mô hình từ function được gọi thành process chạy liên tục—với quản lý vòng đời service, cô lập request context, và cơ chế phục hồi lỗi như circuit breaker.

2.2 Docker: Không chỉ là công cụ đóng gói

Docker image là "chứng minh thư số" và "khoang cách ly" của mô hình. Giá trị cốt lõi nằm ở tính xác định và khả năng audit—loại bỏ triệt để vấn đề "chạy được trên máy tôi".

Một image production chuẩn chỉ chứa tối thiểu: OS cơ bản, Python runtime, dependency thiết yếu, model weights, và web server nhẹ. Không pip, không curl, không bash—tuân thủ nguyên tắc least privilege.

2.3 Tách biệt feature engineering: Tại sao không nên nhét vào model service

Đưa logic feature engineering trực tiếp vào predict() là cái bẫy phổ biến. Feature thay đổi thường xuyên hơn model, và nhiều model có thể dùng chung feature.

Pattern đúng: Feature Store—dịch vụ độc lập nhận entity ID, trả về feature vector qua REST/gRPC. Model service chỉ gọi API này rồi inference thuần túy. Tách biệt này rút ngắn chu kỳ iteration từ ngày xuống giờ, đồng thời tăng độ ổn định.

3. Xây dựng service: Từ Dockerfile đến API

3.1 Dockerfile chuẩn production

# Chọn base image: ubuntu thay vì alpine để tránh lỗi musl libc
FROM ubuntu:22.04

# Tạo non-root user (UID 1001)
RUN groupadd -g 1001 -r mlrunner && \
    useradd -S -u 1001 -r -g mlrunner -m mlrunner
USER mlrunner

# Cài dependency hệ thống cho scientific computing
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libglib2.0-0 libsm6 libxext6 libxrender-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1

# Layer caching: requirements trước, code sau
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

COPY . .

# Tải model từ artifact repository, đặt quyền read-only
RUN mkdir -p /app/weights && \
    wget -O /app/weights/production_model.bin \
    https://artifacts.company.com/models/v2.1.0/production_model.bin && \
    chmod 444 /app/weights/production_model.bin

# exec để nhận SIGTERM đúng cách
ENTRYPOINT ["sh", "-c"]
CMD ["exec uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4"]

Giải thích chi tiết:

  • ubuntu:22.04: glibc tương thích 100% với PyTorch/TensorFlow, tránh lỗi biên dịch trên alpine
  • USER mlrunner: Giảm attack surface nếu container bị compromise
  • Layer caching: requirements.txt ít thay đổi hơn source code, tách riêng giúp tái sử dụng layer
  • wget model: Tránh commit file lớn vào Git, đảm bảo versioned artifacts
  • exec uvicorn: Thay thế shell process, đảm bảo PID 1 nhận signal từ Kubernetes

3.2 FastAPI + Uvicorn: Lựa chọn tối ưu cho ML serving

So sánh thực nghiệm trên server 4-core 8GB:

FrameworkQPS peakP99 latency
Flask (4 workers)1,200280ms
FastAPI + Uvicorn (4 workers)3,80095ms

Lợi thế then chốt: async-native. Uvicorn dựa trên uvloophttptools, cho phép xử lý concurrent request trong một worker—khi một request đang chờ feature service, worker chuyển sang xử lý request khác thay vì block.

3.3 Pattern API với validation tự động

from fastapi import FastAPI
from pydantic import BaseModel, Field
import torch

app = FastAPI()

class InferenceRequest(BaseModel):
    user_id: str = Field(..., min_length=8, max_length=32)
    item_ids: list[str] = Field(..., min_items=1, max_items=100)
    context: dict = Field(default_factory=dict)

class PredictionResponse(BaseModel):
    scores: list[float]
    model_version: str
    inference_time_ms: float

@app.post("/predict", response_model=PredictionResponse)
async def predict(payload: InferenceRequest):
    # Validation tự động qua Pydantic
    features = await fetch_features(payload.user_id, payload.item_ids)
    scores = model_engine.inference(features)
    return PredictionResponse(
        scores=scores.tolist(),
        model_version="2.1.0",
        inference_time_ms=calculate_elapsed()
    )

FastAPI tự động sinh OpenAPI docs, validate input type, và trả 422 với message rõ ràng nếu client gửi sai định dạng—không cần viết defensive code thủ công.

4. Tối ưu inference: Cold start và memory management

4.1 Giải quyết cold start

Model BERT 1.2GB có thể mất 45s khởi động—vượt quá timeout của Kubernetes liveness probe. Giải pháp: warm-up trong startup event.

@app.on_event("startup")
async def initialize():
    global inference_engine
    inference_engine = ModelEngine.load("/app/weights/production_model.bin")
    
    # Warm-up với dummy input
    dummy = torch.randn(1, 128, dtype=torch.long)
    with torch.no_grad():
        _ = inference_engine.predict(dummy)
    logger.info("Model warm-up completed, ready for traffic")

4.2 Kiểm soát memory

File weights 500MB có thể chiếm 2.3GB VRAM vì PyTorch mặc định FP32 + gradient buffer. Giải pháp:

# Quantization động sang INT8
model.eval()
optimized = torch.quantization.quantize_dynamic(
    model, {torch.nn.Linear}, dtype=torch.qint8
)

# Giải phóng cache sau load
torch.cuda.empty_cache()

Quantization giảm 75% memory, tăng 2-3x throughput, với đánh đổi accuracy ~1-2%—chấp nhận được cho hầu hết use case.

5. Local development: Mô phỏng production với Docker Compose

version: '3.8'
services:
  inference-api:
    build: .
    ports: ["8000:8000"]
    environment:
      FEATURE_ENDPOINT: http://feature-mock:8080
      REDIS_URL: redis://cache:6379
    depends_on: [feature-mock, cache]

  feature-mock:
    image: feature-service-mock:latest
    volumes: ["./mocks/features:/data"]
    ports: ["8080:8080"]

  cache:
    image: redis:7-alpine
    ports: ["6379:6379"]

Chạy docker-compose up để có môi trường giống hệt production topology—phát hiện 90% vấn đề integration (DNS, timeout, authentication) trước khi deploy.

Testing strategy

  • Unit test: pytest với boundary inputs (NaN, empty, oversized)
  • Integration test: httpx gọi thật đến local endpoint
  • Chaos test: docker pause feature-mock, verify circuit breaker và fallback response

6. CI/CD pipeline: Chuỗi vàng tự động hóa

  1. Static analysis: pylint + bandit—block nếu có critical/high severity
  2. Test execution: pytest với coverage threshold 85%
  3. Multi-arch build: docker buildx cho linux/amd64linux/arm64
  4. Vulnerability scan: trivy—fail pipeline nếu phát hiện CRITICAL/HIGH CVE
  5. GitOps deploy: Tự động tạo PR vào infra repo với image tag mới, trigger kubectl apply

7. Kubernetes: Governance tinh vi

7.1 Resource quota

resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "2Gi"
    cpu: "1000m"

Đặt dựa trên kết quả stress test: limits = 1.3 × P95 peak, requests = 1.1 × P50 median.

7.2 HPA theo custom metric

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: inference-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: inference-api
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: External
    external:
      metric:
        name: nginx_ingress_requests_per_second
      target:
        type: AverageValue
        averageValue: "100"

Dùng QPS thay vì CPU vì inference có thể CPU-intensive nhưng throughput thấp (model phức tạp), hoặc ngược lại.

7.3 Service mesh (Istio)

  • Distributed tracing: Trace ID xuyên suốt gateway → inference → feature service
  • Circuit breaker: Tự động ngắt khi downstream error rate > 50%
  • Canary release: Route 10% traffic đến version mới, monitor rồi mới tăng dần

8. Observability: Tam giác vàng metrics-logs-traces

LayerCông cụKey signals
MetricsPrometheus + Grafana5xx rate, P95 latency, memory utilization, downstream success rate
LogsLokiStructured JSON với request_id, user_id, model_version
TracesJaegerWaterfall breakdown: network vs compute vs external calls

Query mẫu cho troubleshooting

# Loki: Tìm lỗi 500 cụ thể
{service="inference-api"} |~ "500" | json | __error__!="None"

# Prometheus: Tính error rate 5 phút
rate(http_requests_total{code=~"5.."}[5m])

# Jaeger: Tìm trace chậm nhất
service="inference-api" duration>500ms

9. Troubleshooting guide: Những vấn đề kinh điển

SymptomRoot causeFix
Pod restart loop, livenessProbe fail Cold start > probe timeout Tăng initialDelaySeconds hoặc thêm warm-up
High QPS, low CPU, slow response I/O block (feature service timeout) Giảm HTTP timeout, thêm circuit breaker
Memory tăng dần rồi OOM Memory leak hoặc batch quá lớn Giới hạn batch size, gọi gc.collect()
Latency spike định kỳ GC pause hoặc model reload Tune GC, tách reload sang background thread

Thẻ: docker FastAPI Kubernetes mlops prometheus

Đăng vào ngày 13 tháng 8 lúc 12:34