leiyun 2 недель назад
Родитель
Сommit
5ae2ba6561
5 измененных файлов: 664 добавлений и 3 удалений
  1. +128
    -0
      isolated/apply-update.sh
  2. +316
    -0
      isolated/build-update.sh
  3. +6
    -1
      isolated/docker-compose.runtime.yml
  4. +7
    -2
      isolated/test/docker-compose.yml
  5. +207
    -0
      isolated/update.MD

+ 128
- 0
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[@]}"

+ 316
- 0
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 <<EOF

Next steps on target server:
mkdir -p /tmp/emp-update
tar -xzf $(basename "$PACKAGE_ARCHIVE") -C /tmp/emp-update --strip-components=1
cd /tmp/emp-update
bash apply-update.sh

EOF

+ 6
- 1
isolated/docker-compose.runtime.yml Просмотреть файл

@@ -187,7 +187,7 @@ services:
tdengine:
condition: service_healthy
emp-pdf:
condition: service_started
condition: service_healthy

emp-data:
<<: *app-env
@@ -206,6 +206,11 @@ services:
image: ${IMAGE_NAMESPACE:-emp-test}/emp-pdf:${IMAGE_TAG:-latest}
ports:
- "127.0.0.1:${PDF_HOST_PORT:-3100}:3100"
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


+ 7
- 2
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:
nacos_logs:

+ 207
- 0
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
```

Загрузка…
Отмена
Сохранить