Companion code for "Dockerizing Spring Boot 4: Layered Jars, Buildpacks, Distroless and Image Size Benchmarks". Fat jar on JDK and JRE, layered jar on Debian, Alpine and distroless, jlink, the JDK 25 AOT cache, Paketo buildpacks and Jib, each measured for size on disk and pushed, rebuild delta, startup, user and shell. Also PID 1 and signal handling, the jdeps module gap, AOT cache mismatches and buildpacks memory calculation. Transcripts in docs/output/, regenerated by scripts/run-all.sh. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
79 lines
4.1 KiB
Bash
79 lines
4.1 KiB
Bash
#!/usr/bin/env bash
|
|
# Measure every variant: size on disk, size over the wire, layers, user, shell, packages,
|
|
# startup to readiness, and memory at idle. -> docs/output/image-matrix.txt
|
|
# Needs a registry at $REGISTRY (crane registry serve --address localhost:5000 is enough).
|
|
set -uo pipefail
|
|
source "$(dirname "$0")/env.sh"
|
|
RUNS="${RUNS:-3}"
|
|
PORT=18090
|
|
|
|
compressed() { # image in registry -> "bytes layers"
|
|
crane manifest "$1" | python3 -c 'import json,sys
|
|
m=json.load(sys.stdin); L=m["layers"]
|
|
print(sum(l["size"] for l in L)+m["config"]["size"], len(L))'
|
|
}
|
|
|
|
packages() { # count OS packages from the image filesystem, without running anything in it
|
|
local cid; cid=$(docker create "$1" /x 2>/dev/null)
|
|
docker export "$cid" > /tmp/img-fs.tar; docker rm "$cid" > /dev/null
|
|
local n=""
|
|
if tar -tf /tmp/img-fs.tar var/lib/dpkg/status > /dev/null 2>&1; then
|
|
n=$(tar -xOf /tmp/img-fs.tar var/lib/dpkg/status | grep -c '^Package:')
|
|
n="$n (dpkg)"
|
|
elif [ "$(tar -tf /tmp/img-fs.tar | grep -c '^var/lib/dpkg/status.d/.')" -gt 0 ]; then
|
|
# distroless and Paketo "tiny" keep one status file per package (plus .md5sums) in status.d/
|
|
n=$(tar -tf /tmp/img-fs.tar | grep -E '^var/lib/dpkg/status.d/[^/]+$' | grep -vc '\.md5sums$')
|
|
n="$n (status.d)"
|
|
elif tar -tf /tmp/img-fs.tar lib/apk/db/installed > /dev/null 2>&1; then
|
|
n=$(tar -xOf /tmp/img-fs.tar lib/apk/db/installed | grep -c '^P:')
|
|
n="$n (apk)"
|
|
else n="?"; fi
|
|
rm -f /tmp/img-fs.tar; echo "$n"
|
|
}
|
|
|
|
startup() { # image -> "ms_to_ready startedIn rss"
|
|
local name="measure-$$" t0 t1
|
|
docker rm -f "$name" > /dev/null 2>&1
|
|
t0=$(date +%s%N)
|
|
docker run -d --name "$name" -p "$PORT:8080" "$1" > /dev/null
|
|
for _ in $(seq 1 600); do
|
|
if [ "$(curl -s -o /dev/null -w '%{http_code}' localhost:$PORT/actuator/health/readiness)" = 200 ]; then break; fi
|
|
sleep 0.05
|
|
done
|
|
t1=$(date +%s%N)
|
|
for _ in 1 2 3 4 5; do curl -s -o /dev/null localhost:$PORT/api/items; done
|
|
sleep 2
|
|
local rss started
|
|
rss=$(docker stats --no-stream --format '{{.MemUsage}}' "$name" | awk '{print $1}')
|
|
started=$(docker logs "$name" 2>&1 | grep -o 'Started ImagesApplication in [0-9.]* seconds' | awk '{print $4}')
|
|
docker rm -f "$name" > /dev/null
|
|
echo "$(( (t1 - t0) / 1000000 )) ${started:-?} $rss"
|
|
}
|
|
|
|
{
|
|
echo "# Spring Boot 4.1.1 application (webmvc + actuator + validation + prometheus), fat jar $(du -h "$MODULE_DIR/target/app.jar" | cut -f1)"
|
|
echo "# Docker Engine $(docker version --format '{{.Server.Version}}'), classic overlay2 store. $RUNS startup runs each, median shown."
|
|
echo "# 'on disk' = docker image inspect .Size (uncompressed). 'pushed' = layer blobs + config in the registry (compressed)."
|
|
echo
|
|
printf '%-20s %9s %9s %7s %-9s %-6s %-16s %9s %9s %9s %s\n' variant "on disk" pushed layers user shell packages "ready ms" "Started" "RSS" "JVM"
|
|
for v in "${VARIANTS[@]}"; do
|
|
img="sbd/docker-images:$v"
|
|
docker tag "$img" "$REPO:$v" && docker push -q "$REPO:$v" > /dev/null
|
|
disk=$(docker image inspect -f '{{.Size}}' "$img")
|
|
read -r wire layers < <(compressed "$REPO:$v")
|
|
user=$(docker image inspect -f '{{.Config.User}}' "$img"); user="${user:-root}"
|
|
if docker run --rm --entrypoint /bin/sh "$img" -c true > /dev/null 2>&1; then shell=yes; else shell=no; fi
|
|
pkgs=$(packages "$img")
|
|
samples=()
|
|
for _ in $(seq 1 "$RUNS"); do samples+=("$(startup "$img")"); done
|
|
median=$(printf '%s\n' "${samples[@]}" | sort -n | sed -n "$(( (RUNS + 1) / 2 ))p")
|
|
read -r ready started rss <<< "$median"
|
|
name="measure-$$"; docker run -d --name "$name" -p "$PORT:8080" "$img" > /dev/null
|
|
for _ in $(seq 1 600); do curl -s -o /dev/null localhost:$PORT/api/runtime && break; sleep 0.05; done
|
|
jvm=$(curl -s localhost:$PORT/api/runtime | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["java.vendor"].split()[0], r["java.version"])')
|
|
docker rm -f "$name" > /dev/null
|
|
printf '%-20s %8sM %8sM %7s %-9s %-6s %-16s %9s %9s %9s %s\n' "$v" \
|
|
"$(( disk / 1000000 ))" "$(( wire / 1000000 ))" "$layers" "$user" "$shell" "$pkgs" "$ready" "${started}s" "$rss" "$jvm"
|
|
done
|
|
} | tee "$OUT/image-matrix.txt"
|