github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/cmd/pebble/replay.go (about)

     1  // Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"path/filepath"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"syscall"
    19  	"time"
    20  	"unicode"
    21  
    22  	"github.com/cockroachdb/errors"
    23  	"github.com/cockroachdb/pebble"
    24  	"github.com/cockroachdb/pebble/bloom"
    25  	"github.com/cockroachdb/pebble/internal/base"
    26  	"github.com/cockroachdb/pebble/internal/cache"
    27  	"github.com/cockroachdb/pebble/replay"
    28  	"github.com/cockroachdb/pebble/vfs"
    29  	"github.com/spf13/cobra"
    30  )
    31  
    32  func initReplayCmd() *cobra.Command {
    33  	c := replayConfig{
    34  		pacer:            pacerFlag{Pacer: replay.PaceByFixedReadAmp(10)},
    35  		runDir:           "",
    36  		count:            1,
    37  		streamLogs:       false,
    38  		ignoreCheckpoint: false,
    39  	}
    40  	cmd := &cobra.Command{
    41  		Use:   "replay <workload>",
    42  		Short: "run the provided captured write workload",
    43  		Args:  cobra.ExactArgs(1),
    44  		RunE:  c.runE,
    45  	}
    46  	cmd.Flags().IntVar(
    47  		&c.count, "count", 1, "the number of times to replay the workload")
    48  	cmd.Flags().StringVar(
    49  		&c.name, "name", "", "the name of the workload being replayed")
    50  	cmd.Flags().VarPF(
    51  		&c.pacer, "pacer", "p", "the pacer to use: unpaced, reference-ramp, or fixed-ramp=N")
    52  	cmd.Flags().Uint64Var(
    53  		&c.maxWritesMB, "max-writes", 0, "the maximum volume of writes (MB) to apply, with 0 denoting unlimited")
    54  	cmd.Flags().StringVar(
    55  		&c.optionsString, "options", "", "Pebble options to override, in the OPTIONS ini format but with any whitespace as field delimiters instead of newlines")
    56  	cmd.Flags().StringVar(
    57  		&c.runDir, "run-dir", c.runDir, "the directory to use for the replay data directory; defaults to a random dir in pwd")
    58  	cmd.Flags().Int64Var(
    59  		&c.maxCacheSize, "max-cache-size", c.maxCacheSize, "the max size of the block cache")
    60  	cmd.Flags().BoolVar(
    61  		&c.streamLogs, "stream-logs", c.streamLogs, "stream the Pebble logs to stdout during replay")
    62  	cmd.Flags().BoolVar(
    63  		&c.ignoreCheckpoint, "ignore-checkpoint", c.ignoreCheckpoint, "ignore the workload's initial checkpoint")
    64  	cmd.Flags().StringVar(
    65  		&c.checkpointDir, "checkpoint-dir", c.checkpointDir, "path to the checkpoint to use if not <WORKLOAD_DIR>/checkpoint")
    66  	return cmd
    67  }
    68  
    69  type replayConfig struct {
    70  	name             string
    71  	pacer            pacerFlag
    72  	runDir           string
    73  	count            int
    74  	maxWritesMB      uint64
    75  	streamLogs       bool
    76  	checkpointDir    string
    77  	ignoreCheckpoint bool
    78  	optionsString    string
    79  	maxCacheSize     int64
    80  
    81  	cleanUpFuncs []func() error
    82  }
    83  
    84  func (c *replayConfig) args() (args []string) {
    85  	if c.name != "" {
    86  		args = append(args, "--name", c.name)
    87  	}
    88  	if c.pacer.spec != "" {
    89  		args = append(args, "--pacer", c.pacer.spec)
    90  	}
    91  	if c.runDir != "" {
    92  		args = append(args, "--run-dir", c.runDir)
    93  	}
    94  	if c.count != 0 {
    95  		args = append(args, "--count", fmt.Sprint(c.count))
    96  	}
    97  	if c.maxWritesMB != 0 {
    98  		args = append(args, "--max-writes", fmt.Sprint(c.maxWritesMB))
    99  	}
   100  	if c.maxCacheSize != 0 {
   101  		args = append(args, "--max-cache-size", fmt.Sprint(c.maxCacheSize))
   102  	}
   103  	if c.streamLogs {
   104  		args = append(args, "--stream-logs")
   105  	}
   106  	if c.checkpointDir != "" {
   107  		args = append(args, "--checkpoint-dir", c.checkpointDir)
   108  	}
   109  	if c.ignoreCheckpoint {
   110  		args = append(args, "--ignore-checkpoint")
   111  	}
   112  	if c.optionsString != "" {
   113  		args = append(args, "--options", c.optionsString)
   114  	}
   115  	return args
   116  }
   117  
   118  func (c *replayConfig) runE(cmd *cobra.Command, args []string) error {
   119  	if c.ignoreCheckpoint && c.checkpointDir != "" {
   120  		return errors.Newf("cannot provide both --checkpoint-dir and --ignore-checkpoint")
   121  	}
   122  	stdout := cmd.OutOrStdout()
   123  
   124  	workloadPath := args[0]
   125  	if err := c.runOnce(stdout, workloadPath); err != nil {
   126  		return err
   127  	}
   128  	c.count--
   129  
   130  	// If necessary, run it again. We run again replacing our existing process
   131  	// with the next run so that we're truly starting over. This helps avoid the
   132  	// possibility of state within the Go runtime, the fragmentation of the
   133  	// heap, or global state within Pebble from interfering with the
   134  	// independence of individual runs. Previously we called runOnce multiple
   135  	// times without exec-ing, but we observed less variance between runs from
   136  	// within the same process.
   137  	if c.count > 0 {
   138  		fmt.Printf("%d runs remaining.", c.count)
   139  		executable, err := os.Executable()
   140  		if err != nil {
   141  			return err
   142  		}
   143  		execArgs := append(append([]string{executable, "bench", "replay"}, c.args()...), workloadPath)
   144  		syscall.Exec(executable, execArgs, os.Environ())
   145  	}
   146  	return nil
   147  }
   148  
   149  func (c *replayConfig) runOnce(stdout io.Writer, workloadPath string) error {
   150  	defer c.cleanUp()
   151  	if c.name == "" {
   152  		c.name = vfs.Default.PathBase(workloadPath)
   153  	}
   154  
   155  	r := &replay.Runner{
   156  		RunDir:       c.runDir,
   157  		WorkloadFS:   vfs.Default,
   158  		WorkloadPath: workloadPath,
   159  		Pacer:        c.pacer,
   160  		Opts:         &pebble.Options{},
   161  	}
   162  	if c.maxWritesMB > 0 {
   163  		r.MaxWriteBytes = c.maxWritesMB * (1 << 20)
   164  	}
   165  	if err := c.initRunDir(r); err != nil {
   166  		return err
   167  	}
   168  	if err := c.initOptions(r); err != nil {
   169  		return err
   170  	}
   171  	if verbose {
   172  		fmt.Fprintln(stdout, "Options:")
   173  		fmt.Fprintln(stdout, r.Opts.String())
   174  	}
   175  
   176  	// Begin the workload. Run does not block.
   177  	ctx := context.Background()
   178  	if err := r.Run(ctx); err != nil {
   179  		return errors.Wrapf(err, "starting workload")
   180  	}
   181  
   182  	// Wait blocks until the workload is complete. Once Wait returns, all of the
   183  	// workload's write operations have been replayed AND the database's
   184  	// compactions have quiesced.
   185  	m, err := r.Wait()
   186  	if err != nil {
   187  		return errors.Wrapf(err, "waiting for workload to complete")
   188  	}
   189  	if err := r.Close(); err != nil {
   190  		return errors.Wrapf(err, "cleaning up")
   191  	}
   192  	fmt.Fprintln(stdout, "Workload complete.")
   193  	if err := m.WriteBenchmarkString(c.name, stdout); err != nil {
   194  		return err
   195  	}
   196  	for _, plot := range m.Plots(120 /* width */, 30 /* height */) {
   197  		fmt.Fprintln(stdout, plot.Name)
   198  		fmt.Fprintln(stdout, plot.Plot)
   199  		fmt.Fprintln(stdout)
   200  	}
   201  	fmt.Fprintln(stdout, m.Final.String())
   202  	return nil
   203  }
   204  
   205  func (c *replayConfig) initRunDir(r *replay.Runner) error {
   206  	if r.RunDir == "" {
   207  		// Default to replaying in a new directory within the current working
   208  		// directory.
   209  		wd, err := os.Getwd()
   210  		if err != nil {
   211  			return err
   212  		}
   213  		r.RunDir, err = os.MkdirTemp(wd, "replay-")
   214  		if err != nil {
   215  			return err
   216  		}
   217  		c.cleanUpFuncs = append(c.cleanUpFuncs, func() error {
   218  			return os.RemoveAll(r.RunDir)
   219  		})
   220  	}
   221  	if !c.ignoreCheckpoint {
   222  		checkpointDir := c.getCheckpointDir(r)
   223  		fmt.Printf("%s: Attempting to initialize with checkpoint %q.\n", time.Now().Format(time.RFC3339), checkpointDir)
   224  		ok, err := vfs.Clone(
   225  			r.WorkloadFS,
   226  			vfs.Default,
   227  			checkpointDir,
   228  			filepath.Join(r.RunDir),
   229  			vfs.CloneTryLink)
   230  		if err != nil {
   231  			return err
   232  		}
   233  		if !ok {
   234  			return errors.Newf("no checkpoint %q exists; you may re-run with --ignore-checkpoint", checkpointDir)
   235  		}
   236  		fmt.Printf("%s: Run directory initialized with checkpoint %q.\n", time.Now().Format(time.RFC3339), checkpointDir)
   237  	}
   238  	return nil
   239  }
   240  
   241  func (c *replayConfig) initOptions(r *replay.Runner) error {
   242  	// If using a workload checkpoint, load the Options from it.
   243  	// TODO(jackson): Allow overriding the OPTIONS.
   244  	if !c.ignoreCheckpoint {
   245  		ls, err := r.WorkloadFS.List(c.getCheckpointDir(r))
   246  		if err != nil {
   247  			return err
   248  		}
   249  		sort.Strings(ls)
   250  		var optionsFilepath string
   251  		for _, l := range ls {
   252  			path := r.WorkloadFS.PathJoin(r.WorkloadPath, "checkpoint", l)
   253  			typ, _, ok := base.ParseFilename(r.WorkloadFS, path)
   254  			if ok && typ == base.FileTypeOptions {
   255  				optionsFilepath = path
   256  			}
   257  		}
   258  		f, err := r.WorkloadFS.Open(optionsFilepath)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		o, err := io.ReadAll(f)
   263  		if err != nil {
   264  			return err
   265  		}
   266  		if err := f.Close(); err != nil {
   267  			return err
   268  		}
   269  		if err := r.Opts.Parse(string(o), c.parseHooks()); err != nil {
   270  			return err
   271  		}
   272  	}
   273  	if err := c.parseCustomOptions(c.optionsString, r.Opts); err != nil {
   274  		return err
   275  	}
   276  	// TODO(jackson): If r.Opts.Comparer == nil, peek at the workload's
   277  	// manifests and pull the comparer out of them.
   278  	//
   279  	// r.Opts.Comparer can only be nil at this point if ignoreCheckpoint is
   280  	// set; otherwise we'll have already extracted the Comparer from the
   281  	// checkpoint's OPTIONS file.
   282  
   283  	if c.streamLogs {
   284  		r.Opts.AddEventListener(pebble.MakeLoggingEventListener(pebble.DefaultLogger))
   285  	}
   286  	r.Opts.EnsureDefaults()
   287  	return nil
   288  }
   289  
   290  func (c *replayConfig) getCheckpointDir(r *replay.Runner) string {
   291  	if c.checkpointDir != "" {
   292  		return c.checkpointDir
   293  	}
   294  	return r.WorkloadFS.PathJoin(r.WorkloadPath, `checkpoint`)
   295  }
   296  
   297  func (c *replayConfig) parseHooks() *pebble.ParseHooks {
   298  	return &pebble.ParseHooks{
   299  		NewCache: func(size int64) *cache.Cache {
   300  			if c.maxCacheSize != 0 && size > c.maxCacheSize {
   301  				size = c.maxCacheSize
   302  			}
   303  			return cache.New(size)
   304  		},
   305  		NewComparer: makeComparer,
   306  		NewFilterPolicy: func(name string) (pebble.FilterPolicy, error) {
   307  			switch name {
   308  			case "none":
   309  				return nil, nil
   310  			case "rocksdb.BuiltinBloomFilter":
   311  				return bloom.FilterPolicy(10), nil
   312  			default:
   313  				return nil, errors.Errorf("invalid filter policy name %q", name)
   314  			}
   315  		},
   316  		NewMerger: makeMerger,
   317  	}
   318  }
   319  
   320  // parseCustomOptions parses Pebble Options passed through a CLI flag.
   321  // Ordinarily Pebble Options are specified through an INI file with newlines
   322  // delimiting fields. That doesn't translate well to a CLI interface, so this
   323  // function accepts fields are that delimited by any whitespace. This is the
   324  // same format that CockroachDB accepts Pebble Options through the --store flag,
   325  // and this code is copied from there.
   326  func (c *replayConfig) parseCustomOptions(optsStr string, opts *pebble.Options) error {
   327  	if optsStr == "" {
   328  		return nil
   329  	}
   330  	// Pebble options are supplied in the Pebble OPTIONS ini-like
   331  	// format, but allowing any whitespace to delimit lines. Convert
   332  	// the options to a newline-delimited format. This isn't a trivial
   333  	// character replacement because whitespace may appear within a
   334  	// stanza, eg ["Level 0"].
   335  	value := strings.TrimSpace(optsStr)
   336  	var buf bytes.Buffer
   337  	for len(value) > 0 {
   338  		i := strings.IndexFunc(value, func(r rune) bool {
   339  			return r == '[' || unicode.IsSpace(r)
   340  		})
   341  		switch {
   342  		case i == -1:
   343  			buf.WriteString(value)
   344  			value = value[len(value):]
   345  		case value[i] == '[':
   346  			// If there's whitespace within [ ], we write it verbatim.
   347  			j := i + strings.IndexRune(value[i:], ']')
   348  			buf.WriteString(value[:j+1])
   349  			value = value[j+1:]
   350  		case unicode.IsSpace(rune(value[i])):
   351  			// NB: This doesn't handle multibyte whitespace.
   352  			buf.WriteString(value[:i])
   353  			buf.WriteRune('\n')
   354  			value = strings.TrimSpace(value[i+1:])
   355  		}
   356  	}
   357  	return opts.Parse(buf.String(), c.parseHooks())
   358  }
   359  
   360  func (c *replayConfig) cleanUp() error {
   361  	for _, f := range c.cleanUpFuncs {
   362  		if err := f(); err != nil {
   363  			return err
   364  		}
   365  	}
   366  	return nil
   367  }
   368  
   369  func makeComparer(name string) (*pebble.Comparer, error) {
   370  	switch name {
   371  	case base.DefaultComparer.Name:
   372  		return base.DefaultComparer, nil
   373  	case "cockroach_comparator":
   374  		return mvccComparer, nil
   375  	default:
   376  		return nil, errors.Newf("unrecognized comparer %q", name)
   377  	}
   378  }
   379  
   380  func makeMerger(name string) (*pebble.Merger, error) {
   381  	switch name {
   382  	case base.DefaultMerger.Name:
   383  		return base.DefaultMerger, nil
   384  	case "cockroach_merge_operator":
   385  		// We don't want to reimplement the cockroach merger. Instead we
   386  		// implement this merger to return the newer of the two operands. This
   387  		// doesn't exactly model cockroach's true use but should be good enough.
   388  		// TODO(jackson): Consider lifting replay into a `cockroach debug`
   389  		// command so we can use the true merger and comparer.
   390  		merger := new(pebble.Merger)
   391  		merger.Merge = func(key, value []byte) (pebble.ValueMerger, error) {
   392  			return &overwriteValueMerger{value: append([]byte{}, value...)}, nil
   393  		}
   394  		merger.Name = name
   395  		return merger, nil
   396  	default:
   397  		return nil, errors.Newf("unrecognized comparer %q", name)
   398  	}
   399  }
   400  
   401  // pacerFlag provides a command line flag interface for specifying the pacer to
   402  // use. It implements the flag.Value interface.
   403  type pacerFlag struct {
   404  	replay.Pacer
   405  	spec string
   406  }
   407  
   408  var _ flag.Value = (*pacerFlag)(nil)
   409  
   410  func (f *pacerFlag) String() string { return f.spec }
   411  func (f *pacerFlag) Type() string   { return "pacer" }
   412  
   413  // Set implements the Flag.Value interface.
   414  func (f *pacerFlag) Set(spec string) error {
   415  	f.spec = spec
   416  	switch {
   417  	case spec == "unpaced":
   418  		f.Pacer = replay.Unpaced{}
   419  	case spec == "reference-ramp":
   420  		f.Pacer = replay.PaceByReferenceReadAmp{}
   421  	case strings.HasPrefix(spec, "fixed-ramp="):
   422  		rAmp, err := strconv.Atoi(strings.TrimPrefix(spec, "fixed-ramp="))
   423  		if err != nil {
   424  			return errors.Newf("unable to parse fixed r-amp: %s", err)
   425  		}
   426  		f.Pacer = replay.PaceByFixedReadAmp(rAmp)
   427  	default:
   428  		return errors.Newf("unrecognized pacer spec: %q", errors.Safe(spec))
   429  	}
   430  	return nil
   431  }
   432  
   433  type overwriteValueMerger struct {
   434  	value []byte
   435  }
   436  
   437  func (o *overwriteValueMerger) MergeNewer(value []byte) error {
   438  	o.value = append(o.value[:0], value...)
   439  	return nil
   440  }
   441  
   442  func (o *overwriteValueMerger) MergeOlder(value []byte) error {
   443  	return nil
   444  }
   445  
   446  func (o *overwriteValueMerger) Finish(includesBase bool) ([]byte, io.Closer, error) {
   447  	return o.value, nil, nil
   448  }