Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions storage/drivers/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ type DifferOptions struct {

// UseFsVerity defines whether fs-verity is used
UseFsVerity DifferFsVerity

// StagingDirectory is a writable directory the differ can use for
// temporary scratch data. It must reside on the same filesystem
// as the destination directory.
StagingDirectory string
}

// Differ defines the interface for using a custom differ.
Expand Down
3 changes: 2 additions & 1 deletion storage/drivers/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -2242,7 +2242,8 @@ func (d *Driver) ApplyDiffWithDiffer(options *graphdriver.ApplyDiffWithDifferOpt
logrus.Debugf("Applying differ in %s", applyDir)

differOptions := graphdriver.DifferOptions{
Format: graphdriver.DifferOutputFormatDir,
Format: graphdriver.DifferOutputFormatDir,
StagingDirectory: layerDir,
}
if d.usingComposefs {
differOptions.Format = graphdriver.DifferOutputFormatFlat
Expand Down
88 changes: 85 additions & 3 deletions storage/pkg/chunked/storage_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"go.podman.io/storage/pkg/chunked/internal/minimal"
path "go.podman.io/storage/pkg/chunked/internal/path"
"go.podman.io/storage/pkg/chunked/toc"
"go.podman.io/storage/pkg/fileutils"
"go.podman.io/storage/pkg/fsverity"
"go.podman.io/storage/pkg/idtools"
"go.podman.io/storage/pkg/system"
Expand Down Expand Up @@ -625,6 +626,40 @@ func collectIDs(entries []fileMetadata) ([]uint32, []uint32) {
return mapToSlice(uids), mapToSlice(gids)
}

func isReflinkNotSupported(err error) bool {
return errors.Is(err, unix.EOPNOTSUPP) ||
errors.Is(err, unix.ENOSYS) ||
errors.Is(err, unix.EXDEV) ||
errors.Is(err, unix.EINVAL)
}

func createReflink(srcRoot, srcPath, dstDir string) (string, error) {
parentDirfd, err := unix.Open(srcRoot, unix.O_RDONLY|unix.O_CLOEXEC, 0)
if err != nil {
return "", err
}
defer unix.Close(parentDirfd)

srcFile, err := openFileUnderRoot(parentDirfd, srcPath, unix.O_RDONLY|unix.O_CLOEXEC, 0)
if err != nil {
return "", err
}
defer srcFile.Close()

dstFile, err := os.CreateTemp(dstDir, "")
if err != nil {
return "", err
}
defer dstFile.Close()

if err := fileutils.Reflink(srcFile, dstFile); err != nil {
os.Remove(dstFile.Name())
return "", err
}

return filepath.Base(dstFile.Name()), nil
}

type originFile struct {
Root string
Path string
Expand Down Expand Up @@ -1777,6 +1812,29 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff

wg.Wait()

// Reflink chunk dedup: when findChunkInOtherLayers locates a chunk
// in an existing layer, we cannot use that path directly because
// the source layer may be deleted before storeMissingFiles reads
// from it. Instead we reflink (CoW clone) the source file into a
// scratch directory so the copy is immune to concurrent deletion.
// If the filesystem does not support reflinks we skip chunk dedup
// entirely and fetch everything from the network.
var chunkRefsDir string
if differOpts != nil && differOpts.StagingDirectory != "" {
d, err := os.MkdirTemp(differOpts.StagingDirectory, "chunk-refs-")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: I would log an error at least as debug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks.

Thinking more about it, I think it should be a hard error. If we fail to create a temporary directory, we need to report that

if err != nil {
return output, fmt.Errorf("create chunk-refs directory: %w", err)
}
chunkRefsDir = d
defer os.RemoveAll(chunkRefsDir)
}
// reflinkMap caches reflinks: (source root, source path) → reflinked
// filename in chunkRefsDir. Multiple chunks at different offsets
// within the same source file share one reflink.
type reflinkKey struct{ root, path string }
reflinkMap := make(map[reflinkKey]string)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Documenting the key/value semantics would be nice.)

reflinkSupported := chunkRefsDir != ""

for _, res := range copyResults[:filesToWaitFor] {
r := &mergedEntries[res.index]

Expand Down Expand Up @@ -1820,15 +1878,39 @@ func (c *chunkedDiffer) ApplyDiff(dest string, options *archive.TarOptions, diff

switch chunk.ChunkType {
case minimal.ChunkTypeData:
if !reflinkSupported {
break
}
root, path, offset, err := c.layersCache.findChunkInOtherLayers(chunk)
if err != nil {
return output, err
}
if offset >= 0 && validateChunkChecksum(chunk, root, path, offset, c.copyBuffer) {
if offset < 0 {
break
}
key := reflinkKey{root: root, path: path}
refName, ok := reflinkMap[key]
if !ok {
refName, err = createReflink(root, path, chunkRefsDir)
if err != nil {
if isReflinkNotSupported(err) {
reflinkSupported = false
break
}
// ENOENT is expected: the source layer can
// be deleted concurrently.
if errors.Is(err, unix.ENOENT) {
break
}
return output, fmt.Errorf("create reflink for %q: %w", path, err)
}
reflinkMap[key] = refName
}
if validateChunkChecksum(chunk, chunkRefsDir, refName, offset, c.copyBuffer) {
missingPartsSize -= size
mp.OriginFile = &originFile{
Root: root,
Path: path,
Root: chunkRefsDir,
Path: refName,
Offset: offset,
}
}
Expand Down
11 changes: 8 additions & 3 deletions storage/pkg/fileutils/reflink_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ import (
"golang.org/x/sys/unix"
)

// Reflink attempts to reflink (CoW clone) the source to the destination fd.
// Returns an error if the filesystem does not support reflinks.
func Reflink(src, dst *os.File) error {
return unix.IoctlFileClone(int(dst.Fd()), int(src.Fd()))
}

// ReflinkOrCopy attempts to reflink the source to the destination fd.
// If reflinking fails or is unsupported, it falls back to io.Copy().
func ReflinkOrCopy(src, dst *os.File) error {
err := unix.IoctlFileClone(int(dst.Fd()), int(src.Fd()))
if err == nil {
if err := Reflink(src, dst); err == nil {
return nil
}

_, err = io.Copy(dst, src)
_, err := io.Copy(dst, src)
return err
}
7 changes: 7 additions & 0 deletions storage/pkg/fileutils/reflink_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
package fileutils

import (
"errors"
"io"
"os"
)

// Reflink attempts to reflink (CoW clone) the source to the destination fd.
// Returns an error if the filesystem does not support reflinks.
func Reflink(src, dst *os.File) error {
return errors.ErrUnsupported
}

// ReflinkOrCopy attempts to reflink the source to the destination fd.
// If reflinking fails or is unsupported, it falls back to io.Copy().
func ReflinkOrCopy(src, dst *os.File) error {
Expand Down
Loading