github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/cmd/pebble/ycsb.go (about) 1 // Copyright 2019 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 "fmt" 9 "log" 10 "strconv" 11 "strings" 12 "sync" 13 "sync/atomic" 14 "time" 15 16 "github.com/cockroachdb/errors" 17 "github.com/cockroachdb/pebble" 18 "github.com/cockroachdb/pebble/internal/ackseq" 19 "github.com/cockroachdb/pebble/internal/randvar" 20 "github.com/cockroachdb/pebble/internal/rate" 21 "github.com/spf13/cobra" 22 "golang.org/x/exp/rand" 23 ) 24 25 const ( 26 ycsbInsert = iota 27 ycsbRead 28 ycsbScan 29 ycsbReverseScan 30 ycsbUpdate 31 ycsbNumOps 32 ) 33 34 var ycsbConfig struct { 35 batch *randvar.Flag 36 keys string 37 initialKeys int 38 prepopulatedKeys int 39 numOps uint64 40 scans *randvar.Flag 41 values *randvar.BytesFlag 42 workload string 43 } 44 45 var ycsbCmd = &cobra.Command{ 46 Use: "ycsb <dir>", 47 Short: "run customizable YCSB benchmark", 48 Long: ` 49 Run a customizable YCSB workload. The workload is specified by the --workload 50 flag which can take either one of the standard workload mixes (A-F), or 51 customizable workload fixes specified as a command separated list of op=weight 52 pairs. For example, --workload=read=50,update=50 performs a workload composed 53 of 50% reads and 50% updates. This is identical to the standard workload A. 54 55 The --batch, --scans, and --values flags take the specification for a random 56 variable: [<type>:]<min>[-<max>]. The <type> parameter must be one of "uniform" 57 or "zipf". If <type> is omitted, a uniform distribution is used. If <max> is 58 omitted it is set to the same value as <min>. The specification "1000" results 59 in a constant 1000. The specification "10-100" results in a uniformly random 60 variable in the range [10,100). The specification "zipf(10,100)" results in a 61 zipf distribution with a minimum value of 10 and a maximum value of 100. 62 63 The --batch flag controls the size of batches used for insert and update 64 operations. The --scans flag controls the number of iterations performed by a 65 scan operation. Read operations always read a single key. 66 67 The --values flag provides for an optional "/<target-compression-ratio>" 68 suffix. The default target compression ratio is 1.0 (i.e. incompressible random 69 data). A value of 2 will cause random data to be generated that should compress 70 to 50% of its uncompressed size. 71 72 Standard workloads: 73 74 A: 50% reads / 50% updates 75 B: 95% reads / 5% updates 76 C: 100% reads 77 D: 95% reads / 5% inserts 78 E: 95% scans / 5% inserts 79 F: 100% inserts 80 `, 81 Args: cobra.ExactArgs(1), 82 RunE: runYcsb, 83 } 84 85 func init() { 86 initYCSB(ycsbCmd) 87 } 88 89 func initYCSB(cmd *cobra.Command) { 90 ycsbConfig.batch = randvar.NewFlag("1") 91 cmd.Flags().Var( 92 ycsbConfig.batch, "batch", 93 "batch size distribution [{zipf,uniform}:]min[-max]") 94 cmd.Flags().StringVar( 95 &ycsbConfig.keys, "keys", "zipf", "latest, uniform, or zipf") 96 cmd.Flags().IntVar( 97 &ycsbConfig.initialKeys, "initial-keys", 10000, 98 "initial number of keys to insert before beginning workload") 99 cmd.Flags().IntVar( 100 &ycsbConfig.prepopulatedKeys, "prepopulated-keys", 0, 101 "number of keys that were previously inserted into the database") 102 cmd.Flags().Uint64VarP( 103 &ycsbConfig.numOps, "num-ops", "n", 0, 104 "maximum number of operations (0 means unlimited)") 105 ycsbConfig.scans = randvar.NewFlag("zipf:1-1000") 106 cmd.Flags().Var( 107 ycsbConfig.scans, "scans", 108 "scan length distribution [{zipf,uniform}:]min[-max]") 109 cmd.Flags().StringVar( 110 &ycsbConfig.workload, "workload", "B", 111 "workload type (A-F) or spec (read=X,update=Y,...)") 112 ycsbConfig.values = randvar.NewBytesFlag("1000") 113 cmd.Flags().Var( 114 ycsbConfig.values, "values", 115 "value size distribution [{zipf,uniform}:]min[-max][/<target-compression>]") 116 } 117 118 type ycsbWeights []float64 119 120 func (w ycsbWeights) get(i int) float64 { 121 if i >= len(w) { 122 return 0 123 } 124 return w[i] 125 } 126 127 var ycsbWorkloads = map[string]ycsbWeights{ 128 "A": { 129 ycsbRead: 0.5, 130 ycsbUpdate: 0.5, 131 }, 132 "B": { 133 ycsbRead: 0.95, 134 ycsbUpdate: 0.05, 135 }, 136 "C": { 137 ycsbRead: 1.0, 138 }, 139 "D": { 140 ycsbInsert: 0.05, 141 ycsbRead: 0.95, 142 // TODO(peter): default to skewed-latest distribution. 143 }, 144 "E": { 145 ycsbInsert: 0.05, 146 ycsbScan: 0.95, 147 }, 148 "F": { 149 ycsbInsert: 1.0, 150 // TODO(peter): the real workload is read-modify-write. 151 }, 152 } 153 154 func ycsbParseWorkload(w string) (ycsbWeights, error) { 155 if weights := ycsbWorkloads[w]; weights != nil { 156 return weights, nil 157 } 158 iWeights := make([]int, ycsbNumOps) 159 for _, p := range strings.Split(w, ",") { 160 parts := strings.Split(p, "=") 161 if len(parts) != 2 { 162 return nil, errors.Errorf("malformed weights: %s", errors.Safe(w)) 163 } 164 weight, err := strconv.Atoi(parts[1]) 165 if err != nil { 166 return nil, err 167 } 168 switch parts[0] { 169 case "insert": 170 iWeights[ycsbInsert] = weight 171 case "read": 172 iWeights[ycsbRead] = weight 173 case "scan": 174 iWeights[ycsbScan] = weight 175 case "rscan": 176 iWeights[ycsbReverseScan] = weight 177 case "update": 178 iWeights[ycsbUpdate] = weight 179 } 180 } 181 182 var sum int 183 for _, w := range iWeights { 184 sum += w 185 } 186 if sum == 0 { 187 return nil, errors.Errorf("zero weight specified: %s", errors.Safe(w)) 188 } 189 190 weights := make(ycsbWeights, ycsbNumOps) 191 for i := range weights { 192 weights[i] = float64(iWeights[i]) / float64(sum) 193 } 194 return weights, nil 195 } 196 197 func ycsbParseKeyDist(d string) (randvar.Dynamic, error) { 198 totalKeys := uint64(ycsbConfig.initialKeys + ycsbConfig.prepopulatedKeys) 199 switch strings.ToLower(d) { 200 case "latest": 201 return randvar.NewDefaultSkewedLatest() 202 case "uniform": 203 return randvar.NewUniform(1, totalKeys), nil 204 case "zipf": 205 return randvar.NewZipf(1, totalKeys, 0.99) 206 default: 207 return nil, errors.Errorf("unknown distribution: %s", errors.Safe(d)) 208 } 209 } 210 211 func runYcsb(cmd *cobra.Command, args []string) error { 212 if wipe && ycsbConfig.prepopulatedKeys > 0 { 213 return errors.New("--wipe and --prepopulated-keys both specified which is nonsensical") 214 } 215 216 weights, err := ycsbParseWorkload(ycsbConfig.workload) 217 if err != nil { 218 return err 219 } 220 221 keyDist, err := ycsbParseKeyDist(ycsbConfig.keys) 222 if err != nil { 223 return err 224 } 225 226 batchDist := ycsbConfig.batch 227 scanDist := ycsbConfig.scans 228 if err != nil { 229 return err 230 } 231 232 valueDist := ycsbConfig.values 233 y := newYcsb(weights, keyDist, batchDist, scanDist, valueDist) 234 runTest(args[0], test{ 235 init: y.init, 236 tick: y.tick, 237 done: y.done, 238 }) 239 return nil 240 } 241 242 type ycsbBuf struct { 243 rng *rand.Rand 244 keyBuf []byte 245 valueBuf []byte 246 keyNums []uint64 247 } 248 249 type ycsb struct { 250 db DB 251 writeOpts *pebble.WriteOptions 252 weights ycsbWeights 253 reg *histogramRegistry 254 keyDist randvar.Dynamic 255 batchDist randvar.Static 256 scanDist randvar.Static 257 valueDist *randvar.BytesFlag 258 readAmpCount atomic.Uint64 259 readAmpSum atomic.Uint64 260 keyNum *ackseq.S 261 numOps atomic.Uint64 262 limiter *rate.Limiter 263 opsMap map[string]int 264 } 265 266 func newYcsb( 267 weights ycsbWeights, 268 keyDist randvar.Dynamic, 269 batchDist, scanDist randvar.Static, 270 valueDist *randvar.BytesFlag, 271 ) *ycsb { 272 y := &ycsb{ 273 reg: newHistogramRegistry(), 274 weights: weights, 275 keyDist: keyDist, 276 batchDist: batchDist, 277 scanDist: scanDist, 278 valueDist: valueDist, 279 opsMap: make(map[string]int), 280 } 281 y.writeOpts = pebble.Sync 282 if disableWAL { 283 y.writeOpts = pebble.NoSync 284 } 285 286 ops := map[string]int{ 287 "insert": ycsbInsert, 288 "read": ycsbRead, 289 "rscan": ycsbReverseScan, 290 "scan": ycsbScan, 291 "update": ycsbUpdate, 292 } 293 for name, op := range ops { 294 w := y.weights.get(op) 295 if w == 0 { 296 continue 297 } 298 wstr := fmt.Sprint(int(100 * w)) 299 fill := strings.Repeat("_", 3-len(wstr)) 300 if fill == "" { 301 fill = "_" 302 } 303 fullName := fmt.Sprintf("%s%s%s", name, fill, wstr) 304 y.opsMap[fullName] = op 305 } 306 return y 307 } 308 309 func (y *ycsb) init(db DB, wg *sync.WaitGroup) { 310 y.db = db 311 312 if ycsbConfig.initialKeys > 0 { 313 buf := &ycsbBuf{rng: randvar.NewRand()} 314 315 b := db.NewBatch() 316 size := 0 317 start := time.Now() 318 last := start 319 for i := 1; i <= ycsbConfig.initialKeys; i++ { 320 if now := time.Now(); now.Sub(last) >= time.Second { 321 fmt.Printf("%5s inserted %d keys (%0.1f%%)\n", 322 time.Duration(now.Sub(start).Seconds()+0.5)*time.Second, 323 i-1, 100*float64(i-1)/float64(ycsbConfig.initialKeys)) 324 last = now 325 } 326 if size >= 1<<20 { 327 if err := b.Commit(y.writeOpts); err != nil { 328 log.Fatal(err) 329 } 330 b = db.NewBatch() 331 size = 0 332 } 333 key := y.makeKey(uint64(i+ycsbConfig.prepopulatedKeys), buf) 334 value := y.randBytes(buf) 335 if err := b.Set(key, value, nil); err != nil { 336 log.Fatal(err) 337 } 338 size += len(key) + len(value) 339 } 340 if err := b.Commit(y.writeOpts); err != nil { 341 log.Fatal(err) 342 } 343 _ = b.Close() 344 fmt.Printf("inserted keys [%d-%d)\n", 345 1+ycsbConfig.prepopulatedKeys, 346 1+ycsbConfig.prepopulatedKeys+ycsbConfig.initialKeys) 347 } 348 y.keyNum = ackseq.New(uint64(ycsbConfig.initialKeys + ycsbConfig.prepopulatedKeys)) 349 350 y.limiter = maxOpsPerSec.newRateLimiter() 351 352 wg.Add(concurrency) 353 354 // If this workload doesn't produce reads, sample the worst case read-amp 355 // from Metrics() periodically. 356 if y.weights.get(ycsbRead) == 0 && y.weights.get(ycsbScan) == 0 && y.weights.get(ycsbReverseScan) == 0 { 357 wg.Add(1) 358 go y.sampleReadAmp(db, wg) 359 } 360 361 for i := 0; i < concurrency; i++ { 362 go y.run(db, wg) 363 } 364 } 365 366 func (y *ycsb) run(db DB, wg *sync.WaitGroup) { 367 defer wg.Done() 368 369 var latency [ycsbNumOps]*namedHistogram 370 for name, op := range y.opsMap { 371 latency[op] = y.reg.Register(name) 372 } 373 374 buf := &ycsbBuf{rng: randvar.NewRand()} 375 376 ops := randvar.NewWeighted(nil, y.weights...) 377 for { 378 wait(y.limiter) 379 380 start := time.Now() 381 382 op := ops.Int() 383 switch op { 384 case ycsbInsert: 385 y.insert(db, buf) 386 case ycsbRead: 387 y.read(db, buf) 388 case ycsbScan: 389 y.scan(db, buf, false /* reverse */) 390 case ycsbReverseScan: 391 y.scan(db, buf, true /* reverse */) 392 case ycsbUpdate: 393 y.update(db, buf) 394 default: 395 panic("not reached") 396 } 397 398 latency[op].Record(time.Since(start)) 399 if ycsbConfig.numOps > 0 && y.numOps.Add(1) >= ycsbConfig.numOps { 400 break 401 } 402 } 403 } 404 405 func (y *ycsb) sampleReadAmp(db DB, wg *sync.WaitGroup) { 406 defer wg.Done() 407 408 ticker := time.NewTicker(time.Second) 409 defer ticker.Stop() 410 for range ticker.C { 411 m := db.Metrics() 412 y.readAmpCount.Add(1) 413 y.readAmpSum.Add(uint64(m.ReadAmp())) 414 if ycsbConfig.numOps > 0 && y.numOps.Load() >= ycsbConfig.numOps { 415 break 416 } 417 } 418 } 419 420 func (y *ycsb) hashKey(key uint64) uint64 { 421 // Inlined version of fnv.New64 + Write. 422 const offset64 = 14695981039346656037 423 const prime64 = 1099511628211 424 425 h := uint64(offset64) 426 for i := 0; i < 8; i++ { 427 h *= prime64 428 h ^= uint64(key & 0xff) 429 key >>= 8 430 } 431 return h 432 } 433 434 func (y *ycsb) makeKey(keyNum uint64, buf *ycsbBuf) []byte { 435 const size = 24 + 10 436 if cap(buf.keyBuf) < size { 437 buf.keyBuf = make([]byte, size) 438 } 439 key := buf.keyBuf[:4] 440 copy(key, "user") 441 key = strconv.AppendUint(key, y.hashKey(keyNum), 10) 442 // Use the MVCC encoding for keys. This appends a timestamp with 443 // walltime=1. That knowledge is utilized by rocksDB.Scan. 444 key = append(key, '\x00', '\x00', '\x00', '\x00', '\x00', 445 '\x00', '\x00', '\x00', '\x01', '\x09') 446 buf.keyBuf = key 447 return key 448 } 449 450 func (y *ycsb) nextReadKey(buf *ycsbBuf) []byte { 451 // NB: the range of values returned by keyDist is tied to the range returned 452 // by keyNum.Base. See how these are both incremented by ycsb.insert(). 453 keyNum := y.keyDist.Uint64(buf.rng) 454 return y.makeKey(keyNum, buf) 455 } 456 457 func (y *ycsb) randBytes(buf *ycsbBuf) []byte { 458 buf.valueBuf = y.valueDist.Bytes(buf.rng, buf.valueBuf) 459 return buf.valueBuf 460 } 461 462 func (y *ycsb) insert(db DB, buf *ycsbBuf) { 463 count := y.batchDist.Uint64(buf.rng) 464 if cap(buf.keyNums) < int(count) { 465 buf.keyNums = make([]uint64, count) 466 } 467 keyNums := buf.keyNums[:count] 468 469 b := db.NewBatch() 470 for i := range keyNums { 471 keyNums[i] = y.keyNum.Next() 472 _ = b.Set(y.makeKey(keyNums[i], buf), y.randBytes(buf), nil) 473 } 474 if err := b.Commit(y.writeOpts); err != nil { 475 log.Fatal(err) 476 } 477 _ = b.Close() 478 479 for i := range keyNums { 480 delta, err := y.keyNum.Ack(keyNums[i]) 481 if err != nil { 482 log.Fatal(err) 483 } 484 if delta > 0 { 485 y.keyDist.IncMax(delta) 486 } 487 } 488 } 489 490 func (y *ycsb) read(db DB, buf *ycsbBuf) { 491 key := y.nextReadKey(buf) 492 iter := db.NewIter(nil) 493 iter.SeekGE(key) 494 if iter.Valid() { 495 _ = iter.Key() 496 _ = iter.Value() 497 } 498 499 type metrics interface { 500 Metrics() pebble.IteratorMetrics 501 } 502 if m, ok := iter.(metrics); ok { 503 y.readAmpCount.Add(1) 504 y.readAmpSum.Add(uint64(m.Metrics().ReadAmp)) 505 } 506 507 if err := iter.Close(); err != nil { 508 log.Fatal(err) 509 } 510 } 511 512 func (y *ycsb) scan(db DB, buf *ycsbBuf, reverse bool) { 513 count := y.scanDist.Uint64(buf.rng) 514 key := y.nextReadKey(buf) 515 iter := db.NewIter(nil) 516 if err := db.Scan(iter, key, int64(count), reverse); err != nil { 517 log.Fatal(err) 518 } 519 520 type metrics interface { 521 Metrics() pebble.IteratorMetrics 522 } 523 if m, ok := iter.(metrics); ok { 524 y.readAmpCount.Add(1) 525 y.readAmpSum.Add(uint64(m.Metrics().ReadAmp)) 526 } 527 528 if err := iter.Close(); err != nil { 529 log.Fatal(err) 530 } 531 } 532 533 func (y *ycsb) update(db DB, buf *ycsbBuf) { 534 count := int(y.batchDist.Uint64(buf.rng)) 535 b := db.NewBatch() 536 for i := 0; i < count; i++ { 537 _ = b.Set(y.nextReadKey(buf), y.randBytes(buf), nil) 538 } 539 if err := b.Commit(y.writeOpts); err != nil { 540 log.Fatal(err) 541 } 542 _ = b.Close() 543 } 544 545 func (y *ycsb) tick(elapsed time.Duration, i int) { 546 if i%20 == 0 { 547 fmt.Println("____optype__elapsed__ops/sec(inst)___ops/sec(cum)__p50(ms)__p95(ms)__p99(ms)_pMax(ms)") 548 } 549 y.reg.Tick(func(tick histogramTick) { 550 h := tick.Hist 551 552 fmt.Printf("%10s %8s %14.1f %14.1f %8.1f %8.1f %8.1f %8.1f\n", 553 tick.Name, 554 time.Duration(elapsed.Seconds()+0.5)*time.Second, 555 float64(h.TotalCount())/tick.Elapsed.Seconds(), 556 float64(tick.Cumulative.TotalCount())/elapsed.Seconds(), 557 time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, 558 time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, 559 time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, 560 time.Duration(h.ValueAtQuantile(100)).Seconds()*1000, 561 ) 562 }) 563 } 564 565 func (y *ycsb) done(elapsed time.Duration) { 566 fmt.Println("\n____optype__elapsed_____ops(total)___ops/sec(cum)__avg(ms)__p50(ms)__p95(ms)__p99(ms)_pMax(ms)") 567 568 resultTick := histogramTick{} 569 y.reg.Tick(func(tick histogramTick) { 570 h := tick.Cumulative 571 if resultTick.Cumulative == nil { 572 resultTick.Now = tick.Now 573 resultTick.Cumulative = h 574 } else { 575 resultTick.Cumulative.Merge(h) 576 } 577 578 fmt.Printf("%10s %7.1fs %14d %14.1f %8.1f %8.1f %8.1f %8.1f %8.1f\n", 579 tick.Name, elapsed.Seconds(), h.TotalCount(), 580 float64(h.TotalCount())/elapsed.Seconds(), 581 time.Duration(h.Mean()).Seconds()*1000, 582 time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, 583 time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, 584 time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, 585 time.Duration(h.ValueAtQuantile(100)).Seconds()*1000) 586 }) 587 fmt.Println() 588 589 resultHist := resultTick.Cumulative 590 m := y.db.Metrics() 591 total := m.Total() 592 593 readAmpCount := y.readAmpCount.Load() 594 readAmpSum := y.readAmpSum.Load() 595 if readAmpCount == 0 { 596 readAmpSum = 0 597 readAmpCount = 1 598 } 599 600 fmt.Printf("Benchmarkycsb/%s/values=%s %d %0.1f ops/sec %d read %d write %.2f r-amp %0.2f w-amp\n\n", 601 ycsbConfig.workload, ycsbConfig.values, 602 resultHist.TotalCount(), 603 float64(resultHist.TotalCount())/elapsed.Seconds(), 604 total.BytesRead, 605 total.BytesFlushed+total.BytesCompacted, 606 float64(readAmpSum)/float64(readAmpCount), 607 total.WriteAmp(), 608 ) 609 }