Skip to content

Feat/images max size - #176

Merged
hi-liang merged 2 commits into
mainfrom
feat/images-max-size
Oct 8, 2026
Merged

hi-liang merged 2 commits into
mainfrom
feat/images-max-size

Conversation

@hi-liang

@hi-liang hi-liang commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Adds images --max-size , the mirror of --min-size: images that would free more are evicted only once nothing else is left. With --lru, --stale-after (no default) lets images unused for longer go by age alone, so replaced builds don't hold disk. Order with --lru: stale, the rest, under --min-size, over --max-size, oldest first within each group.

On cork's workers, re-pulling evicted builds accounts for 67% of pull wait. The worst re-pulls are 2–3.5 GB images at 20–46 s each, against about 0.5 s for a small one. Measured deletes cost per file rather than per byte, and several small deletes stall container creates less than one large one.

The use record gains a # began first line, which 1.4.0 skips. Images seeded when the record started age from that time, so a new or deleted record doesn't make every image stale at once. A record showing no use within --stale-after triggers a warning instead of making everything stale. Bad --target/--threshold or size bounds are now refused at startup; before, a pass logged the error and exited 0 without evicting anything.

Verified: 72/72 tests against a live Docker 29.8 daemon, plus live eviction-order and record scenarios. README and CHANGELOG updated.

Adds images --max-size <size>, the mirror of --min-size: images that
would free more are evicted only once nothing else is left. Size is what
removal frees, the bytes in layers no other image shares, which is also
what a re-pull fetches and unpacks. With --lru, --stale-after <duration>
exempts images unused for longer from both size rules, so a replaced
build or retired challenge still goes by age rather than being held
until nothing smaller is left. Off by default, so --min-size behaves as
before unless it is set. --max-size below --min-size is refused.

With --lru, the order is: stale images, then the rest, then images under
--min-size, then images over --max-size, oldest first within each.

Why: on the cork workers (75 GB, --threshold 89 --target 84 --min-size
100MiB --lru) eviction goes oldest first among images of 100 MiB or
more, with no regard to size within that group, so a multi-GB image
is as likely to go as a 100 MiB one of the same age. From cork's launch
records, 2026-09-27..10-07: 684 of 90,688 launches pulled; 68 of those
re-pulled the same build on the same worker after eviction and account
for 67% of all pull wait. The worst are 2-3.5 GB images at 20-46 s
each, while non-pulled launches are p99 279 ms and small re-pulls take
~0.5 s.

Deletion cost was measured too (Docker 29.8, cold caches): 800 MB in 100
files deletes in 0.02 s, 800 MB in 100k files in 0.8-1.1 s whether as one
image or ten, and a create loop got 1 iteration through the single large
delete against 9-10 through the ten small ones, since the layer store
lock is released between images. So --min-size's premise (fewer, larger
removals are cheaper) does not hold, and small removals stall launches
less.

--stale-after requires --lru, whose record is the only source of when an
image was last used; --max-size works on size alone without it. Like
--lru, --stale-after keeps its last value when repeated, so a deployment
can append it to a command that may already carry it.

An image stamped 0, seeded when the record began, ages from when the
record began, which the record now keeps as a first line `# began
<secs>` (1.4.0 skips it as malformed; a 1.4.0 record is taken to begin
when this release first saves it). Counting a seeded image as unused
forever made every image stale on the first pass after a record was
created or deleted, and they tied at 0, so the largest went first. A
record that would make something stale yet shows no use within
--stale-after is taken to be no longer kept: that pass treats nothing as
stale and warns.

Bounds no pass can use (--target at or above --threshold, --max-size
below --min-size) are refused at startup. A pass only logs its errors,
so a bad bound used to leave the process exiting 0 while evicting
nothing.

Sizes are those at the start of a pass, and the README says so: an
image left as the only owner of shared layers mid-pass keeps its place
until the next pass. Listing images after every removal would catch it
(~12 ms per list at ~300 images, measured), but on the cork workers
every large shared base belongs to images already over 512 MiB, so it
was left out.
@hi-liang
hi-liang merged commit 0b4dc11 into main Oct 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant