313 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			313 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| //go:build linux
 | |
| // +build linux
 | |
| 
 | |
| /*
 | |
|    Copyright The containerd Authors.
 | |
| 
 | |
|    Licensed under the Apache License, Version 2.0 (the "License");
 | |
|    you may not use this file except in compliance with the License.
 | |
|    You may obtain a copy of the License at
 | |
| 
 | |
|        http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
|    Unless required by applicable law or agreed to in writing, software
 | |
|    distributed under the License is distributed on an "AS IS" BASIS,
 | |
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|    See the License for the specific language governing permissions and
 | |
|    limitations under the License.
 | |
| */
 | |
| 
 | |
| package benchsuite
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/rand"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"sync/atomic"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/containerd/continuity/fs/fstest"
 | |
| 	"github.com/sirupsen/logrus"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 
 | |
| 	"github.com/containerd/containerd/mount"
 | |
| 	"github.com/containerd/containerd/snapshots"
 | |
| 	"github.com/containerd/containerd/snapshots/devmapper"
 | |
| 	"github.com/containerd/containerd/snapshots/native"
 | |
| 	"github.com/containerd/containerd/snapshots/overlay"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	dmPoolDev       string
 | |
| 	dmRootPath      string
 | |
| 	overlayRootPath string
 | |
| 	nativeRootPath  string
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	flag.StringVar(&dmPoolDev, "dm.thinPoolDev", "", "Pool device to run benchmark on")
 | |
| 	flag.StringVar(&dmRootPath, "dm.rootPath", "", "Root dir for devmapper snapshotter")
 | |
| 	flag.StringVar(&overlayRootPath, "overlay.rootPath", "", "Root dir for overlay snapshotter")
 | |
| 	flag.StringVar(&nativeRootPath, "native.rootPath", "", "Root dir for native snapshotter")
 | |
| 
 | |
| 	// Avoid mixing benchmark output and INFO messages
 | |
| 	logrus.SetLevel(logrus.ErrorLevel)
 | |
| }
 | |
| 
 | |
| func BenchmarkNative(b *testing.B) {
 | |
| 	if nativeRootPath == "" {
 | |
| 		b.Skip("native root dir must be provided")
 | |
| 	}
 | |
| 
 | |
| 	snapshotter, err := native.NewSnapshotter(nativeRootPath)
 | |
| 	assert.Nil(b, err)
 | |
| 
 | |
| 	defer func() {
 | |
| 		err = snapshotter.Close()
 | |
| 		assert.Nil(b, err)
 | |
| 
 | |
| 		err = os.RemoveAll(nativeRootPath)
 | |
| 		assert.Nil(b, err)
 | |
| 	}()
 | |
| 
 | |
| 	benchmarkSnapshotter(b, snapshotter)
 | |
| }
 | |
| 
 | |
| func BenchmarkOverlay(b *testing.B) {
 | |
| 	if overlayRootPath == "" {
 | |
| 		b.Skip("overlay root dir must be provided")
 | |
| 	}
 | |
| 
 | |
| 	snapshotter, err := overlay.NewSnapshotter(overlayRootPath)
 | |
| 	assert.Nil(b, err, "failed to create overlay snapshotter")
 | |
| 
 | |
| 	defer func() {
 | |
| 		err = snapshotter.Close()
 | |
| 		assert.Nil(b, err)
 | |
| 
 | |
| 		err = os.RemoveAll(overlayRootPath)
 | |
| 		assert.Nil(b, err)
 | |
| 	}()
 | |
| 
 | |
| 	benchmarkSnapshotter(b, snapshotter)
 | |
| }
 | |
| 
 | |
| func BenchmarkDeviceMapper(b *testing.B) {
 | |
| 	if dmPoolDev == "" {
 | |
| 		b.Skip("devmapper benchmark requires thin-pool device to be prepared in advance and provided")
 | |
| 	}
 | |
| 
 | |
| 	if dmRootPath == "" {
 | |
| 		b.Skip("devmapper snapshotter root dir must be provided")
 | |
| 	}
 | |
| 
 | |
| 	config := &devmapper.Config{
 | |
| 		PoolName:      dmPoolDev,
 | |
| 		RootPath:      dmRootPath,
 | |
| 		BaseImageSize: "16Mb",
 | |
| 	}
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	snapshotter, err := devmapper.NewSnapshotter(ctx, config)
 | |
| 	assert.Nil(b, err)
 | |
| 
 | |
| 	defer func() {
 | |
| 		err := snapshotter.ResetPool(ctx)
 | |
| 		assert.Nil(b, err)
 | |
| 
 | |
| 		err = snapshotter.Close()
 | |
| 		assert.Nil(b, err)
 | |
| 
 | |
| 		err = os.RemoveAll(dmRootPath)
 | |
| 		assert.Nil(b, err)
 | |
| 	}()
 | |
| 
 | |
| 	benchmarkSnapshotter(b, snapshotter)
 | |
| }
 | |
| 
 | |
| // benchmarkSnapshotter tests snapshotter performance.
 | |
| // It writes 16 layers with randomly created, modified, or removed files.
 | |
| // Depending on layer index different sets of files are modified.
 | |
| // In addition to total snapshotter execution time, benchmark outputs a few additional
 | |
| // details - time taken to Prepare layer, mount, write data and unmount time,
 | |
| // and Commit snapshot time.
 | |
| func benchmarkSnapshotter(b *testing.B, snapshotter snapshots.Snapshotter) {
 | |
| 	const (
 | |
| 		layerCount    = 16
 | |
| 		fileSizeBytes = int64(1 * 1024 * 1024) // 1 MB
 | |
| 	)
 | |
| 
 | |
| 	var (
 | |
| 		total      = 0
 | |
| 		layers     = make([]fstest.Applier, 0, layerCount)
 | |
| 		layerIndex = int64(0)
 | |
| 	)
 | |
| 
 | |
| 	for i := 1; i <= layerCount; i++ {
 | |
| 		appliers := makeApplier(i, fileSizeBytes)
 | |
| 		layers = append(layers, fstest.Apply(appliers...))
 | |
| 		total += len(appliers)
 | |
| 	}
 | |
| 
 | |
| 	var (
 | |
| 		benchN          int
 | |
| 		prepareDuration time.Duration
 | |
| 		writeDuration   time.Duration
 | |
| 		commitDuration  time.Duration
 | |
| 	)
 | |
| 
 | |
| 	// Wrap test with Run so additional details output will be added right below the benchmark result
 | |
| 	b.Run("run", func(b *testing.B) {
 | |
| 		var (
 | |
| 			ctx     = context.Background()
 | |
| 			parent  string
 | |
| 			current string
 | |
| 		)
 | |
| 
 | |
| 		// Reset durations since test might be ran multiple times
 | |
| 		prepareDuration = 0
 | |
| 		writeDuration = 0
 | |
| 		commitDuration = 0
 | |
| 		benchN = b.N
 | |
| 
 | |
| 		b.SetBytes(int64(total) * fileSizeBytes)
 | |
| 
 | |
| 		var timer time.Time
 | |
| 		for i := 0; i < b.N; i++ {
 | |
| 			for l := 0; l < layerCount; l++ {
 | |
| 				current = fmt.Sprintf("prepare-layer-%d", atomic.AddInt64(&layerIndex, 1))
 | |
| 
 | |
| 				timer = time.Now()
 | |
| 				mounts, err := snapshotter.Prepare(ctx, current, parent)
 | |
| 				assert.Nil(b, err)
 | |
| 				prepareDuration += time.Since(timer)
 | |
| 
 | |
| 				timer = time.Now()
 | |
| 				err = mount.WithTempMount(ctx, mounts, layers[l].Apply)
 | |
| 				assert.Nil(b, err)
 | |
| 				writeDuration += time.Since(timer)
 | |
| 
 | |
| 				parent = fmt.Sprintf("committed-%d", atomic.AddInt64(&layerIndex, 1))
 | |
| 
 | |
| 				timer = time.Now()
 | |
| 				err = snapshotter.Commit(ctx, parent, current)
 | |
| 				assert.Nil(b, err)
 | |
| 				commitDuration += time.Since(timer)
 | |
| 			}
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	// Output extra measurements - total time taken to Prepare, mount and write data, and Commit
 | |
| 	const outputFormat = "%-25s\t%s\n"
 | |
| 	fmt.Fprintf(os.Stdout,
 | |
| 		outputFormat,
 | |
| 		b.Name()+"/prepare",
 | |
| 		testing.BenchmarkResult{N: benchN, T: prepareDuration})
 | |
| 
 | |
| 	fmt.Fprintf(os.Stdout,
 | |
| 		outputFormat,
 | |
| 		b.Name()+"/write",
 | |
| 		testing.BenchmarkResult{N: benchN, T: writeDuration})
 | |
| 
 | |
| 	fmt.Fprintf(os.Stdout,
 | |
| 		outputFormat,
 | |
| 		b.Name()+"/commit",
 | |
| 		testing.BenchmarkResult{N: benchN, T: commitDuration})
 | |
| 
 | |
| 	fmt.Fprintln(os.Stdout)
 | |
| }
 | |
| 
 | |
| // makeApplier returns a slice of fstest.Applier where files are written randomly.
 | |
| // Depending on layer index, the returned layers will overwrite some files with the
 | |
| // same generated names with new contents or deletions.
 | |
| func makeApplier(layerIndex int, fileSizeBytes int64) []fstest.Applier {
 | |
| 	seed := time.Now().UnixNano()
 | |
| 
 | |
| 	switch {
 | |
| 	case layerIndex%3 == 0:
 | |
| 		return []fstest.Applier{
 | |
| 			updateFile("/a"),
 | |
| 			updateFile("/b"),
 | |
| 			fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
 | |
| 			updateFile("/d"),
 | |
| 			fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
 | |
| 			updateFile("/e"),
 | |
| 			fstest.RemoveAll("/g"),
 | |
| 			fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
 | |
| 			updateFile("/i"),
 | |
| 			fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
 | |
| 		}
 | |
| 	case layerIndex%2 == 0:
 | |
| 		return []fstest.Applier{
 | |
| 			updateFile("/a"),
 | |
| 			fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
 | |
| 			fstest.RemoveAll("/c"),
 | |
| 			fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
 | |
| 			updateFile("/e"),
 | |
| 			fstest.RemoveAll("/f"),
 | |
| 			fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
 | |
| 			updateFile("/h"),
 | |
| 			fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
 | |
| 			updateFile("/j"),
 | |
| 		}
 | |
| 	default:
 | |
| 		return []fstest.Applier{
 | |
| 			fstest.CreateRandomFile("/a", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/e", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
 | |
| 			fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // applierFn represents helper func that implements fstest.Applier
 | |
| type applierFn func(root string) error
 | |
| 
 | |
| func (fn applierFn) Apply(root string) error {
 | |
| 	return fn(root)
 | |
| }
 | |
| 
 | |
| // updateFile modifies a few bytes in the middle in order to demonstrate the difference in performance
 | |
| // for block-based snapshotters (like devicemapper) against file-based snapshotters (like overlay, which need to
 | |
| // perform a copy-up of the full file any time a single bit is modified).
 | |
| func updateFile(name string) applierFn {
 | |
| 	return func(root string) error {
 | |
| 		path := filepath.Join(root, name)
 | |
| 		file, err := os.OpenFile(path, os.O_WRONLY, 0600)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to open %q: %w", path, err)
 | |
| 		}
 | |
| 
 | |
| 		info, err := file.Stat()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		var (
 | |
| 			offset = info.Size() / 2
 | |
| 			buf    = make([]byte, 4)
 | |
| 		)
 | |
| 
 | |
| 		if _, err := rand.Read(buf); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if _, err := file.WriteAt(buf, offset); err != nil {
 | |
| 			return fmt.Errorf("failed to write %q at offset %d: %w", path, offset, err)
 | |
| 		}
 | |
| 
 | |
| 		return file.Close()
 | |
| 	}
 | |
| }
 | 
