179 lines
5.8 KiB
PowerShell
179 lines
5.8 KiB
PowerShell
# 主应用镜像体积和分层检查工具
|
||
# 用于发布前记录体积基线,帮助判断哪些层需要继续瘦身。
|
||
|
||
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
|
||
}
|