# 主应用镜像体积和分层检查工具 # 用于发布前记录体积基线,帮助判断哪些层需要继续瘦身。 param( [Parameter(Mandatory = $true)] [string]$Image, [int]$TopLayers = 12, [string]$OutputPath = "", [int]$MaxImageSizeMB = 0, [int]$MaxLayerCount = 0, [int]$MaxLargestLayerMB = 0 ) $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 $largestLayerSize = if ($largestLayers.Count -gt 0) { [double]$largestLayers[0].SizeBytes } else { 0 } $thresholdProblems = New-Object System.Collections.Generic.List[string] if ($MaxImageSizeMB -gt 0 -and $totalSize -gt ($MaxImageSizeMB * 1MB)) { $thresholdProblems.Add("镜像总大小超过阈值:$(Format-Bytes $totalSize) > ${MaxImageSizeMB} MB") } if ($MaxLayerCount -gt 0 -and $layerCount -gt $MaxLayerCount) { $thresholdProblems.Add("RootFS 层数超过阈值:$layerCount > $MaxLayerCount") } if ($MaxLargestLayerMB -gt 0 -and $largestLayerSize -gt ($MaxLargestLayerMB * 1MB)) { $thresholdProblems.Add("最大分层超过阈值:$(Format-Bytes $largestLayerSize) > ${MaxLargestLayerMB} MB") } $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("- 最大分层:$(Format-Bytes $largestLayerSize)") $report.Add("- 生成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") if ($MaxImageSizeMB -gt 0 -or $MaxLayerCount -gt 0 -or $MaxLargestLayerMB -gt 0) { $report.Add("") $report.Add("## 阈值检查") $report.Add("") $report.Add("- 镜像总大小上限:$(if ($MaxImageSizeMB -gt 0) { "$MaxImageSizeMB MB" } else { '未设置' })") $report.Add("- RootFS 层数上限:$(if ($MaxLayerCount -gt 0) { "$MaxLayerCount" } else { '未设置' })") $report.Add("- 最大分层上限:$(if ($MaxLargestLayerMB -gt 0) { "$MaxLargestLayerMB MB" } else { '未设置' })") if ($thresholdProblems.Count -eq 0) { $report.Add("- 结果:通过") } else { $report.Add("- 结果:未通过") foreach ($problem in $thresholdProblems) { $report.Add(" - $problem") } } } $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 } if ($thresholdProblems.Count -gt 0) { foreach ($problem in $thresholdProblems) { Write-Host "[ERROR] $problem" -ForegroundColor Red } exit 64 }