前言
在团队内部署文档翻译服务时,我们通常面临几个挑战:
- 环境一致性:开发、测试、生产环境差异导致的诡异问题
- 依赖管理:AI翻译依赖的Python包、模型权重、系统库等
- 弹性扩缩容:不同时间段的翻译请求量差异巨大
- 资源隔离:避免单个翻译任务占用过多GPU/CPU影响其他任务
Docker容器化是解决这些问题的标准方案。本文以PDFTranslator翻译服务为例,分享一套完整的容器化部署方案,包括Dockerfile、docker-compose、Nginx反向代理、生产环境调优等实战内容。
环境准备
- Docker 20.10+
- Docker Compose v2.0+
- 推荐运行环境:Linux(Ubuntu 20.04+ / CentOS 8+)
- 至少 2 核 4GB 内存(小团队场景)
项目结构
pdf-translator-deploy/
├── docker/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── entrypoint.sh
├── nginx/
│ ├── nginx.conf
│ └── conf.d/
│ └── translator.conf
├── docker-compose.yml
├── .env.example
└── README.md
Step 1: Dockerfile
多阶段构建镜像,减小最终镜像体积。
# docker/Dockerfile
# ============================================
# Stage 1: 依赖构建
# ============================================
FROM python:3.11-slim AS builder
WORKDIR /build
# 安装系统依赖(构建阶段需要的编译工具)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
libffi-dev \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
# 使用 wheels 缓存加速构建
RUN pip wheel --no-cache-dir --wheel-dir /wheels \
-r requirements.txt
# ============================================
# Stage 2: 运行时镜像
# ============================================
FROM python:3.11-slim AS runtime
# 元数据
LABEL maintainer="ops@example.com" \
version="1.0.0" \
description="PDFTranslator Service Container"
# 安装运行时系统依赖(精简版)
RUN apt-get update && apt-get install -y --no-install-recommends \
libgomp1 \
libxml2 \
libxslt1.1 \
fonts-noto-cjk \
fonts-noto-color-emoji \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# 创建非root用户(安全最佳实践)
RUN groupadd -r appuser && useradd -r -g appuser -d /app appuser
WORKDIR /app
# 复制 wheels 并安装
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \
&& rm -rf /wheels
# 复制应用代码
COPY --chown=appuser:appuser . /app
# 设置权限
RUN chmod +x /app/entrypoint.sh
USER appuser
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
EXPOSE 8000
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["gunicorn", "app:app", "--config", "gunicorn_conf.py"]
# docker/requirements.txt
flask==3.0.0
gunicorn==21.2.0
requests==2.31.0
pdfplumber==0.10.4
PyMuPDF==1.23.21
python-dotenv==1.0.0
redis==5.0.1
celery==5.3.4
prometheus-client==0.19.0
# docker/entrypoint.sh
#!/bin/bash
set -e
echo "=== PDFTranslator Service Starting ==="
echo "Working directory: $(pwd)"
echo "User: $(whoami)"
echo "Python version: $(python --version)"
# 等待依赖服务(Redis等)
if [ -n "$REDIS_HOST" ]; then
echo "Waiting for Redis at $REDIS_HOST:${REDIS_PORT:-6379}..."
until python -c "import socket; s=socket.socket(); s.settimeout(2); s.connect(('$REDIS_HOST', ${REDIS_PORT:-6379}))" 2>/dev/null; do
echo " Redis not ready, retrying in 2s..."
sleep 2
done
echo "Redis is ready!"
fi
# 执行传入的命令
exec "$@"
Step 2: docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
# ==========================================
# 主翻译服务
# ==========================================
translator-app:
build:
context: ./docker
dockerfile: Dockerfile
image: pdf-translator:latest
container_name: pdf-translator-app
restart: unless-stopped
environment:
- FLASK_ENV=production
- REDIS_HOST=redis
- REDIS_PORT=6379
- MAX_UPLOAD_MB=20
- WORKERS=4
- LOG_LEVEL=INFO
volumes:
- ./logs:/app/logs
- ./uploads:/app/uploads
depends_on:
redis:
condition: service_healthy
networks:
- translator-net
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
reservations:
cpus: '0.5'
memory: 1G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# ==========================================
# 异步任务队列(Celery worker)
# ==========================================
translator-worker:
image: pdf-translator:latest
container_name: pdf-translator-worker
restart: unless-stopped
command: celery -A tasks.celery_app worker --loglevel=info --concurrency=2
environment:
- REDIS_HOST=redis
- CELERY_BROKER_URL=redis://redis:6379/1
volumes:
- ./logs:/app/logs
depends_on:
- redis
- translator-app
networks:
- translator-net
deploy:
resources:
limits:
cpus: '4.0'
memory: 8G
# ==========================================
# Redis 缓存与队列
# ==========================================
redis:
image: redis:7.2-alpine
container_name: pdf-translator-redis
restart: unless-stopped
command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- translator-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ==========================================
# Nginx 反向代理
# ==========================================
nginx:
image: nginx:1.25-alpine
container_name: pdf-translator-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./ssl:/etc/nginx/ssl:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- translator-app
networks:
- translator-net
volumes:
redis-data:
driver: local
networks:
translator-net:
driver: bridge
Step 3: Nginx 配置
# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time';
access_log /var/log/nginx/access.log main;
# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# 上传文件大小限制(PDF翻译需要支持20MB)
client_max_body_size 25M;
# 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript
application/xml+rss application/atom+xml image/svg+xml;
# 包含其他配置
include /etc/nginx/conf.d/*.conf;
}
# nginx/conf.d/translator.conf
# 强制 HTTPS
server {
listen 80;
server_name translate.example.com;
# Let's Encrypt 验证路径
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# 其他请求强制跳转 HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS 主服务
server {
listen 443 ssl http2;
server_name translate.example.com;
# SSL 证书
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# 安全响应头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# PDF 翻译接口(大文件上传)
location /api/translate {
proxy_pass http://translator-app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 长超时(PDF翻译可能需要3-5分钟)
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# 缓冲设置
proxy_request_buffering off;
proxy_buffering off;
}
# 静态资源
location /static/ {
alias /app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# 健康检查端点
location /health {
access_log off;
proxy_pass http://translator-app:8000/health;
}
# 限流(防止滥用)
location / {
limit_req zone=translator burst=20 nodelay;
proxy_pass http://translator-app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 限流配置(在 http 块中添加)
# limit_req_zone $binary_remote_addr zone=translator:10m rate=10r/s;
Step 4: Gunicorn 配置
# docker/gunicorn_conf.py
import multiprocessing
import os
# 基础配置
bind = "0.0.0.0:8000"
workers = int(os.getenv("WORKERS", multiprocessing.cpu_count() * 2 + 1))
worker_class = "gthread" # 使用线程处理 IO 密集型请求
threads = 2
# 超时设置
timeout = 300 # 5分钟超时(PDF翻译可能耗时较长)
graceful_timeout = 60
keepalive = 5
# 性能优化
max_requests = 1000
max_requests_jitter = 100
preload_app = True
# 日志
accesslog = "-"
errorlog = "-"
loglevel = os.getenv("LOG_LEVEL", "info").lower()
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(L)s'
# 进程名
proc_name = "pdf-translator"
Step 5: 部署与运维命令
# ==========================================
# 首次部署
# ==========================================
# 1. 复制环境变量模板
cp .env.example .env
vim .env # 修改配置
# 2. 构建镜像
docker-compose build --no-cache
# 3. 启动服务
docker-compose up -d
# 4. 查看启动日志
docker-compose logs -f translator-app
# ==========================================
# 日常运维
# ==========================================
# 查看服务状态
docker-compose ps
# 重启某个服务
docker-compose restart translator-app
# 查看资源使用
docker stats pdf-translator-app
# 查看实时日志
docker-compose logs -f --tail=100
# 进入容器调试
docker-compose exec translator-app bash
# ==========================================
# 扩缩容
# ==========================================
# 水平扩展 worker 节点(适合 CPU 密集型场景)
docker-compose up -d --scale translator-worker=3
# 临时调整 app 实例数
docker-compose up -d --scale translator-app=2
# ==========================================
# 滚动更新
# ==========================================
# 1. 重新构建镜像
docker-compose build translator-app
# 2. 滚动重启(不中断服务)
docker-compose up -d --no-deps --build translator-app
# 3. 清理旧镜像
docker image prune -f
Step 6: 监控与日志
# 添加到 docker-compose.yml(Prometheus + Grafana 监控)
prometheus:
image: prom/prometheus:latest
container_name: pdf-translator-prometheus
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
networks:
- translator-net
grafana:
image: grafana/grafana:latest
container_name: pdf-translator-grafana
volumes:
- grafana-data:/var/lib/grafana
ports:
- "3000:3000"
depends_on:
- prometheus
networks:
- translator-net
volumes:
prometheus-data:
grafana-data:
性能调优 Checklist
部署完成后,建议按以下顺序进行性能验证:
- 冷启动时间:首次请求 vs 第 100 次请求的响应时间对比
- 并发能力:用 wrk 或 ab 测试并发 100 时的 TPS
- 内存泄漏:跑 24 小时压测,观察内存变化趋势
- GPU 利用率:如果是 GPU 版本,用 nvidia-smi 监控利用率
- 磁盘 IO:翻译过程中是否有大量磁盘读写,必要时换 SSD
- 网络带宽:大文件上传时是否打满带宽
常见问题排查
| 问题 | 排查方向 |
|---|---|
| 服务启动失败 | docker-compose logs translator-app 查看启动日志 |
| 上传 413 错误 | 检查 Nginx client_max_body_size 和 Flask MAX_CONTENT_LENGTH |
| 翻译超时 | 调整 Nginx proxy_read_timeout 和 Gunicorn timeout |
| 内存占用高 | 检查是否有未释放的文件句柄,添加 worker_max_requests |
| Redis 连接失败 | 检查网络连通性:docker-compose exec translator-app ping redis |
总结
容器化部署的核心价值不在于技术本身,而在于**"一次构建,到处运行"的可重复性**。无论是开发自测、CI集成、生产部署,都能基于同一套镜像完成。
对于 PDF 翻译这种IO 密集 + CPU 密集混合型的服务,关键是:
- 合理拆分:Web 服务 + Worker 队列独立扩缩容
- 资源限制:避免单个任务占用全部资源
- 优雅降级:超时和异常情况下给出友好提示
- 可观测性:完整的日志、监控、告警体系
这套方案已经在多个生产环境稳定运行半年以上,可以作为团队内部部署文档翻译服务的参考模板。
参考资料
标签:Docker、容器化、Nginx、DevOps、AI服务部署

163

被折叠的 条评论
为什么被折叠?



