From 5ae2ba6561db08aac5632463df2cd7fe816ffdfd Mon Sep 17 00:00:00 2001 From: leiyun Date: Tue, 2 Jun 2026 15:31:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=9C=8D=E5=8A=A1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- isolated/apply-update.sh | 128 +++++++++++ isolated/build-update.sh | 316 ++++++++++++++++++++++++++++ isolated/docker-compose.runtime.yml | 7 +- isolated/test/docker-compose.yml | 9 +- isolated/update.MD | 207 ++++++++++++++++++ 5 files changed, 664 insertions(+), 3 deletions(-) create mode 100644 isolated/apply-update.sh create mode 100644 isolated/build-update.sh create mode 100644 isolated/update.MD diff --git a/isolated/apply-update.sh b/isolated/apply-update.sh new file mode 100644 index 0000000..c723b6b --- /dev/null +++ b/isolated/apply-update.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Apply an incremental image update on the target runtime server. +# Usage: +# bash apply-update.sh +# bash apply-update.sh emp-admin emp-monitor +# PROJECT_NAME=emp-test ENV_FILE=.env COMPOSE_FILE=docker-compose.yml bash apply-update.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +PROJECT_NAME="${PROJECT_NAME:-emp-test}" +ENV_FILE="${ENV_FILE:-.env}" +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" +IMAGE_TAR="${IMAGE_TAR:-images.tar}" +SERVICE_FILE="${SERVICE_FILE:-services.txt}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" +} + +normalize_service() { + case "$1" in + gateway|emp-gateway) echo "emp-gateway" ;; + auth|emp-auth) echo "emp-auth" ;; + monitor|emp-monitor) echo "emp-monitor" ;; + data|emp-data) echo "emp-data" ;; + pdf|emp-pdf) echo "emp-pdf" ;; + ws|emp-ws) echo "emp-ws" ;; + admin|emp-admin) echo "emp-admin" ;; + *) die "Unknown service: $1. Allowed: gateway auth monitor data pdf ws admin" ;; + esac +} + +resolve_compose_cmd() { + if docker compose version >/dev/null 2>&1; then + echo "docker compose" + elif command -v docker-compose >/dev/null 2>&1; then + echo "docker-compose" + else + die "Missing docker compose" + fi +} + +resolve_runtime_file() { + local requested="$1" + local default_name="$2" + if [[ -f "$requested" ]]; then + echo "$requested" + return + fi + if [[ "$requested" != "$default_name" ]]; then + echo "$requested" + return + fi + + local candidates=( + "../runtime/$default_name" + "/home/admin-x99/emp-test/runtime/$default_name" + ) + local candidate + for candidate in "${candidates[@]}"; do + if [[ -f "$candidate" ]]; then + echo "$candidate" + return + fi + done + + echo "$requested" +} + +read_services() { + local raw_services=() + if [[ "$#" -gt 0 ]]; then + raw_services=("$@") + elif [[ -f "$SERVICE_FILE" ]]; then + mapfile -t raw_services < "$SERVICE_FILE" + else + die "No services specified. Pass services as args or provide $SERVICE_FILE." + fi + + local service + for service in "${raw_services[@]}"; do + [[ -n "$service" ]] || continue + normalize_service "$service" + done +} + +need_cmd docker +ENV_FILE="$(resolve_runtime_file "$ENV_FILE" ".env")" +COMPOSE_FILE="$(resolve_runtime_file "$COMPOSE_FILE" "docker-compose.yml")" +[[ -f "$ENV_FILE" ]] || die "Missing env file: $ENV_FILE" +[[ -f "$COMPOSE_FILE" ]] || die "Missing compose file: $COMPOSE_FILE" +[[ -f "$IMAGE_TAR" ]] || die "Missing image tar: $IMAGE_TAR" + +mapfile -t UPDATE_SERVICES < <(read_services "$@") +[[ "${#UPDATE_SERVICES[@]}" -gt 0 ]] || die "No services to update." + +COMPOSE_CMD="$(resolve_compose_cmd)" + +log "Load images: $IMAGE_TAR" +docker load -i "$IMAGE_TAR" + +log "Recreate services: ${UPDATE_SERVICES[*]}" +# shellcheck disable=SC2086 +$COMPOSE_CMD \ + --env-file "$ENV_FILE" \ + -f "$COMPOSE_FILE" \ + -p "$PROJECT_NAME" \ + up -d --no-deps --force-recreate "${UPDATE_SERVICES[@]}" + +log "Current service status" +# shellcheck disable=SC2086 +$COMPOSE_CMD \ + --env-file "$ENV_FILE" \ + -f "$COMPOSE_FILE" \ + -p "$PROJECT_NAME" \ + ps "${UPDATE_SERVICES[@]}" diff --git a/isolated/build-update.sh b/isolated/build-update.sh new file mode 100644 index 0000000..083e83c --- /dev/null +++ b/isolated/build-update.sh @@ -0,0 +1,316 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Build selected app images and package them as an incremental update. +# Usage: +# ./build-update.sh admin monitor +# SERVICES="admin monitor" ./build-update.sh +# EMP_ROOT=/path/to/emp IMAGE_TAG=20260602153000 ./build-update.sh emp-admin emp-monitor +# +# Output: +# dist/emp-test-update-${IMAGE_TAG}-${services}.tar.gz + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DIST_DIR="${DIST_DIR:-$SCRIPT_DIR/dist}" +BUILD_CONTEXT_DIR="$SCRIPT_DIR/.build-context" + +IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-emp-test}" +IMAGE_TAG="${IMAGE_TAG:-$(date '+%Y%m%d%H%M%S')}" +NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" +SKIP_BUILD="${SKIP_BUILD:-0}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" +} + +has_emp_root() { + [[ -d "$1/emp_server" && -d "$1/emp_admin" && -d "$1/emp_ws" ]] +} + +resolve_root_dir() { + if [[ -n "${EMP_ROOT:-}" ]]; then + local root + root="$(cd "$EMP_ROOT" && pwd)" + has_emp_root "$root" || die "EMP_ROOT is invalid: $EMP_ROOT" + printf '%s\n' "$root" + return + fi + + local candidates=( + "$SCRIPT_DIR/../.." + "$SCRIPT_DIR/../../emp" + "$SCRIPT_DIR/.." + "$PWD" + ) + local candidate root + for candidate in "${candidates[@]}"; do + if [[ -d "$candidate" ]]; then + root="$(cd "$candidate" && pwd)" + if has_emp_root "$root"; then + printf '%s\n' "$root" + return + fi + fi + done + + die "Cannot find EMP root. Set EMP_ROOT=/path/to/emp where emp_server, emp_admin and emp_ws exist." +} + +normalize_service() { + case "$1" in + gateway|emp-gateway) echo "gateway" ;; + auth|emp-auth) echo "auth" ;; + monitor|emp-monitor) echo "monitor" ;; + data|emp-data) echo "data" ;; + pdf|emp-pdf) echo "pdf" ;; + ws|emp-ws) echo "ws" ;; + admin|emp-admin) echo "admin" ;; + *) die "Unknown service: $1. Allowed: gateway auth monitor data pdf ws admin" ;; + esac +} + +compose_service_name() { + echo "emp-$1" +} + +image_name() { + echo "$IMAGE_NAMESPACE/emp-$1" +} + +java_module_name() { + case "$1" in + gateway) echo "emp_gateway" ;; + auth) echo "emp_auth" ;; + monitor) echo "emp_monitor" ;; + data) echo "emp_data" ;; + *) return 1 ;; + esac +} + +is_java_service() { + case "$1" in + gateway|auth|monitor|data) return 0 ;; + *) return 1 ;; + esac +} + +read_requested_services() { + local raw_services=() + if [[ "$#" -gt 0 ]]; then + raw_services=("$@") + elif [[ -n "${SERVICES:-}" ]]; then + # shellcheck disable=SC2206 + raw_services=($SERVICES) + else + die "No services specified. Example: ./build-update.sh admin monitor" + fi + + local -A seen=() + local service normalized + for service in "${raw_services[@]}"; do + normalized="$(normalize_service "$service")" + if [[ -z "${seen[$normalized]:-}" ]]; then + seen[$normalized]=1 + echo "$normalized" + fi + done +} + +join_by_comma() { + local IFS=, + echo "$*" +} + +run_step() { + local workdir="$1" + shift + (cd "$workdir" && "$@") +} + +build_java_images() { + local services=("$@") + [[ "${#services[@]}" -gt 0 ]] || return 0 + + local server_dir="$ROOT_DIR/emp_server" + local modules=() + local service module jar target_dir + + for service in "${services[@]}"; do + modules+=("$(java_module_name "$service")") + done + + log "Build backend jars: ${modules[*]}" + run_step "$server_dir" mvn package -DskipTests -B -pl "$(join_by_comma "${modules[@]}")" -am + + for service in "${services[@]}"; do + module="$(java_module_name "$service")" + target_dir="$server_dir/$module/target" + jar="$(find "$target_dir" -maxdepth 1 -name '*.jar' ! -name '*original*' ! -name 'app.jar' | head -1)" + [[ -n "$jar" ]] || die "Jar not found for $module" + cp "$jar" "$target_dir/app.jar" + + log "Build image: $(image_name "$service"):$IMAGE_TAG" + run_step "$server_dir" docker build \ + -f Dockerfile.service \ + --build-arg "MODULE=$module" \ + -t "$(image_name "$service"):$IMAGE_TAG" \ + -t "$(image_name "$service"):latest" \ + . + done +} + +build_pdf_image() { + local server_dir="$ROOT_DIR/emp_server" + log "Build image: $(image_name pdf):$IMAGE_TAG" + run_step "$server_dir" docker build \ + -f emp_pdf/Dockerfile \ + -t "$(image_name pdf):$IMAGE_TAG" \ + -t "$(image_name pdf):latest" \ + emp_pdf +} + +build_ws_image() { + log "Build image: $(image_name ws):$IMAGE_TAG" + run_step "$ROOT_DIR" docker build \ + -f "$SCRIPT_DIR/dockerfiles/emp-ws.Dockerfile" \ + --build-arg "NPM_REGISTRY=$NPM_REGISTRY" \ + -t "$(image_name ws):$IMAGE_TAG" \ + -t "$(image_name ws):latest" \ + . +} + +build_admin_image() { + local admin_dir="$ROOT_DIR/emp_admin" + local context_dir="$BUILD_CONTEXT_DIR/emp-admin" + + log "Build frontend dist" + run_step "$admin_dir" pnpm install --frozen-lockfile + run_step "$admin_dir" pnpm run build:shunfeng + + log "Prepare frontend image context" + rm -rf "$context_dir" + mkdir -p "$context_dir" + cp -R "$admin_dir/dist" "$context_dir/dist" + cp "$SCRIPT_DIR/nginx/admin.conf" "$context_dir/admin.conf" + + log "Build image: $(image_name admin):$IMAGE_TAG" + run_step "$context_dir" docker build \ + -f "$SCRIPT_DIR/dockerfiles/emp-admin.Dockerfile" \ + -t "$(image_name admin):$IMAGE_TAG" \ + -t "$(image_name admin):latest" \ + . +} + +prepare_package() { + local services_slug="$1" + PACKAGE_NAME="${PACKAGE_NAME:-emp-test-update-${IMAGE_TAG}-${services_slug}}" + PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" + PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" + + rm -rf "$PACKAGE_DIR" + mkdir -p "$PACKAGE_DIR" + + cp "$SCRIPT_DIR/apply-update.sh" "$PACKAGE_DIR/apply-update.sh" + chmod +x "$PACKAGE_DIR/apply-update.sh" + + local service + for service in "${REQUESTED_SERVICES[@]}"; do + compose_service_name "$service" + done > "$PACKAGE_DIR/services.txt" + + { + echo "image_namespace=$IMAGE_NAMESPACE" + echo "image_tag=$IMAGE_TAG" + echo "services=${REQUESTED_SERVICES[*]}" + echo "compose_services=$(tr '\n' ' ' < "$PACKAGE_DIR/services.txt")" + echo "created_at=$(date '+%Y-%m-%d %H:%M:%S')" + } > "$PACKAGE_DIR/manifest.txt" +} + +save_images() { + local images=() + local service image + for service in "${REQUESTED_SERVICES[@]}"; do + image="$(image_name "$service")" + images+=("$image:$IMAGE_TAG" "$image:latest") + done + + log "Save images: ${images[*]}" + docker save -o "$PACKAGE_DIR/images.tar" "${images[@]}" +} + +archive_package() { + mkdir -p "$DIST_DIR" + rm -f "$PACKAGE_ARCHIVE" + tar -czf "$PACKAGE_ARCHIVE" -C "$DIST_DIR" "$PACKAGE_NAME" + log "Update package created: $PACKAGE_ARCHIVE" +} + +need_cmd docker +need_cmd tar + +mapfile -t REQUESTED_SERVICES < <(read_requested_services "$@") +[[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "No services to update." + +ROOT_DIR="$(resolve_root_dir)" +SERVICES_SLUG="$(IFS=-; echo "${REQUESTED_SERVICES[*]}")" + +log "EMP root: $ROOT_DIR" +log "Deploy root: $SCRIPT_DIR" +log "Update services: ${REQUESTED_SERVICES[*]}" +log "Image tag: $IMAGE_TAG" + +if [[ "$SKIP_BUILD" != "1" ]]; then + JAVA_SERVICES=() + NEED_PNPM=0 + for service in "${REQUESTED_SERVICES[@]}"; do + if is_java_service "$service"; then + JAVA_SERVICES+=("$service") + fi + if [[ "$service" == "admin" ]]; then + NEED_PNPM=1 + fi + done + + if [[ "${#JAVA_SERVICES[@]}" -gt 0 ]]; then + need_cmd mvn + fi + if [[ "$NEED_PNPM" == "1" ]]; then + need_cmd pnpm + fi + + build_java_images "${JAVA_SERVICES[@]}" + + for service in "${REQUESTED_SERVICES[@]}"; do + case "$service" in + pdf) build_pdf_image ;; + ws) build_ws_image ;; + admin) build_admin_image ;; + esac + done +else + log "Skip build, package existing local images only" +fi + +prepare_package "$SERVICES_SLUG" +save_images +archive_package + +cat < process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\""] + interval: 10s + timeout: 5s + retries: 30 emp-ws: <<: *app-env diff --git a/isolated/test/docker-compose.yml b/isolated/test/docker-compose.yml index 1255bdf..c9b7dc9 100644 --- a/isolated/test/docker-compose.yml +++ b/isolated/test/docker-compose.yml @@ -173,7 +173,7 @@ services: tdengine: condition: service_healthy emp-pdf: - condition: service_started + condition: service_healthy emp-data: <<: *app-env @@ -190,6 +190,11 @@ services: emp-pdf: <<: *app-env image: ${IMAGE_NAMESPACE:-emp-test}/emp-pdf:${IMAGE_TAG:-latest} + healthcheck: + test: ["CMD-SHELL", "node -e \"require('http').get('http://127.0.0.1:3100/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\""] + interval: 10s + timeout: 5s + retries: 30 emp-ws: <<: *app-env @@ -228,4 +233,4 @@ volumes: tdengine_data: tdengine_log: nacos_data: - nacos_logs: \ No newline at end of file + nacos_logs: diff --git a/isolated/update.MD b/isolated/update.MD new file mode 100644 index 0000000..fa6d9ba --- /dev/null +++ b/isolated/update.MD @@ -0,0 +1,207 @@ +# 隔离测试环境增量更新说明 + +本文档用于后续只更新部分 EMP 服务镜像,不再每次全量打包中间件和所有服务。 + +## 一、适用场景 + +适用于只更新以下应用服务中的一个或多个: + +| 简写 | Compose 服务 | 镜像 | +| --- | --- | --- | +| gateway | emp-gateway | emp-test/emp-gateway | +| auth | emp-auth | emp-test/emp-auth | +| monitor | emp-monitor | emp-test/emp-monitor | +| data | emp-data | emp-test/emp-data | +| pdf | emp-pdf | emp-test/emp-pdf | +| ws | emp-ws | emp-test/emp-ws | +| admin | emp-admin | emp-test/emp-admin | + +中间件 MySQL、Redis、Kafka、TDengine、Nacos 不走增量更新包。 + +## 二、构建机生成增量包 + +进入部署脚本目录: + +```bash +cd /home/git/emp_test_deploy/isolated +``` + +只更新 `admin` 和 `monitor`: + +```bash +EMP_ROOT=/home/git/emp \ +IMAGE_NAMESPACE=emp-test \ +./build-update.sh admin monitor +``` + +生成文件在: + +```bash +dist/emp-test-update-<时间戳>-admin-monitor.tar.gz +``` + +也可以更新其他服务: + +```bash +# 只更新前端 +EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh admin + +# 只更新数据服务 +EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh data + +# 更新模拟器 / WebSocket +EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh ws + +# 更新 PDF 服务 +EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh pdf +``` + +如果镜像已经在本机构建好,只想重新打包已有镜像: + +```bash +EMP_ROOT=/home/git/emp \ +IMAGE_NAMESPACE=emp-test \ +SKIP_BUILD=1 \ +./build-update.sh admin monitor +``` + +## 三、传输到甲方服务器 + +将增量包传到甲方服务器任意目录,例如: + +```bash +/home/admin-x99/emp-test/update/emp-test-update-20260602153000-admin-monitor.tar.gz +``` + +## 四、甲方服务器应用增量包 + +进入服务器: + +```bash +cd /home/admin-x99/emp-test +mkdir -p update-runtime +tar -xzf update/emp-test-update-20260602153000-admin-monitor.tar.gz \ + -C update-runtime \ + --strip-components=1 +cd update-runtime +``` + +执行增量更新: + +```bash +bash apply-update.sh +``` + +脚本会优先使用当前目录下的 `.env`、`docker-compose.yml`;如果当前目录没有,会自动查找 `../runtime/` 和 `/home/admin-x99/emp-test/runtime/` 下的运行配置。 + +脚本会自动执行: + +```bash +docker load -i images.tar +docker compose --env-file .env -f docker-compose.yml -p emp-test \ + up -d --no-deps --force-recreate emp-admin emp-monitor +``` + +如果当前目录没有 `.env` 和 `docker-compose.yml`,可以显式指定运行环境目录中的文件: + +```bash +PROJECT_NAME=emp-test \ +ENV_FILE=/home/admin-x99/emp-test/runtime/.env \ +COMPOSE_FILE=/home/admin-x99/emp-test/runtime/docker-compose.yml \ +bash apply-update.sh +``` + +也可以手动指定更新服务,覆盖包内 `services.txt`: + +```bash +PROJECT_NAME=emp-test \ +ENV_FILE=/home/admin-x99/emp-test/runtime/.env \ +COMPOSE_FILE=/home/admin-x99/emp-test/runtime/docker-compose.yml \ +bash apply-update.sh emp-admin emp-monitor +``` + +## 五、验证 + +查看服务状态: + +```bash +cd /home/admin-x99/emp-test/runtime +docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-admin emp-monitor +``` + +查看后端日志: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test logs --tail=100 emp-monitor +``` + +验证前端: + +```bash +curl -I http://127.0.0.1:37361 +``` + +## 六、注意事项 + +1. 增量更新默认同时打 `latest` 和时间戳 tag。 +2. 当前测试环境 `.env` 建议继续使用 `IMAGE_TAG=latest`,不要为了增量包修改成时间戳。 +3. 如果把 `.env` 的 `IMAGE_TAG` 改成某个新时间戳,但只传了部分服务镜像,其他服务会因为缺少该 tag 而无法重建。 +4. 如果修改了 `docker-compose.yml`、`.env.example`、中间件初始化逻辑,建议重新全量打包或单独同步配置文件。 +5. 数据库结构变更不包含在镜像增量包中,需要单独执行 SQL 迁移。 + +## 七、PDF 导出故障排查 + +报错: + +```text +PDF导出失败: I/O error on GET request for "http://emp-pdf:3100/pdf": Connection refused +``` + +含义:`emp-monitor` 已经访问到 Docker 内网地址 `emp-pdf:3100`,但 PDF 服务端口没有进程监听,常见原因是 `emp-pdf` 容器未启动、启动后退出、正在重启,或 Node 服务未正常监听 3100。 + +新版 `docker-compose.yml` 已给 `emp-pdf` 增加 `/health` 健康检查,并让 `emp-monitor` 等待 `emp-pdf` 健康后再启动。若服务器仍使用旧 compose,需要先同步新的 `docker-compose.yml` 或按下面命令手动重启 PDF 服务。 + +先看容器状态: + +```bash +cd /home/admin-x99/emp-test/runtime +docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-pdf emp-monitor +``` + +查看 PDF 服务日志: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test logs --tail=200 emp-pdf +``` + +在 PDF 容器内检查健康接口: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test exec emp-pdf \ + node -e "require('http').get('http://127.0.0.1:3100/health', r => { console.log(r.statusCode); r.pipe(process.stdout) }).on('error', e => { console.error(e.message); process.exit(1) })" +``` + +在同一个 Docker 网络里检查 `emp-monitor` 到 `emp-pdf` 的访问: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test exec emp-monitor \ + sh -lc "curl -sS http://emp-pdf:3100/health || wget -qO- http://emp-pdf:3100/health" +``` + +如果 `emp-pdf` 未运行或健康检查失败,先重启 PDF 服务: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test up -d --no-deps --force-recreate emp-pdf +``` + +再重启 monitor,使其重新调用可用的 PDF 服务: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test restart emp-monitor +``` + +如果日志中出现 Chromium/Puppeteer 相关错误,重新构建并增量更新 `pdf`: + +```bash +EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh pdf +```