From e839ef100a06a951dac639365ed88f7706f43bfb Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Mon, 1 Jun 2026 11:40:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=8D=87=E7=BA=A7=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E6=8C=87=E5=BC=95=E4=B8=8E=E9=95=9C=E5=83=8F=E4=BD=93?= =?UTF-8?q?=E7=A7=AF=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + README.md | 5 +- deploy-registry.ps1 | 54 +++++++++++++ docs/deployment/部署说明.md | 20 ++++- flnxi-agent-go/install.sh | 3 +- scripts/README.md | 16 ++++ scripts/measure-image-size.ps1 | 134 +++++++++++++++++++++++++++++++++ 7 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 .gitattributes create mode 100644 scripts/measure-image-size.ps1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/README.md b/README.md index 18565e2..fdffaa9 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ gongxiangp2-source-reference-old-style-20260513-132942/ - `flnxi-agent/`:Node.js / Express / Socket.IO - `flnxi-agent-go/`:Go / Gin / Docker SDK +Agent 是实施和运维编排工具,不是普通客户日常入口,也不是当前唯一正式安装器。正式客户首装仍以 `docker-compose.prod.yml + .env + scripts/smoke-install.ps1` 为主,Agent 只在交付方案明确采用时启用。 + ## 6. 本地开发 ### 安装依赖 @@ -205,11 +207,12 @@ docker compose -f docker-compose.prod.yml up -d - `deploy-registry.ps1` - `scripts/smoke-install.ps1` - `scripts/backup-restore.ps1` +- `scripts/measure-image-size.ps1` - `deploy-new-fixed.ps1` - `发布管理.ps1` - `license-server/deploy.ps1` -其中 `scripts/smoke-install.ps1` 用于首装后的基础验收,`scripts/backup-restore.ps1` 用于客户环境备份、恢复和恢复后验收。其它脚本执行前必须先读内容和参数,不应把内部固定服务器或固定镜像仓库当成客户默认交付配置。客户交付时要显式写入版本化 `APP_IMAGE` / `IMAGE_TAG`,`latest` 只保留给内部测试或兼容场景。当前新 Registry 发布脚本是主线,旧远程部署脚本只作历史或内部维护参考。AI、钉钉、内网穿透等可选能力依赖真实外部服务和密钥,不能只凭页面入口当成客户验收已完成。 +其中 `scripts/smoke-install.ps1` 用于首装后的基础验收,`scripts/backup-restore.ps1` 用于客户环境备份、恢复和恢复后验收,`scripts/measure-image-size.ps1` 用于发布前记录镜像体积和分层。其它脚本执行前必须先读内容和参数,不应把内部固定服务器或固定镜像仓库当成客户默认交付配置。客户交付时要显式写入版本化 `APP_IMAGE` / `IMAGE_TAG`,`latest` 只保留给内部测试或兼容场景。当前新 Registry 发布脚本是主线,旧远程部署脚本只作历史或内部维护参考。AI、钉钉、内网穿透等可选能力依赖真实外部服务和密钥,不能只凭页面入口当成客户验收已完成。 ## 10. 文档入口 diff --git a/deploy-registry.ps1 b/deploy-registry.ps1 index ba14d40..e8b6ea0 100644 --- a/deploy-registry.ps1 +++ b/deploy-registry.ps1 @@ -550,6 +550,9 @@ backup_dir="./backups/deploy-$deploy_started_at" mkdir -p "$backup_dir" env_backup="$backup_dir/.env.before-upgrade" cp .env "$env_backup" +db_backup="" +object_backup="" +minio_volume_name="" upsert_env() { key="$1" @@ -580,6 +583,54 @@ get_service_container_id() { docker compose -f "$compose_file" ps -q "$compose_service" 2>/dev/null || true } +write_restore_plan() { + plan_file="$backup_dir/RESTORE_PLAN.txt" + { + echo "FLNXI 升级恢复分级" + echo "备份目录:$backup_dir" + echo "" + echo "L1:只恢复应用镜像和 .env。适用于拉镜像失败、容器启动失败、未执行数据库或素材结构变更的场景。" + echo "L2:恢复应用镜像 + PostgreSQL。适用于本次升级已经执行数据库迁移,且需要回到升级前数据结构的场景。" + echo "L3:恢复应用镜像 + PostgreSQL + MinIO/S3 素材文件。适用于数据库和素材对象都可能被本次升级写入或调整的场景。" + echo "" + echo "本次备份文件:" + echo "- .env:$env_backup" + if [ -n "${db_backup:-}" ] && [ -s "$db_backup" ]; then + echo "- PostgreSQL:$db_backup" + else + echo "- PostgreSQL:未生成 Compose 内置数据库备份,请检查是否使用外部数据库或已跳过备份。" + fi + if [ -n "${object_backup:-}" ] && [ -s "$object_backup" ]; then + echo "- MinIO 数据卷:$object_backup" + else + echo "- MinIO 数据卷:未生成 Compose 内置对象存储备份,请检查是否使用外部 S3/MinIO 或已跳过备份。" + fi + echo "" + echo "恢复命令示例:" + echo "L1 应用回滚:" + echo " cp \"$env_backup\" .env" + echo " docker compose -f \"$compose_file\" up -d --no-deps --pull never --force-recreate \"$compose_service\"" + if [ -n "${db_backup:-}" ] && [ -s "$db_backup" ]; then + echo "" + echo "L2 数据库恢复(执行前必须停写入并确认备份来源):" + echo " docker compose -f \"$compose_file\" stop \"$compose_service\"" + echo " docker compose -f \"$compose_file\" exec -T postgres sh -c 'pg_restore -U \"\${POSTGRES_USER:-postgres}\" -d \"\${POSTGRES_DB:-flnxi}\" --clean --if-exists --no-owner --no-privileges' < \"$db_backup\"" + fi + if [ -n "${object_backup:-}" ] && [ -s "$object_backup" ]; then + object_backup_name="$(basename "$object_backup")" + [ -n "${minio_volume_name:-}" ] || minio_volume_name="flnxi-minio-data" + echo "" + echo "L3 对象存储恢复(执行前必须确认不会覆盖新上传素材):" + echo " docker compose -f \"$compose_file\" stop \"$compose_service\" minio" + echo " docker run --rm -v \"${minio_volume_name}:/target\" -v \"$(pwd)/${backup_dir#./}:/backup:ro\" \"$remote_backup_helper_image\" sh -c 'rm -rf /target/* && tar -xzf /backup/$object_backup_name -C /target'" + echo " docker compose -f \"$compose_file\" up -d" + fi + echo "" + echo "恢复后必须验收:docker compose -f \"$compose_file\" ps,并确认 /api/health 返回 200。" + } > "$plan_file" + echo "恢复分级说明:$plan_file" +} + old_env_app_image="$(read_env_value APP_IMAGE || true)" old_container_id="$(get_service_container_id)" old_container_image="" @@ -625,6 +676,8 @@ rollback() { else echo "[ROLLBACK] 未找到旧镜像,无法自动恢复应用容器" fi + write_restore_plan || true + echo "[ROLLBACK] 如需要恢复数据库或素材文件,请按 RESTORE_PLAN.txt 的 L2/L3 步骤人工确认后执行" exit "$code" } @@ -727,6 +780,7 @@ verify_running_image() { backup_postgres backup_minio_volume +write_restore_plan '@) if (-not $NoRollback) { diff --git a/docs/deployment/部署说明.md b/docs/deployment/部署说明.md index fe6807a..b260258 100644 --- a/docs/deployment/部署说明.md +++ b/docs/deployment/部署说明.md @@ -217,12 +217,13 @@ docker logs flnxi-material --tail 100 - 升级前在远端创建 `backups/deploy-时间戳/`,保存升级前 `.env`。 - 如果远端 PostgreSQL 是当前 Compose 管理的 `postgres` 服务,会先执行 `pg_dump` 备份;如果使用外部数据库,会提示需要确认外部备份。 - 如果远端 MinIO 是当前 Compose 管理的 `minio` 服务,会先把 `MINIO_VOLUME_NAME` 对应的数据卷打包备份;如果使用外部 S3 / 外部 MinIO,会在备份目录写入提示文件,要求按客户现场方案单独备份对象存储。 +- 备份目录会生成 `RESTORE_PLAN.txt`,把本次 `.env`、PostgreSQL、MinIO 备份文件和 L1/L2/L3 恢复命令列出来,便于升级失败时按影响范围恢复。 - 部署失败时默认尝试恢复升级前 `.env`,并按旧运行镜像或旧 `APP_IMAGE` 重建应用容器。 - 部署后会在远端校验应用容器实际运行镜像、镜像内 `VERSION.txt` 和 Prisma 迁移状态,然后再做外部 `/api/health` 检查。 只有在已有其它备份和回滚体系时,才考虑使用 `-NoRemoteDbBackup`、`-NoRemoteObjectStorageBackup` 或 `-NoRollback`。离线客户现场如果没有 `busybox:1.36`,需要先把该 helper 镜像放到目标机,或通过 `-RemoteBackupHelperImage` 指定现场已有的小型 Linux 镜像。 -必须注意:脚本的自动回滚只恢复应用镜像和 `.env`,不自动恢复数据库和对象存储。只要本次升级包含数据库迁移或素材文件结构变化,升级前的数据库备份和对象存储备份就必须保留,必要时由人工确认后恢复。 +必须注意:脚本的自动回滚只恢复应用镜像和 `.env`,不自动恢复数据库和对象存储。只要本次升级包含数据库迁移或素材文件结构变化,升级前的数据库备份和对象存储备份就必须保留,并按 `RESTORE_PLAN.txt` 的 L2/L3 步骤由人工确认后恢复。 典型用法: @@ -240,15 +241,28 @@ $sshPassword = Read-Host 'SSH密码' -AsSecureString - 客户升级和内部发布优先按版本 tag 走,不要把 `latest` 当成客户部署依据。 - 客户现场部署使用 `-DeployChannel release -Version x.x.x`;测试通道只用于内部验证,必须显式传 `-DeployChannel test`。 - `DeployApp` 远程部署时默认会更新远端 `.env` 的 `APP_IMAGE`、访问地址和运行时镜像;只有临时验证时才使用 `-NoUpdateRemoteEnv`。 -- `DeployApp` 默认会做远端 `.env` 备份、Compose PostgreSQL 备份、失败自动回滚、运行镜像校验、版本文件校验和 Prisma 迁移状态校验。 +- `DeployApp` 默认会做远端 `.env` 备份、Compose PostgreSQL 备份、Compose MinIO 备份、恢复分级说明、失败自动回滚、运行镜像校验、版本文件校验和 Prisma 迁移状态校验。 - 旧的 `deploy-new-fixed.ps1` 仍可作为内部维护参考,但不再作为当前主线部署入口。 - 如果要做远程更新,先确认 `Registry`、SSH、`.env` 和目标机器都属于当前维护环境。 +### 4.1 镜像体积基线 + +发布前可以对主应用镜像做一次体积和分层记录: + +```powershell +.\scripts\measure-image-size.ps1 ` + -Image "registry.example.com/flnxi/material/flnxi-material:1.0.1" ` + -OutputPath ".\reports\image-size-1.0.1.md" +``` + +这个脚本不会改镜像,只会读取 Docker 本地镜像信息,列出总大小、层数和最大的若干层。后续做镜像瘦身时,先用这个报告确认是 `ffmpeg`、字体、`canvas`、`node_modules` 还是 Prisma 迁移工具占用更大,再决定是否拆成可选能力或独立迁移镜像。 + ## 5. 部署前检查清单 - [ ] 已确认目标机器品牌、系统和 Docker Compose 支持情况;如使用 Podman,已单独实机验证 - [ ] 已确认端口、域名、证书、反向代理方案 - [ ] 已确认数据目录、对象存储目录和备份位置 +- [ ] 已确认升级失败时使用 L1 应用回滚、L2 数据库恢复还是 L3 数据库+素材恢复 - [ ] `.env` 已填写真实环境变量;内置 PostgreSQL 模式下 `DATABASE_URL` 没有错误占位值 - [ ] `APP_IMAGE` 已固定到完整版本镜像,不使用 `latest` - [ ] 如果 Registry 是 HTTP,本机和目标机 Docker 都已配置 `insecure-registries` @@ -325,6 +339,8 @@ $sshPassword = Read-Host 'SSH密码' -AsSecureString - Compose 管理的 MinIO 数据卷:`minio-data.tar.gz` - `manifest.json`:记录备份时间、应用镜像、访问地址、服务名等关键信息 +`DeployApp` 升级时生成的 `backups/deploy-时间戳/RESTORE_PLAN.txt` 是升级恢复指引;`backup-restore.ps1 -Action Backup` 生成的 `flnxi-backup-时间戳/manifest.json` 是完整备份包记录。两者用途不同,客户升级失败时优先看当次升级目录里的 `RESTORE_PLAN.txt`。 + 如果 `DATABASE_URL` 指向外部 PostgreSQL,脚本不会假装自动备份,会在备份目录写入 `EXTERNAL_DATABASE_BACKUP_REQUIRED.txt`,客户现场必须按外部数据库方案单独备份。 如果对象存储是外部 S3 / 外部 MinIO,或现场已有独立对象存储快照,可加: diff --git a/flnxi-agent-go/install.sh b/flnxi-agent-go/install.sh index 2468cf3..a4271cb 100644 --- a/flnxi-agent-go/install.sh +++ b/flnxi-agent-go/install.sh @@ -303,7 +303,8 @@ show_completion() { echo -e " 停止服务: ${YELLOW}docker stop $CONTAINER_NAME${NC}" echo -e " 卸载服务: ${YELLOW}docker rm -f $CONTAINER_NAME${NC}" echo "" - echo -e "请在浏览器中打开上述地址,完成系统安装向导。" + echo -e "请在浏览器中打开上述地址,按实施方案使用 Agent 编排或运维。" + echo -e "${YELLOW}正式客户首装主线仍是 docker-compose.prod.yml + .env + smoke-install.ps1;Agent 不是唯一安装入口。${NC}" echo "" } diff --git a/scripts/README.md b/scripts/README.md index 233d0f3..d88a2be 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -43,6 +43,8 @@ npm run verify:template-data 客户首装后的基础验收脚本,可检查 Compose 配置、容器状态、运行镜像、版本文件、迁移状态、MinIO bucket 和 `/api/health` - `backup-restore.ps1` 客户环境备份、恢复和健康验收脚本;恢复必须显式输入确认短语,避免误覆盖客户数据 +- `measure-image-size.ps1` + 发布前读取主应用镜像体积和分层,记录后续瘦身基线;只读检查,不修改镜像 - `generate-shared-test-env.ps1` 为共享测试机生成隔离后的 `.env`,避免容器名、卷名、网络名与其他项目冲突;这是内部测试脚本,不是客户安装器 @@ -133,6 +135,20 @@ npx tsx scripts/fix-missing-templates.ts - 外部数据库、外部对象存储由客户现场方案单独备份;脚本会写提示文件,不会假装自动覆盖。 - 对象存储备份/恢复使用 `busybox:1.36` 作为 helper 镜像;离线客户现场需要提前放入该镜像或通过 `-HelperImage` 指定现场已有镜像。 +### 镜像体积和分层基线 + +```powershell +.\scripts\measure-image-size.ps1 ` + -Image "registry.example.com/flnxi/material/flnxi-material:1.0.1" ` + -OutputPath ".\reports\image-size-1.0.1.md" +``` + +说明: + +- 该脚本只读取本地 Docker 镜像,不构建、不推送、不删除。 +- 报告会列出镜像总大小、RootFS 层数和最大的若干层。 +- 先有基线,再做 `ffmpeg`、字体、`canvas`、Prisma CLI 或生产依赖瘦身,避免凭感觉判断体积来源。 + ### 手工触发单个视频转码 ```bash diff --git a/scripts/measure-image-size.ps1 b/scripts/measure-image-size.ps1 new file mode 100644 index 0000000..dc49cd4 --- /dev/null +++ b/scripts/measure-image-size.ps1 @@ -0,0 +1,134 @@ +# 主应用镜像体积和分层检查工具 +# 用于发布前记录体积基线,帮助判断哪些层需要继续瘦身。 + +param( + [Parameter(Mandatory = $true)] + [string]$Image, + + [int]$TopLayers = 12, + + [string]$OutputPath = "" +) + +$ErrorActionPreference = "Stop" + +function Format-Bytes { + param([double]$Bytes) + + if ($Bytes -ge 1GB) { + return ("{0:N2} GB" -f ($Bytes / 1GB)) + } + if ($Bytes -ge 1MB) { + return ("{0:N2} MB" -f ($Bytes / 1MB)) + } + if ($Bytes -ge 1KB) { + return ("{0:N2} KB" -f ($Bytes / 1KB)) + } + + return ("{0:N0} B" -f $Bytes) +} + +function Convert-DockerSizeToBytes { + param([string]$Value) + + if ([string]::IsNullOrWhiteSpace($Value) -or $Value -eq "0B") { + return 0 + } + + if ($Value -match '^([0-9.]+)\s*([kmgt]?B)$') { + $number = [double]$Matches[1] + switch ($Matches[2].ToUpperInvariant()) { + "KB" { return $number * 1KB } + "MB" { return $number * 1MB } + "GB" { return $number * 1GB } + "TB" { return $number * 1TB } + default { return $number } + } + } + + return 0 +} + +function Shorten { + param([string]$Text, [int]$Length = 110) + + if ([string]::IsNullOrWhiteSpace($Text)) { + return "" + } + + $singleLine = ($Text -replace '\s+', ' ').Trim() + if ($singleLine.Length -le $Length) { + return $singleLine + } + + return $singleLine.Substring(0, $Length - 3) + "..." +} + +Write-Host "[INFO] 检查镜像:$Image" -ForegroundColor Cyan + +$inspectJson = docker image inspect $Image --format '{{json .}}' +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($inspectJson)) { + throw "无法读取镜像信息:$Image" +} + +$inspect = $inspectJson | ConvertFrom-Json +$historyRows = docker history $Image --no-trunc --format '{{json .}}' | + ForEach-Object { + if (-not [string]::IsNullOrWhiteSpace($_)) { + $_ | ConvertFrom-Json + } + } + +$layers = @($historyRows | ForEach-Object { + [pscustomobject]@{ + Id = $_.ID + Size = $_.Size + SizeBytes = Convert-DockerSizeToBytes $_.Size + CreatedBy = Shorten $_.CreatedBy + } +}) + +$largestLayers = @($layers | Sort-Object SizeBytes -Descending | Select-Object -First $TopLayers) +$layerCount = if ($inspect.RootFS -and $inspect.RootFS.Layers) { @($inspect.RootFS.Layers).Count } else { $layers.Count } +$totalSize = [double]$inspect.Size + +$report = New-Object System.Collections.Generic.List[string] +$report.Add("# FLNXI 镜像体积基线") +$report.Add("") +$report.Add("- 镜像:``$Image``") +$report.Add("- 总大小:$(Format-Bytes $totalSize)") +$report.Add("- RootFS 层数:$layerCount") +$report.Add("- 生成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") +$report.Add("") +$report.Add("## 最大分层") +$report.Add("") +$report.Add("| 排名 | 大小 | 指令 |") +$report.Add("| --- | ---: | --- |") + +$rank = 1 +foreach ($layer in $largestLayers) { + $createdBy = ($layer.CreatedBy -replace '\|', '\|') + $report.Add("| $rank | $($layer.Size) | ``$createdBy`` |") + $rank += 1 +} + +$report.Add("") +$report.Add("## 重点观察") +$report.Add("") +$report.Add("- 如果 `apk add`、字体、`ffmpeg`、`canvas` 相关层过大,优先评估是否能拆成可选运行能力。") +$report.Add("- 如果 `npm ci` 或 `node_modules` 层过大,优先检查生产依赖和 standalone 输出是否被重复复制。") +$report.Add("- 如果 `prisma` 相关层过大,先确认容器启动迁移是否必须携带完整 CLI,再决定是否拆分迁移镜像。") +$report.Add("- 发布前保留这个报告,后续升级才能看到镜像体积是否真的下降。") + +$text = $report -join [Environment]::NewLine + +if ([string]::IsNullOrWhiteSpace($OutputPath)) { + Write-Host $text +} else { + $parent = Split-Path -Parent $OutputPath + if ($parent -and -not (Test-Path -LiteralPath $parent -PathType Container)) { + New-Item -ItemType Directory -Path $parent | Out-Null + } + $text | Out-File -LiteralPath $OutputPath -Encoding utf8 + Write-Host "[OK] 已写入镜像体积报告:$OutputPath" -ForegroundColor Green +}