完善升级恢复指引与镜像体积基线
This commit is contained in:
parent
7068732479
commit
e839ef100a
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
@ -94,6 +94,8 @@ gongxiangp2-source-reference-old-style-20260513-132942/
|
|||||||
- `flnxi-agent/`:Node.js / Express / Socket.IO
|
- `flnxi-agent/`:Node.js / Express / Socket.IO
|
||||||
- `flnxi-agent-go/`:Go / Gin / Docker SDK
|
- `flnxi-agent-go/`:Go / Gin / Docker SDK
|
||||||
|
|
||||||
|
Agent 是实施和运维编排工具,不是普通客户日常入口,也不是当前唯一正式安装器。正式客户首装仍以 `docker-compose.prod.yml + .env + scripts/smoke-install.ps1` 为主,Agent 只在交付方案明确采用时启用。
|
||||||
|
|
||||||
## 6. 本地开发
|
## 6. 本地开发
|
||||||
|
|
||||||
### 安装依赖
|
### 安装依赖
|
||||||
@ -205,11 +207,12 @@ docker compose -f docker-compose.prod.yml up -d
|
|||||||
- `deploy-registry.ps1`
|
- `deploy-registry.ps1`
|
||||||
- `scripts/smoke-install.ps1`
|
- `scripts/smoke-install.ps1`
|
||||||
- `scripts/backup-restore.ps1`
|
- `scripts/backup-restore.ps1`
|
||||||
|
- `scripts/measure-image-size.ps1`
|
||||||
- `deploy-new-fixed.ps1`
|
- `deploy-new-fixed.ps1`
|
||||||
- `发布管理.ps1`
|
- `发布管理.ps1`
|
||||||
- `license-server/deploy.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. 文档入口
|
## 10. 文档入口
|
||||||
|
|
||||||
|
|||||||
@ -550,6 +550,9 @@ backup_dir="./backups/deploy-$deploy_started_at"
|
|||||||
mkdir -p "$backup_dir"
|
mkdir -p "$backup_dir"
|
||||||
env_backup="$backup_dir/.env.before-upgrade"
|
env_backup="$backup_dir/.env.before-upgrade"
|
||||||
cp .env "$env_backup"
|
cp .env "$env_backup"
|
||||||
|
db_backup=""
|
||||||
|
object_backup=""
|
||||||
|
minio_volume_name=""
|
||||||
|
|
||||||
upsert_env() {
|
upsert_env() {
|
||||||
key="$1"
|
key="$1"
|
||||||
@ -580,6 +583,54 @@ get_service_container_id() {
|
|||||||
docker compose -f "$compose_file" ps -q "$compose_service" 2>/dev/null || true
|
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_env_app_image="$(read_env_value APP_IMAGE || true)"
|
||||||
old_container_id="$(get_service_container_id)"
|
old_container_id="$(get_service_container_id)"
|
||||||
old_container_image=""
|
old_container_image=""
|
||||||
@ -625,6 +676,8 @@ rollback() {
|
|||||||
else
|
else
|
||||||
echo "[ROLLBACK] 未找到旧镜像,无法自动恢复应用容器"
|
echo "[ROLLBACK] 未找到旧镜像,无法自动恢复应用容器"
|
||||||
fi
|
fi
|
||||||
|
write_restore_plan || true
|
||||||
|
echo "[ROLLBACK] 如需要恢复数据库或素材文件,请按 RESTORE_PLAN.txt 的 L2/L3 步骤人工确认后执行"
|
||||||
exit "$code"
|
exit "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -727,6 +780,7 @@ verify_running_image() {
|
|||||||
|
|
||||||
backup_postgres
|
backup_postgres
|
||||||
backup_minio_volume
|
backup_minio_volume
|
||||||
|
write_restore_plan
|
||||||
'@)
|
'@)
|
||||||
|
|
||||||
if (-not $NoRollback) {
|
if (-not $NoRollback) {
|
||||||
|
|||||||
@ -217,12 +217,13 @@ docker logs flnxi-material --tail 100
|
|||||||
- 升级前在远端创建 `backups/deploy-时间戳/`,保存升级前 `.env`。
|
- 升级前在远端创建 `backups/deploy-时间戳/`,保存升级前 `.env`。
|
||||||
- 如果远端 PostgreSQL 是当前 Compose 管理的 `postgres` 服务,会先执行 `pg_dump` 备份;如果使用外部数据库,会提示需要确认外部备份。
|
- 如果远端 PostgreSQL 是当前 Compose 管理的 `postgres` 服务,会先执行 `pg_dump` 备份;如果使用外部数据库,会提示需要确认外部备份。
|
||||||
- 如果远端 MinIO 是当前 Compose 管理的 `minio` 服务,会先把 `MINIO_VOLUME_NAME` 对应的数据卷打包备份;如果使用外部 S3 / 外部 MinIO,会在备份目录写入提示文件,要求按客户现场方案单独备份对象存储。
|
- 如果远端 MinIO 是当前 Compose 管理的 `minio` 服务,会先把 `MINIO_VOLUME_NAME` 对应的数据卷打包备份;如果使用外部 S3 / 外部 MinIO,会在备份目录写入提示文件,要求按客户现场方案单独备份对象存储。
|
||||||
|
- 备份目录会生成 `RESTORE_PLAN.txt`,把本次 `.env`、PostgreSQL、MinIO 备份文件和 L1/L2/L3 恢复命令列出来,便于升级失败时按影响范围恢复。
|
||||||
- 部署失败时默认尝试恢复升级前 `.env`,并按旧运行镜像或旧 `APP_IMAGE` 重建应用容器。
|
- 部署失败时默认尝试恢复升级前 `.env`,并按旧运行镜像或旧 `APP_IMAGE` 重建应用容器。
|
||||||
- 部署后会在远端校验应用容器实际运行镜像、镜像内 `VERSION.txt` 和 Prisma 迁移状态,然后再做外部 `/api/health` 检查。
|
- 部署后会在远端校验应用容器实际运行镜像、镜像内 `VERSION.txt` 和 Prisma 迁移状态,然后再做外部 `/api/health` 检查。
|
||||||
|
|
||||||
只有在已有其它备份和回滚体系时,才考虑使用 `-NoRemoteDbBackup`、`-NoRemoteObjectStorageBackup` 或 `-NoRollback`。离线客户现场如果没有 `busybox:1.36`,需要先把该 helper 镜像放到目标机,或通过 `-RemoteBackupHelperImage` 指定现场已有的小型 Linux 镜像。
|
只有在已有其它备份和回滚体系时,才考虑使用 `-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` 当成客户部署依据。
|
- 客户升级和内部发布优先按版本 tag 走,不要把 `latest` 当成客户部署依据。
|
||||||
- 客户现场部署使用 `-DeployChannel release -Version x.x.x`;测试通道只用于内部验证,必须显式传 `-DeployChannel test`。
|
- 客户现场部署使用 `-DeployChannel release -Version x.x.x`;测试通道只用于内部验证,必须显式传 `-DeployChannel test`。
|
||||||
- `DeployApp` 远程部署时默认会更新远端 `.env` 的 `APP_IMAGE`、访问地址和运行时镜像;只有临时验证时才使用 `-NoUpdateRemoteEnv`。
|
- `DeployApp` 远程部署时默认会更新远端 `.env` 的 `APP_IMAGE`、访问地址和运行时镜像;只有临时验证时才使用 `-NoUpdateRemoteEnv`。
|
||||||
- `DeployApp` 默认会做远端 `.env` 备份、Compose PostgreSQL 备份、失败自动回滚、运行镜像校验、版本文件校验和 Prisma 迁移状态校验。
|
- `DeployApp` 默认会做远端 `.env` 备份、Compose PostgreSQL 备份、Compose MinIO 备份、恢复分级说明、失败自动回滚、运行镜像校验、版本文件校验和 Prisma 迁移状态校验。
|
||||||
- 旧的 `deploy-new-fixed.ps1` 仍可作为内部维护参考,但不再作为当前主线部署入口。
|
- 旧的 `deploy-new-fixed.ps1` 仍可作为内部维护参考,但不再作为当前主线部署入口。
|
||||||
- 如果要做远程更新,先确认 `Registry`、SSH、`.env` 和目标机器都属于当前维护环境。
|
- 如果要做远程更新,先确认 `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. 部署前检查清单
|
## 5. 部署前检查清单
|
||||||
|
|
||||||
- [ ] 已确认目标机器品牌、系统和 Docker Compose 支持情况;如使用 Podman,已单独实机验证
|
- [ ] 已确认目标机器品牌、系统和 Docker Compose 支持情况;如使用 Podman,已单独实机验证
|
||||||
- [ ] 已确认端口、域名、证书、反向代理方案
|
- [ ] 已确认端口、域名、证书、反向代理方案
|
||||||
- [ ] 已确认数据目录、对象存储目录和备份位置
|
- [ ] 已确认数据目录、对象存储目录和备份位置
|
||||||
|
- [ ] 已确认升级失败时使用 L1 应用回滚、L2 数据库恢复还是 L3 数据库+素材恢复
|
||||||
- [ ] `.env` 已填写真实环境变量;内置 PostgreSQL 模式下 `DATABASE_URL` 没有错误占位值
|
- [ ] `.env` 已填写真实环境变量;内置 PostgreSQL 模式下 `DATABASE_URL` 没有错误占位值
|
||||||
- [ ] `APP_IMAGE` 已固定到完整版本镜像,不使用 `latest`
|
- [ ] `APP_IMAGE` 已固定到完整版本镜像,不使用 `latest`
|
||||||
- [ ] 如果 Registry 是 HTTP,本机和目标机 Docker 都已配置 `insecure-registries`
|
- [ ] 如果 Registry 是 HTTP,本机和目标机 Docker 都已配置 `insecure-registries`
|
||||||
@ -325,6 +339,8 @@ $sshPassword = Read-Host 'SSH密码' -AsSecureString
|
|||||||
- Compose 管理的 MinIO 数据卷:`minio-data.tar.gz`
|
- Compose 管理的 MinIO 数据卷:`minio-data.tar.gz`
|
||||||
- `manifest.json`:记录备份时间、应用镜像、访问地址、服务名等关键信息
|
- `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`,客户现场必须按外部数据库方案单独备份。
|
如果 `DATABASE_URL` 指向外部 PostgreSQL,脚本不会假装自动备份,会在备份目录写入 `EXTERNAL_DATABASE_BACKUP_REQUIRED.txt`,客户现场必须按外部数据库方案单独备份。
|
||||||
|
|
||||||
如果对象存储是外部 S3 / 外部 MinIO,或现场已有独立对象存储快照,可加:
|
如果对象存储是外部 S3 / 外部 MinIO,或现场已有独立对象存储快照,可加:
|
||||||
|
|||||||
@ -303,7 +303,8 @@ show_completion() {
|
|||||||
echo -e " 停止服务: ${YELLOW}docker stop $CONTAINER_NAME${NC}"
|
echo -e " 停止服务: ${YELLOW}docker stop $CONTAINER_NAME${NC}"
|
||||||
echo -e " 卸载服务: ${YELLOW}docker rm -f $CONTAINER_NAME${NC}"
|
echo -e " 卸载服务: ${YELLOW}docker rm -f $CONTAINER_NAME${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "请在浏览器中打开上述地址,完成系统安装向导。"
|
echo -e "请在浏览器中打开上述地址,按实施方案使用 Agent 编排或运维。"
|
||||||
|
echo -e "${YELLOW}正式客户首装主线仍是 docker-compose.prod.yml + .env + smoke-install.ps1;Agent 不是唯一安装入口。${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,8 @@ npm run verify:template-data
|
|||||||
客户首装后的基础验收脚本,可检查 Compose 配置、容器状态、运行镜像、版本文件、迁移状态、MinIO bucket 和 `/api/health`
|
客户首装后的基础验收脚本,可检查 Compose 配置、容器状态、运行镜像、版本文件、迁移状态、MinIO bucket 和 `/api/health`
|
||||||
- `backup-restore.ps1`
|
- `backup-restore.ps1`
|
||||||
客户环境备份、恢复和健康验收脚本;恢复必须显式输入确认短语,避免误覆盖客户数据
|
客户环境备份、恢复和健康验收脚本;恢复必须显式输入确认短语,避免误覆盖客户数据
|
||||||
|
- `measure-image-size.ps1`
|
||||||
|
发布前读取主应用镜像体积和分层,记录后续瘦身基线;只读检查,不修改镜像
|
||||||
- `generate-shared-test-env.ps1`
|
- `generate-shared-test-env.ps1`
|
||||||
为共享测试机生成隔离后的 `.env`,避免容器名、卷名、网络名与其他项目冲突;这是内部测试脚本,不是客户安装器
|
为共享测试机生成隔离后的 `.env`,避免容器名、卷名、网络名与其他项目冲突;这是内部测试脚本,不是客户安装器
|
||||||
|
|
||||||
@ -133,6 +135,20 @@ npx tsx scripts/fix-missing-templates.ts
|
|||||||
- 外部数据库、外部对象存储由客户现场方案单独备份;脚本会写提示文件,不会假装自动覆盖。
|
- 外部数据库、外部对象存储由客户现场方案单独备份;脚本会写提示文件,不会假装自动覆盖。
|
||||||
- 对象存储备份/恢复使用 `busybox:1.36` 作为 helper 镜像;离线客户现场需要提前放入该镜像或通过 `-HelperImage` 指定现场已有镜像。
|
- 对象存储备份/恢复使用 `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
|
```bash
|
||||||
|
|||||||
134
scripts/measure-image-size.ps1
Normal file
134
scripts/measure-image-size.ps1
Normal file
@ -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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user