New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add pkg/blobcache from Buildah #1459
Conversation
Added two commits on top to remove the dependencies on buidah and libimage. |
I'd been looking at this at the end of last week, and there's more that needs to be done:
|
Thanks, @nalind! I am mostly interested in moving the code here so we can work on cosign without the blobcache removing signatures. The points you mention above seem outside of this scope and independent from where the code lives. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK to moving the code, and not touching the implementation as part of this PR, unless necessary.
I worry, at this point, about the API, because we need to keep that stable, though. Primarily:
- Should we have a “longer-term”
BlobCache
object that holds the directory/configuration, and only allow wrapping references through that? - Is a single ”remap any reference to a reference with a cache” the right API, or should we have separate ones for ({c/storage, the registry}, {cache writer, cache consumer})? Or a single “remap (reference, shouldConsumeCache, shouldUpdateCache)”) API?
pkg/blobcache/blobcache.go
Outdated
// can be cleared by calling the returned BlobCache()'s ClearCache() method. | ||
// The compress argument controls whether or not the cache will try to substitute a compressed | ||
// or different version of a blob when preparing the list of layers when reading an image. | ||
func NewBlobCache(ref types.ImageReference, directory string, compress types.LayerCompression) (BlobCache, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d prefer a pair of CacheWriterImageReference(ref …)
and CacheConsumerImageReference(ref …)
methods (or some other naming for the “local c/storage” vs. “remote registry” split?).
The caller usually knows which one of the two (whatever “the two” means) is intended, and having that cleanly separated would allow implementing the two differently.
(The naming is not just a “source”/“destination”, because AFAICS an ImageSource is typically the “cache writer”, so a “source” name is confusing.)
Once we add a single method like this, we won’t be able to split the two (without continuing to support the single-method approach.) OTOH if we understand how the two conceptually differ, the two public functions can, for now, just call the single implementation internally.
Or is there some structural reason why a single reference is always both a cache writer and consumer, and the code should behave the same on both wrapped references participating? It does make sense that “if we have the blob in cache, reading it from there is cheaper than both reading from a remote registry and constructing a tarball from c/storage”. OTOH, if we wrap a registry reference, I don’t think we actually want to ever write a blob that we are pushing to the registry also to disk, do we? (Or is that inherently caller-situation specific and do we eventually need an API for the caller to choose?)
It’s fairly likely I’m thinking about this wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nalind PTAL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cache is populated in PutBlob()
(ImageDestination
) and consulted in GetBlob()
(ImageSource
). It could conceivably also have populated the cache on GetBlob()
, but it didn't occur to me to do that at the time.
An ImageReference
can be asked to create both ImageSource
s and ImageDestination
s. I didn't see much benefit in having two functions which took an ImageReference
argument and returned a different ImageReference
when the implementation logic didn't require them to be different. If an ImageSource
was also writing to the cache, I might have thought differently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tl;dr — skip to the bottom
Personally I just found it hard to think about the properties… let me see if I get this right:
On (new, pull):
Activated through libimageOptions.DestinationLookupReferenceFunc
, i.e. a c/storage storageDest
is cache-wrapped, the registry dockerSrc
isn’t. The compress
argument to NewBlobCache
is ignored because the cache isn’t used on a source (and anyway, it is set to PreserveOriginal
which means “don’t change LayerInfosForCopy
”).
- The original manifest +
dockerSrc.LayerInfosForCopy
is used without modification storageDest.TryReusingBlob
, if the layer isn’t in c/storage already, checks for a cached blob; if so, it writes it tostorageDest
, and we don’t contact the registry. (This can AFAICS happen only if the cache is longer-lived than the c/storage backend.)dockerSrc.GetBlob
just contacts the registrystorageDest.PutBlob
always stores the original layer blob to the cache, and if it is gzip-compressed, also stores an uncompressed variant to the cache.
I.e. on pull, we almost always record the compressed versions in cache (on the first pull of that layer), and usually the uncompressed version.
On push:
Activated through libimageOptions.SourceLookupReferenceFunc
, i.e. a c/storage storageSrc
is cache-wrapped, the registry dockerDest
isn’t. compress
defaults to types.Compress
unless the push invoker disables compression.
storageSrc.LayerInfosForCopy
starts with thestorageSrc
version, but can replace digests of uncompressed storage layers with digests of cached compressed versions.dockerDest.TryReusingBlob
is not modified, but it can check the registry first for the compressed version per the update above, before falling back to BlobInfoCache. [The difference is thatTryReusingBlob
always queries the registry for the primary digest, butBlobInfoCache
-known associations are, currently, only used if we saw that blob in the same registry. So using a compressed-digest version for the physical query has a higher chance of avoiding an upload.]storageSrc.GetBlob
first checks the cache, which helps both for a compressed artifact (no need to re-compress) and an uncompressed one (no need to traverse the graph driver’s fragmented storage and re-assemble tar-split; the uncompressed data is then compressed by the generic code).dockerDest.PutBlob
just contacts the registry
I.e. on push, for any pre-existing layers we first check for an on-registry version, and if it isn’t there, we push them from the cache, not reading from c/storage.Store.Diff
. Only new layers are compressed+written — and we don’t update the cache with any newly created layers (uncompressed nor compressed).
On commit:
(commit source = buildah.containerImageSource
; reads from a prepared blob or from c/storage. commit destination = usually a c/storage destination, but not quite always.)
Activated manually; both the containerSrc
source and commitDest
destination is cache-wrapped. compress
defaults to types.PreserveOriginal
unless the commit invoker enables compression.
containerSrc.LayerInfosForCopy
starts with thecontainerImageSource
version (= the newly-built manifest contents); it can replace digests of uncompressed storage layers with digests of cached compressed versions, but it doesn’t do so by default.commitDest.TryReusingBlob
, if the layer isn’t in the destination already, checks for a cached blob; if so, it writes it tocommitDest
, and we don’t use thecontainerSrc
version. (This can AFAICS basically never happen for a c/storage commit destination, where the layer either exists in the same c/storage, or it is entirely new; we would read the cache only for other destination types.)containerSrc.GetBlob
first checks the cache, which does nothing for a c/storage destination — either the layer already exists there and we don’t get to GetBlob, or it is genuinely new and not in the cache. For other commit destinations, this potentially helps both for a compressed artifact (no need to re-compress) and an uncompressed one (no need to traverse the graph driver’s fragmented storage and re-assemble tar-split; the generic code can compress).commitDest.PutBlob
, if the blob is not being compressed on the fly, stores the original layer blob to the cache (it is not gzip-compressed as coming from fromcontainerSrc
). If it is being compressed on the fly, it is not stored in the cache.
I.e. for a c/storage commit destination, this has the net effect of storing the uncompressed version of any new layers to the cache (and that in turn means that a future push won’t need a Store.Diff
to read that). For non-c/storage commit destinations, this uses the cache similarly to a push, using it to optimize writes of previously-pulled layers (but not any commit-created layers).
So, in principle,
- on a commit to c/storage, we would only need to cache-wrap the destination (which would be usefully restricted, “BlobCache is only ever used to wrap c/image/storage”)
- direct commits to non-c/storage behave more like pushes (where we want to wrap the source); in rare cases (direct commits without compression) we potentially benefit from caching the commit outcome, which happens in the
PutBlob
destination, but mostly we only need to cache-wrap the source.
So the pull/push cases are straightforward; I was originally primarily focusing on the commit case (the only direct use of NewCache
), which is non-obvious, tends to do nothing for most layers, but what it does do (avoid a second .Diff
for pushing newly created layers) might actually be quite relevant for performance.
Conceptually I quite like the idea that “the cache is an implementation optimization of a c/storage user (both c/storage/image
and buildah.containerImageSrc
)”, where both PutBlob
and GetBlob
can update the cache. Going even further …
Hypothetically, in some far-future idealized world, the cache could be integrated into c/image/storageImageDestination
so that its PutBlob
doesn’t need to do any private copies just to compute digests, and into buildah.containerImageSrc
so that it can write the uncompressed blobs for the new layers directly into the cache instead of making a private copy first. That might be a performance improvement for low-memory / IO-starved systems. And integration of that kind would look so completely different from the current ImageReference
wrapper (direct cache support in the two transports) that it’s really not worth worrying about — we would probably provide a “cache” object with some kind of “get blob” / “record blob” API. So that’s clearly out of scope and not worth worrying about short-term, apart from maybe defining that “cache” object now.
So, yes, I was thinking about this wrong — splitting the uses into “producer”/“consumer”, as I was suggesting originally, is not really the right approach. The cache would be inherently better if it were directly integrated, which is a completely different API.
So, on balance, let’s keep the one wrapping function here for now, and just document that
- it updates the cache on
PutBlob
andPutManifest
(destinations), reads from it onGetBlob
,GetManifest
(sources) andTryReusingBlob
(destinations) — that is enough for callers to get the effects they need, - it might use the cache in more ways in the future — that gives us flexibility.
Separately, I think it still makes sense to introduce a BlobCache
type, with a WrapImageReference
method replacing this NewBlobCache
function, but that’s a weak preference and not blocking I guess.
pkg/blobcache/blobcache.go
Outdated
// can be cleared by calling the returned BlobCache()'s ClearCache() method. | ||
// The compress argument controls whether or not the cache will try to substitute a compressed | ||
// or different version of a blob when preparing the list of layers when reading an image. | ||
func NewBlobCache(ref types.ImageReference, directory string, compress types.LayerCompression) (BlobCache, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this have an Options
struct, or “a functional options” interface?
I suppose we can always add a new API of that kind later…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed
Isn’t that generic issue with the existing
Is it just “retried without it”? Ideally we try to use a cached zstd, and if that fails, we should still try to use cached gzip, not just turn off the cache entirely. That seems to me to, ideally, involve some kind of extra data to
Do you propose a “cache” object that can be used to remap references, and survives any one remapped reference? That does seem to be a more accurate mapping of the underlying concepts. OTOH the “determine if the cache may have caused a copy attempt to fail” question feels actually scoped to a specific cache-wrapped reference — if a “single cache” is involved in 10 copies, and the first one involves such a risky remapping, a per-cache bit would be set for all 10 following copies (… in abstract principle. In practice we would have a single- |
Yes.
That's a good point.
For writing, yes, I suppose scope could be used to infer if a given destination supported a given compression algorithm. It seems we need a more expressive way to tell it what types of substitutions it can/should make. |
I am not sure whether this PR can be merged as is or not. Most (all?) seem like follow-up items? |
Primarily I worry about committing to a an API we couldn’t evolve.
Most of the review comments are actually aimed at the documentation, and the ~newly introduced reference mapping API. At least to the extent it is entirely or almost entirely new, and not a long-standing buildah/pkg/… documentation, I’d prefer for it to be just cleaned up now rather than committing a new to-do item — the effort of tracking that for later is relatively large vs. just fixing it now. |
Buildah's blobcache is a cache to speed up builds by avoiding CPU intensive (de)compressing of blobs. Move the code from Buildah into containers/image to allow for extending internal interfaces (e.g., image destination/source) *and* supporting newly added features in the blobcache. There are a number of planned features (e.g., sigstore/cosign) which will likely be implemented entirely by extending the internal interfaces and by casting to internal types in containers/image/copy as already done. Having Buildah's blobcache here allows for extending its internal interface more easily. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
The MIME type is already in c/image/manifest. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
The history of the blobcache is well-preserved in the git history. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
According to [1], this is not being used by any external caller. Preserve the method for testing purposes and potential future (internal) callers. [1] https://github.com/containers/image/pull/1459/files#r800944125 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
The clojure is used in Buildah to pass a reference-reference mapping to libimage and Buildah can continue doing that without c/image having to worry about it. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
Maintaining and evolving interfaces is hard, so let's keep thing simple and turn `BlobCache` into a struct that we can easily extend without having to worry about (potential) external implementations of an interface. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
Addressed some of the issues, most notably turned BlobCache into a struct, see individual commits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! LGTM.
Buildah's pkg/blobcache has been moved into containers/image to consolidate implementations of interfaces such as image destinations and image sources. Move the callsites of the blobcache over to containers/image and remove the package from Buildah. [1] containers/image#1459 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
Buildah's pkg/blobcache has been moved into containers/image to consolidate implementations of interfaces such as image destinations and image sources. Create a type alias in pkg/buildcache and redirect NewBlobCache to the containers/image function. While it is still an API break as it changes an interface to a struct, known callers will continue building since they are not implementing the interface. Since there are no functional changes: [NO NEW TESTS NEEDED] [1] containers/image#1459 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
Buildah's pkg/blobcache has been moved into containers/image to consolidate implementations of interfaces such as image destinations and image sources. Create a type alias in pkg/buildcache and redirect NewBlobCache to the containers/image function. While it is still an API break as it changes an interface to a struct, known callers will continue building since they are not implementing the interface. Since there are no functional changes: [NO NEW TESTS NEEDED] [1] containers/image#1459 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
Buildah's pkg/blobcache has been moved into containers/image [1] to consolidate implementations of interfaces such as image destinations and image sources. Since there are no functional changes: [NO NEW TESTS NEEDED] [1] containers/image#1459 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
Buildah's pkg/blobcache has been moved into containers/image [1] to consolidate implementations of interfaces such as image destinations and image sources. Since there are no functional changes: [NO NEW TESTS NEEDED] [1] containers/image#1459 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
Buildah's blobcache is a cache to speed up builds by avoiding CPU
intensive (de)compressing of blobs.
Move the code from Buildah into containers/image to allow for extending
internal interfaces (e.g., image destination/source) and supporting
newly added features in the blobcache.
There are a number of planned features (e.g., sigstore/cosign) which
will likely be implemented entirely by extending the internal interfaces
and by casting to internal types in containers/image/copy as already
done. Having Buildah's blobcache here allows for extending its internal
interface more easily.
Signed-off-by: Valentin Rothberg vrothberg@redhat.com
@mtrmac @nalind PTAL
Once merged, I can prepare a follow-up PR in Buildah and do the aliasing.