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  }