Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

📖 ⏱️ ~24 min read 🎯 Intermediate

部署与运维

📝 Before You Continue: 请先读完 11.511.4。本节把它们容器化编排成一个可一键启动、可监控、可排障的系统。

前面完成了离线、在线、前端的开发,可在本地跑通。但如何在其他机器快速复现?本节用 Docker Compose 做容器化部署。

读完本章,你将能够:

  • 解释为何用 Docker Compose(环境一致/快速启动/隔离/易扩展)
  • 读懂 docker-compose.yaml 中五个服务的配置,理解容器间用服务名 DNS 通信
  • 描述前端多阶段构建(Node 构建 + Nginx 服务)如何减小镜像
  • 执行完整启动流程:启基础设施 → 跑离线 → 导数据 → 建索引 → 访问
  • docker compose ps、健康检查、redis-cli 排查常见问题
  • 完成 4 道分层练习题

11.6.0 为什么用 Docker Compose

本项目依赖五个服务:PostgreSQL(业务数据)、Redis(特征缓存)、Elasticsearch(搜索)、后端 API、前端应用。模型文件经共享目录在离/在线间传递。

手动部署需每台机装 PG/Redis/ES、配网络、处理版本兼容——繁琐易错、环境差异致各种问题。Docker Compose 用 声明式 YAML 描述所有服务及依赖,一条命令启动全系统。优势:

  1. 环境一致性 :容器含全部依赖,开发/测试/生产环境一致。
  2. 快速启动docker compose up 按依赖顺序自动起,免手动装配。
  3. 隔离与安全 :各服务独立容器,互不干扰。
  4. 易于扩展 :加服务只需改配置,不动现有。

11.6.1 Docker Compose 配置详解

docker-compose.yaml 定义六个服务(含后端构建)。逐一介绍。

数据库 PostgreSQL

services:
  postgres:
    image: postgres:15-alpine
    container_name: funrec-postgres
    environment:
      POSTGRES_USER: funrec
      POSTGRES_PASSWORD: funrec123
      POSTGRES_DB: funrec_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data          # ← KEY LINE: 命名卷持久化数据
    networks:
      - funrec-network

volumes 命名卷 postgres_data 持久化数据,容器删数据仍在;networks 使其能与其他服务通信。

缓存 Redis

  redis:
    image: redis:7-alpine
    container_name: funrec-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - funrec-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5                                       # ← KEY LINE: 连续失败才判不健康

healthcheck 定期 redis-cli ping,连续 5 次超时(每次 3s)判不健康,依赖它的服务可等其健康再起。

搜索 Elasticsearch

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.2.0
    container_name: funrec-elasticsearch
    environment:
      - discovery.type=single-node                     # ← KEY LINE: 单节点模式,适合开发
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"               # ← KEY LINE: 限制 JVM 堆,防开发机内存爆
    ports:
      - "9200:9200"
      - "9300:9300"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    networks:
      - funrec-network
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

后端 FastAPI

  backend:
    build:
      context: ./backend
      dockerfile: dockerfile
    container_name: funrec-backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://funrec:funrec123@postgres:5432/funrec_db
      - REDIS_URL=redis://redis:6379/0
      - ELASTICSEARCH_URL=http://elasticsearch:9200
      - MODEL_DEPLOY_DIR=/app/tmp/web_project/deployed_models
    volumes:
      - ./backend:/app
      - ../tmp:/app/tmp
      - ${FUNREC_RAW_DATA_PATH}:/data
    depends_on:
      - postgres
      - elasticsearch
      - redis
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    networks:
      - funrec-network

注意数据库/Redis 地址用 服务名 (如 postgresredis)而非 localhost——容器间靠 Docker DNS 解析。后端 Dockerfile 分层构建(先装依赖再拷代码),改码不重装依赖:

FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y gcc postgresql-client curl \
    && rm -rf /var/lib/apt/lists/*
RUN pip install uv
COPY pyproject.toml ./
RUN uv pip install --system -e .                  # ← KEY LINE: 先装依赖层,利用缓存
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

前端(多阶段构建) :先 Node 构建静态文件,再 Nginx 服务:

  frontend:
    build:
      context: ./frontend
      dockerfile: dockerfile
    container_name: funrec-frontend
    ports:
      - "3000:80"
    depends_on:
      - backend
    networks:
      - funrec-network

Dockerfile 多阶段——最终镜像仅含构建产物 + Nginx,不含 Node/开发依赖:

# 构建阶段
FROM node:22-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build                                # ← KEY LINE: 生成 dist 静态产物
# 生产阶段
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html   # ← KEY LINE: 仅拷产物,镜像更小
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

网络与数据 :末尾定义共享网络与命名卷:

volumes:
  postgres_data:
  redis_data:
  elasticsearch_data:
networks:
  funrec-network:
    driver: bridge                             # ← KEY LINE: 同桥接网络,服务名互通

部署拓扑:五服务同处 funrec-network,共享卷持久化,模型经共享目录在离/在线传递


11.6.2 环境准备与启动流程

前置条件 :安装 Docker、Docker Compose(Desktop 内置)、uv(pip install uv)。验证:

docker --version
docker compose version
uv --version

数据准备 :下载 funrec-movielens-1m.zip 解压,记录绝对路径(含 movies.pkl/ratings.pkl/users.pkl/image/)。

获取代码 :本项目全部代码位于 datawhalechina/fun-rec 仓库的 web_project/ 目录:

git clone https://github.com/datawhalechina/fun-rec.git
cd fun-rec/web_project

环境变量 :复制 .env.example.env 并设数据路径:

cd web_project
cp .env.example .env
# 编辑 .env:
# FUNREC_RAW_DATA_PATH=/path/to/funrec-movielens-1m
# FUNREC_PROCESSED_DATA_PATH=/path/to/funrec-processed

FUNREC_PROCESSED_DATA_PATH 存特征工程与训练中间产物,需可写。

启动基础设施

docker compose up --build                    # 首次构建镜像
docker compose up -d --build                 # 后台运行
docker compose logs -f backend               # 看后端日志

运行离线流程 (训练模型、初始化数据):

cd backend
uv sync
make run-offline-pipeline

依次执行:特征工程 → 训练 YoutubeDNN/DeepFM → 特征上线 Redis → 模型部署共享目录(约 10–20 分钟)。

加载数据到数据库

make ingest-data-to-database                 # 建表 + 导入用户/电影/评分 + 建测试用户

索引电影到 Elasticsearch

make index-movies-to-elasticsearch           # 标题/类型/演员可搜索

访问应用

服务地址说明
前端http://localhost:3000用户界面
后端 APIhttp://localhost:8000API 服务
API 文档http://localhost:8000/docsSwagger
Elasticsearchhttp://localhost:9200搜索服务

测试账号:test@funrec.com / test123456。登录后可见个性化推荐、搜索、详情、评分。


11.6.3 服务健康检查与调试

检查状态

docker compose ps
# NAME  STATUS  PORTS ... 全部应为 Up

某服务 Exited/Restarting 即启动失败,查日志。

验证各服务

curl http://localhost:8000/health           # 后端 → {"status": "healthy"}
docker exec -it funrec-postgres pg_isready -U funrec   # PG → accepting connections
docker exec -it funrec-redis redis-cli ping            # Redis → PONG
curl http://localhost:9200                          # ES → 版本信息

查 Redis 数据 (验证特征上线):

docker exec -it funrec-redis redis-cli hget user:6041:profile frequent_genres
docker exec -it funrec-redis redis-cli llen user:6041:history

常见问题排查

  • 容器启动失败docker compose logs backend;查 .env 路径、端口占用、依赖未就绪。
  • 数据库连接失败docker compose logs postgres,看 ready to accept connections
  • 模型加载失败ls ${FUNREC_PROCESSED_DATA_PATH}/web_project/deployed_models/;无文件则重跑离线流程。
  • 搜索无结果curl http://localhost:9200/_cat/indices;无 movies 索引则重跑索引命令。

Analysis: 部署的难点不在「写配置」,而在「让五服务按依赖顺序健康起来、并能快速定位故障」。健康检查(healthcheck)、命名卷、服务名 DNS、日志与 redis-cli 探查,共同构成可观测、可恢复的交付基线。


⚠️ Common Mistakes in 11.6

#MistakeExampleWhy It's WrongFix
1容器间用 localhostbackend 连 localhost:5432容器内 localhost 是自身,非 PG用服务名 postgres
2不挂持久卷容器删数据丢命名卷才持久化挂 postgres_data 等卷
3ES 不限内存默认堆吃满开发机卡顿/OOMES_JAVA_OPTS 限 512m
4忘跑离线流程直接访问推荐 → 空无模型/特征先 make run-offline-pipeline
5前端不构建只拷源码不 npm run buildNginx 无 dist多阶段构建生成 dist

本章小结

📌 Key Takeaways

ConceptKey PointsWhy It Matters
Compose 价值一致/快启/隔离/易扩一键复现多服务系统
服务名 DNSpostgres/redis 互访容器间通信基础
持久卷命名卷存数据容器删数据不丢
多阶段构建Node 构建 + Nginx 服务前端镜像最小
启动流程启设施→离线→导数→索引→访问顺序不可乱
健康与排障healthcheck + logs + cli可观测可恢复

❓ FAQ

Q1: 为什么后端用服务名而不是宿主机 IP?

A: 同一 Docker 网络内,Compose 内置 DNS 把服务名解析到容器 IP。用 localhost 在容器内指向自身而非 PG,必然连不上。服务名是容器间通信的正确方式。

Q2: 模型文件怎么从离线容器到在线容器?

A: 经共享目录(卷挂载):离线 deploy_localdeployed_models/,在线 RecallResourceManager 从同挂载路径读,靠 active.json 指版本。本质是「文件传递」而非「网络调用」。

Q3: 前端多阶段构建省了什么?

A: 最终镜像只含 dist/ + Nginx,不含 Node.js 与 node_modules 等开发依赖,镜像体积与攻击面都小,生产更安全更快。

🔗 前后关联

  • 11.3make run-offline-pipeline 即本节离线步骤的入口。
  • 11.4 在线服务经 MODEL_DEPLOY_DIR 卷加载本节部署的模型。
  • 11.5 前端经本节 Nginx 多阶段构建提供静态服务。
  • 11.1 的技术选型(PG/Redis/ES/Compose)在此落成运维配置。

Practice Problems

Work through all problems in order — they get progressively harder. Each has a complete solution you can reveal after trying it yourself.


Problem 11.6.1 — 容器通信 🟢 Easy

后端 DATABASE_URLpostgresql://funrec:funrec123@localhost:5432/funrec_db 会怎样?正确写法是什么?

💡 Solution (click to reveal)

答: 容器内 localhost 指向后端自身,连不到 PostgreSQL,启动报连接拒绝。正确用服务名:@postgres:5432(Compose DNS 解析到 PG 容器)。

Key points:

  • 容器网络内用服务名互访。
  • localhost 在容器里是「自己」。

Problem 11.6.2 — 数据持久化 🟢 Easy

若不挂 postgres_data 命名卷,容器 docker compose down 后数据会怎样?挂卷后呢?

💡 Solution (click to reveal)

答: 不挂卷:容器文件系统随删而失,用户/电影/评分全丢,下次需重新 ingest-data-to-database。挂命名卷:数据存宿主机卷,容器删重建后数据仍在。

Key points:

  • 有状态服务必须挂持久卷。
  • 卷与容器生命周期解耦。

Problem 11.6.3 — 启动顺序 🟡 Medium

若跳过 make run-offline-pipeline 直接访问前端首页推荐,会发生什么?给出根因与最小修复步骤。

💡 Solution (click to reveal)

答: 后端 /health 可能 healthy(服务起了),但推荐 API 加载不到模型/物品向量 → RecallResourceManager 资源缺失,召回失败或返回空。根因:在线依赖离线产出的模型与特征。修复:cd backend && uv sync && make run-offline-pipeline,再 make ingest-data-to-databasemake index-movies-to-elasticsearch

Key points:

  • 离线是「生产」、在线是「消费」,顺序不能反。
  • 健康 ≠ 功能就绪,需验证资源存在。

🏆 Challenge: 加一个缓存预热 🔴 Hard

生产希望后端启动时主动把热门电影向量与高频用户画像预热进 Redis,减少首屏冷请求延迟。请基于本章组件,指出这一改动要动哪一层、需注意什么(150 字内)。

💡 Hint

改动在线服务启动钩子(如 RecallResourceManager._ensure_resources_loaded 后):批量从 PG 读高频用户画像/历史写入 Redis,热门电影向量本就在 item_embeddings.npy 直接加载。注意:预热要在依赖的 PG/Redis 健康后做(depends_on + 重试),且只预热热点避免 Redis 膨胀;可用后台任务不阻塞启动。