From 671341f0e3571dc1a1774c8498a0806204dd58bf Mon Sep 17 00:00:00 2001 From: Sargun Dhillon Date: Tue, 29 Aug 2017 09:01:23 -0700 Subject: [PATCH 1/2] Separate daemon/graphdriver/overlay/copy into its own package Signed-off-by: Sargun Dhillon Upstream-commit: 5298785b8e612ca5d3943fada08a46978971ba70 Component: engine --- .../daemon/graphdriver/{overlay => copy}/copy.go | 16 +++++++++++----- .../engine/daemon/graphdriver/overlay/overlay.go | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) rename components/engine/daemon/graphdriver/{overlay => copy}/copy.go (89%) diff --git a/components/engine/daemon/graphdriver/overlay/copy.go b/components/engine/daemon/graphdriver/copy/copy.go similarity index 89% rename from components/engine/daemon/graphdriver/overlay/copy.go rename to components/engine/daemon/graphdriver/copy/copy.go index f7e35e2bd3..d7abccbea5 100644 --- a/components/engine/daemon/graphdriver/overlay/copy.go +++ b/components/engine/daemon/graphdriver/copy/copy.go @@ -1,6 +1,6 @@ // +build linux -package overlay +package copy import ( "fmt" @@ -15,10 +15,13 @@ import ( "golang.org/x/sys/unix" ) -type copyFlags int +type Mode int const ( - copyHardlink copyFlags = 1 << iota + // Content creates a new file, and copies the content of the file + Content Mode = iota + // Hardlink creates a new hardlink to the existing file + Hardlink ) func copyRegular(srcPath, dstPath string, mode os.FileMode) error { @@ -52,7 +55,9 @@ func copyXattr(srcPath, dstPath, attr string) error { return nil } -func copyDir(srcDir, dstDir string, flags copyFlags) error { +// DirCopy copies or hardlinks the contents of one directory to another, +// properly handling xattrs, and soft links +func DirCopy(srcDir, dstDir string, copyMode Mode) error { err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error { if err != nil { return err @@ -78,12 +83,13 @@ func copyDir(srcDir, dstDir string, flags copyFlags) error { switch f.Mode() & os.ModeType { case 0: // Regular file - if flags©Hardlink != 0 { + if copyMode == Hardlink { isHardlink = true if err := os.Link(srcPath, dstPath); err != nil { return err } } else { + // Always fall back to Content copymode if err := copyRegular(srcPath, dstPath, f.Mode()); err != nil { return err } diff --git a/components/engine/daemon/graphdriver/overlay/overlay.go b/components/engine/daemon/graphdriver/overlay/overlay.go index 9012722c20..3db84bc229 100644 --- a/components/engine/daemon/graphdriver/overlay/overlay.go +++ b/components/engine/daemon/graphdriver/overlay/overlay.go @@ -13,6 +13,7 @@ import ( "strconv" "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/daemon/graphdriver/copy" "github.com/docker/docker/daemon/graphdriver/overlayutils" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/containerfs" @@ -327,7 +328,7 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) (retErr return err } - return copyDir(parentUpperDir, upperDir, 0) + return copy.DirCopy(parentUpperDir, upperDir, copy.Content) } func (d *Driver) dir(id string) string { @@ -443,7 +444,7 @@ func (d *Driver) ApplyDiff(id string, parent string, diff io.Reader) (size int64 } }() - if err = copyDir(parentRootDir, tmpRootDir, copyHardlink); err != nil { + if err = copy.DirCopy(parentRootDir, tmpRootDir, copy.Hardlink); err != nil { return 0, err } From a04c3458dcf5abb2d9829814b2c9f094304d9c8c Mon Sep 17 00:00:00 2001 From: Sargun Dhillon Date: Wed, 30 Aug 2017 19:07:02 -0700 Subject: [PATCH 2/2] Add zero-copy support to copy module This changeset allows Docker's VFS, and Overlay to take advantage of Linux's zerocopy APIs. The copy function first tries to use the ficlone ioctl. Reason being: - they do not allow partial success (aka short writes) - clones are expected to be a fast metadata operation See: http://oss.sgi.com/archives/xfs/2015-12/msg00356.html If the clone fails, we fall back to copy_file_range, which internally may fall back to splice, which has an upper limit on the size of copy it can perform. Given that, we have to loop until the copy is done. For a given dirCopy operation, if the clone fails, we will not try it again during any other file copy. Same is true with copy_file_range. If all else fails, we fall back to traditional copy. Signed-off-by: Sargun Dhillon Upstream-commit: 3ec4ec2857c714387e7b59c2cf324565f6ae55e2 Component: engine --- .../engine/daemon/graphdriver/copy/copy.go | 68 ++++++++++++++++--- .../daemon/graphdriver/copy/copy_test.go | 67 ++++++++++++++++++ 2 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 components/engine/daemon/graphdriver/copy/copy_test.go diff --git a/components/engine/daemon/graphdriver/copy/copy.go b/components/engine/daemon/graphdriver/copy/copy.go index d7abccbea5..8ec458d6a4 100644 --- a/components/engine/daemon/graphdriver/copy/copy.go +++ b/components/engine/daemon/graphdriver/copy/copy.go @@ -2,8 +2,17 @@ package copy +/* +#include + +#ifndef FICLONE +#define FICLONE _IOW(0x94, 9, int) +#endif +*/ +import "C" import ( "fmt" + "io" "os" "path/filepath" "syscall" @@ -15,6 +24,7 @@ import ( "golang.org/x/sys/unix" ) +// Mode indicates whether to use hardlink or copy content type Mode int const ( @@ -24,20 +34,61 @@ const ( Hardlink ) -func copyRegular(srcPath, dstPath string, mode os.FileMode) error { +func copyRegular(srcPath, dstPath string, fileinfo os.FileInfo, copyWithFileRange, copyWithFileClone *bool) error { srcFile, err := os.Open(srcPath) if err != nil { return err } defer srcFile.Close() - dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE, mode) + // If the destination file already exists, we shouldn't blow it away + dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileinfo.Mode()) if err != nil { return err } defer dstFile.Close() - _, err = pools.Copy(dstFile, srcFile) + if *copyWithFileClone { + _, _, err = unix.Syscall(unix.SYS_IOCTL, dstFile.Fd(), C.FICLONE, srcFile.Fd()) + if err == nil { + return nil + } + + *copyWithFileClone = false + if err == unix.EXDEV { + *copyWithFileRange = false + } + } + if *copyWithFileRange { + err = doCopyWithFileRange(srcFile, dstFile, fileinfo) + // Trying the file_clone may not have caught the exdev case + // as the ioctl may not have been available (therefore EINVAL) + if err == unix.EXDEV || err == unix.ENOSYS { + *copyWithFileRange = false + } else if err != nil { + return err + } + } + return legacyCopy(srcFile, dstFile) +} + +func doCopyWithFileRange(srcFile, dstFile *os.File, fileinfo os.FileInfo) error { + amountLeftToCopy := fileinfo.Size() + + for amountLeftToCopy > 0 { + n, err := unix.CopyFileRange(int(srcFile.Fd()), nil, int(dstFile.Fd()), nil, int(amountLeftToCopy), 0) + if err != nil { + return err + } + + amountLeftToCopy = amountLeftToCopy - int64(n) + } + + return nil +} + +func legacyCopy(srcFile io.Reader, dstFile io.Writer) error { + _, err := pools.Copy(dstFile, srcFile) return err } @@ -58,6 +109,8 @@ func copyXattr(srcPath, dstPath, attr string) error { // DirCopy copies or hardlinks the contents of one directory to another, // properly handling xattrs, and soft links func DirCopy(srcDir, dstDir string, copyMode Mode) error { + copyWithFileRange := true + copyWithFileClone := true err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error { if err != nil { return err @@ -85,13 +138,12 @@ func DirCopy(srcDir, dstDir string, copyMode Mode) error { case 0: // Regular file if copyMode == Hardlink { isHardlink = true - if err := os.Link(srcPath, dstPath); err != nil { - return err + if err2 := os.Link(srcPath, dstPath); err2 != nil { + return err2 } } else { - // Always fall back to Content copymode - if err := copyRegular(srcPath, dstPath, f.Mode()); err != nil { - return err + if err2 := copyRegular(srcPath, dstPath, f, ©WithFileRange, ©WithFileClone); err2 != nil { + return err2 } } diff --git a/components/engine/daemon/graphdriver/copy/copy_test.go b/components/engine/daemon/graphdriver/copy/copy_test.go new file mode 100644 index 0000000000..6976503e18 --- /dev/null +++ b/components/engine/daemon/graphdriver/copy/copy_test.go @@ -0,0 +1,67 @@ +// +build linux + +package copy + +import ( + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "testing" + + "github.com/docker/docker/pkg/parsers/kernel" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsCopyFileRangeSyscallAvailable(t *testing.T) { + // Verifies: + // 1. That copyFileRangeEnabled is being set to true when copy_file_range syscall is available + // 2. That isCopyFileRangeSyscallAvailable() works on "new" kernels + v, err := kernel.GetKernelVersion() + require.NoError(t, err) + + copyWithFileRange := true + copyWithFileClone := false + doCopyTest(t, ©WithFileRange, ©WithFileClone) + + if kernel.CompareKernelVersion(*v, kernel.VersionInfo{Kernel: 4, Major: 5, Minor: 0}) < 0 { + assert.False(t, copyWithFileRange) + } else { + assert.True(t, copyWithFileRange) + } + +} + +func TestCopy(t *testing.T) { + copyWithFileRange := true + copyWithFileClone := true + doCopyTest(t, ©WithFileRange, ©WithFileClone) +} + +func TestCopyWithoutRange(t *testing.T) { + copyWithFileRange := false + copyWithFileClone := false + doCopyTest(t, ©WithFileRange, ©WithFileClone) +} + +func doCopyTest(t *testing.T, copyWithFileRange, copyWithFileClone *bool) { + dir, err := ioutil.TempDir("", "docker-copy-check") + require.NoError(t, err) + defer os.RemoveAll(dir) + srcFilename := filepath.Join(dir, "srcFilename") + dstFilename := filepath.Join(dir, "dstilename") + + r := rand.New(rand.NewSource(0)) + buf := make([]byte, 1024) + _, err = r.Read(buf) + require.NoError(t, err) + require.NoError(t, ioutil.WriteFile(srcFilename, buf, 0777)) + fileinfo, err := os.Stat(srcFilename) + require.NoError(t, err) + + require.NoError(t, copyRegular(srcFilename, dstFilename, fileinfo, copyWithFileRange, copyWithFileClone)) + readBuf, err := ioutil.ReadFile(dstFilename) + require.NoError(t, err) + assert.Equal(t, buf, readBuf) +}