SW-7 Fill image store with a fake large image, then watch GC
| Domain | software | Module | — |
|---|---|---|---|
| Priority | high | Automated | no |
| Tags | stress | Requires | — |
| Depends on | SW-6 (pass) | ||
Preconditions
- Robot is ON, PC/Steam Deck connected to the unit's WiFi AP.
- SW-6 has passed (disk/image state healthy: ≥ 100 GB free, ≤ 5 images).
- SSH access to the control PC (
root@10.42.0.106). - Record the baseline before starting:
export CONTAINER_RUNTIME_ENDPOINT=unix:///run/k3s/containerd/containerd.sock
du -sh /var/lib/containerd ; crictl images -q | wc -l ; df -h /var/lib/containerd
Steps
Step 1 — take the 5 GB fake image Build one image whose single layer is 5 GB of incompressible data, import it into the k8s containerd namespace.
dd if=/dev/urandom of=/tmp/blob-0 bs=1M count=5120 # 5 GB, incompressible
# assemble a minimal image (FROM scratch + COPY blob) and import it:
ctr -n k8s.io images import /tmp/fake-0.tar # -> docker.io/library/fake:0
ctr -n k8s.io images ls | grep fake
> TODO(bench): confirm the image-build tool available on the unit (ctr/buildctl; > the robot has no docker). Wire this into scripts/fill_image_store.sh when automated.
Step 2 — loop-load until images total 100 GB (5 GB × 20) Each iteration must be a distinct 5 GB layer — fresh random content, not a retag (retagging shares layers and consumes no extra space).
for i in $(seq 1 19); do
dd if=/dev/urandom of=/tmp/blob-$i bs=1M count=5120
# build image fake:$i with blob-$i as its layer, then:
ctr -n k8s.io images import /tmp/fake-$i.tar # -> docker.io/library/fake:$i
done
du -sh /var/lib/containerd # expect ~100 GB (20 × 5 GB distinct layers)
crictl images -q | wc -l # ~20 fake images + baseline
Step 3 — seed 5 terminated/unknown-state pods Create pods that immediately terminate (Completed/Error) or land in an unknown state, referencing the fake images so those images are momentarily pinned then released.
for i in $(seq 1 5); do
kubectl run dead-$i --image=docker.io/library/fake:$i --restart=Never \
--command -- /bin/false || true
done
kubectl get pods -A | grep dead- # expect Error/Completed (terminated)
> TODO(bench): kubectl needs the k3s server kubeconfig; this node is a k3s agent. > Run from the server/control plane, or seed equivalent dead containers via crictl runp/crictl create.
Step 4 — observe GC / assert reclamation Watch kubelet/k3s image garbage collection: once imagefs usage crosses the image-GC high threshold, unused images (the fake ones not backing a running pod) are reclaimed.
# in one shell: watch the store shrink
watch -n5 'du -sh /var/lib/containerd; crictl images -q | wc -l; df -h /var/lib/containerd | tail -1'
# in another: follow the GC log lines
journalctl -u k3s-agent -f | grep -iE 'image.?gc|garbage|freeing|evict'
> TODO(bench): confirm the k3s image-GC flags/defaults in effect > (--image-gc-high-threshold default 85%, --image-gc-low-threshold 80%, min-age).
Expected
- After Step 2,
/var/lib/containerdis ~100 GB and holds ~20 fake images. - When imagefs usage crosses the image-GC high threshold, kubelet/k3s image GC
runs automatically and reclaims space by deleting unused images down toward the low threshold — store size and image count drop measurably.
- Images backing the terminated/unknown pods (Step 3) are not pinned and are
eligible for collection; the release-aru image and any running workload's images are retained (never GC'd out from under a running container).
- No running workload is evicted or restarted as a side effect of the fill/GC.
Teardown
Remove everything created and confirm the store returns to the SW-6 baseline.
for i in $(seq 1 5); do kubectl delete pod dead-$i 2>/dev/null || true; done
for i in $(seq 0 19); do ctr -n k8s.io images rm docker.io/library/fake:$i 2>/dev/null || true; done
rm -f /tmp/blob-* /tmp/fake-*.tar
crictl rmi --prune 2>/dev/null || true
du -sh /var/lib/containerd ; crictl images -q | wc -l # back to baseline (≤ 5 images)