明白了!既然您需要使用 Shell 脚本 来完成部署,而不使用 Dockerfile,我将为您提供一个系统性的教程,帮助您优化部署步骤,并编写两个高效的 Shell 脚本来完成一键部署启动。
目录
前提条件
在开始之前,请确保您具备以下条件:
- 操作系统:Linux 或支持容器管理的类 Unix 系统。
- Docker:已安装并正确配置。参考 Docker 官方安装文档。
- Docker Compose(可选):如果需要同时管理多个容器,建议安装 Docker Compose。参考 Docker Compose 安装指南。
- Conda:已安装在宿主机上。参考 Conda 官方安装指南。
- 项目文件:包括前台服务代码、大模型权重文件、以及两个 Conda 环境的
tar.gz
包。
项目结构
假设您的项目目录结构如下:
project-root/
├── frontend/
│ ├── startup.py
│ ├── start/
│ │ └── ... (其他代码文件)
│ └── ... (其他前台相关文件)
├── model/
│ ├── Qwen2___5-7B-Instruct/
│ │ └── ... (模型权重文件)
│ └── ... (其他模型相关文件)
├── conda_env_frontend.tar.gz
├── conda_env_model.tar.gz
├── deploy_frontend.sh
├── deploy_model.sh
└── README.md
准备 Conda 环境压缩包
确保您已经将两个 Conda 环境导出为 tar.gz
文件。如果尚未完成,请按照以下步骤操作:
-
安装
conda-pack
工具(如果尚未安装):conda install -c conda-forge conda-pack
-
打包前台服务的 Conda 环境:
conda activate frontend_env conda pack -o conda_env_frontend.tar.gz
-
打包大模型服务的 Conda 环境:
conda activate model_env conda pack -o conda_env_model.tar.gz
确保生成的 conda_env_frontend.tar.gz
和 conda_env_model.tar.gz
位于项目根目录下。
编写部署脚本
接下来,我们将编写两个 Shell 脚本:一个用于部署前台服务,另一个用于部署大模型服务。这些脚本将自动执行以下操作:
- 创建容器。
- 复制必要的文件到容器中。
- 解压 Conda 环境并激活。
- 启动相应的服务。
部署前台服务的 Shell 脚本
创建一个名为 deploy_frontend.sh
的脚本,内容如下:
#!/bin/bash
# 部署前台服务脚本
# 使用方法: ./deploy_frontend.sh
# 设置变量
CONTAINER_NAME="frontend_service"
IMAGE_NAME="ubuntu:20.04"
CONDA_ENV_TAR="conda_env_frontend.tar.gz"
HOST_PORT=8000
CONTAINER_PORT=8000
FRONTEND_CODE_DIR="/app/frontend"
STARTUP_DIR="/app/frontend/start"
START_COMMAND="python startup.py -a"
# 检查是否以 root 或 sudo 用户运行
if [[ $EUID -ne 0 ]]; then
echo "此脚本需要以 root 或 sudo 权限运行。"
exit 1
fi
# 检查 Docker 是否安装
if ! command -v docker &> /dev/null
then
echo "Docker 未安装,请先安装 Docker。"
exit 1
fi
# 检查 Conda 环境压缩包是否存在
if [ ! -f "$CONDA_ENV_TAR" ]; then
echo "Conda 环境压缩包 $CONDA_ENV_TAR 不存在。"
exit 1
fi
# 如果容器已存在,先停止并删除
if [ "$(docker ps -a | grep $CONTAINER_NAME)" ]; then
echo "停止并删除已有的容器 $CONTAINER_NAME ..."
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME
fi
# 创建容器
echo "创建容器 $CONTAINER_NAME ..."
docker run -d --name $CONTAINER_NAME -p $HOST_PORT:$CONTAINER_PORT $IMAGE_NAME sleep infinity
# 复制前台代码到容器
echo "复制前台代码到容器 ..."
docker cp frontend/ $CONTAINER_NAME:$FRONTEND_CODE_DIR/
# 复制 Conda 环境压缩包到容器
echo "复制 Conda 环境压缩包到容器 ..."
docker cp $CONDA_ENV_TAR $CONTAINER_NAME:/tmp/
# 在容器中安装必要的系统依赖和 Miniconda
echo "在容器中安装系统依赖和 Miniconda ..."
docker exec $CONTAINER_NAME bash -c "
apt-get update && \
apt-get install -y wget bzip2 ca-certificates curl git && \
rm -rf /var/lib/apt/lists/* && \
wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && \
bash /tmp/miniconda.sh -b -p /opt/conda && \
rm /tmp/miniconda.sh && \
export PATH=/opt/conda/bin:\$PATH && \
conda config --set always_yes yes --set changeps1 no && \
mkdir -p /opt/conda_env && \
tar -xzf /tmp/$CONDA_ENV_TAR -C /opt/conda_env && \
rm /tmp/$CONDA_ENV_TAR
"
# 设置环境变量并启动前台服务
echo "设置环境变量并启动前台服务 ..."
docker exec $CONTAINER_NAME bash -c "
export PATH=/opt/conda_env/bin:\$PATH && \
cd $STARTUP_DIR && \
$START_COMMAND &
"
echo "前台服务已成功部署并启动。访问 http://<宿主机IP>:$HOST_PORT 查看服务。"
脚本说明:
- 变量设置:定义容器名称、基础镜像、Conda 环境压缩包路径、端口号等。
- 权限检查:确保脚本以
root
或sudo
权限运行,因为 Docker 需要这些权限。 - 依赖检查:检查 Docker 是否已安装,Conda 环境压缩包是否存在。
- 容器管理:如果同名容器已存在,先停止并删除。
- 创建容器:基于指定的基础镜像(
ubuntu:20.04
)创建一个新的容器,并在后台运行。 - 文件复制:将前台服务代码和 Conda 环境压缩包复制到容器中。
- 容器内设置:
- 安装系统依赖(
wget
,bzip2
, 等)。 - 安装 Miniconda。
- 解压 Conda 环境。
- 安装系统依赖(
- 启动服务:激活 Conda 环境,进入前台服务的启动目录,执行启动命令。
部署大模型服务的 Shell 脚本
创建一个名为 deploy_model.sh
的脚本,内容如下:
#!/bin/bash
# 部署大模型服务脚本
# 使用方法: ./deploy_model.sh
# 设置变量
CONTAINER_NAME="model_service"
IMAGE_NAME="ubuntu:20.04"
CONDA_ENV_TAR="conda_env_model.tar.gz"
HOST_PORT=5000
CONTAINER_PORT=5000
MODEL_DIR="/app/model/Qwen2___5-7B-Instruct"
START_COMMAND="python -m vllm.entrypoints.openai.api_server --model /app/model/Qwen2___5-7B-Instruct --served-model-name Qwen2.5-7B-Instruct --max-model-len=2048"
# 检查是否以 root 或 sudo 用户运行
if [[ $EUID -ne 0 ]]; then
echo "此脚本需要以 root 或 sudo 权限运行。"
exit 1
fi
# 检查 Docker 是否安装
if ! command -v docker &> /dev/null
then
echo "Docker 未安装,请先安装 Docker。"
exit 1
fi
# 检查 Conda 环境压缩包是否存在
if [ ! -f "$CONDA_ENV_TAR" ]; then
echo "Conda 环境压缩包 $CONDA_ENV_TAR 不存在。"
exit 1
fi
# 检查模型权重文件是否存在
if [ ! -d "model/Qwen2___5-7B-Instruct/" ]; then
echo "模型权重目录 model/Qwen2___5-7B-Instruct/ 不存在。"
exit 1
fi
# 如果容器已存在,先停止并删除
if [ "$(docker ps -a | grep $CONTAINER_NAME)" ]; then
echo "停止并删除已有的容器 $CONTAINER_NAME ..."
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME
fi
# 创建容器
echo "创建容器 $CONTAINER_NAME ..."
docker run -d --name $CONTAINER_NAME -p $HOST_PORT:$CONTAINER_PORT $IMAGE_NAME sleep infinity
# 复制模型权重到容器
echo "复制模型权重到容器 ..."
docker cp model/Qwen2___5-7B-Instruct/ $CONTAINER_NAME:$MODEL_DIR/
# 复制 Conda 环境压缩包到容器
echo "复制 Conda 环境压缩包到容器 ..."
docker cp $CONDA_ENV_TAR $CONTAINER_NAME:/tmp/
# 在容器中安装必要的系统依赖和 Miniconda
echo "在容器中安装系统依赖和 Miniconda ..."
docker exec $CONTAINER_NAME bash -c "
apt-get update && \
apt-get install -y wget bzip2 ca-certificates curl git && \
rm -rf /var/lib/apt/lists/* && \
wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && \
bash /tmp/miniconda.sh -b -p /opt/conda && \
rm /tmp/miniconda.sh && \
export PATH=/opt/conda/bin:\$PATH && \
conda config --set always_yes yes --set changeps1 no && \
mkdir -p /opt/conda_env && \
tar -xzf /tmp/$CONDA_ENV_TAR -C /opt/conda_env && \
rm /tmp/$CONDA_ENV_TAR && \
pip install vllm
"
# 设置环境变量并启动大模型服务
echo "设置环境变量并启动大模型服务 ..."
docker exec $CONTAINER_NAME bash -c "
export PATH=/opt/conda_env/bin:\$PATH && \
cd / && \
$START_COMMAND &
"
echo "大模型服务已成功部署并启动。访问 http://<宿主机IP>:$HOST_PORT 查看服务。"
脚本说明:
- 变量设置:定义容器名称、基础镜像、Conda 环境压缩包路径、端口号、模型路径等。
- 权限检查:确保脚本以
root
或sudo
权限运行。 - 依赖检查:检查 Docker 是否已安装,Conda 环境压缩包和模型权重文件是否存在。
- 容器管理:如果同名容器已存在,先停止并删除。
- 创建容器:基于指定的基础镜像(
ubuntu:20.04
)创建一个新的容器,并在后台运行。 - 文件复制:将模型权重和 Conda 环境压缩包复制到容器中。
- 容器内设置:
- 安装系统依赖(
wget
,bzip2
, 等)。 - 安装 Miniconda。
- 解压 Conda 环境。
- 安装
vllm
包。
- 安装系统依赖(
- 启动服务:激活 Conda 环境,进入根目录,执行启动命令。
注意:
- 请根据实际情况修改
START_COMMAND
中的模型路径和参数。 - 确保模型权重路径和名称与实际一致。
执行部署脚本
1. 赋予脚本执行权限
在终端中导航到项目根目录,并为脚本赋予执行权限:
cd project-root/
chmod +x deploy_frontend.sh deploy_model.sh
2. 部署前台服务
以 root
或 sudo
权限运行前台服务部署脚本:
sudo ./deploy_frontend.sh
脚本执行过程:
- 检查权限、依赖和文件。
- 创建并配置 Docker 容器。
- 复制代码和环境到容器中。
- 安装系统依赖和 Conda 环境。
- 启动前台服务。
3. 部署大模型服务
同样,以 root
或 sudo
权限运行大模型服务部署脚本:
sudo ./deploy_model.sh
脚本执行过程:
- 检查权限、依赖和文件。
- 创建并配置 Docker 容器。
- 复制模型权重和环境到容器中。
- 安装系统依赖、Conda 环境和
vllm
。 - 启动大模型服务。
4. 验证部署
前台服务
打开浏览器,访问 http://<宿主机IP>:8000
,检查前台是否正常运行。
大模型服务
使用工具(如 curl
或 Postman)发送请求到 http://<宿主机IP>:5000
,确认大模型 API 是否正常响应。例如:
curl http://localhost:5000/v1/models
预期返回已加载的模型列表。
优化建议
-
脚本参数化:
为脚本添加参数,使其更灵活。例如,可以传递端口号、容器名称等参数,而不是在脚本中硬编码。
-
日志记录:
在脚本中添加日志记录功能,方便调试和监控。例如,将输出重定向到日志文件。
-
错误处理:
在关键步骤添加错误检查,确保每一步都成功执行,否则终止脚本并输出错误信息。
if [ $? -ne 0 ]; then echo "某某步骤失败,退出脚本。" exit 1 fi
-
环境变量管理:
将配置参数(如端口号、路径等)提取到配置文件或环境变量中,增强脚本的可维护性和灵活性。
-
容器重启策略:
在创建容器时,添加重启策略,确保容器在主机重启或容器崩溃时自动重启。
docker run -d --name $CONTAINER_NAME -p $HOST_PORT:$CONTAINER_PORT --restart unless-stopped $IMAGE_NAME sleep infinity
-
安全性:
- 非 root 用户:在容器内创建非 root 用户运行应用,提升安全性。
- 资源限制:为容器设置资源限制(如 CPU、内存),防止资源滥用。
-
并行部署:
考虑编写一个总的部署脚本,依次调用
deploy_frontend.sh
和deploy_model.sh
,实现一键部署所有服务。# deploy_all.sh #!/bin/bash sudo ./deploy_frontend.sh sudo ./deploy_model.sh
-
自动化更新:
实现脚本自动检测代码或环境更新,并重新部署容器。例如,使用
git pull
更新代码后重启容器。
故障排除
-
容器未启动:
- 检查脚本执行过程中是否有错误信息。
- 使用
docker ps -a
查看容器状态。 - 使用
docker logs <容器名称>
查看容器日志。
-
服务无法访问:
- 确认宿主机的防火墙是否允许相应端口的流量。
- 确认服务在容器内是否正确启动,并监听指定端口。
-
Conda 环境问题:
- 确认 Conda 环境压缩包是否正确解压。
- 确认所需的依赖是否已安装。
-
权限问题:
- 确认脚本以
root
或sudo
权限运行。 - 确认 Docker 有足够的权限访问宿主机的文件系统。
- 确认脚本以
Shell指令
很高兴听到您的部署取得了成功!为了进一步提升您的技能,以下是一个系统性的指南,介绍 Shell 脚本 与部署相关的语法和重要操作。此指南涵盖基础知识、常用语法、控制结构、函数、错误处理、常用命令以及最佳实践,帮助您更高效地编写和管理部署脚本。
目录
Shell 脚本基础
Shebang 和执行权限
Shebang 是脚本文件的第一行,用于指定解释器。例如:
#!/bin/bash
#!/bin/bash
:使用 Bash 解释器。#!/bin/sh
:使用系统默认的 Shell,通常是dash
或bash
。
赋予执行权限:
在终端中,使用 chmod
命令为脚本赋予执行权限:
chmod +x script.sh
变量和数据类型
Shell 中变量无需声明类型,直接赋值即可。变量名通常使用大写字母。
# 变量赋值
NAME="前台服务"
PORT=8000
# 使用变量
echo "正在启动 $NAME,监听端口 $PORT"
注意:
- 变量赋值时等号
=
两边不能有空格。 - 使用
${VARIABLE}
或$VARIABLE
访问变量。
注释
使用 #
开始的行或行尾部分为注释。
# 这是一个注释
echo "Hello, World!" # 这是行尾注释
Shell 脚本语法
命令和命令替换
直接在脚本中执行系统命令。
# 执行 ls 命令
ls -l /home/user
命令替换:将命令的输出赋值给变量。
CURRENT_DIR=$(pwd)
# 或使用反引号
CURRENT_DIR=`pwd`
引号
-
双引号 (
" "
): 保留大多数特殊字符的含义,支持变量和命令替换。NAME="前台服务" echo "服务名称:$NAME"
-
单引号 (
' '
): 完全保留字符,不进行变量替换。echo '服务名称:$NAME'
-
**反引号 (
\``):** 用于命令替换(较少使用,推荐使用
$()`)。DATE=`date` echo "当前日期:$DATE"
环境变量
将变量导出为环境变量,使其在子进程中可用。
export PATH="/usr/local/bin:$PATH"
export DATABASE_URL="mysql://user:pass@localhost/db"
控制结构
条件判断
使用 if
, elif
, else
进行条件判断。
if [ -f "/path/to/file" ]; then
echo "文件存在"
elif [ -d "/path/to/directory" ]; then
echo "目录存在"
else
echo "文件和目录都不存在"
fi
常用测试表达式:
- 文件测试:
-f FILE
:检查是否为常规文件。-d DIRECTORY
:检查是否为目录。-e FILE
:检查文件是否存在。
- 字符串测试:
-z STRING
:字符串长度为零。-n STRING
:字符串长度非零。STRING1 = STRING2
:字符串相等。
- 数值测试:
-eq
、-ne
、-lt
、-le
、-gt
、-ge
:等于、不等于、小于、小于等于、大于、大于等于。
循环结构
For 循环:
for i in {1..5}; do
echo "第 $i 次循环"
done
While 循环:
COUNT=1
while [ $COUNT -le 5 ]; do
echo "第 $COUNT 次循环"
COUNT=$((COUNT + 1))
done
Until 循环:
COUNT=1
until [ $COUNT -gt 5 ]; do
echo "第 $COUNT 次循环"
COUNT=$((COUNT + 1))
done
Case 语句
用于多条件匹配,类似于其他语言的 switch 语句。
read -p "请输入选项 (start/stop/restart): " OPTION
case $OPTION in
start)
echo "启动服务"
;;
stop)
echo "停止服务"
;;
restart)
echo "重启服务"
;;
*)
echo "无效选项"
;;
esac
函数
定义和调用函数,提高脚本的模块化和可重用性。
# 定义函数
greet() {
echo "Hello, $1!"
}
# 调用函数
greet "前台服务"
带返回值的函数:
add() {
return $(($1 + $2))
}
add 5 3
echo "结果:$?"
注意:return
用于返回状态码(0-255),要返回更大的数值,可以使用 echo
。
add() {
echo $(($1 + $2))
}
RESULT=$(add 5 3)
echo "结果:$RESULT"
错误处理
退出状态码
每个命令执行后都有一个退出状态码,存储在 $?
中。0
表示成功,非零表示失败。
cp source.txt destination.txt
if [ $? -eq 0 ]; then
echo "复制成功"
else
echo "复制失败"
exit 1
fi
set
命令
用于控制脚本的行为,提高错误处理能力。
set -e
:一旦有命令返回非零状态,立即退出脚本。set -u
:使用未定义的变量时,立即退出。set -o pipefail
:如果管道中的任何命令失败,整个管道返回失败状态。
#!/bin/bash
set -euo pipefail
# 现在,如果有命令失败或使用未定义变量,脚本会立即退出
陷阱 (trap
)
用于捕获和处理脚本中的信号或错误,执行清理操作。
#!/bin/bash
cleanup() {
echo "清理临时文件..."
rm -f /tmp/tempfile
}
trap cleanup EXIT
# 创建临时文件
touch /tmp/tempfile
# 模拟脚本执行
echo "执行脚本..."
# 如果脚本退出(无论成功或失败),都会执行 cleanup
常用命令和操作
文件和目录操作
-
复制文件:
cp source.txt destination.txt cp -r source_dir/ destination_dir/
-
移动文件:
mv source.txt destination.txt
-
删除文件和目录:
rm file.txt rm -rf directory/
-
创建目录:
mkdir -p /path/to/directory
-
查找文件:
find /path -name "filename"
文本处理
-
查找文本:
grep "pattern" file.txt
-
替换文本:
sed -i 's/old/new/g' file.txt
-
提取字段:
echo "a,b,c" | awk -F',' '{print $2}'
网络操作
-
下载文件:
wget http://example.com/file.tar.gz curl -O http://example.com/file.tar.gz
-
发送 HTTP 请求:
curl -X GET http://localhost:8000/health
进程控制
-
查看进程:
ps aux | grep process_name
-
停止进程:
kill PID kill -9 PID # 强制杀死
Docker 操作
在部署脚本中常用的 Docker 命令包括:
-
运行容器:
docker run -d --name container_name -p host_port:container_port image_name
-
停止容器:
docker stop container_name
-
删除容器:
docker rm container_name
-
复制文件到容器:
docker cp source_path container_name:destination_path
-
在容器内执行命令:
docker exec container_name command
-
查看容器日志:
docker logs -f container_name
最佳实践
脚本可读性
-
使用有意义的变量名:提高脚本的可读性和可维护性。
CONTAINER_NAME="frontend_service"
-
适当的缩进和格式:使代码结构清晰。
if [ condition ]; then # 执行操作 fi
-
添加注释:解释复杂的逻辑或步骤。
# 检查文件是否存在 if [ -f "file.txt" ]; then echo "文件存在" fi
模块化和重用性
-
使用函数:将重复的代码封装到函数中。
backup_file() { cp "$1" "$1.bak" } backup_file "config.yaml"
-
参数化脚本:使用参数传递不同的值,而不是硬编码。
#!/bin/bash CONTAINER_NAME=$1 IMAGE_NAME=$2
日志记录
-
记录日志:将输出重定向到日志文件,便于调试和监控。
LOG_FILE="/var/log/deploy.log" echo "部署开始" >> $LOG_FILE
-
使用日志级别:
log_info() { echo "[INFO] $1" | tee -a $LOG_FILE } log_error() { echo "[ERROR] $1" | tee -a $LOG_FILE >&2 }
安全性
-
避免硬编码敏感信息:使用环境变量或配置文件管理密码和密钥。
export DB_PASSWORD="your_password"
-
使用最小权限:以非 root 用户运行脚本和服务,减少安全风险。
-
验证输入:确保脚本接收到的参数和输入是有效和安全的。
if [ -z "$1" ]; then echo "参数缺失" exit 1 fi
示例:常见部署任务
示例 1:检查服务状态并重启
以下脚本检查某个服务是否在运行,如果未运行则启动服务。
#!/bin/bash
set -euo pipefail
SERVICE_NAME="frontend_service"
LOG_FILE="/var/log/service_monitor.log"
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a $LOG_FILE
}
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a $LOG_FILE >&2
}
check_service() {
if docker ps | grep -q "$SERVICE_NAME"; then
log_info "$SERVICE_NAME 正在运行。"
else
log_error "$SERVICE_NAME 未运行,尝试启动。"
start_service
fi
}
start_service() {
docker start "$SERVICE_NAME" && log_info "$SERVICE_NAME 启动成功。" || log_error "$SERVICE_NAME 启动失败。"
}
# 主程序
check_service
说明:
- 函数:定义日志函数和检查/启动服务的函数。
- 日志记录:将信息和错误记录到日志文件。
- 错误处理:使用
set -euo pipefail
提高脚本的健壮性。
示例 2:自动化备份和恢复
以下脚本实现数据库的备份和恢复。
#!/bin/bash
set -euo pipefail
ACTION=$1
BACKUP_DIR="/backup/db"
DB_CONTAINER="db_service"
DB_NAME="my_database"
LOG_FILE="/var/log/db_backup.log"
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a $LOG_FILE
}
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a $LOG_FILE >&2
}
backup_db() {
TIMESTAMP=$(date '+%Y%m%d%H%M%S')
BACKUP_FILE="$BACKUP_DIR/$DB_NAME-$TIMESTAMP.sql.gz"
mkdir -p "$BACKUP_DIR"
docker exec "$DB_CONTAINER" pg_dump "$DB_NAME" | gzip > "$BACKUP_FILE"
log_info "数据库备份完成:$BACKUP_FILE"
}
restore_db() {
BACKUP_FILE=$2
if [ ! -f "$BACKUP_FILE" ]; then
log_error "备份文件不存在:$BACKUP_FILE"
exit 1
fi
gunzip < "$BACKUP_FILE" | docker exec -i "$DB_CONTAINER" psql "$DB_NAME"
log_info "数据库恢复完成:$BACKUP_FILE"
}
# 主程序
case $ACTION in
backup)
backup_db
;;
restore)
if [ $# -ne 2 ]; then
echo "用法: $0 restore <backup_file>"
exit 1
fi
restore_db "$2"
;;
*)
echo "用法: $0 {backup|restore <backup_file>}"
exit 1
;;
esac
说明:
- 参数化:通过传递参数选择备份或恢复操作。
- 备份:使用
pg_dump
导出数据库并压缩保存。 - 恢复:解压备份文件并导入数据库。
- 日志记录:记录操作日志,便于审计和调试。