Data API 8 — Triển khai & vận hành production

13 thg 7, 2026 3 lượt xem
#deployment
#observability
#kubernetes
#docker
#python
#fastapi

Bảy bài trước đã dựng nên một data API tương đối hoàn chỉnh: tổng quan REST, FastAPI căn bản, validation, tích hợp database, bảo mật, async & hiệu năngtesting & tài liệu. Bài cuối này trả lời câu hỏi thực tế nhất: làm sao đưa nó lên production và vận hành ổn định cho một hệ thống dữ liệu ngân hàng — nơi downtime, rò rỉ credential hay mất dữ liệu đều là sự cố nghiêm trọng.

1. Chạy production: ASGI server và số worker

FastAPI là ứng dụng ASGI, không tự phục vụ HTTP. Ở dev bạn dùng lệnh quen thuộc:

uvicorn app.main:app --reload --port 8000

--reload theo dõi file và khởi động lại tiến trình khi code đổi — cực tiện lúc viết nhưng tuyệt đối không dùng trên production (tốn CPU, không ổn định, chỉ 1 tiến trình).

Vấn đề cốt lõi: một tiến trình Python bị giới hạn bởi GIL, chỉ thực thi bytecode trên một core tại một thời điểm. Muốn tận dụng máy nhiều core, bạn phải chạy nhiều tiến trình worker, mỗi worker là một bản sao ứng dụng, và một tiến trình cha (process manager) phân phối kết nối, giám sát, tự khởi động lại worker chết. Có hai cách phổ biến:

# Cách 1: uvicorn tự quản nhiều worker
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

# Cách 2: gunicorn làm process manager, uvicorn làm worker class
gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 --bind 0.0.0.0:8000 \
  --timeout 60 --graceful-timeout 30

Gunicorn trưởng thành hơn về mặt quản lý tiến trình (max-requests để chống rò rỉ bộ nhớ, timeout tách bạch). Trong môi trường Kubernetes, nhiều team lại chọn 1 worker/pod và để K8s scale số pod — đơn giản hơn, mỗi pod một đơn vị tài nguyên rõ ràng.

Chọn số worker thế nào? Công thức tham khảo cho workload I/O-bound (data API chủ yếu chờ DB) là 2 × số_core + 1. Nhưng con số này chỉ là điểm khởi đầu — data API thường bị chặn ở connection pool của database, không phải CPU. Nếu mỗi worker mở pool 10 connection và bạn có 4 worker × 8 pod = 320 connection hướng vào một Postgres chỉ chịu được 200, bạn sẽ làm sập DB trước khi hết CPU. Luôn tính tổng connection = worker × pod × pool_size và so với giới hạn DB.

Tình huốngGợi ý
Máy đơn, bare-metalgunicorn + uvicorn worker, 2×core+1
Kubernetes1–2 worker/pod, để HPA scale số pod
Endpoint CPU nặng (tính toán, ML inference)worker theo số core, cân nhắc offload

Một điểm hay bị bỏ sót: process manager tồn tại để tự chữa lành. Một worker có thể chết vì exception không bắt được, rò rỉ bộ nhớ, hoặc bị OOM-kill. Nếu bạn tự chạy python main.py không có gì giám sát, worker chết là service ngừng. Gunicorn/uvicorn parent theo dõi worker, restart tiến trình chết, và (với --max-requests) chủ động recycle worker sau N request để dọn rò rỉ bộ nhớ tích luỹ — quan trọng với service chạy nhiều ngày liền. Trong K8s, vai trò "chữa lành" này được chia sẻ: liveness probe restart pod, còn process manager lo trong phạm vi pod.

2. Đóng gói Docker

Container hoá cho môi trường build reproducible và deploy nhất quán. Dùng multi-stage build để tách môi trường cài đặt khỏi image chạy, cho image nhỏ và ít bề mặt tấn công.

# ---- Stage build: cài dependency ----
FROM python:3.12-slim AS builder
WORKDIR /app
ENV PIP_NO_CACHE_DIR=1
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt

# ---- Stage runtime: image gọn, chạy non-root ----
FROM python:3.12-slim
WORKDIR /app
# Tạo user không đặc quyền
RUN useradd --create-home --uid 10001 appuser
COPY --from=builder /install /usr/local
COPY ./app ./app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD python -c "import urllib.request,sys; \
  sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"
CMD ["gunicorn", "app.main:app", "--worker-class", \
     "uvicorn.workers.UvicornWorker", "--workers", "2", \
     "--bind", "0.0.0.0:8000"]

Nguyên tắc quan trọng:

  • Base slim thay vì full: nhỏ hơn hàng trăm MB. Cân nhắc distroless nếu muốn tối giản tối đa.
  • Non-root: chạy dưới appuser (UID cố định), không dùng root — nguyên tắc bảo mật bắt buộc với hệ thống ngân hàng.
  • .dockerignore: loại .git, __pycache__, .env, tests/, .venv khỏi build context. Vừa nhanh vừa tránh vô tình đóng gói secret vào image.
  • HEALTHCHECK: cho Docker/orchestrator biết container có sống không, map với health endpoint từ bài 7.
  • Ghim version base image (python:3.12-slim, tốt hơn nữa là ghim theo digest) để build ổn định.

Lưu ý build sau proxy công ty: khi build image trong mạng NCB có proxy chặn TLS, pip/npm sẽ báo lỗi SSL vì không tin CA nội bộ. Phải copy CA công ty vào image và cập nhật trust store (hoặc dùng PIP_CERT) trước bước pip install. Chi tiết đã ghi ở bài Docker.

3. Triển khai Kubernetes

K8s cho phép scale, self-healing và rollout an toàn. Manifest tối thiểu gồm Deployment + Service, gắn probe vào health endpoint.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ncb-data-api
spec:
  replicas: 3
  selector:
    matchLabels: { app: ncb-data-api }
  template:
    metadata:
      labels: { app: ncb-data-api }
    spec:
      containers:
        - name: api
          image: registry.ncb.internal/data-api:1.8.0
          ports: [{ containerPort: 8000 }]
          envFrom:
            - configMapRef: { name: data-api-config }   # cấu hình
            - secretRef: { name: data-api-secret }       # credential
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits:   { cpu: "1",    memory: "512Mi" }
          readinessProbe:
            httpGet: { path: /health/ready, port: 8000 }
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /health/live, port: 8000 }
            initialDelaySeconds: 15
            periodSeconds: 20

Điểm mấu chốt là phân biệt hai probe (xem thêm k8s workloads):

  • livenessProbe hỏi "tiến trình còn sống không?". Fail → K8s kill và restart pod. Chỉ nên trả fail khi thực sự treo, KHÔNG kiểm tra DB ở đây (DB chậm không có nghĩa app cần restart).
  • readinessProbe hỏi "pod có sẵn sàng nhận traffic không?". Fail → K8s ngừng gửi request vào pod nhưng không kill. Ở đây nên kiểm tra kết nối DB/dependency: nếu DB down, pod tự rút khỏi Service để không nhận request lỗi.

Tự động scale bằng HPA (Horizontal Pod Autoscaler):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ncb-data-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ncb-data-api
  minReplicas: 3
  maxReplicas: 15
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }

Đặt minReplicas ≥ 2 (ở đây là 3) để luôn có dự phòng khi rollout hoặc pod chết.

4. Cấu hình qua biến môi trường & secret

Nguyên tắc 12-factor: mọi thứ khác nhau giữa dev/staging/prod phải nằm ở biến môi trường, không hardcode trong code hay image. Pydantic BaseSettings đọc env sạch sẽ:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    jwt_secret: str
    log_level: str = "INFO"
    class Config:
        env_file = ".env"   # chỉ cho dev, KHÔNG commit

Phân biệt rạch ròi:

  • ConfigMap cho cấu hình không nhạy cảm: log level, timeout, feature flag, tên service.
  • Secret cho database_url, jwt_secret, API key. Không bao giờ để trong ConfigMap, image, hay Git. Với ngân hàng, tốt nhất tích hợp secret manager (Vault, cloud KMS) để credential được xoay vòng (rotate) tự động và có audit ai truy cập lúc nào. K8s Secret mặc định chỉ base64-encode (không phải mã hoá) — phải bật encryption-at-rest cho etcd.

5. Observability

Không có quan sát được thì không vận hành được. Ba trụ cột:

Structured logging — log dạng JSON kèm request_id (correlation id) xuyên suốt một request. Middleware gắn id vào mỗi request và mọi dòng log của nó:

import logging, uuid
from starlette.middleware.base import BaseHTTPMiddleware

class RequestIDMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        rid = request.headers.get("X-Request-ID", str(uuid.uuid4()))
        request.state.request_id = rid
        response = await call_next(request)
        response.headers["X-Request-ID"] = rid
        return response

Khi một truy vấn giao dịch báo lỗi, bạn tra request_id là gom được toàn bộ hành trình của request đó qua các service.

Metrics — expose /metrics theo định dạng Prometheus (thư viện prometheus-fastapi-instrumentator hoặc prometheus-client): số request, latency theo percentile (p50/p95/p99), tỷ lệ lỗi 5xx, độ sâu connection pool. Đây là dữ liệu để HPA scale và để cảnh báo.

Tracing — với kiến trúc nhiều service, distributed tracing (OpenTelemetry → Jaeger/Tempo) cho biết một request tiêu tốn thời gian ở đâu: gateway, API hay DB.

Health/readiness endpoint — như đã dùng ở probe: /health/live nhẹ (chỉ trả 200), /health/ready kiểm tra dependency.

Bốn tín hiệu nên cảnh báo (alert) ngay từ đầu, thường gọi là golden signals: latency (p95/p99 vượt ngưỡng), traffic (QPS bất thường), errors (tỷ lệ 5xx), và saturation (CPU/memory/connection pool gần cạn). Đừng chờ người dùng báo lỗi — connection pool đầy 100% hay tỷ lệ 5xx tăng phải kích hoạt cảnh báo tới on-call trước khi thành sự cố diện rộng. Với ngân hàng, gắn thêm cảnh báo nghiệp vụ: đột biến số lần từ chối xác thực (401/403) có thể là dấu hiệu tấn công dò credential.

6. Kiến trúc triển khai

Traffic đi từ client qua Ingress/Gateway (nơi kết thúc TLS, áp rate limit và xác thực), tới Service, phân phối vào các pod; HPA theo dõi tải và tăng/giảm số pod; pod đọc/ghi Postgres và đẩy metrics/log ra hệ thống giám sát.

7. Vận hành

Graceful shutdown. Khi rollout, K8s gửi SIGTERM rồi chờ terminationGracePeriodSeconds. Ứng dụng phải ngừng nhận request mới nhưng hoàn tất các request đang xử lý rồi mới thoát — nếu không, người dùng gặp lỗi giữa chừng. Uvicorn/gunicorn xử lý SIGTERM sẵn; dùng FastAPI lifespan để đóng connection pool sạch sẽ. Đặt graceful-timeout (gunicorn) đủ lớn hơn request dài nhất.

Migration DB an toàn. Thay đổi schema là điểm rủi ro nhất. Nguyên tắc backward-compatible / expand-contract: tách migration thành các bước không phá vỡ code cũ (thêm cột nullable trước, deploy code dùng cột mới, rồi mới xoá cột cũ ở lần sau). Chạy migration (Alembic) như một bước riêng trước khi rollout code — dùng K8s Job hoặc init step, KHÔNG chạy tự động lúc app khởi động (nhiều pod cùng migrate sẽ đua nhau). Luôn backup trước migration trên hệ thống ngân hàng.

Blue-green / canary. Giảm rủi ro rollout:

  • Blue-green: dựng môi trường mới (green) song song, kiểm tra xong mới chuyển toàn bộ traffic; lỗi thì switch lại blue tức thì.
  • Canary: đưa version mới cho một phần nhỏ traffic (ví dụ 5%), theo dõi error rate/latency, tốt thì tăng dần. Hợp với API dữ liệu có lưu lượng lớn.

Rate limit ở gateway. Áp giới hạn tốc độ tại Ingress/API Gateway (theo API key hoặc IP) thay vì trong app — bảo vệ DB khỏi bị một client quét dữ liệu làm nghẽn, và tách concern khỏi business logic. Bổ trợ cho phần bảo mật ở bài 5.

Toàn bộ chuỗi build → test → scan → deploy nên tự động qua pipeline CI/CD (xem devops CI/CD), để mọi lần lên production đều lặp lại được và có dấu vết audit.

Use case thực tế

Team dữ liệu NCB triển khai API tra cứu lịch sử giao dịch cho ứng dụng nội bộ chi nhánh. Yêu cầu: giờ cao điểm (9–11h sáng) khoảng 1.200 request/giây, p95 latency phải < 300ms, không được downtime khi phát hành bản mới.

Cấu hình thực tế:

  1. Đóng gói: Dockerfile multi-stage, base python:3.12-slim, chạy non-root, image ~180MB. Build trong CI có CA công ty đã cài để pip qua proxy không lỗi TLS.
  2. Kubernetes: Deployment minReplicas: 3, mỗi pod 2 uvicorn worker × pool 10 = 20 connection/pod. HPA maxReplicas: 15 → tối đa 300 connection, dưới giới hạn 350 của Postgres primary. Read query định tuyến sang replica.
  3. Probe: /health/ready kiểm tra DB → khi replica lag, pod tự rút khỏi Service. /health/live chỉ trả 200.
  4. Rollout canary: version mới nhận 10% traffic trong 15 phút; dashboard Prometheus theo dõi 5xx và p95; đạt ngưỡng thì mở 100%.
  5. Sự cố: một hôm p99 tăng vọt. Nhờ request_id trong log JSON + trace, xác định nghẽn ở một query thiếu index trên transactions.account_id, không phải lỗi API. Thêm index, p95 về lại 210ms.

Kết quả: giờ cao điểm HPA scale lên 11 pod, p95 ổn định ~230ms, phát hành 2 lần/tuần không downtime nhờ canary + graceful shutdown.

Ghi nhớ

  • Production KHÔNG dùng --reload; chạy nhiều worker qua gunicorn + uvicorn worker hoặc để K8s scale số pod.
  • Số worker/pool phải tính theo tổng connection vào DB (worker × pod × pool), không chỉ theo CPU — data API thường nghẽn ở DB.
  • Docker: multi-stage, base slim, non-root, .dockerignore (tránh lọt secret), HEALTHCHECK; build sau proxy công ty phải tin CA nội bộ.
  • K8s: livenessreadiness — liveness đừng check DB; readiness thì có, để pod tự rút khi dependency lỗi. HPA đặt minReplicas ≥ 2.
  • Cấu hình qua env (12-factor); credential vào Secret / secret manager, không hardcode, không commit, bật encryption-at-rest.
  • Observability = structured log có request_id + metrics Prometheus /metrics + tracing; health endpoint làm nền cho probe.
  • Vận hành an toàn: graceful shutdown, migration expand-contract chạy riêng có backup, blue-green/canary, rate limit ở gateway.
  • Checklist go-live ngân hàng: TLS + auth ở gateway, secret được rotate, backup DB + kế hoạch restore, giám sát + cảnh báo, log audit, tài liệu OpenAPI cập nhật.

Bài viết liên quan

Exception handling, context manager (with), đọc/ghi file, JSON/CSV và logging đúng cách.

13 thg 7, 2026 4

Định nghĩa hàm, tham số, *args/**kwargs, lambda, module/package, pip và virtualenv.

13 thg 7, 2026 4

Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.

13 thg 7, 2026 4

Kết nối CSDL với SQLAlchemy, gọi REST API với requests/httpx, và lập trình bất đồng bộ asyncio.

13 thg 7, 2026 4