mirror of
https://github.com/containers/image.git
synced 2025-04-18 19:44:05 +03:00
Fix excessive memory and disk usage when sharing layers
createNewLayer can add new entries to s.filenames for uncompressed versions of layers; these entries don't match manifest.LayerInfos, causing the existing logic to assume they are a config and to store them as ImageBigData. That's both a potentially unlimited disk space waste (recording uncompressed versions of layers unnecessarily), and a memory pressure concern (ImageBigData is created by reading the whole blob into memory). Instead, precisely track the digest of the config, and never include more than one file in ImageBigData. Blobs added via PutBlob/TryReusingBlob but not referenced from the manifest are now ignored instead of being recorded inside the image. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
parent
f8d6234cf1
commit
64440ba01d
@ -21,7 +21,6 @@ import (
|
||||
"github.com/containers/image/v5/internal/imagedestination/stubs"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
"github.com/containers/image/v5/internal/putblobdigest"
|
||||
"github.com/containers/image/v5/internal/set"
|
||||
"github.com/containers/image/v5/internal/signature"
|
||||
"github.com/containers/image/v5/internal/tmpdir"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
@ -84,6 +83,9 @@ type storageImageDestination struct {
|
||||
indexToAddedLayerInfo map[int]addedLayerInfo // Mapping from layer (by index) to blob to add to the image
|
||||
blobAdditionalLayer map[digest.Digest]storage.AdditionalLayer // Mapping from layer blobsums to their corresponding additional layer
|
||||
diffOutputs map[digest.Digest]*graphdriver.DriverWithDifferOutput // Mapping from digest to differ output
|
||||
|
||||
// Config
|
||||
configDigest digest.Digest // "" if N/A or not known yet.
|
||||
}
|
||||
|
||||
// addedLayerInfo records data about a layer to use in this image.
|
||||
@ -170,7 +172,17 @@ func (s *storageImageDestination) PutBlobWithOptions(ctx context.Context, stream
|
||||
return info, err
|
||||
}
|
||||
|
||||
if options.IsConfig || options.LayerIndex == nil {
|
||||
if options.IsConfig {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if s.configDigest != "" {
|
||||
return private.UploadedBlob{}, fmt.Errorf("after config %q, refusing to record another config %q",
|
||||
s.configDigest.String(), info.Digest.String())
|
||||
}
|
||||
s.configDigest = info.Digest
|
||||
return info, nil
|
||||
}
|
||||
if options.LayerIndex == nil {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
@ -776,22 +788,14 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t
|
||||
options.CreationDate = *inspect.Created
|
||||
}
|
||||
|
||||
// Set up to save the non-layer blobs as data items. Since we only share layers, they should all be in files, so
|
||||
// we just need to screen out the ones that are actually layers to get the list of non-layers.
|
||||
dataBlobs := set.New[digest.Digest]()
|
||||
for blob := range s.filenames {
|
||||
dataBlobs.Add(blob)
|
||||
}
|
||||
for _, layerBlob := range layerBlobs {
|
||||
dataBlobs.Delete(layerBlob.Digest)
|
||||
}
|
||||
for _, blob := range dataBlobs.Values() {
|
||||
v, err := os.ReadFile(s.filenames[blob])
|
||||
// Set up to save the config as a data item. Since we only share layers, the config should be in a file.
|
||||
if s.configDigest != "" {
|
||||
v, err := os.ReadFile(s.filenames[s.configDigest])
|
||||
if err != nil {
|
||||
return fmt.Errorf("copying non-layer blob %q to image: %w", blob, err)
|
||||
return fmt.Errorf("copying config blob %q to image: %w", s.configDigest, err)
|
||||
}
|
||||
options.BigData = append(options.BigData, storage.ImageBigDataOption{
|
||||
Key: blob.String(),
|
||||
Key: s.configDigest.String(),
|
||||
Data: v,
|
||||
Digest: digest.Canonical.FromBytes(v),
|
||||
})
|
||||
|
@ -277,11 +277,11 @@ func makeLayer(t *testing.T, compression archive.Compression) testBlob {
|
||||
}
|
||||
}
|
||||
|
||||
func (l testBlob) storeBlob(t *testing.T, dest types.ImageDestination, cache types.BlobInfoCache, mimeType string) manifest.Schema2Descriptor {
|
||||
func (l testBlob) storeBlob(t *testing.T, dest types.ImageDestination, cache types.BlobInfoCache, mimeType string, isConfig bool) manifest.Schema2Descriptor {
|
||||
_, err := dest.PutBlob(context.Background(), bytes.NewReader(l.data), types.BlobInfo{
|
||||
Size: l.compressedSize,
|
||||
Digest: l.compressedDigest,
|
||||
}, cache, false)
|
||||
}, cache, isConfig)
|
||||
require.NoError(t, err)
|
||||
return manifest.Schema2Descriptor{
|
||||
MediaType: mimeType,
|
||||
@ -312,12 +312,12 @@ func createUncommittedImageDest(t *testing.T, ref types.ImageReference, cache ty
|
||||
|
||||
layerDescriptors := []manifest.Schema2Descriptor{}
|
||||
for _, layer := range layers {
|
||||
desc := layer.storeBlob(t, dest, cache, manifest.DockerV2Schema2LayerMediaType)
|
||||
desc := layer.storeBlob(t, dest, cache, manifest.DockerV2Schema2LayerMediaType, false)
|
||||
layerDescriptors = append(layerDescriptors, desc)
|
||||
}
|
||||
configDescriptor := manifest.Schema2Descriptor{} // might be good enough
|
||||
if config != nil {
|
||||
configDescriptor = config.storeBlob(t, dest, cache, manifest.DockerV2Schema2ConfigMediaType)
|
||||
configDescriptor = config.storeBlob(t, dest, cache, manifest.DockerV2Schema2ConfigMediaType, true)
|
||||
}
|
||||
|
||||
manifest := manifest.Schema2FromComponents(configDescriptor, layerDescriptors)
|
||||
@ -429,9 +429,9 @@ func TestWriteRead(t *testing.T) {
|
||||
compression = archive.Gzip
|
||||
}
|
||||
layer := makeLayer(t, compression)
|
||||
_ = layer.storeBlob(t, dest, cache, manifest.DockerV2Schema2LayerMediaType)
|
||||
_ = layer.storeBlob(t, dest, cache, manifest.DockerV2Schema2LayerMediaType, false)
|
||||
t.Logf("Wrote randomly-generated layer %q (%d/%d bytes) to destination", layer.compressedDigest, layer.compressedSize, layer.uncompressedSize)
|
||||
_ = config.storeBlob(t, dest, cache, manifest.DockerV2Schema2ConfigMediaType)
|
||||
_ = config.storeBlob(t, dest, cache, manifest.DockerV2Schema2ConfigMediaType, true)
|
||||
|
||||
manifest := strings.ReplaceAll(manifestFmt, "%lh", layer.compressedDigest.String())
|
||||
manifest = strings.ReplaceAll(manifest, "%ch", config.compressedDigest.String())
|
||||
|
Loading…
x
Reference in New Issue
Block a user