github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/tool/sstable.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 tool 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "slices" 14 "text/tabwriter" 15 16 "github.com/cockroachdb/pebble" 17 "github.com/cockroachdb/pebble/internal/base" 18 "github.com/cockroachdb/pebble/internal/humanize" 19 "github.com/cockroachdb/pebble/internal/keyspan" 20 "github.com/cockroachdb/pebble/internal/private" 21 "github.com/cockroachdb/pebble/internal/rangedel" 22 "github.com/cockroachdb/pebble/sstable" 23 "github.com/cockroachdb/pebble/vfs" 24 "github.com/spf13/cobra" 25 ) 26 27 // sstableT implements sstable-level tools, including both configuration state 28 // and the commands themselves. 29 type sstableT struct { 30 Root *cobra.Command 31 Check *cobra.Command 32 Layout *cobra.Command 33 Properties *cobra.Command 34 Scan *cobra.Command 35 Space *cobra.Command 36 37 // Configuration and state. 38 opts *pebble.Options 39 comparers sstable.Comparers 40 mergers sstable.Mergers 41 42 // Flags. 43 fmtKey keyFormatter 44 fmtValue valueFormatter 45 start key 46 end key 47 filter key 48 count int64 49 verbose bool 50 } 51 52 func newSSTable( 53 opts *pebble.Options, comparers sstable.Comparers, mergers sstable.Mergers, 54 ) *sstableT { 55 s := &sstableT{ 56 opts: opts, 57 comparers: comparers, 58 mergers: mergers, 59 } 60 s.fmtKey.mustSet("quoted") 61 s.fmtValue.mustSet("[%x]") 62 63 s.Root = &cobra.Command{ 64 Use: "sstable", 65 Short: "sstable introspection tools", 66 } 67 s.Check = &cobra.Command{ 68 Use: "check <sstables>", 69 Short: "verify checksums and metadata", 70 Long: ``, 71 Args: cobra.MinimumNArgs(1), 72 Run: s.runCheck, 73 } 74 s.Layout = &cobra.Command{ 75 Use: "layout <sstables>", 76 Short: "print sstable block and record layout", 77 Long: ` 78 Print the layout for the sstables. The -v flag controls whether record layout 79 is displayed or omitted. 80 `, 81 Args: cobra.MinimumNArgs(1), 82 Run: s.runLayout, 83 } 84 s.Properties = &cobra.Command{ 85 Use: "properties <sstables>", 86 Short: "print sstable properties", 87 Long: ` 88 Print the properties for the sstables. The -v flag controls whether the 89 properties are pretty-printed or displayed in a verbose/raw format. 90 `, 91 Args: cobra.MinimumNArgs(1), 92 Run: s.runProperties, 93 } 94 s.Scan = &cobra.Command{ 95 Use: "scan <sstables>", 96 Short: "print sstable records", 97 Long: ` 98 Print the records in the sstables. The sstables are scanned in command line 99 order which means the records will be printed in that order. Raw range 100 tombstones are displayed interleaved with point records. 101 `, 102 Args: cobra.MinimumNArgs(1), 103 Run: s.runScan, 104 } 105 s.Space = &cobra.Command{ 106 Use: "space <sstables>", 107 Short: "print filesystem space used", 108 Long: ` 109 Print the estimated space usage in the specified files for the 110 inclusive-inclusive range specified by --start and --end. 111 `, 112 Args: cobra.MinimumNArgs(1), 113 Run: s.runSpace, 114 } 115 116 s.Root.AddCommand(s.Check, s.Layout, s.Properties, s.Scan, s.Space) 117 s.Root.PersistentFlags().BoolVarP(&s.verbose, "verbose", "v", false, "verbose output") 118 119 s.Check.Flags().Var( 120 &s.fmtKey, "key", "key formatter") 121 s.Layout.Flags().Var( 122 &s.fmtKey, "key", "key formatter") 123 s.Layout.Flags().Var( 124 &s.fmtValue, "value", "value formatter") 125 s.Scan.Flags().Var( 126 &s.fmtKey, "key", "key formatter") 127 s.Scan.Flags().Var( 128 &s.fmtValue, "value", "value formatter") 129 for _, cmd := range []*cobra.Command{s.Scan, s.Space} { 130 cmd.Flags().Var( 131 &s.start, "start", "start key for the range") 132 cmd.Flags().Var( 133 &s.end, "end", "end key for the range") 134 } 135 s.Scan.Flags().Var( 136 &s.filter, "filter", "only output records with matching prefix or overlapping range tombstones") 137 s.Scan.Flags().Int64Var( 138 &s.count, "count", 0, "key count for scan (0 is unlimited)") 139 140 return s 141 } 142 143 func (s *sstableT) newReader(f vfs.File) (*sstable.Reader, error) { 144 readable, err := sstable.NewSimpleReadable(f) 145 if err != nil { 146 return nil, err 147 } 148 o := sstable.ReaderOptions{ 149 Cache: pebble.NewCache(128 << 20 /* 128 MB */), 150 Comparer: s.opts.Comparer, 151 Filters: s.opts.Filters, 152 } 153 defer o.Cache.Unref() 154 return sstable.NewReader(readable, o, s.comparers, s.mergers, 155 private.SSTableRawTombstonesOpt.(sstable.ReaderOption)) 156 } 157 158 func (s *sstableT) runCheck(cmd *cobra.Command, args []string) { 159 stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() 160 s.foreachSstable(stderr, args, func(arg string) { 161 f, err := s.opts.FS.Open(arg) 162 if err != nil { 163 fmt.Fprintf(stderr, "%s\n", err) 164 return 165 } 166 167 fmt.Fprintf(stdout, "%s\n", arg) 168 169 r, err := s.newReader(f) 170 171 if err != nil { 172 fmt.Fprintf(stdout, "%s\n", err) 173 return 174 } 175 defer r.Close() 176 177 // Update the internal formatter if this comparator has one specified. 178 s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers) 179 s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers) 180 181 iter, err := r.NewIter(nil, nil) 182 if err != nil { 183 fmt.Fprintf(stderr, "%s\n", err) 184 return 185 } 186 187 // If a split function is defined for the comparer, verify that 188 // SeekPrefixGE can find every key in the table. 189 var prefixIter sstable.Iterator 190 if r.Split != nil { 191 var err error 192 prefixIter, err = r.NewIter(nil, nil) 193 if err != nil { 194 fmt.Fprintf(stderr, "%s\n", err) 195 return 196 } 197 } 198 199 var lastKey base.InternalKey 200 for key, _ := iter.First(); key != nil; key, _ = iter.Next() { 201 if base.InternalCompare(r.Compare, lastKey, *key) >= 0 { 202 fmt.Fprintf(stdout, "WARNING: OUT OF ORDER KEYS!\n") 203 if s.fmtKey.spec != "null" { 204 fmt.Fprintf(stdout, " %s >= %s\n", 205 lastKey.Pretty(s.fmtKey.fn), key.Pretty(s.fmtKey.fn)) 206 } 207 } 208 lastKey.Trailer = key.Trailer 209 lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...) 210 211 if prefixIter != nil { 212 n := r.Split(key.UserKey) 213 prefix := key.UserKey[:n] 214 key2, _ := prefixIter.SeekPrefixGE(prefix, key.UserKey, base.SeekGEFlagsNone) 215 if key2 == nil { 216 fmt.Fprintf(stdout, "WARNING: PREFIX ITERATION FAILURE!\n") 217 if s.fmtKey.spec != "null" { 218 fmt.Fprintf(stdout, " %s not found\n", key.Pretty(s.fmtKey.fn)) 219 } 220 } 221 } 222 } 223 224 if err := iter.Close(); err != nil { 225 fmt.Fprintf(stdout, "%s\n", err) 226 } 227 if prefixIter != nil { 228 if err := prefixIter.Close(); err != nil { 229 fmt.Fprintf(stdout, "%s\n", err) 230 } 231 } 232 }) 233 } 234 235 func (s *sstableT) runLayout(cmd *cobra.Command, args []string) { 236 stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() 237 s.foreachSstable(stderr, args, func(arg string) { 238 f, err := s.opts.FS.Open(arg) 239 if err != nil { 240 fmt.Fprintf(stderr, "%s\n", err) 241 return 242 } 243 244 fmt.Fprintf(stdout, "%s\n", arg) 245 246 r, err := s.newReader(f) 247 if err != nil { 248 fmt.Fprintf(stdout, "%s\n", err) 249 return 250 } 251 defer r.Close() 252 253 // Update the internal formatter if this comparator has one specified. 254 s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers) 255 s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers) 256 257 l, err := r.Layout() 258 if err != nil { 259 fmt.Fprintf(stderr, "%s\n", err) 260 return 261 } 262 fmtRecord := func(key *base.InternalKey, value []byte) { 263 formatKeyValue(stdout, s.fmtKey, s.fmtValue, key, value) 264 } 265 if s.fmtKey.spec == "null" && s.fmtValue.spec == "null" { 266 fmtRecord = nil 267 } 268 l.Describe(stdout, s.verbose, r, fmtRecord) 269 }) 270 } 271 272 func (s *sstableT) runProperties(cmd *cobra.Command, args []string) { 273 stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() 274 s.foreachSstable(stderr, args, func(arg string) { 275 f, err := s.opts.FS.Open(arg) 276 if err != nil { 277 fmt.Fprintf(stderr, "%s\n", err) 278 return 279 } 280 281 fmt.Fprintf(stdout, "%s\n", arg) 282 283 r, err := s.newReader(f) 284 if err != nil { 285 fmt.Fprintf(stdout, "%s\n", err) 286 return 287 } 288 defer r.Close() 289 290 if s.verbose { 291 fmt.Fprintf(stdout, "%s", r.Properties.String()) 292 return 293 } 294 295 stat, err := f.Stat() 296 if err != nil { 297 fmt.Fprintf(stderr, "%s\n", err) 298 return 299 } 300 301 formatNull := func(s string) string { 302 switch s { 303 case "", "nullptr": 304 return "-" 305 } 306 return s 307 } 308 309 tw := tabwriter.NewWriter(stdout, 2, 1, 2, ' ', 0) 310 fmt.Fprintf(tw, "size\t\n") 311 fmt.Fprintf(tw, " file\t%s\n", humanize.Bytes.Int64(stat.Size())) 312 fmt.Fprintf(tw, " data\t%s\n", humanize.Bytes.Uint64(r.Properties.DataSize)) 313 fmt.Fprintf(tw, " blocks\t%d\n", r.Properties.NumDataBlocks) 314 fmt.Fprintf(tw, " index\t%s\n", humanize.Bytes.Uint64(r.Properties.IndexSize)) 315 fmt.Fprintf(tw, " blocks\t%d\n", 1+r.Properties.IndexPartitions) 316 fmt.Fprintf(tw, " top-level\t%s\n", humanize.Bytes.Uint64(r.Properties.TopLevelIndexSize)) 317 fmt.Fprintf(tw, " filter\t%s\n", humanize.Bytes.Uint64(r.Properties.FilterSize)) 318 fmt.Fprintf(tw, " raw-key\t%s\n", humanize.Bytes.Uint64(r.Properties.RawKeySize)) 319 fmt.Fprintf(tw, " raw-value\t%s\n", humanize.Bytes.Uint64(r.Properties.RawValueSize)) 320 fmt.Fprintf(tw, " pinned-key\t%d\n", r.Properties.SnapshotPinnedKeySize) 321 fmt.Fprintf(tw, " pinned-val\t%d\n", r.Properties.SnapshotPinnedValueSize) 322 fmt.Fprintf(tw, " point-del-key-size\t%d\n", r.Properties.RawPointTombstoneKeySize) 323 fmt.Fprintf(tw, " point-del-value-size\t%d\n", r.Properties.RawPointTombstoneValueSize) 324 fmt.Fprintf(tw, "records\t%d\n", r.Properties.NumEntries) 325 fmt.Fprintf(tw, " set\t%d\n", r.Properties.NumEntries- 326 (r.Properties.NumDeletions+r.Properties.NumMergeOperands)) 327 fmt.Fprintf(tw, " delete\t%d\n", r.Properties.NumPointDeletions()) 328 fmt.Fprintf(tw, " delete-sized\t%d\n", r.Properties.NumSizedDeletions) 329 fmt.Fprintf(tw, " range-delete\t%d\n", r.Properties.NumRangeDeletions) 330 fmt.Fprintf(tw, " range-key-set\t%d\n", r.Properties.NumRangeKeySets) 331 fmt.Fprintf(tw, " range-key-unset\t%d\n", r.Properties.NumRangeKeyUnsets) 332 fmt.Fprintf(tw, " range-key-delete\t%d\n", r.Properties.NumRangeKeyDels) 333 fmt.Fprintf(tw, " merge\t%d\n", r.Properties.NumMergeOperands) 334 fmt.Fprintf(tw, " global-seq-num\t%d\n", r.Properties.GlobalSeqNum) 335 fmt.Fprintf(tw, " pinned\t%d\n", r.Properties.SnapshotPinnedKeys) 336 fmt.Fprintf(tw, "index\t\n") 337 fmt.Fprintf(tw, " key\t") 338 fmt.Fprintf(tw, " value\t") 339 fmt.Fprintf(tw, "comparer\t%s\n", r.Properties.ComparerName) 340 fmt.Fprintf(tw, "merger\t%s\n", formatNull(r.Properties.MergerName)) 341 fmt.Fprintf(tw, "filter\t%s\n", formatNull(r.Properties.FilterPolicyName)) 342 fmt.Fprintf(tw, " prefix\t%t\n", r.Properties.PrefixFiltering) 343 fmt.Fprintf(tw, " whole-key\t%t\n", r.Properties.WholeKeyFiltering) 344 fmt.Fprintf(tw, "compression\t%s\n", r.Properties.CompressionName) 345 fmt.Fprintf(tw, " options\t%s\n", r.Properties.CompressionOptions) 346 fmt.Fprintf(tw, "user properties\t\n") 347 fmt.Fprintf(tw, " collectors\t%s\n", r.Properties.PropertyCollectorNames) 348 keys := make([]string, 0, len(r.Properties.UserProperties)) 349 for key := range r.Properties.UserProperties { 350 keys = append(keys, key) 351 } 352 slices.Sort(keys) 353 for _, key := range keys { 354 fmt.Fprintf(tw, " %s\t%s\n", key, r.Properties.UserProperties[key]) 355 } 356 tw.Flush() 357 }) 358 } 359 360 func (s *sstableT) runScan(cmd *cobra.Command, args []string) { 361 stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() 362 s.foreachSstable(stderr, args, func(arg string) { 363 f, err := s.opts.FS.Open(arg) 364 if err != nil { 365 fmt.Fprintf(stderr, "%s\n", err) 366 return 367 } 368 369 // In filter-mode, we prefix ever line that is output with the sstable 370 // filename. 371 var prefix string 372 if s.filter == nil { 373 fmt.Fprintf(stdout, "%s\n", arg) 374 } else { 375 prefix = fmt.Sprintf("%s: ", arg) 376 } 377 378 r, err := s.newReader(f) 379 if err != nil { 380 fmt.Fprintf(stdout, "%s%s\n", prefix, err) 381 return 382 } 383 defer r.Close() 384 385 // Update the internal formatter if this comparator has one specified. 386 s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers) 387 s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers) 388 389 iter, err := r.NewIter(nil, s.end) 390 if err != nil { 391 fmt.Fprintf(stderr, "%s%s\n", prefix, err) 392 return 393 } 394 defer iter.Close() 395 key, value := iter.SeekGE(s.start, base.SeekGEFlagsNone) 396 397 // We configured sstable.Reader to return raw tombstones which requires a 398 // bit more work here to put them in a form that can be iterated in 399 // parallel with the point records. 400 rangeDelIter, err := func() (keyspan.FragmentIterator, error) { 401 iter, err := r.NewRawRangeDelIter() 402 if err != nil { 403 return nil, err 404 } 405 if iter == nil { 406 return keyspan.NewIter(r.Compare, nil), nil 407 } 408 defer iter.Close() 409 410 var tombstones []keyspan.Span 411 for t := iter.First(); t != nil; t = iter.Next() { 412 if s.end != nil && r.Compare(s.end, t.Start) <= 0 { 413 // The range tombstone lies after the scan range. 414 continue 415 } 416 if r.Compare(s.start, t.End) >= 0 { 417 // The range tombstone lies before the scan range. 418 continue 419 } 420 tombstones = append(tombstones, t.ShallowClone()) 421 } 422 423 slices.SortFunc(tombstones, func(a, b keyspan.Span) int { 424 return r.Compare(a.Start, b.Start) 425 }) 426 return keyspan.NewIter(r.Compare, tombstones), nil 427 }() 428 if err != nil { 429 fmt.Fprintf(stdout, "%s%s\n", prefix, err) 430 return 431 } 432 433 defer rangeDelIter.Close() 434 rangeDel := rangeDelIter.First() 435 count := s.count 436 437 var lastKey base.InternalKey 438 for key != nil || rangeDel != nil { 439 if key != nil && (rangeDel == nil || r.Compare(key.UserKey, rangeDel.Start) < 0) { 440 // The filter specifies a prefix of the key. 441 // 442 // TODO(peter): Is using prefix comparison like this kosher for all 443 // comparers? Probably not, but it is for common ones such as the 444 // Pebble default and CockroachDB's comparer. 445 if s.filter == nil || bytes.HasPrefix(key.UserKey, s.filter) { 446 fmt.Fprint(stdout, prefix) 447 v, _, err := value.Value(nil) 448 if err != nil { 449 fmt.Fprintf(stdout, "%s%s\n", prefix, err) 450 return 451 } 452 formatKeyValue(stdout, s.fmtKey, s.fmtValue, key, v) 453 454 } 455 if base.InternalCompare(r.Compare, lastKey, *key) >= 0 { 456 fmt.Fprintf(stdout, "%s WARNING: OUT OF ORDER KEYS!\n", prefix) 457 } 458 lastKey.Trailer = key.Trailer 459 lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...) 460 key, value = iter.Next() 461 } else { 462 // If a filter is specified, we want to output any range tombstone 463 // which overlaps the prefix. The comparison on the start key is 464 // somewhat complex. Consider the tombstone [aaa,ccc). We want to 465 // output this tombstone if filter is "aa", and if it "bbb". 466 if s.filter == nil || 467 ((r.Compare(s.filter, rangeDel.Start) >= 0 || 468 bytes.HasPrefix(rangeDel.Start, s.filter)) && 469 r.Compare(s.filter, rangeDel.End) < 0) { 470 fmt.Fprint(stdout, prefix) 471 if err := rangedel.Encode(rangeDel, func(k base.InternalKey, v []byte) error { 472 formatKeyValue(stdout, s.fmtKey, s.fmtValue, &k, v) 473 return nil 474 }); err != nil { 475 fmt.Fprintf(stdout, "%s\n", err) 476 os.Exit(1) 477 } 478 } 479 rangeDel = rangeDelIter.Next() 480 } 481 482 if count > 0 { 483 count-- 484 if count == 0 { 485 break 486 } 487 } 488 } 489 490 // Handle range keys. 491 rkIter, err := r.NewRawRangeKeyIter() 492 if err != nil { 493 fmt.Fprintf(stdout, "%s\n", err) 494 os.Exit(1) 495 } 496 if rkIter != nil { 497 defer rkIter.Close() 498 for span := rkIter.SeekGE(s.start); span != nil; span = rkIter.Next() { 499 // By default, emit the key, unless there is a filter. 500 emit := s.filter == nil 501 // Skip spans that start after the end key (if provided). End keys are 502 // exclusive, e.g. [a, b), so we consider the interval [b, +inf). 503 if s.end != nil && r.Compare(span.Start, s.end) >= 0 { 504 emit = false 505 } 506 // Filters override the provided start / end bounds, if provided. 507 if s.filter != nil && bytes.HasPrefix(span.Start, s.filter) { 508 // In filter mode, each line is prefixed with the filename. 509 fmt.Fprint(stdout, prefix) 510 emit = true 511 } 512 if emit { 513 formatSpan(stdout, s.fmtKey, s.fmtValue, span) 514 } 515 } 516 } 517 518 if err := iter.Close(); err != nil { 519 fmt.Fprintf(stdout, "%s\n", err) 520 } 521 }) 522 } 523 524 func (s *sstableT) runSpace(cmd *cobra.Command, args []string) { 525 stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() 526 s.foreachSstable(stderr, args, func(arg string) { 527 f, err := s.opts.FS.Open(arg) 528 if err != nil { 529 fmt.Fprintf(stderr, "%s\n", err) 530 return 531 } 532 r, err := s.newReader(f) 533 if err != nil { 534 fmt.Fprintf(stderr, "%s\n", err) 535 return 536 } 537 defer r.Close() 538 539 bytes, err := r.EstimateDiskUsage(s.start, s.end) 540 if err != nil { 541 fmt.Fprintf(stderr, "%s\n", err) 542 return 543 } 544 fmt.Fprintf(stdout, "%s: %d\n", arg, bytes) 545 }) 546 } 547 548 func (s *sstableT) foreachSstable(stderr io.Writer, args []string, fn func(arg string)) { 549 // Loop over args, invoking fn for each file. Each directory is recursively 550 // listed and fn is invoked on any file with an .sst or .ldb suffix. 551 for _, arg := range args { 552 info, err := s.opts.FS.Stat(arg) 553 if err != nil || !info.IsDir() { 554 fn(arg) 555 continue 556 } 557 walk(stderr, s.opts.FS, arg, func(path string) { 558 switch filepath.Ext(path) { 559 case ".sst", ".ldb": 560 fn(path) 561 } 562 }) 563 } 564 }