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

     1  // Copyright 2022 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 sstable
     6  
     7  import (
     8  	"github.com/cockroachdb/errors"
     9  	"github.com/cockroachdb/pebble/internal/base"
    10  )
    11  
    12  // TableFormat specifies the format version for sstables. The legacy LevelDB
    13  // format is format version 1.
    14  type TableFormat uint32
    15  
    16  // The available table formats, representing the tuple (magic number, version
    17  // number). Note that these values are not (and should not) be serialized to
    18  // disk. The ordering should follow the order the versions were introduced to
    19  // Pebble (i.e. the history is linear).
    20  const (
    21  	TableFormatUnspecified TableFormat = iota
    22  	TableFormatLevelDB
    23  	TableFormatRocksDBv2
    24  	TableFormatPebblev1 // Block properties.
    25  	TableFormatPebblev2 // Range keys.
    26  	TableFormatPebblev3 // Value blocks.
    27  	TableFormatPebblev4 // DELSIZED tombstones.
    28  	NumTableFormats
    29  
    30  	TableFormatMax = NumTableFormats - 1
    31  )
    32  
    33  // TableFormatPebblev4, in addition to DELSIZED, introduces the use of
    34  // InternalKeyKindSSTableInternalObsoleteBit.
    35  //
    36  // 1. Motivation
    37  //
    38  // We have various related problems caused by Pebble snapshots:
    39  //
    40  // - P1: RANGEDELs that delete points in the same sstable, but the points
    41  //   happen to not get deleted during compactions because of an open snapshot.
    42  //   This causes very expensive iteration, that has been observed in
    43  //   production deployments
    44  //
    45  // - P2: When iterating over a foreign sstable (in disaggregated storage), we
    46  //   need to do (a) point collapsing to expose at most one point per user key,
    47  //   (b) apply RANGEDELs in the sstable to hide deleted points in the same
    48  //   sstable. This per-sstable point collapsing iteration needs to be very
    49  //   efficient (ideally as efficient from a CPU perspective as iteration over
    50  //   regular sstables) since foreign sstables can be very long-lived -- one of
    51  //   the goals of disaggregated storage is to scale compute and disk bandwidth
    52  //   resources as a function of the hot (from a write perspective) data and
    53  //   not the whole data, so we don't want to have to rewrite foreign sstables
    54  //   solely to improve read performance.
    55  //
    56  // The ideal solution for P2 would allow user-facing reads to utilize the
    57  // existing SST iterators (with slight modifications) and with no loss of
    58  // efficiency. And for P1 and P2 we would like to skip whole blocks of
    59  // overwritten/deleted points. Even when we can't skip whole blocks, avoiding
    60  // key comparisons at iteration time to discover what points are deleted is
    61  // very desirable, since keys can be long.
    62  //
    63  // We observe that:
    64  //
    65  // - Reads:
    66  //   - All user-facing reads in CockroachDB use iterators over the DB, hence
    67  //     have a higher read seqnum than all sstables (there are some rare cases
    68  //     that can violate this, but those are not important from a performance
    69  //     optimization perspective).
    70  //
    71  //   - Certain internal-facing reads in CockroachDB use snapshots, but the
    72  //     snapshots are shortlived enough that most L5 and L6 sstables will have
    73  //     all seqnums lower than the snapshot seqnum.
    74  //
    75  // - Writes:
    76  //   - We already do key comparisons between points when writing the sstable
    77  //     to ensure that the sstable invariant (monotonically increasing internal
    78  //     keys) is not violated. So we know which points share the same userkey,
    79  //     and thereby which points are obsolete because there is a more recent
    80  //     point in the same sstable.
    81  //
    82  //   - The compactionIter knows which point id deleted by a RANGEDEL even if
    83  //     the point does need to be written because of a snapshot.
    84  //
    85  //   So this known information can be encoded in the sstable at write time and
    86  //   utilized for optimized reading.
    87  //
    88  // 2. Solution
    89  //
    90  // We primarily scope the solution to the following point kinds: SET,
    91  // SETWITHDEL, DEL, DELSIZED, SINGLEDEL. These are the ones marked locally
    92  // obsolete, i.e., obsolete within the sstable, and we can guarantee that at
    93  // most one point will be exposed per user key. MERGE keys create more
    94  // complexity: MERGE followed by MERGE causes multiple keys to not be
    95  // obsolete. Same applies for MERGE followed by SET/SETWITHDEL/DEL*. Note
    96  // that:
    97  //
    98  // - For regular sst iteration, the obsolete marking is a performance
    99  //   optimization, and multiple keys for the same userkey can be handled by
   100  //   higher layers in the iterator tree (specifically pebble.Iterator).
   101  //
   102  // - For foreign sst iteration, we disallow MERGEs to be written to such
   103  //   shared ssts (details below).
   104  //
   105  // The key kinds are marked with an obsolete bit
   106  // (InternalKeyKindSSTableInternalObsoleteBit) when the key-value pair is
   107  // obsolete. This marking is done within blockWriter, based on information
   108  // passed to it by Writer. In turn, Writer uses a combination of key
   109  // comparisons, and information provided by compactionIter to decide whether a
   110  // key-value pair is obsolete. Additionally, a Pebble-internal
   111  // BlockPropertyCollector (obsoleteKeyBlockPropertyCollector) is used to mark
   112  // blocks where all key-value pairs are obsolete. Since the common case is
   113  // non-obsolete blocks, this block property collector uses the empty byte
   114  // slice to represent a non-obsolete block, which consumes no space in
   115  // BlockHandleWithProperties.Props.
   116  //
   117  // At read time, the obsolete bit is only visible to the blockIter, which can
   118  // be optionally configured to hide obsolete points. This hiding is only
   119  // configured for data block iterators for sstables being read by user-facing
   120  // iterators at a seqnum greater than the max seqnum in the sstable.
   121  // Additionally, when this hiding is configured, a Pebble-internal block
   122  // property filter (obsoleteKeyBlockPropertyFilter), is used to skip whole
   123  // blocks that are obsolete.
   124  //
   125  // 2.1 Correctness
   126  //
   127  // Due to the level invariant, the sequence of seqnums for a user key in a
   128  // sstable represents a contiguous subsequence of the seqnums for the userkey
   129  // across the whole LSM, and is more recent than the seqnums in a sstable in a
   130  // lower level. So exposing exactly one point from a sstable for a userkey
   131  // will also mask the points for the userkey in lower levels. If we expose no
   132  // point, because of RANGEDELs, that RANGEDEL will also mask the points in
   133  // lower levels.
   134  //
   135  // Note that we do not need to do anything special at write time for
   136  // SETWITHDEL and SINGLEDEL. This is because these key kinds are treated
   137  // specially only by compactions, which do not hide obsolete points. For
   138  // regular reads, SETWITHDEL behaves the same as SET and SINGLEDEL behaves the
   139  // same as DEL.
   140  //
   141  // 2.2 Strictness and MERGE
   142  //
   143  // Setting the obsolete bit on point keys is advanced usage, so we support two
   144  // modes, both of which must be truthful when setting the obsolete bit, but
   145  // vary in when they don't set the obsolete bit.
   146  //
   147  // - Non-strict: In this mode, the bit does not need to be set for keys that
   148  //   are obsolete. Additionally, any sstable containing MERGE keys can only
   149  //   use this mode. An iterator over such an sstable, when configured to
   150  //   hideObsoletePoints, can expose multiple internal keys per user key, and
   151  //   can expose keys that are deleted by rangedels in the same sstable. This
   152  //   is the mode that non-advanced users should use. Pebble without
   153  //   disaggregated storage will also use this mode and will best-effort set
   154  //   the obsolete bit, to optimize iteration when snapshots have retained many
   155  //   obsolete keys.
   156  //
   157  // - Strict: In this mode, every obsolete key must have the obsolete bit set,
   158  //   and no MERGE keys are permitted. An iterator over such an sstable, when
   159  //   configured to hideObsoletePoints satisfies two properties:
   160  //   - S1: will expose at most one internal key per user key, which is the
   161  //     most recent one.
   162  //   - S2: will never expose keys that are deleted by rangedels in the same
   163  //     sstable.
   164  //
   165  //   This is the mode for two use cases in disaggregated storage (which will
   166  //   exclude parts of the key space that has MERGEs), for levels that contain
   167  //   sstables that can become foreign sstables:
   168  //   - Pebble compaction output to these levels that can become foreign
   169  //     sstables.
   170  //
   171  //   - CockroachDB ingest operations that can ingest into the levels that can
   172  //     become foreign sstables. Note, these are not sstables corresponding to
   173  //     copied data for CockroachDB range snapshots. This case occurs for
   174  //     operations like index backfills: these trivially satisfy the strictness
   175  //     criteria since they only write one key per userkey.
   176  //
   177  //     TODO(sumeer): this latter case is not currently supported, since only
   178  //     Writer.AddWithForceObsolete calls are permitted for writing strict
   179  //     obsolete sstables. This is done to reduce the likelihood of bugs. One
   180  //     simple way to lift this limitation would be to disallow adding any
   181  //     RANGEDELs when a Pebble-external writer is trying to construct a strict
   182  //     obsolete sstable.
   183  
   184  // ParseTableFormat parses the given magic bytes and version into its
   185  // corresponding internal TableFormat.
   186  func ParseTableFormat(magic []byte, version uint32) (TableFormat, error) {
   187  	switch string(magic) {
   188  	case levelDBMagic:
   189  		return TableFormatLevelDB, nil
   190  	case rocksDBMagic:
   191  		if version != rocksDBFormatVersion2 {
   192  			return TableFormatUnspecified, base.CorruptionErrorf(
   193  				"pebble/table: unsupported rocksdb format version %d", errors.Safe(version),
   194  			)
   195  		}
   196  		return TableFormatRocksDBv2, nil
   197  	case pebbleDBMagic:
   198  		switch version {
   199  		case 1:
   200  			return TableFormatPebblev1, nil
   201  		case 2:
   202  			return TableFormatPebblev2, nil
   203  		case 3:
   204  			return TableFormatPebblev3, nil
   205  		case 4:
   206  			return TableFormatPebblev4, nil
   207  		default:
   208  			return TableFormatUnspecified, base.CorruptionErrorf(
   209  				"pebble/table: unsupported pebble format version %d", errors.Safe(version),
   210  			)
   211  		}
   212  	default:
   213  		return TableFormatUnspecified, base.CorruptionErrorf(
   214  			"pebble/table: invalid table (bad magic number: 0x%x)", magic,
   215  		)
   216  	}
   217  }
   218  
   219  // AsTuple returns the TableFormat's (Magic String, Version) tuple.
   220  func (f TableFormat) AsTuple() (string, uint32) {
   221  	switch f {
   222  	case TableFormatLevelDB:
   223  		return levelDBMagic, 0
   224  	case TableFormatRocksDBv2:
   225  		return rocksDBMagic, 2
   226  	case TableFormatPebblev1:
   227  		return pebbleDBMagic, 1
   228  	case TableFormatPebblev2:
   229  		return pebbleDBMagic, 2
   230  	case TableFormatPebblev3:
   231  		return pebbleDBMagic, 3
   232  	case TableFormatPebblev4:
   233  		return pebbleDBMagic, 4
   234  	default:
   235  		panic("sstable: unknown table format version tuple")
   236  	}
   237  }
   238  
   239  // String returns the TableFormat (Magic String,Version) tuple.
   240  func (f TableFormat) String() string {
   241  	switch f {
   242  	case TableFormatLevelDB:
   243  		return "(LevelDB)"
   244  	case TableFormatRocksDBv2:
   245  		return "(RocksDB,v2)"
   246  	case TableFormatPebblev1:
   247  		return "(Pebble,v1)"
   248  	case TableFormatPebblev2:
   249  		return "(Pebble,v2)"
   250  	case TableFormatPebblev3:
   251  		return "(Pebble,v3)"
   252  	case TableFormatPebblev4:
   253  		return "(Pebble,v4)"
   254  	default:
   255  		panic("sstable: unknown table format version tuple")
   256  	}
   257  }