优化 run.sh 三种部署方式的升级清理逻辑

- 共用 clean/build 函数,升级前清理 frontend/dist、backend/static 与旧二进制
- binary upgrade/start/stop/status:停服、备份、重编译、后台启动并持久化启动参数
- docker up/upgrade:--force-recreate --remove-orphans,支持 up fresh 无缓存重建
- kubernetes upgrade:--no-cache 构建 + 时间戳镜像 tag,set image 触发滚动更新
This commit is contained in:
Logan.Li committed 2026-06-26 11:08:58 +08:00
1 parent 921b44d6a4
commit ac69ae75d4
1 file changed
+356 -110
+356 -110
View File
@@ -14,7 +14,8 @@
#
# 方式 3 — 独立二进制:
# ./run.sh build 编译独立二进制 (embed 前端)
# ./run.sh binary 编译并启动
# ./run.sh binary upgrade 停服 → 清旧产物 → 重编译 → 后台启动
# ./run.sh binary start|stop|status
#
# 本地开发:
# ./run.sh dev 前后端热重载
@@ -60,6 +61,8 @@ cd "$SCRIPT_DIR"
# PID 文件
BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid"
FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid"
BINARY_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.pid"
BINARY_ARGS_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.args"
# 检测操作系统
detect_os() {
@@ -290,6 +293,196 @@ cmd_logs() {
wait
}
# ============================================================
# 构建清理(三种部署方式共用,避免旧代码/旧静态资源残留)// Logan.Li
# ============================================================
clean_frontend_artifacts() {
info "清理前端旧构建产物..."
rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/dist"
rm -rf "$SCRIPT_DIR/$FRONTEND_DIR/node_modules/.vite"
}
clean_embed_static() {
info "清理 embed static 旧文件..."
mkdir -p "$SCRIPT_DIR/$BACKEND_DIR/static"
find "$SCRIPT_DIR/$BACKEND_DIR/static" -mindepth 1 -delete 2>/dev/null || rm -rf "$SCRIPT_DIR/$BACKEND_DIR/static"/*
}
clean_go_artifacts() {
info "清理 Go 旧编译产物..."
rm -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN"
rm -f "$SCRIPT_DIR/$BACKEND_DIR/$BACKEND_BIN"
}
# 构建前端并同步到 backend/static(二进制 / embed 镜像共用)// Logan.Li
build_frontend_embed() {
clean_frontend_artifacts
cd "$SCRIPT_DIR/$FRONTEND_DIR"
if [ ! -d "node_modules" ]; then
npm install --registry=https://registry.npmmirror.com
fi
npm run build 2>&1 | tail -8
cd "$SCRIPT_DIR"
clean_embed_static
cp -r "$FRONTEND_DIR/dist/"* "$BACKEND_DIR/static/"
local fe_files
fe_files=$(find "$BACKEND_DIR/static" -type f | wc -l | tr -d ' ')
success "前端已嵌入 static/ ($fe_files 个文件)"
}
# 停止独立二进制进程(PID 文件 + 端口双保险)// Logan.Li
stop_binary_process() {
local port=${1:-8080}
if [ -f "$BINARY_PID_FILE" ]; then
local pid
pid=$(cat "$BINARY_PID_FILE" 2>/dev/null || echo "")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
sleep 1
kill -9 "$pid" 2>/dev/null || true
success "已停止二进制进程 (PID: $pid)"
fi
rm -f "$BINARY_PID_FILE"
fi
# 兜底:按端口和进程名清理残留
if [ "$OS" = "macos" ]; then
local pids
pids=$(lsof -ti:"$port" 2>/dev/null || echo "")
[ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true
else
fuser -k "${port}/tcp" 2>/dev/null || true
pkill -f "${SCRIPT_DIR}/${BACKEND_DIR}/${STANDALONE_BIN}" 2>/dev/null || true
pkill -f "./${STANDALONE_BIN} --port" 2>/dev/null || true
fi
sleep 0.5
}
wait_binary_ready() {
local port=${1:-8080}
local retries=0
while ! curl -sf "http://localhost:${port}/version" > /dev/null 2>&1; do
retries=$((retries + 1))
if [ $retries -ge 30 ]; then
error "二进制启动超时,查看日志: $BACKEND_DIR/logs/aiops.log"
tail -20 "$SCRIPT_DIR/$BACKEND_DIR/logs/aiops.log" 2>/dev/null || true
return 1
fi
sleep 1
done
return 0
}
# 确保二进制默认 config.yaml 存在 // Logan.Li
ensure_binary_config() {
local cfg="$SCRIPT_DIR/$BACKEND_DIR/config.yaml"
if [ -f "$cfg" ]; then
return 0
fi
info "生成默认 config.yaml (SQLite + 无Redis)..."
cat > "$cfg" << 'YAML'
mode: "release"
port: "8080"
log_level: "info"
database:
driver: "sqlite"
host: "localhost"
port: 3306
user: ""
password: ""
dbname: "data/aiops.db"
charset: "utf8mb4"
max_conn: 100
max_idle: 10
redis:
host: "localhost"
port: 6379
password: ""
db: 0
jwt:
secret: "CHANGE_ME_JWT_SECRET" # ⚠️ 生产环境必须替换
expire_hour: 24
websocket:
read_buffer_size: 1024
write_buffer_size: 1024
YAML
}
# 编译独立二进制(含 embed 前端)// Logan.Li
build_standalone_binary() {
build_frontend_embed
ensure_binary_config
clean_go_artifacts
cd "$SCRIPT_DIR/$BACKEND_DIR"
info "编译中..."
CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.24.0 \
go build -ldflags="-w -s" -o "$STANDALONE_BIN" .
if [ ! -f "$STANDALONE_BIN" ]; then
error "编译失败"
return 1
fi
local bin_size
bin_size=$(du -h "$STANDALONE_BIN" | cut -f1)
success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)"
cd "$SCRIPT_DIR"
return 0
}
# 解析/持久化二进制启动参数 // Logan.Li
save_binary_args() {
printf '%s\n' "$@" > "$BINARY_ARGS_FILE"
}
load_binary_args() {
local -n _out=$1
_out=()
if [ -f "$BINARY_ARGS_FILE" ]; then
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$line" ] && _out+=("$line")
done < "$BINARY_ARGS_FILE"
fi
if [ ${#_out[@]} -eq 0 ]; then
_out=(--port 8080 --db-driver sqlite)
fi
}
binary_listen_port() {
local args=("$@")
local i=0
while [ $i -lt ${#args[@]} ]; do
if [ "${args[$i]}" = "--port" ] && [ $((i + 1)) -lt ${#args[@]} ]; then
echo "${args[$((i + 1))]}"
return 0
fi
i=$((i + 1))
done
echo "8080"
}
# 后台启动独立二进制 // Logan.Li
start_binary_daemon() {
local args=("$@")
local port
port=$(binary_listen_port "${args[@]}")
cd "$SCRIPT_DIR/$BACKEND_DIR"
mkdir -p data logs uploads
rotate_log "logs/aiops.log"
stop_binary_process "$port"
nohup ./"$STANDALONE_BIN" "${args[@]}" >> logs/aiops.log 2>&1 &
echo $! > "$BINARY_PID_FILE"
save_binary_args "${args[@]}"
if wait_binary_ready "$port"; then
success "二进制已启动 (PID: $(cat "$BINARY_PID_FILE"), 端口: $port)"
info "日志: $BACKEND_DIR/logs/aiops.log"
info "健康检查: http://localhost:${port}/version"
return 0
fi
return 1
}
# ============================================================
# Docker Compose 部署
# ============================================================
@@ -331,9 +524,21 @@ cmd_docker() {
info "BuildKit: 已启用 (增量编译 + 缓存复用)"
case "$action" in
up)
banner "========== Docker Compose 启动 =========="
$DOCKER_COMPOSE up -d --build
up|upgrade|update)
local fresh_build=0
if [ "${2:-}" = "fresh" ] || [ "$action" = "fresh" ]; then
fresh_build=1
fi
banner "========== Docker Compose 启动/升级 =========="
clean_frontend_artifacts
if [ "$fresh_build" -eq 1 ]; then
info "全量重建镜像 (--no-cache)..."
$DOCKER_COMPOSE build --no-cache
$DOCKER_COMPOSE up -d --force-recreate --remove-orphans
else
info "增量构建 + 强制重建容器..."
$DOCKER_COMPOSE up -d --build --force-recreate --remove-orphans
fi
echo ""
# 清理历史 build cache 和悬空镜像,防止占满磁盘
info "清理历史构建缓存..."
@@ -389,15 +594,20 @@ cmd_docker() {
echo " $(df -h / | awk 'NR==2{print $3, "used /", $2, "total ("$5")"}')"
success "清理完成"
;;
fresh)
cmd_docker up fresh
;;
help|--help|-h)
echo "Docker Compose 部署命令:"
echo " ./run.sh docker up 构建并启动所有容器"
echo " ./run.sh docker down 停止并移除容器"
echo " ./run.sh docker build 仅构建镜像"
echo " ./run.sh docker restart 重启容器"
echo " ./run.sh docker logs 查看实时日志"
echo " ./run.sh docker ps 查看容器状态"
echo " ./run.sh docker clean 清理构建缓存/无用镜像"
echo " ./run.sh docker up 增量构建 + 强制重建容器"
echo " ./run.sh docker up fresh 无缓存全量重建镜像并启动"
echo " ./run.sh docker upgrade 同 up(升级别名)"
echo " ./run.sh docker down 停止并移除容器"
echo " ./run.sh docker build 仅构建镜像"
echo " ./run.sh docker restart 重启容器"
echo " ./run.sh docker logs 查看实时日志"
echo " ./run.sh docker ps 查看容器状态"
echo " ./run.sh docker clean 清理构建缓存/无用镜像"
;;
*)
error "未知 docker 子命令: $action"
@@ -416,78 +626,12 @@ cmd_build() {
case "$target" in
binary|aiops)
banner "========== 编译独立二进制 =========="
# 构建前端 → 拷贝到 embed 目录(go:embed 需要 static/ 下有文件)
info "构建前端 → embed..."
cd "$SCRIPT_DIR/$FRONTEND_DIR"
if [ ! -d "node_modules" ]; then
npm install --registry=https://registry.npmmirror.com
fi
npm run build 2>&1 | tail -5
# 拷贝前端构建产物到后端 embed 目录
cd "$SCRIPT_DIR"
rm -rf "$BACKEND_DIR/static"/*
cp -r "$FRONTEND_DIR/dist/"* "$BACKEND_DIR/static/"
local fe_files=$(find "$BACKEND_DIR/static" -type f | wc -l)
success "前端已嵌入 ($fe_files 文件)"
cd "$SCRIPT_DIR/$BACKEND_DIR"
# 确保 config.yaml 存在(二进制模式默认 SQLite)
if [ ! -f "config.yaml" ]; then
info "生成默认 config.yaml (SQLite + 无Redis)..."
cat > config.yaml << 'YAML'
mode: "release"
port: "8080"
log_level: "info"
database:
driver: "sqlite"
host: "localhost"
port: 3306
user: ""
password: ""
dbname: "data/aiops.db"
charset: "utf8mb4"
max_conn: 100
max_idle: 10
redis:
host: "localhost"
port: 6379
password: ""
db: 0
jwt:
secret: "CHANGE_ME_JWT_SECRET" # ⚠️ 生产环境必须替换
expire_hour: 24
websocket:
read_buffer_size: 1024
write_buffer_size: 1024
YAML
fi
# 静态编译(CGO_ENABLED=1 因为 SQLite 需要 CGO)
info "编译中..."
CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.24.0 go build -ldflags="-w -s" -o "$STANDALONE_BIN" .
if [ -f "$STANDALONE_BIN" ]; then
local bin_size=$(du -h "$STANDALONE_BIN" | cut -f1)
success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)"
echo ""
info "使用方法:"
echo " ./$BACKEND_DIR/$STANDALONE_BIN # 默认启动 (SQLite, 端口 8080)"
echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 9090 # 自定义端口"
echo " ./$BACKEND_DIR/$STANDALONE_BIN --db-driver mysql \\"
echo " --db-host localhost --db-user root \\"
echo " --db-password pass --db-name aiops # MySQL 模式"
echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件"
else
error "编译失败"
exit 1
fi
build_standalone_binary || exit 1
echo ""
info "使用方法:"
echo " ./run.sh binary upgrade # 停服重编译并后台启动"
echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 8080 # 前台启动"
echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件"
;;
agent)
banner "========== 编译 Agent =========="
@@ -531,19 +675,96 @@ YAML
# 独立二进制模式
# ============================================================
cmd_binary() {
cd "$SCRIPT_DIR/$BACKEND_DIR"
# 先编译(如果不存在)
if [ ! -f "$STANDALONE_BIN" ]; then
info "二进制不存在,先编译..."
cmd_build binary
local action="run"
if [ $# -gt 0 ] && [[ "$1" =~ ^(upgrade|start|stop|status|help|--help|-h)$ ]]; then
action=$1
shift
fi
banner "========== 启动独立二进制 =========="
mkdir -p data logs uploads
# 传递额外参数
./"$STANDALONE_BIN" "$@"
case "$action" in
upgrade|update)
banner "========== 二进制升级部署 =========="
local args=()
if [ $# -gt 0 ]; then
args=("$@")
else
load_binary_args args
fi
local port
port=$(binary_listen_port "${args[@]}")
info "启动参数: ${args[*]}"
stop_binary_process "$port"
if [ -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; then
local backup="$SCRIPT_DIR/$BACKEND_DIR/${STANDALONE_BIN}.bak.$(date +%Y%m%d%H%M%S)"
cp "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" "$backup"
info "已备份旧二进制: $backup"
fi
build_standalone_binary || exit 1
start_binary_daemon "${args[@]}" || exit 1
;;
start)
local args=()
if [ $# -gt 0 ]; then
args=("$@")
else
load_binary_args args
fi
if [ ! -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; then
info "二进制不存在,先编译..."
build_standalone_binary || exit 1
fi
start_binary_daemon "${args[@]}" || exit 1
;;
stop)
local port=${1:-8080}
stop_binary_process "$port"
success "二进制已停止"
;;
status)
local args=()
load_binary_args args
local port
port=$(binary_listen_port "${args[@]}")
info "监听端口: $port"
if [ -f "$BINARY_PID_FILE" ]; then
local pid
pid=$(cat "$BINARY_PID_FILE" 2>/dev/null || echo "")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
success "进程运行中 (PID: $pid)"
else
warning "PID 文件存在但进程未运行"
fi
else
warning "未找到 PID 文件"
fi
if curl -sf "http://localhost:${port}/version" > /dev/null 2>&1; then
success "HTTP 就绪: http://localhost:${port}/version"
else
warning "HTTP 未响应"
fi
;;
help|--help|-h)
echo "独立二进制命令:"
echo " ./run.sh binary upgrade [--port 8080 --db-driver sqlite] 停服→清旧→重编译→启动"
echo " ./run.sh binary start [args...] 后台启动(参数会持久化)"
echo " ./run.sh binary stop [port] 停止进程"
echo " ./run.sh binary status 查看状态"
echo " ./run.sh binary [--port 8080 ...] 前台运行(开发调试)"
;;
run|*)
cd "$SCRIPT_DIR/$BACKEND_DIR"
if [ ! -f "$STANDALONE_BIN" ]; then
info "二进制不存在,先编译..."
build_standalone_binary || exit 1
fi
banner "========== 前台启动独立二进制 =========="
mkdir -p data logs uploads
if [ $# -gt 0 ]; then
save_binary_args "$@"
fi
./"$STANDALONE_BIN" "$@"
;;
esac
}
# ============================================================
@@ -553,6 +774,7 @@ KUBERNETES_NAMESPACE="devops"
KUBERNETES_MANIFEST_DIR="$SCRIPT_DIR/deploy/kubernetes"
KUBERNETES_IMAGE="aiops:embed"
KUBERNETES_NODE_PORT=30080
KUBERNETES_BUILT_IMAGE=""
# 检测 kubectl 和 Docker
check_kubernetes_deps() {
@@ -592,34 +814,52 @@ load_image_to_cluster() {
warning "如集群无法拉取镜像,请手动推送: docker push <registry>/$image"
}
# Kubernetes 构建镜像
# Kubernetes 构建镜像(upgrade 时无缓存 + 时间戳 tag 避免旧层残留)// Logan.Li
cmd_kubernetes_build() {
local no_cache=${1:-0}
if ! command -v docker &>/dev/null; then
error "Docker 未安装"
exit 1
fi
banner "========== 构建 embed 镜像 =========="
info "镜像: $KUBERNETES_IMAGE"
clean_frontend_artifacts
clean_embed_static
# 版本信息
local version=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")
local build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local version
version=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")
local build_time
build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local image_tag
image_tag="${KUBERNETES_IMAGE}-$(date +%Y%m%d%H%M%S)"
info "镜像: $KUBERNETES_IMAGE (+ $image_tag)"
local build_args=(
-f Dockerfile.embed
--build-arg VERSION="$version"
--build-arg BUILD_TIME="$build_time"
-t "$KUBERNETES_IMAGE"
-t "$image_tag"
)
if [ "$no_cache" -eq 1 ]; then
build_args+=(--no-cache --pull)
info "无缓存全量构建 (--no-cache --pull)"
fi
build_args+=(.)
info "构建中(前端 + 后端 embed)..."
docker build -f Dockerfile.embed \
--build-arg VERSION="$version" \
--build-arg BUILD_TIME="$build_time" \
-t "$KUBERNETES_IMAGE" . || {
docker build "${build_args[@]}" || {
error "镜像构建失败"
exit 1
}
local img_size=$(docker images "$KUBERNETES_IMAGE" --format "{{.Size}}" | head -1)
local img_size
img_size=$(docker images "$KUBERNETES_IMAGE" --format "{{.Size}}" | head -1)
success "镜像构建完成: $KUBERNETES_IMAGE ($img_size)"
# 加载到本地集群
load_image_to_cluster "$KUBERNETES_IMAGE"
load_image_to_cluster "$image_tag"
KUBERNETES_BUILT_IMAGE="$image_tag"
}
# Kubernetes 部署
@@ -678,10 +918,12 @@ cmd_kubernetes() {
upgrade|update)
check_kubernetes_deps
banner "========== Kubernetes 滚动更新 =========="
cmd_kubernetes_build
info "触发滚动更新..."
kubectl -n "$KUBERNETES_NAMESPACE" rollout restart deployment/aiops
cmd_kubernetes_build 1
info "更新 Deployment 镜像: $KUBERNETES_BUILT_IMAGE"
kubectl -n "$KUBERNETES_NAMESPACE" set image deployment/aiops aiops="$KUBERNETES_BUILT_IMAGE" --record
kubectl -n "$KUBERNETES_NAMESPACE" rollout status deployment/aiops --timeout=180s
info "清理悬空镜像..."
docker image prune -f 2>/dev/null || true
success "滚动更新完成"
;;
@@ -812,7 +1054,9 @@ cmd_help() {
echo " ./run.sh logs 查看实时日志"
echo ""
echo -e " ${CYAN}Docker 容器部署${NC}"
echo " ./run.sh docker up 构建并启动所有容器 (MySQL+Redis+Nginx)"
echo " ./run.sh docker up 增量构建 + 强制重建容器"
echo " ./run.sh docker up fresh 无缓存全量重建并启动"
echo " ./run.sh docker upgrade 同 up(升级别名)"
echo " ./run.sh docker down 停止并移除容器"
echo " ./run.sh docker build 仅构建镜像"
echo " ./run.sh docker restart 重启容器"
@@ -834,7 +1078,9 @@ cmd_help() {
echo ""
echo -e " ${YELLOW}编译 & 独立二进制${NC}"
echo " ./run.sh build 编译独立二进制 ./$BACKEND_DIR/$STANDALONE_BIN"
echo " ./run.sh binary 编译+启动独立二进制"
echo " ./run.sh binary upgrade 停服→清旧→重编译→后台启动(生产升级)"
echo " ./run.sh binary start|stop|status 进程管理"
echo " ./run.sh binary [args] 前台启动(调试)"
echo " ./run.sh build agent 交叉编译 Agent (amd64+arm64)"
echo " ./run.sh build frontend 构建前端生产包"
echo ""