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 }