github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/snapshots/benchsuite/benchmark_test.go (about)

     1  // +build linux
     2  
     3  /*
     4     Copyright The containerd Authors.
     5  
     6     Licensed under the Apache License, Version 2.0 (the "License");
     7     you may not use this file except in compliance with the License.
     8     You may obtain a copy of the License at
     9  
    10         http://www.apache.org/licenses/LICENSE-2.0
    11  
    12     Unless required by applicable law or agreed to in writing, software
    13     distributed under the License is distributed on an "AS IS" BASIS,
    14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15     See the License for the specific language governing permissions and
    16     limitations under the License.
    17  */
    18  
    19  package benchsuite
    20  
    21  import (
    22  	"context"
    23  	"crypto/rand"
    24  	"flag"
    25  	"fmt"
    26  	"os"
    27  	"path/filepath"
    28  	"sync/atomic"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/containerd/continuity/fs/fstest"
    33  	"github.com/pkg/errors"
    34  	"github.com/sirupsen/logrus"
    35  	"gotest.tools/v3/assert"
    36  
    37  	"github.com/containerd/containerd/mount"
    38  	"github.com/containerd/containerd/snapshots"
    39  	"github.com/containerd/containerd/snapshots/devmapper"
    40  	"github.com/containerd/containerd/snapshots/native"
    41  	"github.com/containerd/containerd/snapshots/overlay"
    42  )
    43  
    44  var (
    45  	dmPoolDev       string
    46  	dmRootPath      string
    47  	overlayRootPath string
    48  	nativeRootPath  string
    49  )
    50  
    51  func init() {
    52  	flag.StringVar(&dmPoolDev, "dm.thinPoolDev", "", "Pool device to run benchmark on")
    53  	flag.StringVar(&dmRootPath, "dm.rootPath", "", "Root dir for devmapper snapshotter")
    54  	flag.StringVar(&overlayRootPath, "overlay.rootPath", "", "Root dir for overlay snapshotter")
    55  	flag.StringVar(&nativeRootPath, "native.rootPath", "", "Root dir for native snapshotter")
    56  
    57  	// Avoid mixing benchmark output and INFO messages
    58  	logrus.SetLevel(logrus.ErrorLevel)
    59  }
    60  
    61  func BenchmarkNative(b *testing.B) {
    62  	if nativeRootPath == "" {
    63  		b.Skip("native root dir must be provided")
    64  	}
    65  
    66  	snapshotter, err := native.NewSnapshotter(nativeRootPath)
    67  	assert.NilError(b, err)
    68  
    69  	defer func() {
    70  		err = snapshotter.Close()
    71  		assert.NilError(b, err)
    72  
    73  		err = os.RemoveAll(nativeRootPath)
    74  		assert.NilError(b, err)
    75  	}()
    76  
    77  	benchmarkSnapshotter(b, snapshotter)
    78  }
    79  
    80  func BenchmarkOverlay(b *testing.B) {
    81  	if overlayRootPath == "" {
    82  		b.Skip("overlay root dir must be provided")
    83  	}
    84  
    85  	snapshotter, err := overlay.NewSnapshotter(overlayRootPath)
    86  	assert.NilError(b, err, "failed to create overlay snapshotter")
    87  
    88  	defer func() {
    89  		err = snapshotter.Close()
    90  		assert.NilError(b, err)
    91  
    92  		err = os.RemoveAll(overlayRootPath)
    93  		assert.NilError(b, err)
    94  	}()
    95  
    96  	benchmarkSnapshotter(b, snapshotter)
    97  }
    98  
    99  func BenchmarkDeviceMapper(b *testing.B) {
   100  	if dmPoolDev == "" {
   101  		b.Skip("devmapper benchmark requires thin-pool device to be prepared in advance and provided")
   102  	}
   103  
   104  	if dmRootPath == "" {
   105  		b.Skip("devmapper snapshotter root dir must be provided")
   106  	}
   107  
   108  	config := &devmapper.Config{
   109  		PoolName:      dmPoolDev,
   110  		RootPath:      dmRootPath,
   111  		BaseImageSize: "16Mb",
   112  	}
   113  
   114  	ctx := context.Background()
   115  
   116  	snapshotter, err := devmapper.NewSnapshotter(ctx, config)
   117  	assert.NilError(b, err)
   118  
   119  	defer func() {
   120  		err := snapshotter.ResetPool(ctx)
   121  		assert.NilError(b, err)
   122  
   123  		err = snapshotter.Close()
   124  		assert.NilError(b, err)
   125  
   126  		err = os.RemoveAll(dmRootPath)
   127  		assert.NilError(b, err)
   128  	}()
   129  
   130  	benchmarkSnapshotter(b, snapshotter)
   131  }
   132  
   133  // benchmarkSnapshotter tests snapshotter performance.
   134  // It writes 16 layers with randomly created, modified, or removed files.
   135  // Depending on layer index different sets of files are modified.
   136  // In addition to total snapshotter execution time, benchmark outputs a few additional
   137  // details - time taken to Prepare layer, mount, write data and unmount time,
   138  // and Commit snapshot time.
   139  func benchmarkSnapshotter(b *testing.B, snapshotter snapshots.Snapshotter) {
   140  	const (
   141  		layerCount    = 16
   142  		fileSizeBytes = int64(1 * 1024 * 1024) // 1 MB
   143  	)
   144  
   145  	var (
   146  		total      = 0
   147  		layers     = make([]fstest.Applier, 0, layerCount)
   148  		layerIndex = int64(0)
   149  	)
   150  
   151  	for i := 1; i <= layerCount; i++ {
   152  		appliers := makeApplier(i, fileSizeBytes)
   153  		layers = append(layers, fstest.Apply(appliers...))
   154  		total += len(appliers)
   155  	}
   156  
   157  	var (
   158  		benchN          int
   159  		prepareDuration time.Duration
   160  		writeDuration   time.Duration
   161  		commitDuration  time.Duration
   162  	)
   163  
   164  	// Wrap test with Run so additional details output will be added right below the benchmark result
   165  	b.Run("run", func(b *testing.B) {
   166  		var (
   167  			ctx     = context.Background()
   168  			parent  string
   169  			current string
   170  		)
   171  
   172  		// Reset durations since test might be ran multiple times
   173  		prepareDuration = 0
   174  		writeDuration = 0
   175  		commitDuration = 0
   176  		benchN = b.N
   177  
   178  		b.SetBytes(int64(total) * fileSizeBytes)
   179  
   180  		var timer time.Time
   181  		for i := 0; i < b.N; i++ {
   182  			for l := 0; l < layerCount; l++ {
   183  				current = fmt.Sprintf("prepare-layer-%d", atomic.AddInt64(&layerIndex, 1))
   184  
   185  				timer = time.Now()
   186  				mounts, err := snapshotter.Prepare(ctx, current, parent)
   187  				assert.NilError(b, err)
   188  				prepareDuration += time.Since(timer)
   189  
   190  				timer = time.Now()
   191  				err = mount.WithTempMount(ctx, mounts, layers[l].Apply)
   192  				assert.NilError(b, err)
   193  				writeDuration += time.Since(timer)
   194  
   195  				parent = fmt.Sprintf("committed-%d", atomic.AddInt64(&layerIndex, 1))
   196  
   197  				timer = time.Now()
   198  				err = snapshotter.Commit(ctx, parent, current)
   199  				assert.NilError(b, err)
   200  				commitDuration += time.Since(timer)
   201  			}
   202  		}
   203  	})
   204  
   205  	// Output extra measurements - total time taken to Prepare, mount and write data, and Commit
   206  	const outputFormat = "%-25s\t%s\n"
   207  	fmt.Fprintf(os.Stdout,
   208  		outputFormat,
   209  		b.Name()+"/prepare",
   210  		testing.BenchmarkResult{N: benchN, T: prepareDuration})
   211  
   212  	fmt.Fprintf(os.Stdout,
   213  		outputFormat,
   214  		b.Name()+"/write",
   215  		testing.BenchmarkResult{N: benchN, T: writeDuration})
   216  
   217  	fmt.Fprintf(os.Stdout,
   218  		outputFormat,
   219  		b.Name()+"/commit",
   220  		testing.BenchmarkResult{N: benchN, T: commitDuration})
   221  
   222  	fmt.Fprintln(os.Stdout)
   223  }
   224  
   225  // makeApplier returns a slice of fstest.Applier where files are written randomly.
   226  // Depending on layer index, the returned layers will overwrite some files with the
   227  // same generated names with new contents or deletions.
   228  func makeApplier(layerIndex int, fileSizeBytes int64) []fstest.Applier {
   229  	seed := time.Now().UnixNano()
   230  
   231  	switch {
   232  	case layerIndex%3 == 0:
   233  		return []fstest.Applier{
   234  			updateFile("/a"),
   235  			updateFile("/b"),
   236  			fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
   237  			updateFile("/d"),
   238  			fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
   239  			updateFile("/e"),
   240  			fstest.RemoveAll("/g"),
   241  			fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
   242  			updateFile("/i"),
   243  			fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
   244  		}
   245  	case layerIndex%2 == 0:
   246  		return []fstest.Applier{
   247  			updateFile("/a"),
   248  			fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
   249  			fstest.RemoveAll("/c"),
   250  			fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
   251  			updateFile("/e"),
   252  			fstest.RemoveAll("/f"),
   253  			fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
   254  			updateFile("/h"),
   255  			fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
   256  			updateFile("/j"),
   257  		}
   258  	default:
   259  		return []fstest.Applier{
   260  			fstest.CreateRandomFile("/a", seed, fileSizeBytes, 0777),
   261  			fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
   262  			fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
   263  			fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
   264  			fstest.CreateRandomFile("/e", seed, fileSizeBytes, 0777),
   265  			fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
   266  			fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
   267  			fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
   268  			fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
   269  			fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
   270  		}
   271  	}
   272  }
   273  
   274  // applierFn represents helper func that implements fstest.Applier
   275  type applierFn func(root string) error
   276  
   277  func (fn applierFn) Apply(root string) error {
   278  	return fn(root)
   279  }
   280  
   281  // updateFile modifies a few bytes in the middle in order to demonstrate the difference in performance
   282  // for block-based snapshotters (like devicemapper) against file-based snapshotters (like overlay, which need to
   283  // perform a copy-up of the full file any time a single bit is modified).
   284  func updateFile(name string) applierFn {
   285  	return func(root string) error {
   286  		path := filepath.Join(root, name)
   287  		file, err := os.OpenFile(path, os.O_WRONLY, 0600)
   288  		if err != nil {
   289  			return errors.Wrapf(err, "failed to open %q", path)
   290  		}
   291  
   292  		info, err := file.Stat()
   293  		if err != nil {
   294  			return err
   295  		}
   296  
   297  		var (
   298  			offset = info.Size() / 2
   299  			buf    = make([]byte, 4)
   300  		)
   301  
   302  		if _, err := rand.Read(buf); err != nil {
   303  			return err
   304  		}
   305  
   306  		if _, err := file.WriteAt(buf, offset); err != nil {
   307  			return errors.Wrapf(err, "failed to write %q at offset %d", path, offset)
   308  		}
   309  
   310  		return file.Close()
   311  	}
   312  }