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 }