github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/roachtest/tpcc.go (about)

     1  // Copyright 2018 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 main
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"io/ioutil"
    17  	"math"
    18  	"math/rand"
    19  	"os"
    20  	"path/filepath"
    21  	"runtime"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
    26  	"github.com/cockroachdb/cockroach/pkg/util/binfetcher"
    27  	"github.com/cockroachdb/cockroach/pkg/util/search"
    28  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    29  	"github.com/cockroachdb/cockroach/pkg/util/version"
    30  	"github.com/cockroachdb/cockroach/pkg/workload/histogram"
    31  	"github.com/cockroachdb/cockroach/pkg/workload/tpcc"
    32  	"github.com/cockroachdb/errors"
    33  	"github.com/cockroachdb/ttycolor"
    34  	"github.com/lib/pq"
    35  )
    36  
    37  type tpccOptions struct {
    38  	Warehouses int
    39  	Extra      string
    40  	Chaos      func() Chaos                // for late binding of stopper
    41  	During     func(context.Context) error // for running a function during the test
    42  	Duration   time.Duration
    43  
    44  	// The CockroachDB versions to deploy. The first one indicates the first node,
    45  	// etc. To use the main binary, specify "". When Versions is nil, it defaults
    46  	// to "" for all nodes. When it is specified, len(Versions) needs to match the
    47  	// number of CRDB nodes in the cluster.
    48  	//
    49  	// TODO(tbg): for better coverage at scale of the migration process, we
    50  	// should also be doing a rolling-restart into the new binary while the
    51  	// cluster is running, but that feels like jamming too much into the tpcc
    52  	// setup.
    53  	Versions []string
    54  }
    55  
    56  // tpccFixturesCmd generates the command string to load tpcc data for the
    57  // specified warehouse count into a cluster using either `fixtures import`
    58  // or `fixtures load` depending on the cloud.
    59  func tpccFixturesCmd(t *test, cloud string, warehouses int, extraArgs string) string {
    60  	var command string
    61  	switch cloud {
    62  	case gce:
    63  		// TODO(nvanbenschoten): We could switch to import for both clouds.
    64  		// At the moment, import is still a little unstable and load is still
    65  		// marginally faster.
    66  		command = "./workload fixtures load"
    67  		fixtureWarehouses := -1
    68  		for _, w := range []int{1, 10, 100, 1000, 2000, 5000, 10000} {
    69  			if w >= warehouses {
    70  				fixtureWarehouses = w
    71  				break
    72  			}
    73  		}
    74  		if fixtureWarehouses == -1 {
    75  			t.Fatalf("could not find fixture big enough for %d warehouses", warehouses)
    76  		}
    77  		warehouses = fixtureWarehouses
    78  	case aws, azure:
    79  		// For fixtures import, use the version built into the cockroach binary
    80  		// so the tpcc workload-versions match on release branches.
    81  		command = "./cockroach workload fixtures import"
    82  	default:
    83  		t.Fatalf("unknown cloud: %q", cloud)
    84  	}
    85  	return fmt.Sprintf("%s tpcc --warehouses=%d %s {pgurl:1}",
    86  		command, warehouses, extraArgs)
    87  }
    88  
    89  func setupTPCC(
    90  	ctx context.Context, t *test, c *cluster, warehouses int, versions []string,
    91  ) (crdbNodes, workloadNode nodeListOption) {
    92  	crdbNodes = c.Range(1, c.spec.NodeCount-1)
    93  	workloadNode = c.Node(c.spec.NodeCount)
    94  	if c.isLocal() {
    95  		warehouses = 1
    96  	}
    97  
    98  	if n := len(versions); n == 0 {
    99  		versions = make([]string, c.spec.NodeCount-1)
   100  	} else if n != c.spec.NodeCount-1 {
   101  		t.Fatalf("must specify Versions for all %d nodes: %v", c.spec.NodeCount-1, versions)
   102  	}
   103  
   104  	{
   105  		var regularNodes []option
   106  		for i, v := range versions {
   107  			if v == "" {
   108  				regularNodes = append(regularNodes, c.Node(i+1))
   109  			} else {
   110  				// NB: binfetcher caches the downloaded files.
   111  				binary, err := binfetcher.Download(ctx, binfetcher.Options{
   112  					Binary:  "cockroach",
   113  					Version: v,
   114  					GOOS:    ifLocal(runtime.GOOS, "linux"),
   115  					GOARCH:  "amd64",
   116  				})
   117  				if err != nil {
   118  					t.Fatalf("while fetching %s: %s", v, err)
   119  				}
   120  				c.Put(ctx, binary, "./cockroach", c.Node(i+1))
   121  			}
   122  		}
   123  		c.Put(ctx, cockroach, "./cockroach", regularNodes...)
   124  	}
   125  
   126  	// Fixture import needs ./cockroach workload on workloadNode.
   127  	c.Put(ctx, cockroach, "./cockroach", workloadNode)
   128  	c.Put(ctx, workload, "./workload", workloadNode)
   129  
   130  	func() {
   131  		db := c.Conn(ctx, 1)
   132  		defer db.Close()
   133  		c.Start(ctx, t, crdbNodes, startArgsDontEncrypt)
   134  		waitForFullReplication(t, c.Conn(ctx, crdbNodes[0]))
   135  		t.Status("loading fixture")
   136  		c.Run(ctx, workloadNode, tpccFixturesCmd(t, cloud, warehouses, ""))
   137  		t.Status("")
   138  	}()
   139  	return crdbNodes, workloadNode
   140  }
   141  
   142  func runTPCC(ctx context.Context, t *test, c *cluster, opts tpccOptions) {
   143  	rampDuration := 5 * time.Minute
   144  	if c.isLocal() {
   145  		opts.Warehouses = 1
   146  		opts.Duration = time.Minute
   147  		rampDuration = 30 * time.Second
   148  	}
   149  	crdbNodes, workloadNode := setupTPCC(ctx, t, c, opts.Warehouses, opts.Versions)
   150  	t.Status("waiting")
   151  	m := newMonitor(ctx, c, crdbNodes)
   152  	m.Go(func(ctx context.Context) error {
   153  		t.WorkerStatus("running tpcc")
   154  		cmd := fmt.Sprintf(
   155  			"./workload run tpcc --warehouses=%d --histograms="+perfArtifactsDir+"/stats.json "+
   156  				opts.Extra+" --ramp=%s --duration=%s {pgurl:1-%d}",
   157  			opts.Warehouses, rampDuration, opts.Duration, c.spec.NodeCount-1)
   158  		c.Run(ctx, workloadNode, cmd)
   159  		return nil
   160  	})
   161  	if opts.Chaos != nil {
   162  		chaos := opts.Chaos()
   163  		m.Go(chaos.Runner(c, m))
   164  	}
   165  	if opts.During != nil {
   166  		m.Go(opts.During)
   167  	}
   168  	m.Wait()
   169  
   170  	c.Run(ctx, workloadNode, fmt.Sprintf(
   171  		"./workload check tpcc --warehouses=%d {pgurl:1}", opts.Warehouses))
   172  }
   173  
   174  // tpccSupportedWarehouses returns our claim for the maximum number of tpcc
   175  // warehouses we support for a given hardware configuration.
   176  //
   177  // These should be added to periodically. Ideally when tpccbench finds major
   178  // performance movement, but at the least for every major release.
   179  var tpccSupportedWarehouses = []struct {
   180  	hardware   string
   181  	v          *version.Version
   182  	warehouses int
   183  }{
   184  	// We append "-0" to the version so that we capture all prereleases of the
   185  	// specified version. Otherwise, "v2.1.0" would compare greater than
   186  	// "v2.1.0-alpha.x".
   187  	{hardware: "gce-n4cpu16", v: version.MustParse(`v2.1.0-0`), warehouses: 1300},
   188  	{hardware: "gce-n4cpu16", v: version.MustParse(`v19.1.0-0`), warehouses: 1250},
   189  	{hardware: "aws-n4cpu16", v: version.MustParse(`v19.1.0-0`), warehouses: 2100},
   190  
   191  	// TODO(tbg): this number is copied from gce-n4cpu16. The real number should be a
   192  	// little higher, find out what it is.
   193  	{hardware: "gce-n5cpu16", v: version.MustParse(`v19.1.0-0`), warehouses: 1300},
   194  	// Ditto.
   195  	{hardware: "gce-n5cpu16", v: version.MustParse(`v2.1.0-0`), warehouses: 1300},
   196  }
   197  
   198  func maxSupportedTPCCWarehouses(buildVersion version.Version, cloud string, nodes clusterSpec) int {
   199  	var v *version.Version
   200  	var warehouses int
   201  	hardware := fmt.Sprintf(`%s-%s`, cloud, &nodes)
   202  	for _, x := range tpccSupportedWarehouses {
   203  		if x.hardware != hardware {
   204  			continue
   205  		}
   206  		if buildVersion.AtLeast(x.v) && (v == nil || buildVersion.AtLeast(v)) {
   207  			v = x.v
   208  			warehouses = x.warehouses
   209  		}
   210  	}
   211  	if v == nil {
   212  		panic(fmt.Sprintf(`could not find max tpcc warehouses for %s`, hardware))
   213  	}
   214  	return warehouses
   215  }
   216  
   217  func registerTPCC(r *testRegistry) {
   218  	headroomSpec := makeClusterSpec(4, cpu(16))
   219  	r.Add(testSpec{
   220  		// w=headroom runs tpcc for a semi-extended period with some amount of
   221  		// headroom, more closely mirroring a real production deployment than
   222  		// running with the max supported warehouses.
   223  		Name:  "tpcc/headroom/" + headroomSpec.String(),
   224  		Owner: OwnerKV,
   225  		// TODO(dan): Backfill tpccSupportedWarehouses and remove this "v2.1.0"
   226  		// minimum on gce.
   227  		MinVersion: maxVersion("v2.1.0", maybeMinVersionForFixturesImport(cloud)),
   228  		Tags:       []string{`default`, `release_qualification`},
   229  		Cluster:    headroomSpec,
   230  		Run: func(ctx context.Context, t *test, c *cluster) {
   231  			maxWarehouses := maxSupportedTPCCWarehouses(r.buildVersion, cloud, t.spec.Cluster)
   232  			headroomWarehouses := int(float64(maxWarehouses) * 0.7)
   233  			t.l.Printf("computed headroom warehouses of %d\n", headroomWarehouses)
   234  			runTPCC(ctx, t, c, tpccOptions{
   235  				Warehouses: headroomWarehouses,
   236  				Duration:   120 * time.Minute,
   237  			})
   238  		},
   239  	})
   240  	mixedHeadroomSpec := makeClusterSpec(5, cpu(16))
   241  
   242  	// TODO(tbg): rewrite and extend this using the harness in versionupgrade.go.
   243  	r.Add(testSpec{
   244  		// mixed-headroom is similar to w=headroom, but with an additional node
   245  		// and on a mixed version cluster. It simulates a real production
   246  		// deployment in the middle of the migration into a new cluster version.
   247  		Name:  "tpcc/mixed-headroom/" + mixedHeadroomSpec.String(),
   248  		Owner: OwnerKV,
   249  		// TODO(dan): Backfill tpccSupportedWarehouses and remove this "v2.1.0"
   250  		// minimum on gce.
   251  		MinVersion: maxVersion("v2.1.0", maybeMinVersionForFixturesImport(cloud)),
   252  		// TODO(tbg): add release_qualification tag once we know the test isn't
   253  		// buggy.
   254  		Tags:    []string{`default`},
   255  		Cluster: mixedHeadroomSpec,
   256  		Run: func(ctx context.Context, t *test, c *cluster) {
   257  			maxWarehouses := maxSupportedTPCCWarehouses(r.buildVersion, cloud, t.spec.Cluster)
   258  			headroomWarehouses := int(float64(maxWarehouses) * 0.7)
   259  			oldV, err := PredecessorVersion(r.buildVersion)
   260  			if err != nil {
   261  				t.Fatal(err)
   262  			}
   263  			// Make a git tag out of the version.
   264  			oldV = "v" + oldV
   265  			t.l.Printf("computed headroom warehouses of %d; running mixed with %s\n", headroomWarehouses, oldV)
   266  			runTPCC(ctx, t, c, tpccOptions{
   267  				Warehouses: headroomWarehouses,
   268  				Duration:   120 * time.Minute,
   269  				Versions:   []string{oldV, "", oldV, ""},
   270  			})
   271  			// TODO(tbg): run another TPCC with the final binaries here and
   272  			// teach TPCC to re-use the dataset (seems easy enough) to at least
   273  			// somewhat test the full migration at scale?
   274  		},
   275  	})
   276  	r.Add(testSpec{
   277  		Name:       "tpcc-nowait/nodes=3/w=1",
   278  		Owner:      OwnerKV,
   279  		MinVersion: maybeMinVersionForFixturesImport(cloud),
   280  		Cluster:    makeClusterSpec(4, cpu(16)),
   281  		Run: func(ctx context.Context, t *test, c *cluster) {
   282  			runTPCC(ctx, t, c, tpccOptions{
   283  				Warehouses: 1,
   284  				Duration:   10 * time.Minute,
   285  				Extra:      "--wait=false",
   286  			})
   287  		},
   288  	})
   289  	r.Add(testSpec{
   290  		Name:       "weekly/tpcc/headroom",
   291  		Owner:      OwnerKV,
   292  		MinVersion: maybeMinVersionForFixturesImport(cloud),
   293  		Tags:       []string{`weekly`},
   294  		Cluster:    makeClusterSpec(4, cpu(16)),
   295  		Timeout:    time.Duration(6*24)*time.Hour + time.Duration(10)*time.Minute,
   296  		Run: func(ctx context.Context, t *test, c *cluster) {
   297  			warehouses := 1000
   298  			runTPCC(ctx, t, c, tpccOptions{
   299  				Warehouses: warehouses,
   300  				Duration:   6 * 24 * time.Hour,
   301  			})
   302  		},
   303  	})
   304  
   305  	r.Add(testSpec{
   306  		Name:       "tpcc/w=100/nodes=3/chaos=true",
   307  		Owner:      OwnerKV,
   308  		Cluster:    makeClusterSpec(4),
   309  		MinVersion: maybeMinVersionForFixturesImport(cloud),
   310  		Run: func(ctx context.Context, t *test, c *cluster) {
   311  			duration := 30 * time.Minute
   312  			runTPCC(ctx, t, c, tpccOptions{
   313  				Warehouses: 100,
   314  				Duration:   duration,
   315  				Extra:      "--wait=false --tolerate-errors",
   316  				Chaos: func() Chaos {
   317  					return Chaos{
   318  						Timer: Periodic{
   319  							Period:   45 * time.Second,
   320  							DownTime: 10 * time.Second,
   321  						},
   322  						Target:       func() nodeListOption { return c.Node(1 + rand.Intn(c.spec.NodeCount-1)) },
   323  						Stopper:      time.After(duration),
   324  						DrainAndQuit: false,
   325  					}
   326  				},
   327  			})
   328  		},
   329  	})
   330  
   331  	// Run a few representative tpccbench specs in CI.
   332  	registerTPCCBenchSpec(r, tpccBenchSpec{
   333  		Nodes: 3,
   334  		CPUs:  4,
   335  
   336  		LoadWarehouses: 1000,
   337  		EstimatedMax:   gceOrAws(cloud, 400, 600),
   338  	})
   339  	registerTPCCBenchSpec(r, tpccBenchSpec{
   340  		Nodes: 3,
   341  		CPUs:  16,
   342  
   343  		LoadWarehouses: gceOrAws(cloud, 2000, 3000),
   344  		EstimatedMax:   gceOrAws(cloud, 1600, 2500),
   345  	})
   346  	registerTPCCBenchSpec(r, tpccBenchSpec{
   347  		Nodes: 12,
   348  		CPUs:  16,
   349  
   350  		LoadWarehouses: gceOrAws(cloud, 8000, 10000),
   351  		EstimatedMax:   gceOrAws(cloud, 7000, 8000),
   352  
   353  		Tags: []string{`weekly`},
   354  	})
   355  	registerTPCCBenchSpec(r, tpccBenchSpec{
   356  		Nodes:        6,
   357  		CPUs:         16,
   358  		Distribution: multiZone,
   359  
   360  		LoadWarehouses: 5000,
   361  		EstimatedMax:   2500,
   362  	})
   363  	registerTPCCBenchSpec(r, tpccBenchSpec{
   364  		Nodes:        9,
   365  		CPUs:         4,
   366  		Distribution: multiRegion,
   367  		LoadConfig:   multiLoadgen,
   368  
   369  		LoadWarehouses: 5000,
   370  		EstimatedMax:   3000,
   371  
   372  		MinVersion: "v19.1.0",
   373  	})
   374  	registerTPCCBenchSpec(r, tpccBenchSpec{
   375  		Nodes:      9,
   376  		CPUs:       4,
   377  		Chaos:      true,
   378  		LoadConfig: singlePartitionedLoadgen,
   379  
   380  		LoadWarehouses: 2000,
   381  		EstimatedMax:   900,
   382  
   383  		MinVersion: "v19.1.0",
   384  	})
   385  }
   386  
   387  func maxVersion(vers ...string) string {
   388  	var max *version.Version
   389  	for _, v := range vers {
   390  		v, err := version.Parse(v)
   391  		if err != nil {
   392  			continue
   393  		}
   394  		if max == nil || v.AtLeast(max) {
   395  			max = v
   396  		}
   397  	}
   398  	if max == nil {
   399  		return ""
   400  	}
   401  	return max.String()
   402  }
   403  
   404  func gceOrAws(cloud string, gce, aws int) int {
   405  	if cloud == "aws" {
   406  		return aws
   407  	}
   408  	return gce
   409  }
   410  
   411  func maybeMinVersionForFixturesImport(cloud string) string {
   412  	const minVersionForFixturesImport = "v19.1.0"
   413  	if cloud == "aws" {
   414  		return minVersionForFixturesImport
   415  	}
   416  	return ""
   417  }
   418  
   419  // tpccBenchDistribution represents a distribution of nodes in a tpccbench
   420  // cluster.
   421  type tpccBenchDistribution int
   422  
   423  const (
   424  	// All nodes are within the same zone.
   425  	singleZone tpccBenchDistribution = iota
   426  	// Nodes are distributed across 3 zones, all in the same region.
   427  	multiZone
   428  	// Nodes are distributed across 3 regions.
   429  	multiRegion
   430  )
   431  
   432  func (d tpccBenchDistribution) zones() []string {
   433  	switch d {
   434  	case singleZone:
   435  		return []string{"us-central1-b"}
   436  	case multiZone:
   437  		return []string{"us-central1-a", "us-central1-b", "us-central1-c"}
   438  	case multiRegion:
   439  		return []string{"us-east1-b", "us-west1-b", "europe-west2-b"}
   440  	default:
   441  		panic("unexpected")
   442  	}
   443  }
   444  
   445  // tpccBenchLoadConfig represents configurations of load generators in a
   446  // tpccbench spec.
   447  type tpccBenchLoadConfig int
   448  
   449  const (
   450  	// A single load generator is run.
   451  	singleLoadgen tpccBenchLoadConfig = iota
   452  	// A single load generator is run with partitioning enabled.
   453  	singlePartitionedLoadgen
   454  	// A load generator is run in each zone.
   455  	multiLoadgen
   456  )
   457  
   458  // numLoadNodes returns the number of load generator nodes that the load
   459  // configuration requires for the given node distribution.
   460  func (l tpccBenchLoadConfig) numLoadNodes(d tpccBenchDistribution) int {
   461  	switch l {
   462  	case singleLoadgen:
   463  		return 1
   464  	case singlePartitionedLoadgen:
   465  		return 1
   466  	case multiLoadgen:
   467  		return len(d.zones())
   468  	default:
   469  		panic("unexpected")
   470  	}
   471  }
   472  
   473  type tpccBenchSpec struct {
   474  	Nodes        int
   475  	CPUs         int
   476  	Chaos        bool
   477  	Distribution tpccBenchDistribution
   478  	LoadConfig   tpccBenchLoadConfig
   479  
   480  	// The number of warehouses to load into the cluster before beginning
   481  	// benchmarking. Should be larger than EstimatedMax and should be a
   482  	// value that is unlikely to be achievable.
   483  	LoadWarehouses int
   484  	// An estimate of the maximum number of warehouses achievable in the
   485  	// cluster config. The closer this is to the actual max achievable
   486  	// warehouse count, the faster the benchmark will be in producing a
   487  	// result. This can be adjusted over time as performance characteristics
   488  	// change (i.e. CockroachDB gets faster!).
   489  	EstimatedMax int
   490  
   491  	// MinVersion to pass to testRegistry.Add.
   492  	MinVersion string
   493  	// Tags to pass to testRegistry.Add.
   494  	Tags []string
   495  }
   496  
   497  // partitions returns the number of partitions specified to the load generator.
   498  func (s tpccBenchSpec) partitions() int {
   499  	switch s.LoadConfig {
   500  	case singleLoadgen:
   501  		return 0
   502  	case singlePartitionedLoadgen:
   503  		return s.Nodes / 3
   504  	case multiLoadgen:
   505  		return len(s.Distribution.zones())
   506  	default:
   507  		panic("unexpected")
   508  	}
   509  }
   510  
   511  // startOpts returns any extra start options that the spec requires.
   512  func (s tpccBenchSpec) startOpts() []option {
   513  	opts := []option{startArgsDontEncrypt}
   514  	if s.LoadConfig == singlePartitionedLoadgen {
   515  		opts = append(opts, racks(s.partitions()))
   516  	}
   517  	return opts
   518  }
   519  
   520  func registerTPCCBenchSpec(r *testRegistry, b tpccBenchSpec) {
   521  	nameParts := []string{
   522  		"tpccbench",
   523  		fmt.Sprintf("nodes=%d", b.Nodes),
   524  		fmt.Sprintf("cpu=%d", b.CPUs),
   525  	}
   526  	if b.Chaos {
   527  		nameParts = append(nameParts, "chaos")
   528  	}
   529  
   530  	opts := []createOption{cpu(b.CPUs)}
   531  	switch b.Distribution {
   532  	case singleZone:
   533  		// No specifier.
   534  	case multiZone:
   535  		nameParts = append(nameParts, "multi-az")
   536  		opts = append(opts, geo(), zones(strings.Join(b.Distribution.zones(), ",")))
   537  	case multiRegion:
   538  		nameParts = append(nameParts, "multi-region")
   539  		opts = append(opts, geo(), zones(strings.Join(b.Distribution.zones(), ",")))
   540  	default:
   541  		panic("unexpected")
   542  	}
   543  
   544  	switch b.LoadConfig {
   545  	case singleLoadgen:
   546  		// No specifier.
   547  	case singlePartitionedLoadgen:
   548  		nameParts = append(nameParts, "partition")
   549  	case multiLoadgen:
   550  		// No specifier.
   551  	default:
   552  		panic("unexpected")
   553  	}
   554  
   555  	name := strings.Join(nameParts, "/")
   556  
   557  	numNodes := b.Nodes + b.LoadConfig.numLoadNodes(b.Distribution)
   558  	nodes := makeClusterSpec(numNodes, opts...)
   559  
   560  	minVersion := b.MinVersion
   561  	if minVersion == "" {
   562  		minVersion = maybeMinVersionForFixturesImport(cloud)
   563  	}
   564  
   565  	r.Add(testSpec{
   566  		Name:       name,
   567  		Owner:      OwnerKV,
   568  		Cluster:    nodes,
   569  		MinVersion: minVersion,
   570  		Tags:       b.Tags,
   571  		Run: func(ctx context.Context, t *test, c *cluster) {
   572  			runTPCCBench(ctx, t, c, b)
   573  		},
   574  	})
   575  }
   576  
   577  // loadTPCCBench loads a TPCC dataset for the specific benchmark spec. The
   578  // function is idempotent and first checks whether a compatible dataset exists,
   579  // performing an expensive dataset restore only if it doesn't.
   580  func loadTPCCBench(
   581  	ctx context.Context, t *test, c *cluster, b tpccBenchSpec, roachNodes, loadNode nodeListOption,
   582  ) error {
   583  	db := c.Conn(ctx, 1)
   584  	defer db.Close()
   585  
   586  	// Check if the dataset already exists and is already large enough to
   587  	// accommodate this benchmarking. If so, we can skip the fixture RESTORE.
   588  	if _, err := db.ExecContext(ctx, `USE tpcc`); err == nil {
   589  		t.l.Printf("found existing tpcc database\n")
   590  
   591  		var curWarehouses int
   592  		if err := db.QueryRowContext(ctx,
   593  			`SELECT count(*) FROM tpcc.warehouse`,
   594  		).Scan(&curWarehouses); err != nil {
   595  			return err
   596  		}
   597  		if curWarehouses >= b.LoadWarehouses {
   598  			// The cluster has enough warehouses. Nothing to do.
   599  			return nil
   600  		}
   601  
   602  		// If the dataset exists but is not large enough, wipe the cluster
   603  		// before restoring.
   604  		c.Wipe(ctx, roachNodes)
   605  		c.Start(ctx, t, append(b.startOpts(), roachNodes)...)
   606  	} else if pqErr := (*pq.Error)(nil); !(errors.As(err, &pqErr) &&
   607  		string(pqErr.Code) == pgcode.InvalidCatalogName) {
   608  		return err
   609  	}
   610  
   611  	// Increase job leniency to prevent restarts due to node liveness.
   612  	if _, err := db.Exec(`
   613  		SET CLUSTER SETTING jobs.registry.leniency = '5m';
   614  	`); err != nil {
   615  		t.Fatal(err)
   616  	}
   617  
   618  	var loadArgs string
   619  	var rebalanceWait time.Duration
   620  	switch b.LoadConfig {
   621  	case singleLoadgen:
   622  		loadArgs = `--scatter --checks=false`
   623  		rebalanceWait = time.Duration(b.LoadWarehouses/250) * time.Minute
   624  	case singlePartitionedLoadgen:
   625  		loadArgs = fmt.Sprintf(`--scatter --checks=false --partitions=%d`, b.partitions())
   626  		rebalanceWait = time.Duration(b.LoadWarehouses/125) * time.Minute
   627  	case multiLoadgen:
   628  		loadArgs = fmt.Sprintf(`--scatter --checks=false --partitions=%d --zones="%s"`,
   629  			b.partitions(), strings.Join(b.Distribution.zones(), ","))
   630  		rebalanceWait = time.Duration(b.LoadWarehouses/50) * time.Minute
   631  	default:
   632  		panic("unexpected")
   633  	}
   634  
   635  	// Load the corresponding fixture.
   636  	t.l.Printf("restoring tpcc fixture\n")
   637  	waitForFullReplication(t, db)
   638  	cmd := tpccFixturesCmd(t, cloud, b.LoadWarehouses, loadArgs)
   639  	if err := c.RunE(ctx, loadNode, cmd); err != nil {
   640  		return err
   641  	}
   642  	if rebalanceWait == 0 || len(roachNodes) <= 3 {
   643  		return nil
   644  	}
   645  
   646  	t.l.Printf("waiting %v for rebalancing\n", rebalanceWait)
   647  	_, err := db.ExecContext(ctx, `SET CLUSTER SETTING kv.snapshot_rebalance.max_rate='128MiB'`)
   648  	if err != nil {
   649  		return err
   650  	}
   651  
   652  	// Split and scatter the tables. Ramp up to the expected load in the desired
   653  	// distribution. This should allow for load-based rebalancing to help
   654  	// distribute load. Optionally pass some load configuration-specific flags.
   655  	cmd = fmt.Sprintf("./workload run tpcc --warehouses=%d --workers=%d --max-rate=%d "+
   656  		"--wait=false --duration=%s --scatter --tolerate-errors {pgurl%s}",
   657  		b.LoadWarehouses, b.LoadWarehouses, b.LoadWarehouses/2, rebalanceWait, roachNodes)
   658  	if out, err := c.RunWithBuffer(ctx, c.l, loadNode, cmd); err != nil {
   659  		return errors.Wrapf(err, "failed with output %q", string(out))
   660  	}
   661  
   662  	_, err = db.ExecContext(ctx, `SET CLUSTER SETTING kv.snapshot_rebalance.max_rate='2MiB'`)
   663  	return err
   664  }
   665  
   666  // tpccbench is a suite of benchmarking tools that run TPC-C against CockroachDB
   667  // clusters in different configurations. The tools search for the maximum number
   668  // of warehouses that a load generator can run TPC-C against while still
   669  // maintaining a minimum acceptable throughput. This maximum warehouse value is
   670  // directly comparable to other runs of the tool in the same cluster config, and
   671  // expresses how well CockroachDB performance scales.
   672  //
   673  // In order to run a benchmark spec, the tool must first load a TPC-C dataset
   674  // large enough to accommodate it. This can take a while, so it is recommended
   675  // to use a combination of `--cluster=<cluster>` and `--wipe=false` flags to
   676  // limit the loading phase to the first run of the tool. Subsequent runs will be
   677  // able to avoid the dataset restore as long as they are not wiped. This allows
   678  // for quick iteration on experimental changes.
   679  //
   680  // It can also be useful to omit the `--cluster` flag during the first run of
   681  // the tool to allow roachtest to create the correct set of VMs required by the
   682  // test. The `--wipe` flag will prevent this cluster from being destroyed, so it
   683  // can then be used during future runs.
   684  func runTPCCBench(ctx context.Context, t *test, c *cluster, b tpccBenchSpec) {
   685  	// Determine the nodes in each load group. A load group consists of a set of
   686  	// Cockroach nodes and a single load generator.
   687  	numLoadGroups := b.LoadConfig.numLoadNodes(b.Distribution)
   688  	numZones := len(b.Distribution.zones())
   689  	loadGroups := makeLoadGroups(c, numZones, b.Nodes, numLoadGroups)
   690  	roachNodes := loadGroups.roachNodes()
   691  	loadNodes := loadGroups.loadNodes()
   692  	c.Put(ctx, cockroach, "./cockroach", roachNodes)
   693  	// Fixture import needs ./cockroach workload on loadNodes[0],
   694  	// and if we use haproxy (see below) we need it on the others
   695  	// as well.
   696  	c.Put(ctx, cockroach, "./cockroach", loadNodes)
   697  	c.Put(ctx, workload, "./workload", loadNodes)
   698  	c.Start(ctx, t, append(b.startOpts(), roachNodes)...)
   699  
   700  	useHAProxy := b.Chaos
   701  	const restartWait = 15 * time.Second
   702  	{
   703  		// Wait after restarting nodes before applying load. This lets
   704  		// things settle down to avoid unexpected cluster states.
   705  		time.Sleep(restartWait)
   706  		if useHAProxy {
   707  			if len(loadNodes) > 1 {
   708  				t.Fatal("distributed chaos benchmarking not supported")
   709  			}
   710  			t.Status("installing haproxy")
   711  			if err := c.Install(ctx, t.l, loadNodes, "haproxy"); err != nil {
   712  				t.Fatal(err)
   713  			}
   714  			c.Run(ctx, loadNodes, "./cockroach gen haproxy --insecure --url {pgurl:1}")
   715  			// Increase the maximum connection limit to ensure that no TPC-C
   716  			// load gen workers get stuck during connection initialization.
   717  			// 10k warehouses requires at least 20,000 connections, so add a
   718  			// bit of breathing room and check the warehouse count.
   719  			c.Run(ctx, loadNodes, "sed -i 's/maxconn [0-9]\\+/maxconn 21000/' haproxy.cfg")
   720  			if b.LoadWarehouses > 1e4 {
   721  				t.Fatal("HAProxy config supports up to 10k warehouses")
   722  			}
   723  			c.Run(ctx, loadNodes, "haproxy -f haproxy.cfg -D")
   724  		}
   725  
   726  		m := newMonitor(ctx, c, roachNodes)
   727  		m.Go(func(ctx context.Context) error {
   728  			t.Status("setting up dataset")
   729  			return loadTPCCBench(ctx, t, c, b, roachNodes, c.Node(loadNodes[0]))
   730  		})
   731  		m.Wait()
   732  	}
   733  
   734  	// Search between 1 and b.LoadWarehouses for the largest number of
   735  	// warehouses that can be operated on while sustaining a throughput
   736  	// threshold, set to a fraction of max tpmC.
   737  	precision := int(math.Max(1.0, float64(b.LoadWarehouses/200)))
   738  	initStepSize := precision
   739  
   740  	// Create a temp directory to store the local copy of results from the
   741  	// workloads.
   742  	resultsDir, err := ioutil.TempDir("", "roachtest-tpcc")
   743  	if err != nil {
   744  		t.Fatal(errors.Wrap(err, "failed to create temp dir"))
   745  	}
   746  	defer func() { _ = os.RemoveAll(resultsDir) }()
   747  	s := search.NewLineSearcher(1, b.LoadWarehouses, b.EstimatedMax, initStepSize, precision)
   748  	if res, err := s.Search(func(warehouses int) (bool, error) {
   749  		m := newMonitor(ctx, c, roachNodes)
   750  		// Restart the cluster before each iteration to help eliminate
   751  		// inter-trial interactions.
   752  		m.ExpectDeaths(int32(len(roachNodes)))
   753  		c.Stop(ctx, roachNodes)
   754  		c.Start(ctx, t, append(b.startOpts(), roachNodes)...)
   755  		time.Sleep(restartWait)
   756  
   757  		// Set up the load generation configuration.
   758  		rampDur := 5 * time.Minute
   759  		loadDur := 10 * time.Minute
   760  		loadDone := make(chan time.Time, numLoadGroups)
   761  
   762  		// If we're running chaos in this configuration, modify this config.
   763  		if b.Chaos {
   764  			// Increase the load generation duration.
   765  			loadDur = 10 * time.Minute
   766  
   767  			// Kill one node at a time.
   768  			ch := Chaos{
   769  				Timer:   Periodic{Period: 90 * time.Second, DownTime: 5 * time.Second},
   770  				Target:  roachNodes.randNode,
   771  				Stopper: loadDone,
   772  			}
   773  			m.Go(ch.Runner(c, m))
   774  		}
   775  		if b.Distribution == multiRegion {
   776  			rampDur = 3 * time.Minute
   777  			loadDur = 15 * time.Minute
   778  		}
   779  
   780  		// If we're running multiple load generators, run them in parallel and then
   781  		// aggregate resultChan. In order to process the results we need to copy
   782  		// over the histograms. Create a temp dir which will contain the fetched
   783  		// data.
   784  		resultChan := make(chan *tpcc.Result, numLoadGroups)
   785  		for groupIdx, group := range loadGroups {
   786  			// Copy for goroutine
   787  			groupIdx := groupIdx
   788  			group := group
   789  			m.Go(func(ctx context.Context) error {
   790  				sqlGateways := group.roachNodes
   791  				if useHAProxy {
   792  					sqlGateways = group.loadNodes
   793  				}
   794  
   795  				extraFlags := ""
   796  				activeWarehouses := warehouses
   797  				switch b.LoadConfig {
   798  				case singleLoadgen:
   799  					// Nothing.
   800  				case singlePartitionedLoadgen:
   801  					extraFlags = fmt.Sprintf(` --partitions=%d`, b.partitions())
   802  				case multiLoadgen:
   803  					extraFlags = fmt.Sprintf(` --partitions=%d --partition-affinity=%d`,
   804  						b.partitions(), groupIdx)
   805  					activeWarehouses = warehouses / numLoadGroups
   806  				default:
   807  					panic("unexpected")
   808  				}
   809  
   810  				t.Status(fmt.Sprintf("running benchmark, warehouses=%d", warehouses))
   811  				histogramsPath := fmt.Sprintf("%s/warehouses=%d/stats.json", perfArtifactsDir, activeWarehouses)
   812  				cmd := fmt.Sprintf("./workload run tpcc --warehouses=%d --active-warehouses=%d "+
   813  					"--tolerate-errors --scatter --ramp=%s --duration=%s%s {pgurl%s} "+
   814  					"--histograms=%s",
   815  					b.LoadWarehouses, activeWarehouses, rampDur,
   816  					loadDur, extraFlags, sqlGateways, histogramsPath)
   817  				err := c.RunE(ctx, group.loadNodes, cmd)
   818  				loadDone <- timeutil.Now()
   819  				if err != nil {
   820  					return errors.Wrapf(err, "error running tpcc load generator")
   821  				}
   822  				roachtestHistogramsPath := filepath.Join(resultsDir, fmt.Sprintf("%d.%d-stats.json", warehouses, groupIdx))
   823  				if err := c.Get(
   824  					ctx, t.l, histogramsPath, roachtestHistogramsPath, group.loadNodes,
   825  				); err != nil {
   826  					t.Fatal(err)
   827  				}
   828  				snapshots, err := histogram.DecodeSnapshots(roachtestHistogramsPath)
   829  				if err != nil {
   830  					return errors.Wrapf(err, "failed to decode histogram snapshots")
   831  				}
   832  				result := tpcc.NewResultWithSnapshots(activeWarehouses, 0, snapshots)
   833  				resultChan <- result
   834  				return nil
   835  			})
   836  		}
   837  		if err = m.WaitE(); err != nil {
   838  			return false, err
   839  		}
   840  		close(resultChan)
   841  		var results []*tpcc.Result
   842  		for partial := range resultChan {
   843  			results = append(results, partial)
   844  		}
   845  		res := tpcc.MergeResults(results...)
   846  		failErr := res.FailureError()
   847  		// Print the result.
   848  		if failErr == nil {
   849  			ttycolor.Stdout(ttycolor.Green)
   850  			t.l.Printf("--- PASS: tpcc %d resulted in %.1f tpmC (%.1f%% of max tpmC)\n\n",
   851  				warehouses, res.TpmC(), res.Efficiency())
   852  		} else {
   853  			ttycolor.Stdout(ttycolor.Red)
   854  			t.l.Printf("--- FAIL: tpcc %d resulted in %.1f tpmC and failed due to %v",
   855  				warehouses, res.TpmC(), failErr)
   856  		}
   857  		ttycolor.Stdout(ttycolor.Reset)
   858  		return failErr == nil, nil
   859  	}); err != nil {
   860  		t.Fatal(err)
   861  	} else {
   862  		ttycolor.Stdout(ttycolor.Green)
   863  		t.l.Printf("------\nMAX WAREHOUSES = %d\n------\n\n", res)
   864  		ttycolor.Stdout(ttycolor.Reset)
   865  	}
   866  }
   867  
   868  func registerTPCCBench(r *testRegistry) {
   869  	specs := []tpccBenchSpec{
   870  		{
   871  			Nodes: 3,
   872  			CPUs:  4,
   873  
   874  			LoadWarehouses: 1000,
   875  			EstimatedMax:   325,
   876  		},
   877  		{
   878  			Nodes: 3,
   879  			CPUs:  16,
   880  
   881  			LoadWarehouses: 2000,
   882  			EstimatedMax:   1300,
   883  		},
   884  		// objective 1, key result 1.
   885  		{
   886  			Nodes: 30,
   887  			CPUs:  16,
   888  
   889  			LoadWarehouses: 10000,
   890  			EstimatedMax:   5300,
   891  		},
   892  		// objective 1, key result 2.
   893  		{
   894  			Nodes:      18,
   895  			CPUs:       16,
   896  			LoadConfig: singlePartitionedLoadgen,
   897  
   898  			LoadWarehouses: 10000,
   899  			EstimatedMax:   8000,
   900  		},
   901  		// objective 2, key result 1.
   902  		{
   903  			Nodes: 7,
   904  			CPUs:  16,
   905  			Chaos: true,
   906  
   907  			LoadWarehouses: 5000,
   908  			EstimatedMax:   2000,
   909  		},
   910  		// objective 3, key result 1.
   911  		{
   912  			Nodes:        3,
   913  			CPUs:         16,
   914  			Distribution: multiZone,
   915  
   916  			LoadWarehouses: 2000,
   917  			EstimatedMax:   1000,
   918  		},
   919  		// objective 3, key result 2.
   920  		{
   921  			Nodes:        9,
   922  			CPUs:         16,
   923  			Distribution: multiRegion,
   924  			LoadConfig:   multiLoadgen,
   925  
   926  			LoadWarehouses: 12000,
   927  			EstimatedMax:   8000,
   928  
   929  			MinVersion: "v19.1.0",
   930  		},
   931  		// objective 4, key result 2.
   932  		{
   933  			Nodes: 64,
   934  			CPUs:  16,
   935  
   936  			LoadWarehouses: 50000,
   937  			EstimatedMax:   40000,
   938  		},
   939  
   940  		// See https://github.com/cockroachdb/cockroach/issues/31409 for the next three specs.
   941  		{
   942  			Nodes: 6,
   943  			CPUs:  16,
   944  
   945  			LoadWarehouses: 5000,
   946  			EstimatedMax:   3000,
   947  			LoadConfig:     singlePartitionedLoadgen,
   948  		},
   949  		{
   950  			Nodes: 12,
   951  			CPUs:  16,
   952  
   953  			LoadWarehouses: 10000,
   954  			EstimatedMax:   6000,
   955  			LoadConfig:     singlePartitionedLoadgen,
   956  		},
   957  		{
   958  			Nodes: 24,
   959  			CPUs:  16,
   960  
   961  			LoadWarehouses: 20000,
   962  			EstimatedMax:   12000,
   963  			LoadConfig:     singlePartitionedLoadgen,
   964  		},
   965  
   966  		// Requested by @awoods87.
   967  		{
   968  			Nodes: 11,
   969  			CPUs:  32,
   970  
   971  			LoadWarehouses: 10000,
   972  			EstimatedMax:   8000,
   973  		},
   974  	}
   975  
   976  	for _, b := range specs {
   977  		registerTPCCBenchSpec(r, b)
   978  	}
   979  }