github.com/petermattis/pebble@v0.0.0-20190905164901-ab51a2166067/internal/pacertoy/pebble/main.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 "context" 9 "fmt" 10 "math" 11 "os" 12 "sync" 13 "sync/atomic" 14 "time" 15 16 "github.com/petermattis/pebble/internal/rate" 17 18 "golang.org/x/exp/rand" 19 ) 20 21 const ( 22 // If measureLatency is true, the simulator outputs p50, p95, and p99 23 // latencies after all writes are completed. In this mode, all writes 24 // are immediately queued. If this is disabled, writes come in continually 25 // at different rates. 26 measureLatency = false 27 28 writeAmount = 2000 << 20 // 2 GB 29 30 // Max rate for all compactions. This is intentionally set low enough that 31 // user writes will have to be delayed. 32 maxCompactionRate = 100 << 20 // 100 MB/s 33 minCompactionRate = 20 << 20 // 20 MB/s 34 35 memtableSize = 64 << 20 // 64 MB 36 maxMemtableCount = 5 37 drainDelayThreshold = 1.05 * memtableSize 38 maxFlushRate = 30 << 20 // 30 MB/s 39 minFlushRate = 4 << 20 // 4 MB/s 40 41 l0CompactionThreshold = 1 42 43 levelRatio = 10 44 numLevels = 7 45 46 compactionDebtSlowdownThreshold = 2 * memtableSize 47 ) 48 49 type compactionPacer struct { 50 level int64 51 maxDrainer *rate.Limiter 52 minDrainer *rate.Limiter 53 } 54 55 func newCompactionPacer() *compactionPacer { 56 p := &compactionPacer{ 57 maxDrainer: rate.NewLimiter(maxCompactionRate, maxCompactionRate), 58 minDrainer: rate.NewLimiter(minCompactionRate, minCompactionRate), 59 } 60 return p 61 } 62 63 func (p *compactionPacer) fill(n int64) { 64 atomic.AddInt64(&p.level, n) 65 } 66 67 func (p *compactionPacer) drain(n int64, delay bool) bool { 68 p.maxDrainer.WaitN(context.Background(), int(n)) 69 70 if delay { 71 p.minDrainer.WaitN(context.Background(), int(n)) 72 } 73 level := atomic.AddInt64(&p.level, -n) 74 return level <= compactionDebtSlowdownThreshold 75 } 76 77 type flushPacer struct { 78 level int64 79 drainDelayLevel float64 80 fillCond sync.Cond 81 // minDrainer is the drainer which sets the minimum speed of draining. 82 minDrainer *rate.Limiter 83 // maxDrainer is the drainer which sets the maximum speed of draining. 84 maxDrainer *rate.Limiter 85 } 86 87 func newFlushPacer(mu *sync.Mutex) *flushPacer { 88 p := &flushPacer{ 89 drainDelayLevel: drainDelayThreshold, 90 minDrainer: rate.NewLimiter(minFlushRate, minFlushRate), 91 maxDrainer: rate.NewLimiter(maxFlushRate, maxFlushRate), 92 } 93 p.fillCond.L = mu 94 return p 95 } 96 97 func (p *flushPacer) fill(n int64) { 98 atomic.AddInt64(&p.level, n) 99 p.fillCond.Signal() 100 } 101 102 func (p *flushPacer) drain(n int64, delay bool) bool { 103 p.maxDrainer.WaitN(context.Background(), int(n)) 104 105 if delay { 106 p.minDrainer.WaitN(context.Background(), int(n)) 107 } 108 level := atomic.AddInt64(&p.level, -n) 109 p.fillCond.Signal() 110 return float64(level) <= p.drainDelayLevel 111 } 112 113 type DB struct { 114 mu sync.Mutex 115 flushPacer *flushPacer 116 flushCond sync.Cond 117 memtables []*int64 118 fill int64 119 drain int64 120 121 compactionMu sync.Mutex 122 compactionPacer *compactionPacer 123 // L0 is represented as an array of integers whereas every other level 124 // is represented as a single integer. 125 L0 []*int64 126 // Non-L0 sstables. sstables[0] == L1. 127 sstables []int64 128 maxSSTableSizes []int64 129 compactionFlushCond sync.Cond 130 prevCompactionDebt float64 131 } 132 133 func newDB() *DB { 134 db := &DB{} 135 db.flushPacer = newFlushPacer(&db.mu) 136 db.flushCond.L = &db.mu 137 db.memtables = append(db.memtables, new(int64)) 138 139 db.compactionFlushCond.L = &db.compactionMu 140 db.L0 = append(db.L0, new(int64)) 141 db.compactionPacer = newCompactionPacer() 142 143 db.maxSSTableSizes = make([]int64, numLevels-1) 144 base := int64(levelRatio) 145 for i := uint64(0); i < numLevels-2; i++ { 146 // Each level is 10 times larger than the one above it. 147 db.maxSSTableSizes[i] = memtableSize * l0CompactionThreshold * base 148 base *= levelRatio 149 150 // Begin with each level full. 151 newLevel := db.maxSSTableSizes[i] 152 153 db.sstables = append(db.sstables, newLevel) 154 } 155 db.sstables = append(db.sstables, 0) 156 db.maxSSTableSizes[numLevels-2] = math.MaxInt64 157 158 go db.drainMemtable() 159 go db.drainCompaction() 160 161 return db 162 } 163 164 // drainCompaction simulates background compactions. 165 func (db *DB) drainCompaction() { 166 rng := rand.New(rand.NewSource(1)) 167 168 for { 169 db.compactionMu.Lock() 170 171 for len(db.L0) <= l0CompactionThreshold { 172 db.compactionFlushCond.Wait() 173 } 174 l0Table := db.L0[0] 175 db.compactionMu.Unlock() 176 177 var delay bool 178 for i, size := int64(0), int64(0); i < *l0Table; i += size { 179 size = 10000 + rng.Int63n(500) 180 if size > (*l0Table - i) { 181 size = *l0Table - i 182 } 183 delay = db.compactionPacer.drain(size, delay) 184 } 185 186 db.compactionMu.Lock() 187 db.L0 = db.L0[1:] 188 db.compactionMu.Unlock() 189 190 singleTableSize := int64(memtableSize) 191 tablesToCompact := 0 192 for i := range db.sstables { 193 newSSTableSize := atomic.AddInt64(&db.sstables[i], singleTableSize) 194 if newSSTableSize > db.maxSSTableSizes[i] { 195 atomic.AddInt64(&db.sstables[i], -singleTableSize) 196 tablesToCompact++ 197 } else { 198 // Lower levels do not need compaction if level above it did not 199 // need compaction. 200 break 201 } 202 } 203 204 totalCompactionBytes := int64(tablesToCompact * memtableSize) 205 206 for t := 0; t < tablesToCompact; t++ { 207 db.compactionPacer.fill(memtableSize) 208 for i, size := int64(0), int64(0); i < memtableSize; i += size { 209 size = 10000 + rng.Int63n(500) 210 if size > (totalCompactionBytes - i) { 211 size = totalCompactionBytes - i 212 } 213 delay = db.compactionPacer.drain(size, delay) 214 } 215 216 db.delayMemtableDrain() 217 } 218 } 219 } 220 221 // fillCompaction fills L0 sstables. 222 func (db *DB) fillCompaction(size int64) { 223 db.compactionMu.Lock() 224 225 db.compactionPacer.fill(size) 226 227 last := db.L0[len(db.L0)-1] 228 if *last+size > memtableSize { 229 last = new(int64) 230 db.L0 = append(db.L0, last) 231 db.compactionFlushCond.Signal() 232 } 233 *last += size 234 235 db.compactionMu.Unlock() 236 } 237 238 // drainMemtable simulates memtable flushing. 239 func (db *DB) drainMemtable() { 240 rng := rand.New(rand.NewSource(2)) 241 242 for { 243 db.mu.Lock() 244 for len(db.memtables) <= 1 { 245 db.flushCond.Wait() 246 } 247 memtable := db.memtables[0] 248 db.mu.Unlock() 249 250 var delay bool 251 for i, size := int64(0), int64(0); i < *memtable; i += size { 252 size = 1000 + rng.Int63n(50) 253 if size > (*memtable - i) { 254 size = *memtable - i 255 } 256 delay = db.flushPacer.drain(size, delay) 257 atomic.AddInt64(&db.drain, size) 258 259 db.fillCompaction(size) 260 } 261 262 db.delayMemtableDrain() 263 264 db.mu.Lock() 265 db.memtables = db.memtables[1:] 266 db.mu.Unlock() 267 } 268 } 269 270 // delayMemtableDrain applies memtable drain delays depending on compaction debt. 271 func (db *DB) delayMemtableDrain() { 272 totalCompactionBytes := atomic.LoadInt64(&db.compactionPacer.level) 273 compactionDebt := math.Max(float64(totalCompactionBytes)-l0CompactionThreshold*memtableSize, 0.0) 274 275 db.mu.Lock() 276 if compactionDebt > compactionDebtSlowdownThreshold { 277 // Compaction debt is above the threshold and the debt is growing. Throttle memtable flushing. 278 drainLimit := maxFlushRate * rate.Limit(compactionDebtSlowdownThreshold/compactionDebt) 279 if drainLimit > 0 && drainLimit <= maxFlushRate { 280 db.flushPacer.maxDrainer.SetLimit(drainLimit) 281 } 282 } else { 283 // Continuously speed up memtable flushing to make sure that slowdown signal did not 284 // decrease the memtable flush rate by too much. 285 drainLimit := db.flushPacer.maxDrainer.Limit() * 1.05 286 if drainLimit > 0 && drainLimit <= maxFlushRate { 287 db.flushPacer.maxDrainer.SetLimit(drainLimit) 288 } 289 } 290 291 db.prevCompactionDebt = compactionDebt 292 db.mu.Unlock() 293 } 294 295 // fillMemtable simulates memtable filling. 296 func (db *DB) fillMemtable(size int64) { 297 db.mu.Lock() 298 299 for len(db.memtables) > maxMemtableCount { 300 db.flushPacer.fillCond.Wait() 301 } 302 db.flushPacer.fill(size) 303 atomic.AddInt64(&db.fill, size) 304 305 last := db.memtables[len(db.memtables)-1] 306 if *last+size > memtableSize { 307 last = new(int64) 308 db.memtables = append(db.memtables, last) 309 db.flushCond.Signal() 310 } 311 *last += size 312 313 db.mu.Unlock() 314 } 315 316 // printLevels prints the levels. 317 func (db *DB) printLevels() { 318 db.mu.Lock() 319 for i := range db.sstables { 320 fmt.Printf("Level %d: %d/%d\n", i+1, db.sstables[i]/(1024*1024), db.maxSSTableSizes[i]/(1024*1024)) 321 } 322 db.mu.Unlock() 323 } 324 325 // simulateWrite simulates user writes. 326 func simulateWrite(db *DB, measureLatencyMode bool) { 327 limiter := rate.NewLimiter(10<<20, 10<<20) // 10 MB/s 328 fmt.Printf("filling at 10 MB/sec\n") 329 330 setRate := func(mb int) { 331 fmt.Printf("filling at %d MB/sec\n", mb) 332 limiter.SetLimit(rate.Limit(mb << 20)) 333 } 334 335 if !measureLatencyMode { 336 go func() { 337 rng := rand.New(rand.NewSource(3)) 338 for { 339 secs := 5 + rng.Intn(5) 340 time.Sleep(time.Duration(secs) * time.Second) 341 mb := 10 + rng.Intn(20) 342 setRate(mb) 343 } 344 }() 345 } 346 347 rng := rand.New(rand.NewSource(uint64(4))) 348 349 totalWrites := int64(0) 350 percentiles := []int64{50, 95, 99} 351 percentileIndex := 0 352 percentileTimes := make([]time.Time, 0) 353 354 startTime := time.Now() 355 for totalWrites <= writeAmount { 356 size := 1000 + rng.Int63n(50) 357 if !measureLatencyMode { 358 limiter.WaitN(context.Background(), int(size)) 359 } 360 db.fillMemtable(size) 361 362 // Calculate latency percentiles 363 totalWrites += size 364 if percentileIndex < len(percentiles) && totalWrites > (percentiles[percentileIndex] * writeAmount / 100) { 365 percentileTimes = append(percentileTimes, time.Now()) 366 percentileIndex += 1 367 } 368 } 369 370 time.Sleep(time.Second * 10) 371 // Latency should only be measured when `limiter.WaitN` is removed. 372 if measureLatencyMode { 373 fmt.Printf("_____p50______p95______p99\n") 374 fmt.Printf("%8s %8s %8s\n", 375 time.Duration(percentileTimes[0].Sub(startTime).Seconds())*time.Second, 376 time.Duration(percentileTimes[1].Sub(startTime).Seconds())*time.Second, 377 time.Duration(percentileTimes[2].Sub(startTime).Seconds())*time.Second) 378 } 379 380 os.Exit(0) 381 } 382 383 func main() { 384 db := newDB() 385 386 go simulateWrite(db, measureLatency) 387 388 tick := time.NewTicker(time.Second) 389 start := time.Now() 390 lastNow := start 391 var lastFill, lastDrain int64 392 393 for i := 0; ; i++ { 394 select { 395 case <-tick.C: 396 if (i % 20) == 0 { 397 fmt.Printf("_elapsed___memtbs____dirty_____fill____drain____cdebt__l0count___max-f-rate\n") 398 } 399 400 if (i % 7) == 0 { 401 //db.printLevels() 402 } 403 404 db.mu.Lock() 405 memtableCount := len(db.memtables) 406 db.mu.Unlock() 407 dirty := atomic.LoadInt64(&db.flushPacer.level) 408 fill := atomic.LoadInt64(&db.fill) 409 drain := atomic.LoadInt64(&db.drain) 410 411 db.compactionMu.Lock() 412 compactionL0 := len(db.L0) 413 db.compactionMu.Unlock() 414 totalCompactionBytes := atomic.LoadInt64(&db.compactionPacer.level) 415 compactionDebt := math.Max(float64(totalCompactionBytes)-l0CompactionThreshold*memtableSize, 0.0) 416 maxFlushRate := db.flushPacer.maxDrainer.Limit() 417 418 now := time.Now() 419 elapsed := now.Sub(lastNow).Seconds() 420 fmt.Printf("%8s %8d %8.1f %8.1f %8.1f %8.1f %8d %12.1f\n", 421 time.Duration(now.Sub(start).Seconds()+0.5)*time.Second, 422 memtableCount, 423 float64(dirty)/(1024.0*1024.0), 424 float64(fill-lastFill)/(1024.0*1024.0*elapsed), 425 float64(drain-lastDrain)/(1024.0*1024.0*elapsed), 426 compactionDebt/(1024.0*1024.0), 427 compactionL0, 428 maxFlushRate/(1024.0*1024.0)) 429 430 lastNow = now 431 lastFill = fill 432 lastDrain = drain 433 } 434 } 435 }