Xây dựng Docker Image chứa Code Inference và Model Artifact
Chúng ta cần đóng gói mã nguồn dự đoán (inference code) cùng với model artifact đã được huấn luyện và lưu trữ trong MLflow vào một Docker image duy nhất. Việc này đảm bảo môi trường chạy inference giống hệt môi trường huấn luyện, loại bỏ lỗi "chạy được trên máy tôi nhưng không chạy trên server".
Tạo file Dockerfile tại đường dẫn /app/Dockerfile với nội dung hoàn chỉnh như sau:
FROM python:3.9-slim
WORKDIR /app
# Cài đặt các thư viện cần thiết cho inference
RUN pip install --no-cache-dir \
flask \
gunicorn \
scikit-learn \
mlflow \
pandas \
numpy
# Copy code inference vào container
COPY app.py .
COPY requirements.in .
# Tải model artifact từ MLflow Registry vào container
# Thay thế các biến bên dưới bằng thông tin thực tế của bạn
ENV MLFLOW_TRACKING_URI="http://mlflow:5000"
ENV MODEL_NAME="churn-prediction"
ENV MODEL_VERSION="1"
# Tải model xuống trước khi khởi động container
RUN mlflow models download \
--model-uri "models:/${MODEL_NAME}/${MODEL_VERSION}" \
--dest-path "/app/model"
# Cấu hình Gunicorn để phục vụ Flask
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--threads", "4", "app:app"]
Kết quả mong đợi: File Dockerfile được tạo, sẵn sàng để build image. Model artifact sẽ được tải xuống vào thư mục /app/model ngay khi image được build.
Build Docker image sử dụng Docker CLI tại thư mục chứa Dockerfile:
docker build -t my-ai-inference:latest .
Kết quả mong đợi: Docker build hoàn tất, xuất hiện dòng "Successfully built [IMAGE_ID]". Image này chứa sẵn code và model, không cần kết nối mạng để tải model lúc runtime.
Verify kết quả bằng cách chạy container thử nghiệm và kiểm tra file model có tồn tại:
docker run --rm my-ai-inference:latest ls -la /app/model
Kết quả mong đợi: Danh sách file trong thư mục model (bao gồm MLmodel, data, conda.yaml...) hiện ra trên terminal.
Viết Kubernetes Manifest (Deployment, Service, ConfigMap)
Chúng ta sẽ tạo các manifest file để định nghĩa cách Kubernetes quản lý container inference. Bao gồm ConfigMap để quản lý biến môi trường, Deployment để giữ số lượng Pod ổn định, và Service để expose API ra bên ngoài.
Tạo file ConfigMap tại /k8s/configmap.yaml để lưu các biến môi trường động, giúp tách biệt cấu hình khỏi code:
apiVersion: v1
kind: ConfigMap
metadata:
name: inference-config
namespace: dataops
data:
MLFLOW_TRACKING_URI: "http://mlflow:5000"
MODEL_NAME: "churn-prediction"
MODEL_VERSION: "1"
WORKERS: "2"
THREADS: "4"
Kết quả mong đợi: File YAML hợp lệ, chứa các key-value cho biến môi trường.
Tạo file Deployment tại /k8s/deployment.yaml để định nghĩa Pod chạy inference service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: inference-deployment
namespace: dataops
spec:
replicas: 2
selector:
matchLabels:
app: inference
template:
metadata:
labels:
app: inference
spec:
containers:
- name: inference
image: my-ai-inference:latest
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: inference-config
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
Kết quả mong đợi: Deployment được cấu hình với 2 replica, sử dụng image đã build, mount biến môi trường từ ConfigMap, và thiết lập resource limits cụ thể.
Tạo file Service tại /k8s/service.yaml để expose Pod inference ra bên ngoài cluster (NodePort hoặc LoadBalancer tùy môi trường):
apiVersion: v1
kind: Service
metadata:
name: inference-service
namespace: dataops
spec:
selector:
app: inference
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer
Kết quả mong đợi: Service được định nghĩa, ánh xạ cổng 80 của Service vào cổng 8000 của container.
Verify kết quả bằng cách validate cú pháp YAML trước khi áp dụng:
kubectl apply --dry-run=client -f /k8s/
Kết quả mong đợi: Dòng thông báo "namespace/dataops created", "configmap/inference-config created", "deployment.apps/inference-deployment created", "service/inference-service created" xuất hiện mà không có lỗi.
Triển khai Container Inference lên Kubernetes Cluster
Bước này sẽ áp dụng toàn bộ manifest đã viết vào cluster thực tế. Chúng ta giả định đã có cluster chạy sẵn và đã cấu hình kubectl để kết nối.
Tạo namespace để cô lập các tài nguyên DataOps:
kubectl create namespace dataops
Kết quả mong đợi: Dòng "namespace/dataops created".
Áp dụng tất cả các manifest file (ConfigMap, Deployment, Service) vào namespace dataops:
kubectl apply -f /k8s/ -n dataops
Kết quả mong đợi: Các resource được tạo hoặc cập nhật (Created/Updated). Không có lỗi syntax hay resource quota.
Đẩy Docker image vào Registry của Kubernetes (nếu chưa đẩy, cần chạy docker push trước đó). Ở đây giả định image đã sẵn sàng trong cluster registry hoặc quay.io:
kubectl rollout status deployment/inference-deployment -n dataops
Kết quả mong đợi: Kubernetes bắt đầu pull image, tạo Pod, và chờ Pod sẵn sàng. Thông báo cuối cùng là "deployment "inference-deployment" successfully rolled out".
Verify kết quả bằng cách kiểm tra trạng thái của Pod và Service:
kubectl get pods -n dataops -l app=inference
Kết quả mong đợi: 2 Pod có trạng thái Running và READY 2/2.
Kiểm tra Service đã được cấp IP chưa:
kubectl get svc -n dataops
Kết quả mong đợi: Service inference-service có trạng thái EXTERNAL-IP (hoặc <none> nếu là ClusterIP, nhưng ở đây dùng LoadBalancer nên sẽ có IP).
Tối ưu hóa Resource Request/Limits cho Pod Inference
Việc đặt đúng request và limits giúp Kubernetes Scheduler đặt Pod vào Node phù hợp và tránh tình trạng Pod bị kill do thiếu RAM (OOMKilled) hoặc CPU bị throttling quá mức gây latency cao.
Quan sát tài nguyên thực tế mà Pod đang tiêu thụ trong quá trình inference:
kubectl top pods -n dataops -l app=inference
Kết quả mong đợi: Hiển thị cột CPU và MEMORY tiêu thụ thực tế của từng Pod. Ví dụ: CPU 120m, Memory 300Mi.
Điều chỉnh file /k8s/deployment.yaml dựa trên số liệu quan sát được. Nếu thấy Pod thường xuyên dùng hết 512Mi RAM, tăng limit lên 768Mi. Nếu CPU thường thấp, giảm request để tiết kiệm chi phí:
apiVersion: apps/v1
kind: Deployment
metadata:
name: inference-deployment
namespace: dataops
spec:
replicas: 2
# ... phần selector và template metadata giữ nguyên ...
template:
spec:
containers:
- name: inference
# ... phần port và envFrom giữ nguyên ...
resources:
requests:
memory: "384Mi"
cpu: "300m"
limits:
memory: "768Mi"
cpu: "800m"
Kết quả mong đợi: File deployment.yaml được cập nhật với các giá trị resource mới phù hợp với tải thực tế.
Áp dụng lại cấu hình để Kubernetes thực hiện rolling update:
kubectl apply -f /k8s/deployment.yaml -n dataops
Kết quả mong đợi: Deployment được cập nhật, Kubernetes sẽ tạo Pod mới với resource mới và loại bỏ Pod cũ từ từ.
Verify kết quả bằng cách kiểm tra lại trạng thái Pod và xem log nếu có sự cố về resource:
kubectl describe pod -n dataops -l app=inference | grep -A 10 "Events"
Kết quả mong đợi: Không có sự kiện nào ghi OOMKilled hoặc BackOff. Các Pod đều ở trạng thái Running.
Test lại API để đảm bảo hiệu năng không bị giảm sau khi điều chỉnh resource:
curl -X POST http://:80/predict -d '{"features": [1, 2, 3]}'
Kết quả mong đợi: Response trả về kết quả dự đoán nhanh chóng (dưới 200ms) và không bị timeout.
Điều hướng series:
Mục lục: Series: Xây dựng nền tảng DataOps với DVC, MLflow và Kubernetes cho vòng đời AI
« Phần 5: Quản lý vòng đời mô hình với MLflow
Phần 6: Triển khai mô hình AI lên Kubernetes »