nlp_structbert_sentence-similarity_chinese-large 容器化部署进阶:使用Docker Compose编排多服务依赖

你是不是已经成功在本地跑通了 nlp_structbert_sentence-similarity_chinese-large 这个强大的中文语义相似度模型?单服务跑起来固然可喜,但一个真正能用的应用,往往不是孤军奋战的。想想看,你的模型服务可能需要一个数据库来存储历史查询、需要一个缓存来加速高频请求、甚至还需要一个前端界面来交互。手动一个个启动、配置、管理这些服务,不仅繁琐,还容易出错。

今天,我们就来解决这个问题。我将带你从“单兵作战”升级到“集团军协同”,使用 Docker Compose 来编排一个完整的语义搜索应用栈。我们会把模型服务、Redis缓存、MySQL数据库这三个服务打包在一起,实现一键启动、统一管理。整个过程基于星图平台的镜像,但我们会更进一步,通过 Dockerfile 进行一些定制化,让整个部署更贴合实际生产需求。

1. 为什么需要 Docker Compose?从单服务到应用栈

刚开始接触容器时,我们习惯用 docker run 一条命令启动一个服务。这就像组装电脑时,你一个个地插上CPU、内存、硬盘。但当你的应用变得复杂,比如我们这个语义相似度服务,它背后可能需要:

  1. Redis:缓存高频的句子对相似度计算结果,下次遇到相同请求直接返回,大幅降低模型推理压力,提升响应速度。
  2. MySQL:持久化存储用户查询日志、句子对数据、或是业务相关的元数据,方便后续分析和审计。

如果手动管理,你需要:

  • 打开三个终端窗口。
  • 分别运行三条复杂的 docker run 命令,每条命令都要设置网络、卷、环境变量。
  • 确保它们启动顺序正确(数据库要先于应用启动)。
  • 清理时也要分别停止、删除三个容器。

这太容易出错了。Docker Compose 就是来解决这个问题的。它允许你用一个 docker-compose.yml 文件,定义整个应用所需的所有服务、网络、数据卷。然后,只需要一条命令 docker-compose up,所有服务就会按照定义好的依赖关系,有序地启动起来。它把部署从“手工组装”变成了“一键部署”。

接下来,我们就要动手创建这样一个完整的编排配置。

2. 项目结构与准备:规划你的容器化蓝图

在写代码之前,好的目录结构能让一切更清晰。我们先创建一个项目文件夹,并规划好里面的内容。

mkdir structbert-similarity-stack && cd structbert-similarity-stack

创建后的目录结构应该是这样的:

structbert-similarity-stack/
├── docker-compose.yml       # 编排核心文件
├── model-server/
│   ├── Dockerfile          # 定制模型服务的Dockerfile
│   ├── app.py              # 我们的Flask/FastAPI应用代码
│   └── requirements.txt    # Python依赖
├── mysql-init/
│   └── init.sql            # 数据库初始化脚本
├── redis/
│   └── redis.conf          # Redis自定义配置文件(可选)
└── .env                    # 环境变量配置文件(用于敏感信息)

我来简单解释一下:

  • docker-compose.yml:这是总指挥棒,定义了所有服务和它们之间的关系。
  • model-server/:这是我们核心模型服务的“家”。里面的 Dockerfile 用于在星图基础镜像上安装我们额外的依赖(比如Web框架)。
  • mysql-init/:存放初始化数据库的SQL脚本,比如创建表、插入基础数据。当MySQL容器首次启动时会自动执行。
  • redis/:可以放一个自定义的Redis配置文件,比如设置密码、调整内存策略。
  • .env:一个非常好的实践,把密码、密钥等敏感信息放在这里,而不是硬编码在YAML文件中。

现在,我们先来编写最核心的模型服务代码。

3. 编写模型服务:让 StructBERT 提供 HTTP API

模型本身很强大,但我们需要一个“翻译官”,把HTTP请求转换成模型调用,并把结果返回。这里我用一个简单的 Flask 应用来示例。你也可以用 FastAPI,性能会更好。

首先,创建 model-server/requirements.txt

flask>=2.0.0
redis>=4.0.0
pymysql>=1.0.0

然后,创建 model-server/app.py。这个文件稍长,但逻辑很清晰:

from flask import Flask, request, jsonify
import torch
from transformers import AutoTokenizer, AutoModel
import numpy as np
from redis import Redis
import pymysql
import json
import os

app = Flask(__name__)

# ===== 初始化组件 =====
# 1. 加载模型和分词器(这是核心)
print("正在加载模型和分词器...")
tokenizer = AutoTokenizer.from_pretrained("/app/model")  # 模型挂载路径
model = AutoModel.from_pretrained("/app/model")
model.eval()  # 设置为评估模式
print("模型加载完毕!")

# 2. 连接Redis缓存
redis_host = os.getenv('REDIS_HOST', 'redis')
redis_port = int(os.getenv('REDIS_PORT', 6379))
redis_client = Redis(host=redis_host, port=redis_port, decode_responses=True)

# 3. 连接MySQL数据库
db_config = {
    'host': os.getenv('MYSQL_HOST', 'mysql'),
    'user': os.getenv('MYSQL_USER', 'root'),
    'password': os.getenv('MYSQL_PASSWORD', 'example'),
    'database': os.getenv('MYSQL_DATABASE', 'similarity_db'),
    'charset': 'utf8mb4'
}
# 注意:应用启动时数据库可能还没就绪,这里先定义配置,实际查询时再连接。

# ===== 工具函数 =====
def get_sentence_embedding(sentence):
    """计算单个句子的向量表示"""
    inputs = tokenizer(sentence, return_tensors='pt', padding=True, truncation=True, max_length=128)
    with torch.no_grad():
        outputs = model(**inputs)
    # 使用 [CLS] token 的表示作为句子向量
    return outputs.last_hidden_state[:, 0, :].squeeze().numpy()

def cosine_similarity(vec_a, vec_b):
    """计算余弦相似度"""
    return np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))

# ===== API路由 =====
@app.route('/health', methods=['GET'])
def health():
    """健康检查端点"""
    return jsonify({'status': 'healthy', 'service': 'structbert-similarity'})

@app.route('/similarity', methods=['POST'])
def calculate_similarity():
    """计算两个句子的语义相似度"""
    data = request.json
    if not data or 'text1' not in data or 'text2' not in data:
        return jsonify({'error': '请提供 text1 和 text2 参数'}), 400

    text1, text2 = data['text1'], data['text2']

    # 第一步:检查Redis缓存
    cache_key = f"sim:{text1}:{text2}"
    cached_result = redis_client.get(cache_key)
    if cached_result:
        print(f"缓存命中: {cache_key}")
        return jsonify({'similarity': float(cached_result), 'cached': True})

    # 第二步:未命中缓存,进行模型推理
    print(f"计算相似度: '{text1}' vs '{text2}'")
    emb1 = get_sentence_embedding(text1)
    emb2 = get_sentence_embedding(text2)
    sim_score = float(cosine_similarity(emb1, emb2))

    # 第三步:将结果存入Redis(设置1小时过期)
    redis_client.setex(cache_key, 3600, sim_score)

    # 第四步:可选,将查询日志存入MySQL
    try:
        connection = pymysql.connect(**db_config)
        with connection.cursor() as cursor:
            sql = "INSERT INTO query_log (text1, text2, similarity) VALUES (%s, %s, %s)"
            cursor.execute(sql, (text1, text2, sim_score))
        connection.commit()
        connection.close()
    except Exception as e:
        print(f"写入数据库失败(不影响主流程): {e}")

    return jsonify({'similarity': sim_score, 'cached': False})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

这段代码做了几件关键事:

  1. 加载模型:从容器内的 /app/model 路径加载我们挂载进来的 StructBERT 模型。
  2. 连接外部服务:通过环境变量获取 Redis 和 MySQL 的连接信息,体现了容器间通信。
  3. 实现核心逻辑/similarity 接口接收两个句子,先查缓存,没有再算,算完存缓存和数据库。
  4. 考虑了容错:数据库写入失败不会影响主流程,只是打印日志。

4. 定制模型服务镜像:编写 Dockerfile

星图平台提供的 nlp_structbert_sentence-similarity_chinese-large 镜像已经包含了模型和基础环境。但我们需要在上面安装 Flask、Redis 等依赖,并复制我们的应用代码。这就需要 Dockerfile

创建 model-server/Dockerfile

# 使用星图平台提供的基础镜像
FROM your-registry.cn-beijing.cr.aliyuncs.com/csdn_mirrors/nlp_structbert_sentence-similarity_chinese-large:latest

# 设置工作目录
WORKDIR /app

# 将当前目录的依赖文件和应用代码复制到容器内
COPY requirements.txt .
COPY app.py .

# 安装Python依赖(在基础镜像的Python环境中)
# 注意:基础镜像的pip可能版本较旧,可以先升级
RUN pip install --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# 暴露Flask应用端口
EXPOSE 5000

# 启动命令
CMD ["python", "app.py"]

重要提示:你需要将 FROM 后面的镜像地址替换成星图镜像广场上该镜像的真实地址。这个地址通常在镜像详情页可以找到。

5. 编写 Docker Compose 编排文件:定义整个乐团

现在,我们来编写指挥整个应用栈的乐谱——docker-compose.yml。把它放在项目根目录。

version: '3.8'

services:
  # 服务1: MySQL 数据库
  mysql:
    image: mysql:8.0
    container_name: similarity-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-strongpassword}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-similarity_db}
      MYSQL_USER: ${MYSQL_USER:-app_user}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-app_password}
    volumes:
      # 持久化数据
      - mysql_data:/var/lib/mysql
      # 初始化脚本
      - ./mysql-init:/docker-entrypoint-initdb.d
    ports:
      - "3306:3306"
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  # 服务2: Redis 缓存
  redis:
    image: redis:7-alpine
    container_name: similarity-redis
    restart: unless-stopped
    command: redis-server --appendonly yes  # 开启持久化
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # 服务3: 我们的核心模型服务
  model-server:
    build: ./model-server  # 使用我们刚才写的Dockerfile构建
    container_name: structbert-similarity-api
    restart: unless-stopped
    depends_on:
      mysql:
        condition: service_healthy  # 等待MySQL健康检查通过
      redis:
        condition: service_healthy   # 等待Redis健康检查通过
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - MYSQL_HOST=mysql
      - MYSQL_USER=${MYSQL_USER:-app_user}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD:-app_password}
      - MYSQL_DATABASE=${MYSQL_DATABASE:-similarity_db}
    volumes:
      # 关键!将星图镜像中的模型挂载到容器内
      # 假设你已经将模型文件下载到了本地的 ./pretrained-model 目录
      - ./pretrained-model:/app/model
    ports:
      - "5000:5000"
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

# 定义网络,让三个服务在同一个内部网络,通过服务名通信
networks:
  app-network:
    driver: bridge

# 定义数据卷,实现数据持久化
volumes:
  mysql_data:
  redis_data:

这个文件是精华所在,我来拆解一下:

  • version:指定 Compose 文件格式版本。
  • services:定义了三个服务。
    • mysqlredis:使用官方镜像,配置了环境变量、数据卷、端口映射和健康检查
    • model-server:使用我们自定义的 Dockerfile 构建。depends_on 确保了启动顺序,并且是等待它们“健康”后才启动。环境变量指向另外两个服务的容器名(redis, mysql),这是 Docker 网络内置的 DNS 解析功能。volumes 将本地模型目录挂载进去。
  • networks:创建了一个名为 app-network 的桥接网络,三个服务加入后,可以直接用服务名相互访问,无需知道IP。
  • volumes:定义了命名的数据卷,确保数据库和Redis的数据在容器删除后依然保留。

6. 准备配置与初始化脚本

为了让应用跑起来,我们还需要最后几步准备工作。

第一步:创建环境变量文件 .env 在项目根目录创建 .env 文件,管理敏感信息:

MYSQL_ROOT_PASSWORD=your_very_strong_root_password
MYSQL_USER=app_user
MYSQL_PASSWORD=your_app_db_password
MYSQL_DATABASE=similarity_db

Compose 文件中的 ${MYSQL_ROOT_PASSWORD:-strongpassword} 语法意思是:优先使用 .env 文件中的值,如果没有则用默认值 strongpassword

第二步:准备模型文件 你需要从星图镜像中提取模型文件,或者从 Hugging Face 等渠道下载 nlp_structbert_sentence-similarity_chinese-large 模型,并放置到项目根目录的 ./pretrained-model 文件夹下。这是挂载卷所指向的路径。

第三步:创建数据库初始化脚本 创建 mysql-init/init.sql,用于创建日志表:

CREATE TABLE IF NOT EXISTS query_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    text1 TEXT NOT NULL,
    text2 TEXT NOT NULL,
    similarity FLOAT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

7. 一键启动与测试:见证编排魔法

所有文件就绪后,启动变得极其简单。

在项目根目录(docker-compose.yml 所在目录)执行:

# 启动所有服务(后台运行)
docker-compose up -d

# 查看所有服务状态
docker-compose ps

# 查看模型服务的日志,确认启动无误
docker-compose logs -f model-server

当看到模型服务日志显示“模型加载完毕!”并且健康检查通过后,就可以测试了。

打开浏览器或使用 curl 命令:

# 健康检查
curl http://localhost:5000/health

# 测试相似度计算
curl -X POST http://localhost:5000/similarity \
  -H "Content-Type: application/json" \
  -d '{"text1": "今天天气真好", "text2": "阳光明媚的一天"}'

你应该会收到一个包含 similarity 分数的 JSON 响应。第一次查询 cachedfalse,再次查询相同的句子对,就会看到 cached 变成 true,响应速度会快很多。

你还可以连接 MySQL 验证数据是否写入:

# 进入MySQL容器
docker-compose exec mysql mysql -uapp_user -p similarity_db
# 输入密码后执行
SELECT * FROM query_log LIMIT 5;

管理命令也非常方便

# 停止所有服务
docker-compose down

# 停止并删除所有数据卷(谨慎使用!会清空数据库)
docker-compose down -v

# 重新构建并启动(修改Dockerfile后使用)
docker-compose up -d --build

8. 总结

走完这一趟,你会发现原本复杂的多服务部署,被 Docker Compose 梳理得井井有条。我们不仅跑通了模型,还构建了一个具备缓存、持久化能力的微服务化应用原型。这种方式的优势非常明显:

  • 环境隔离:每个服务都在自己的容器里,互不干扰。
  • 一键部署docker-compose up -d 解决了所有依赖和启动顺序问题。
  • 易于扩展:如果想增加一个监控服务(如 Prometheus),只需在 docker-compose.yml 里添加几行定义。
  • 配置即代码:整个应用栈的配置都保存在文件中,可以版本化管理,轻松复现。

当然,这只是一个起点。在实际生产环境中,你可能还需要考虑更多,比如用 Nginx 做反向代理和负载均衡、配置更完善的日志收集、或者使用 Docker Swarm / Kubernetes 进行集群编排。但通过今天这个实战,你已经掌握了容器化编排的核心思想和方法,有了这个基础,向更复杂的架构演进也会更加顺畅。

下次当你再遇到需要组合多个服务的项目时,不妨先想想:能不能用 Docker Compose 把它们“包”起来?


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐