github.com/containerd/Containerd@v1.4.13/cmd/containerd-stress/main.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"net/http"
    24  	"os"
    25  	"os/signal"
    26  	"runtime"
    27  	"sync"
    28  	"syscall"
    29  	"time"
    30  
    31  	"github.com/containerd/containerd"
    32  	"github.com/containerd/containerd/namespaces"
    33  	"github.com/containerd/containerd/plugin"
    34  	metrics "github.com/docker/go-metrics"
    35  	"github.com/sirupsen/logrus"
    36  	"github.com/urfave/cli"
    37  )
    38  
    39  const imageName = "docker.io/library/alpine:latest"
    40  
    41  var (
    42  	ct              metrics.LabeledTimer
    43  	execTimer       metrics.LabeledTimer
    44  	errCounter      metrics.LabeledCounter
    45  	binarySizeGauge metrics.LabeledGauge
    46  )
    47  
    48  func init() {
    49  	ns := metrics.NewNamespace("stress", "", nil)
    50  	// if you want more fine grained metrics then you can drill down with the metrics in prom that
    51  	// containerd is outputting
    52  	ct = ns.NewLabeledTimer("run", "Run time of a full container during the test", "commit")
    53  	execTimer = ns.NewLabeledTimer("exec", "Run time of an exec process during the test", "commit")
    54  	binarySizeGauge = ns.NewLabeledGauge("binary_size", "Binary size of compiled binaries", metrics.Bytes, "name")
    55  	errCounter = ns.NewLabeledCounter("errors", "Errors encountered running the stress tests", "err")
    56  	metrics.Register(ns)
    57  
    58  	// set higher ulimits
    59  	if err := setRlimit(); err != nil {
    60  		panic(err)
    61  	}
    62  }
    63  
    64  type run struct {
    65  	total    int
    66  	failures int
    67  
    68  	started time.Time
    69  	ended   time.Time
    70  }
    71  
    72  func (r *run) start() {
    73  	r.started = time.Now()
    74  }
    75  
    76  func (r *run) end() {
    77  	r.ended = time.Now()
    78  }
    79  
    80  func (r *run) seconds() float64 {
    81  	return r.ended.Sub(r.started).Seconds()
    82  }
    83  
    84  func (r *run) gather(workers []*worker) *result {
    85  	for _, w := range workers {
    86  		r.total += w.count
    87  		r.failures += w.failures
    88  	}
    89  	sec := r.seconds()
    90  	return &result{
    91  		Total:               r.total,
    92  		Seconds:             sec,
    93  		ContainersPerSecond: float64(r.total) / sec,
    94  		SecondsPerContainer: sec / float64(r.total),
    95  	}
    96  }
    97  
    98  type result struct {
    99  	Total               int     `json:"total"`
   100  	Failures            int     `json:"failures"`
   101  	Seconds             float64 `json:"seconds"`
   102  	ContainersPerSecond float64 `json:"containersPerSecond"`
   103  	SecondsPerContainer float64 `json:"secondsPerContainer"`
   104  	ExecTotal           int     `json:"execTotal"`
   105  	ExecFailures        int     `json:"execFailures"`
   106  }
   107  
   108  func main() {
   109  	// morr power!
   110  	runtime.GOMAXPROCS(runtime.NumCPU())
   111  
   112  	app := cli.NewApp()
   113  	app.Name = "containerd-stress"
   114  	app.Description = "stress test a containerd daemon"
   115  	app.Flags = []cli.Flag{
   116  		cli.BoolFlag{
   117  			Name:  "debug",
   118  			Usage: "set debug output in the logs",
   119  		},
   120  		cli.StringFlag{
   121  			Name:  "address,a",
   122  			Value: "/run/containerd/containerd.sock",
   123  			Usage: "path to the containerd socket",
   124  		},
   125  		cli.IntFlag{
   126  			Name:  "concurrent,c",
   127  			Value: 1,
   128  			Usage: "set the concurrency of the stress test",
   129  		},
   130  		cli.DurationFlag{
   131  			Name:  "duration,d",
   132  			Value: 1 * time.Minute,
   133  			Usage: "set the duration of the stress test",
   134  		},
   135  		cli.BoolFlag{
   136  			Name:  "exec",
   137  			Usage: "add execs to the stress tests",
   138  		},
   139  		cli.BoolFlag{
   140  			Name:  "json,j",
   141  			Usage: "output results in json format",
   142  		},
   143  		cli.StringFlag{
   144  			Name:  "metrics,m",
   145  			Usage: "address to serve the metrics API",
   146  		},
   147  		cli.StringFlag{
   148  			Name:  "runtime",
   149  			Usage: "set the runtime to stress test",
   150  			Value: plugin.RuntimeLinuxV1,
   151  		},
   152  	}
   153  	app.Before = func(context *cli.Context) error {
   154  		if context.GlobalBool("json") {
   155  			logrus.SetLevel(logrus.WarnLevel)
   156  		}
   157  		if context.GlobalBool("debug") {
   158  			logrus.SetLevel(logrus.DebugLevel)
   159  		}
   160  		return nil
   161  	}
   162  	app.Commands = []cli.Command{
   163  		densityCommand,
   164  	}
   165  	app.Action = func(context *cli.Context) error {
   166  		config := config{
   167  			Address:     context.GlobalString("address"),
   168  			Duration:    context.GlobalDuration("duration"),
   169  			Concurrency: context.GlobalInt("concurrent"),
   170  			Exec:        context.GlobalBool("exec"),
   171  			JSON:        context.GlobalBool("json"),
   172  			Metrics:     context.GlobalString("metrics"),
   173  			Runtime:     context.GlobalString("runtime"),
   174  		}
   175  		if config.Metrics != "" {
   176  			return serve(config)
   177  		}
   178  		return test(config)
   179  	}
   180  	if err := app.Run(os.Args); err != nil {
   181  		fmt.Fprintln(os.Stderr, err)
   182  		os.Exit(1)
   183  	}
   184  }
   185  
   186  type config struct {
   187  	Concurrency int
   188  	Duration    time.Duration
   189  	Address     string
   190  	Exec        bool
   191  	JSON        bool
   192  	Metrics     string
   193  	Runtime     string
   194  }
   195  
   196  func (c config) newClient() (*containerd.Client, error) {
   197  	return containerd.New(c.Address, containerd.WithDefaultRuntime(c.Runtime))
   198  }
   199  
   200  func serve(c config) error {
   201  	go func() {
   202  		if err := http.ListenAndServe(c.Metrics, metrics.Handler()); err != nil {
   203  			logrus.WithError(err).Error("listen and serve")
   204  		}
   205  	}()
   206  	checkBinarySizes()
   207  	return test(c)
   208  }
   209  
   210  func test(c config) error {
   211  	var (
   212  		wg  sync.WaitGroup
   213  		ctx = namespaces.WithNamespace(context.Background(), "stress")
   214  	)
   215  
   216  	client, err := c.newClient()
   217  	if err != nil {
   218  		return err
   219  	}
   220  	defer client.Close()
   221  	if err := cleanup(ctx, client); err != nil {
   222  		return err
   223  	}
   224  	logrus.Infof("pulling %s", imageName)
   225  	image, err := client.Pull(ctx, imageName, containerd.WithPullUnpack)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	tctx, cancel := context.WithTimeout(ctx, c.Duration)
   230  	go func() {
   231  		s := make(chan os.Signal, 1)
   232  		signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
   233  		<-s
   234  		cancel()
   235  	}()
   236  
   237  	var (
   238  		workers []*worker
   239  		r       = &run{}
   240  	)
   241  	logrus.Info("starting stress test run...")
   242  	v, err := client.Version(ctx)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	// create the workers along with their spec
   247  	for i := 0; i < c.Concurrency; i++ {
   248  		wg.Add(1)
   249  		w := &worker{
   250  			id:     i,
   251  			wg:     &wg,
   252  			image:  image,
   253  			client: client,
   254  			commit: v.Revision,
   255  		}
   256  		workers = append(workers, w)
   257  	}
   258  	var exec *execWorker
   259  	if c.Exec {
   260  		for i := c.Concurrency; i < c.Concurrency+c.Concurrency; i++ {
   261  			wg.Add(1)
   262  			exec = &execWorker{
   263  				worker: worker{
   264  					id:     i,
   265  					wg:     &wg,
   266  					image:  image,
   267  					client: client,
   268  					commit: v.Revision,
   269  				},
   270  			}
   271  			go exec.exec(ctx, tctx)
   272  		}
   273  	}
   274  
   275  	// start the timer and run the worker
   276  	r.start()
   277  	for _, w := range workers {
   278  		go w.run(ctx, tctx)
   279  	}
   280  	// wait and end the timer
   281  	wg.Wait()
   282  	r.end()
   283  
   284  	results := r.gather(workers)
   285  	if c.Exec {
   286  		results.ExecTotal = exec.count
   287  		results.ExecFailures = exec.failures
   288  	}
   289  	logrus.Infof("ending test run in %0.3f seconds", results.Seconds)
   290  
   291  	logrus.WithField("failures", r.failures).Infof(
   292  		"create/start/delete %d containers in %0.3f seconds (%0.3f c/sec) or (%0.3f sec/c)",
   293  		results.Total,
   294  		results.Seconds,
   295  		results.ContainersPerSecond,
   296  		results.SecondsPerContainer,
   297  	)
   298  	if c.JSON {
   299  		if err := json.NewEncoder(os.Stdout).Encode(results); err != nil {
   300  			return err
   301  		}
   302  	}
   303  	return nil
   304  }
   305  
   306  // cleanup cleans up any containers in the "stress" namespace before the test run
   307  func cleanup(ctx context.Context, client *containerd.Client) error {
   308  	containers, err := client.Containers(ctx)
   309  	if err != nil {
   310  		return err
   311  	}
   312  	for _, c := range containers {
   313  		task, err := c.Task(ctx, nil)
   314  		if err == nil {
   315  			task.Delete(ctx, containerd.WithProcessKill)
   316  		}
   317  		if err := c.Delete(ctx, containerd.WithSnapshotCleanup); err != nil {
   318  			if derr := c.Delete(ctx); derr == nil {
   319  				continue
   320  			}
   321  			return err
   322  		}
   323  	}
   324  	return nil
   325  }