github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/workload/cli/run.go (about) 1 // Copyright 2015 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package cli 12 13 import ( 14 "context" 15 gosql "database/sql" 16 "encoding/json" 17 "fmt" 18 "net/http" 19 "os" 20 "os/signal" 21 "path/filepath" 22 "runtime" 23 "strconv" 24 "strings" 25 "sync" 26 "sync/atomic" 27 "time" 28 29 "github.com/cockroachdb/cockroach/pkg/util/envutil" 30 "github.com/cockroachdb/cockroach/pkg/util/log" 31 "github.com/cockroachdb/cockroach/pkg/util/log/logflags" 32 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 33 "github.com/cockroachdb/cockroach/pkg/workload" 34 "github.com/cockroachdb/cockroach/pkg/workload/histogram" 35 "github.com/cockroachdb/cockroach/pkg/workload/workloadsql" 36 "github.com/cockroachdb/errors" 37 "github.com/spf13/cobra" 38 "github.com/spf13/pflag" 39 "golang.org/x/time/rate" 40 ) 41 42 var runFlags = pflag.NewFlagSet(`run`, pflag.ContinueOnError) 43 var tolerateErrors = runFlags.Bool("tolerate-errors", false, "Keep running on error") 44 var maxRate = runFlags.Float64( 45 "max-rate", 0, "Maximum frequency of operations (reads/writes). If 0, no limit.") 46 var maxOps = runFlags.Uint64("max-ops", 0, "Maximum number of operations to run") 47 var duration = runFlags.Duration("duration", 0, 48 "The duration to run (in addition to --ramp). If 0, run forever.") 49 var doInit = runFlags.Bool("init", false, "Automatically run init. DEPRECATED: Use workload init instead.") 50 var ramp = runFlags.Duration("ramp", 0*time.Second, "The duration over which to ramp up load.") 51 52 var initFlags = pflag.NewFlagSet(`init`, pflag.ContinueOnError) 53 var drop = initFlags.Bool("drop", false, "Drop the existing database, if it exists") 54 55 var sharedFlags = pflag.NewFlagSet(`shared`, pflag.ContinueOnError) 56 var pprofport = sharedFlags.Int("pprofport", 33333, "Port for pprof endpoint.") 57 var dataLoader = sharedFlags.String("data-loader", `INSERT`, 58 "How to load initial table data. Options are INSERT and IMPORT") 59 60 var displayEvery = runFlags.Duration("display-every", time.Second, "How much time between every one-line activity reports.") 61 62 var displayFormat = runFlags.String("display-format", "simple", "Output display format (simple, incremental-json)") 63 64 var histograms = runFlags.String( 65 "histograms", "", 66 "File to write per-op incremental and cumulative histogram data.") 67 var histogramsMaxLatency = runFlags.Duration( 68 "histograms-max-latency", 100*time.Second, 69 "Expected maximum latency of running a query") 70 71 func init() { 72 AddSubCmd(func(userFacing bool) *cobra.Command { 73 var initCmd = SetCmdDefaults(&cobra.Command{ 74 Use: `init`, 75 Short: `set up tables for a workload`, 76 }) 77 for _, meta := range workload.Registered() { 78 gen := meta.New() 79 var genFlags *pflag.FlagSet 80 if f, ok := gen.(workload.Flagser); ok { 81 genFlags = f.Flags().FlagSet 82 } 83 84 genInitCmd := SetCmdDefaults(&cobra.Command{ 85 Use: meta.Name + " [pgurl...]", 86 Short: meta.Description, 87 Long: meta.Description + meta.Details, 88 Args: cobra.ArbitraryArgs, 89 }) 90 genInitCmd.Flags().AddFlagSet(initFlags) 91 genInitCmd.Flags().AddFlagSet(sharedFlags) 92 genInitCmd.Flags().AddFlagSet(genFlags) 93 genInitCmd.Run = CmdHelper(gen, runInit) 94 if userFacing && !meta.PublicFacing { 95 genInitCmd.Hidden = true 96 } 97 initCmd.AddCommand(genInitCmd) 98 } 99 return initCmd 100 }) 101 AddSubCmd(func(userFacing bool) *cobra.Command { 102 var runCmd = SetCmdDefaults(&cobra.Command{ 103 Use: `run`, 104 Short: `run a workload's operations against a cluster`, 105 }) 106 for _, meta := range workload.Registered() { 107 gen := meta.New() 108 if _, ok := gen.(workload.Opser); !ok { 109 // If Opser is not implemented, this would just fail at runtime, 110 // so omit it. 111 continue 112 } 113 114 var genFlags *pflag.FlagSet 115 if f, ok := gen.(workload.Flagser); ok { 116 genFlags = f.Flags().FlagSet 117 } 118 119 genRunCmd := SetCmdDefaults(&cobra.Command{ 120 Use: meta.Name + " [pgurl...]", 121 Short: meta.Description, 122 Long: meta.Description + meta.Details, 123 Args: cobra.ArbitraryArgs, 124 }) 125 genRunCmd.Flags().AddFlagSet(runFlags) 126 genRunCmd.Flags().AddFlagSet(sharedFlags) 127 genRunCmd.Flags().AddFlagSet(genFlags) 128 initFlags.VisitAll(func(initFlag *pflag.Flag) { 129 // Every init flag is a valid run flag that implies the --init option. 130 f := *initFlag 131 f.Usage += ` (implies --init)` 132 genRunCmd.Flags().AddFlag(&f) 133 }) 134 genRunCmd.Run = CmdHelper(gen, runRun) 135 if userFacing && !meta.PublicFacing { 136 genRunCmd.Hidden = true 137 } 138 runCmd.AddCommand(genRunCmd) 139 } 140 return runCmd 141 }) 142 } 143 144 // CmdHelper handles common workload command logic, such as error handling and 145 // ensuring the database name in the connection string (if provided) matches the 146 // expected one. 147 func CmdHelper( 148 gen workload.Generator, fn func(gen workload.Generator, urls []string, dbName string) error, 149 ) func(*cobra.Command, []string) { 150 const crdbDefaultURL = `postgres://root@localhost:26257?sslmode=disable` 151 152 return HandleErrs(func(cmd *cobra.Command, args []string) error { 153 if ls := cmd.Flags().Lookup(logflags.LogToStderrName); ls != nil { 154 if !ls.Changed { 155 // Unless the settings were overridden by the user, default to logging 156 // to stderr. 157 _ = ls.Value.Set(log.Severity_INFO.String()) 158 } 159 } 160 161 if h, ok := gen.(workload.Hookser); ok { 162 if h.Hooks().Validate != nil { 163 if err := h.Hooks().Validate(); err != nil { 164 return errors.Wrapf(err, "could not validate") 165 } 166 } 167 } 168 169 // HACK: Steal the dbOverride out of flags. This should go away 170 // once more of run.go moves inside workload. 171 var dbOverride string 172 if dbFlag := cmd.Flag(`db`); dbFlag != nil { 173 dbOverride = dbFlag.Value.String() 174 } 175 urls := args 176 if len(urls) == 0 { 177 urls = []string{crdbDefaultURL} 178 } 179 dbName, err := workload.SanitizeUrls(gen, dbOverride, urls) 180 if err != nil { 181 return err 182 } 183 return fn(gen, urls, dbName) 184 }) 185 } 186 187 // SetCmdDefaults ensures that the provided Cobra command will properly report 188 // an error if the user specifies an invalid subcommand. It is safe to call on 189 // any Cobra command. 190 // 191 // This is a wontfix bug in Cobra: https://github.com/spf13/cobra/pull/329 192 func SetCmdDefaults(cmd *cobra.Command) *cobra.Command { 193 if cmd.Run == nil && cmd.RunE == nil { 194 cmd.Run = func(cmd *cobra.Command, args []string) { 195 _ = cmd.Usage() 196 } 197 } 198 if cmd.Args == nil { 199 cmd.Args = cobra.NoArgs 200 } 201 return cmd 202 } 203 204 // numOps keeps a global count of successful operations. 205 var numOps uint64 206 207 // workerRun is an infinite loop in which the worker continuously attempts to 208 // read / write blocks of random data into a table in cockroach DB. The function 209 // returns only when the provided context is canceled. 210 func workerRun( 211 ctx context.Context, 212 errCh chan<- error, 213 wg *sync.WaitGroup, 214 limiter *rate.Limiter, 215 workFn func(context.Context) error, 216 ) { 217 if wg != nil { 218 defer wg.Done() 219 } 220 221 for { 222 if ctx.Err() != nil { 223 return 224 } 225 226 // Limit how quickly the load generator sends requests based on --max-rate. 227 if limiter != nil { 228 if err := limiter.Wait(ctx); err != nil { 229 return 230 } 231 } 232 233 if err := workFn(ctx); err != nil { 234 if ctx.Err() != nil && errors.Is(err, ctx.Err()) { 235 return 236 } 237 errCh <- err 238 continue 239 } 240 241 v := atomic.AddUint64(&numOps, 1) 242 if *maxOps > 0 && v >= *maxOps { 243 return 244 } 245 } 246 } 247 248 func runInit(gen workload.Generator, urls []string, dbName string) error { 249 ctx := context.Background() 250 251 initDB, err := gosql.Open(`cockroach`, strings.Join(urls, ` `)) 252 if err != nil { 253 return err 254 } 255 256 startPProfEndPoint(ctx) 257 return runInitImpl(ctx, gen, initDB, dbName) 258 } 259 260 func runInitImpl( 261 ctx context.Context, gen workload.Generator, initDB *gosql.DB, dbName string, 262 ) error { 263 if *drop { 264 if _, err := initDB.ExecContext(ctx, `DROP DATABASE IF EXISTS `+dbName); err != nil { 265 return err 266 } 267 } 268 if _, err := initDB.ExecContext(ctx, `CREATE DATABASE IF NOT EXISTS `+dbName); err != nil { 269 return err 270 } 271 272 var l workload.InitialDataLoader 273 switch strings.ToLower(*dataLoader) { 274 case `insert`, `inserts`: 275 l = workloadsql.InsertsDataLoader{ 276 // TODO(dan): Don't hardcode this. Similar to dbOverride, this should be 277 // hooked up to a flag directly once once more of run.go moves inside 278 // workload. 279 Concurrency: 16, 280 } 281 case `import`, `imports`: 282 l = workload.ImportDataLoader 283 default: 284 return errors.Errorf(`unknown data loader: %s`, *dataLoader) 285 } 286 287 _, err := workloadsql.Setup(ctx, initDB, gen, l) 288 return err 289 } 290 291 func startPProfEndPoint(ctx context.Context) { 292 b := envutil.EnvOrDefaultInt64("COCKROACH_BLOCK_PROFILE_RATE", 293 10000000 /* 1 sample per 10 milliseconds spent blocking */) 294 295 m := envutil.EnvOrDefaultInt("COCKROACH_MUTEX_PROFILE_RATE", 296 1000 /* 1 sample per 1000 mutex contention events */) 297 runtime.SetBlockProfileRate(int(b)) 298 runtime.SetMutexProfileFraction(m) 299 300 go func() { 301 err := http.ListenAndServe(":"+strconv.Itoa(*pprofport), nil) 302 if err != nil { 303 log.Errorf(ctx, "%v", err) 304 } 305 }() 306 } 307 308 func runRun(gen workload.Generator, urls []string, dbName string) error { 309 ctx := context.Background() 310 311 var formatter outputFormat 312 switch *displayFormat { 313 case "simple": 314 formatter = &textFormatter{} 315 case "incremental-json": 316 formatter = &jsonFormatter{w: os.Stdout} 317 default: 318 return errors.Errorf("unknown display format: %s", *displayFormat) 319 } 320 321 startPProfEndPoint(ctx) 322 initDB, err := gosql.Open(`cockroach`, strings.Join(urls, ` `)) 323 if err != nil { 324 return err 325 } 326 if *doInit || *drop { 327 log.Info(ctx, `DEPRECATION: `+ 328 `the --init flag on "workload run" will no longer be supported after 19.2`) 329 for { 330 err = runInitImpl(ctx, gen, initDB, dbName) 331 if err == nil { 332 break 333 } 334 if !*tolerateErrors { 335 return err 336 } 337 log.Infof(ctx, "retrying after error during init: %v", err) 338 } 339 } 340 341 var limiter *rate.Limiter 342 if *maxRate > 0 { 343 // Create a limiter using maxRate specified on the command line and 344 // with allowed burst of 1 at the maximum allowed rate. 345 limiter = rate.NewLimiter(rate.Limit(*maxRate), 1) 346 } 347 348 o, ok := gen.(workload.Opser) 349 if !ok { 350 return errors.Errorf(`no operations defined for %s`, gen.Meta().Name) 351 } 352 reg := histogram.NewRegistry(*histogramsMaxLatency) 353 var ops workload.QueryLoad 354 for { 355 ops, err = o.Ops(urls, reg) 356 if err == nil { 357 break 358 } 359 if !*tolerateErrors { 360 return err 361 } 362 log.Infof(ctx, "retrying after error while creating load: %v", err) 363 } 364 365 start := timeutil.Now() 366 errCh := make(chan error) 367 var rampDone chan struct{} 368 if *ramp > 0 { 369 // Create a channel to signal when the ramp period finishes. Will 370 // be reset to nil when consumed by the process loop below. 371 rampDone = make(chan struct{}) 372 } 373 374 workersCtx, cancelWorkers := context.WithCancel(ctx) 375 defer cancelWorkers() 376 var wg sync.WaitGroup 377 wg.Add(len(ops.WorkerFns)) 378 go func() { 379 // If a ramp period was specified, start all of the workers gradually 380 // with a new context. 381 var rampCtx context.Context 382 if rampDone != nil { 383 var cancel func() 384 rampCtx, cancel = context.WithTimeout(workersCtx, *ramp) 385 defer cancel() 386 } 387 388 for i, workFn := range ops.WorkerFns { 389 go func(i int, workFn func(context.Context) error) { 390 // If a ramp period was specified, start all of the workers 391 // gradually with a new context. 392 if rampCtx != nil { 393 rampPerWorker := *ramp / time.Duration(len(ops.WorkerFns)) 394 time.Sleep(time.Duration(i) * rampPerWorker) 395 workerRun(rampCtx, errCh, nil /* wg */, limiter, workFn) 396 } 397 398 // Start worker again, this time with the main context. 399 workerRun(workersCtx, errCh, &wg, limiter, workFn) 400 }(i, workFn) 401 } 402 403 if rampCtx != nil { 404 // Wait for the ramp period to finish, then notify the process loop 405 // below to reset timers and histograms. 406 <-rampCtx.Done() 407 close(rampDone) 408 } 409 }() 410 411 ticker := time.NewTicker(*displayEvery) 412 defer ticker.Stop() 413 done := make(chan os.Signal, 3) 414 signal.Notify(done, exitSignals...) 415 416 go func() { 417 wg.Wait() 418 done <- os.Interrupt 419 }() 420 421 if *duration > 0 { 422 go func() { 423 time.Sleep(*duration + *ramp) 424 done <- os.Interrupt 425 }() 426 } 427 428 var jsonEnc *json.Encoder 429 if *histograms != "" { 430 _ = os.MkdirAll(filepath.Dir(*histograms), 0755) 431 jsonF, err := os.Create(*histograms) 432 if err != nil { 433 return err 434 } 435 jsonEnc = json.NewEncoder(jsonF) 436 } 437 438 everySecond := log.Every(*displayEvery) 439 for { 440 select { 441 case err := <-errCh: 442 formatter.outputError(err) 443 if *tolerateErrors { 444 if everySecond.ShouldLog() { 445 log.Errorf(ctx, "%v", err) 446 } 447 continue 448 } 449 return err 450 451 case <-ticker.C: 452 startElapsed := timeutil.Since(start) 453 reg.Tick(func(t histogram.Tick) { 454 formatter.outputTick(startElapsed, t) 455 if jsonEnc != nil && rampDone == nil { 456 _ = jsonEnc.Encode(t.Snapshot()) 457 } 458 }) 459 460 // Once the load generator is fully ramped up, we reset the histogram 461 // and the start time to throw away the stats for the the ramp up period. 462 case <-rampDone: 463 rampDone = nil 464 start = timeutil.Now() 465 formatter.rampDone() 466 reg.Tick(func(t histogram.Tick) { 467 t.Cumulative.Reset() 468 t.Hist.Reset() 469 }) 470 471 case <-done: 472 cancelWorkers() 473 if ops.Close != nil { 474 ops.Close(ctx) 475 } 476 477 startElapsed := timeutil.Since(start) 478 resultTick := histogram.Tick{Name: ops.ResultHist} 479 reg.Tick(func(t histogram.Tick) { 480 formatter.outputTotal(startElapsed, t) 481 if jsonEnc != nil { 482 // Note that we're outputting the delta from the last tick. The 483 // cumulative histogram can be computed by merging all of the 484 // per-tick histograms. 485 _ = jsonEnc.Encode(t.Snapshot()) 486 } 487 if ops.ResultHist == `` || ops.ResultHist == t.Name { 488 if resultTick.Cumulative == nil { 489 resultTick.Now = t.Now 490 resultTick.Cumulative = t.Cumulative 491 } else { 492 resultTick.Cumulative.Merge(t.Cumulative) 493 } 494 } 495 }) 496 formatter.outputResult(startElapsed, resultTick) 497 498 if h, ok := gen.(workload.Hookser); ok { 499 if h.Hooks().PostRun != nil { 500 if err := h.Hooks().PostRun(startElapsed); err != nil { 501 fmt.Printf("failed post-run hook: %v\n", err) 502 } 503 } 504 } 505 506 return nil 507 } 508 } 509 }