Skip to content

Commit ec03187

Browse files
authored
perf: seek past the internal-key region instead of stepping through it (#1243)
### Motivation Item 2.2 from the performance review. Floor/ceiling gets and unbounded scans that exclude internal keys used pebble's `SkipPoint`. `SkipPoint` is a **filter, not a seek**: the iterator still steps through — and loads the blocks of — every internal key it passes. A `CEILING`/`HIGHER` get past the last user key, or a scan with no upper bound, therefore walks the **entire live notification backlog** before returning. With 100k notifications retained a single client `Get` took **3 ms**, and the cost grows without bound with the retention window. ### The subtlety (why the obvious fix is wrong) The internal keys are contiguous, so the tempting fix is to clamp the iterator's `UpperBound` to the start of the internal region. That is **unsafe for natural sorting**: a regular key is stored raw, so one whose bytes sort after the encoded internal prefix `"\xff\xffoxia/"` — e.g. `"\xff\xffz"` — lives *after* the internal keys. Bounding the region away would silently hide those keys. Hierarchical sorting is different: the marker bit puts internal keys strictly last, so a bound *is* safe there. ### Changes `Encoder` gains `InternalKeyRange() (start, end []byte)`, returning the contiguous internal-key region as a half-open range: - **hierarchical** → `end == nil` (region runs to the end of the keyspace): pruned outright with an `UpperBound`, so pebble never visits it. - **natural** → real `end`: the region is **seeked over** in a single jump (it is contiguous, so nothing at/after `end` is internal), in **both directions** — the reverse paths had the same walk, a floor probe above the region stepped *backwards* through the whole backlog. `SkipPoint` is removed on these paths. Besides causing the walk, it hides the internal keys from the wrapper, so we could not detect the region in order to seek past it — the two approaches are mutually exclusive. The skipper is applied at every positioning op (the four point lookups plus `Next`/`Prev`/`SeekGE`/`SeekLT` in both iterator wrappers) so no internal key can leak. `InternalKeyRange` is allocation-free for both encoders (constant, read-only bounds shared as package vars). ### Benchmark `BenchmarkPebbleGetCeilingPastUserKeys` — ceiling lookup past the last user key, N internal keys retained: | backlog | before (SkipPoint) | after (seek) | |---|---|---| | 100 | 4.1 µs | 1.23 µs | | 10,000 | 295 µs | 1.26 µs | | 100,000 | **3.05 ms** | **1.27 µs** | O(N) → O(1). ### Verification - `TestPebbleScanSkipsInternalRegionButKeepsKeysAfterIt` and `TestPebbleGetAcrossInternalRegionNatural` plant a regular key **after** the internal region under natural sorting and assert scans/gets in both directions still return it. Verified to discriminate: they fail against an upper-bound prune (`end == nil` for natural) — exactly the mistake the naive fix would make. - `TestPebbleGetPastInternalRegionHierarchical` pins that hierarchical ceilings past the user keys are genuinely `NOT_FOUND` without walking the region. - `go test -race` green on `oxiad/dataserver/...` and `common/compare`; `golangci-lint` clean across all go.work modules on the CI-pinned v2.6.2 (the change spans `common/` and `oxiad/`). --- Stacked on #1242 (drops a redundant seek in `getHigher`, which this PR also rewrites); please merge that first. Until it does, its one commit shows in this PR's diff. --------- Signed-off-by: Matteo Merli <mmerli@apache.org>
1 parent d27c02a commit ec03187

3 files changed

Lines changed: 362 additions & 58 deletions

File tree

common/compare/encode.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,39 @@ type Encoder interface {
3030
Decode(encodedKey []byte) string
3131

3232
IsInternalKey(encodedKey []byte) bool
33+
34+
// InternalKeyRange returns the encoded half-open range [start, end) that
35+
// contains every internal key. The region is always contiguous, so an
36+
// iterator can seek past it in one jump instead of stepping through it.
37+
//
38+
// end is nil when the region runs to the end of the keyspace — no user key
39+
// can sort after it — which lets the region be pruned with an upper bound
40+
// instead. That is the case for the hierarchical encoder, but NOT for the
41+
// natural one, where a user key is stored raw and can therefore sort after
42+
// the internal keys.
43+
InternalKeyRange() (start []byte, end []byte)
44+
}
45+
46+
// keyRegionSuccessor returns the smallest key that sorts after every key with
47+
// the given prefix, or nil when no such key exists (an all-0xff prefix).
48+
func keyRegionSuccessor(prefix []byte) []byte {
49+
end := bytes.Clone(prefix)
50+
for i := len(end) - 1; i >= 0; i-- {
51+
if end[i] != maxByteValue {
52+
end[i]++
53+
return end[:i+1]
54+
}
55+
}
56+
return nil
3357
}
3458

3559
const (
3660
encodedSeparator = 0xff
3761
internalKeysBitMarker = 1 << 15
62+
63+
// maxByteValue is the largest byte value; a prefix made only of these has
64+
// no successor.
65+
maxByteValue = 0xff
3866
)
3967

4068
var (
@@ -105,6 +133,19 @@ func (encoderHierarchical) IsInternalKey(encodedKey []byte) bool {
105133
return encodedKey[0]&(1<<7) != 0
106134
}
107135

136+
// InternalKeyRange: the internal-key marker is the top bit of the 2-byte
137+
// prefix, so every internal key sorts after every regular one (a regular key
138+
// would need 32768 separators to reach that region) — the region runs to the
139+
// end of the keyspace.
140+
func (encoderHierarchical) InternalKeyRange() (start []byte, end []byte) {
141+
return hierarchicalInternalKeyStart, nil
142+
}
143+
144+
// The bounds returned by InternalKeyRange are constant and read-only, so build
145+
// them once. Callers use them as pebble iterator bounds and seek keys, never
146+
// mutating them (same contract as encodedInternalKeyPrefixBytes).
147+
var hierarchicalInternalKeyStart = []byte{internalKeysBitMarker >> 8}
148+
108149
// EncoderHierarchical ensure that we can sort keys from same level together
109150
// and thus we can easily return the children of a given path
110151
// The encoding is done by prepending 2 bytes with the count of
@@ -163,6 +204,16 @@ func (encoderNatural) IsInternalKey(encodedKey []byte) bool {
163204
return bytes.HasPrefix(encodedKey, encodedInternalKeyPrefixBytes)
164205
}
165206

207+
// InternalKeyRange: internal keys are the "\xff\xffoxia/" prefix region. A
208+
// regular key is stored raw, so one whose bytes sort after that prefix (e.g.
209+
// "\xff\xffz") lives *after* the internal keys — the region has a real upper
210+
// end and cannot be pruned away with an upper bound.
211+
func (encoderNatural) InternalKeyRange() (start []byte, end []byte) {
212+
return encodedInternalKeyPrefixBytes, encodedInternalKeyPrefixSuccessor
213+
}
214+
215+
var encodedInternalKeyPrefixSuccessor = keyRegionSuccessor(encodedInternalKeyPrefixBytes)
216+
166217
var EncoderNatural Encoder = &encoderNatural{}
167218

168219
// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////

oxiad/dataserver/database/kvstore/kv_pebble.go

Lines changed: 107 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -430,14 +430,13 @@ func (p *Pebble) getFloor(key []byte, itOpts IteratorOpts) (returnedKey string,
430430
}
431431

432432
func (p *Pebble) getCeiling(key []byte, itOpts IteratorOpts) (returnedKey string, value []byte, closer io.Closer, err error) {
433-
opts := newIterOptions(p.keyEncoder, itOpts)
434-
opts.LowerBound = key
435-
it, err := p.db.NewIter(opts)
433+
it, err := p.db.NewIter(newIterOptions(p.keyEncoder, itOpts, key, nil))
436434
if err != nil {
437435
return "", nil, nil, err
438436
}
437+
skipper := newInternalRegionSkipper(p.keyEncoder, itOpts)
439438

440-
if !it.First() {
439+
if !it.First() || !skipper.forward(it) {
441440
return "", nil, nil, multierr.Combine(it.Close(), pebble.ErrNotFound)
442441
}
443442

@@ -447,14 +446,15 @@ func (p *Pebble) getCeiling(key []byte, itOpts IteratorOpts) (returnedKey string
447446
}
448447

449448
func (p *Pebble) getLower(key []byte, itOpts IteratorOpts) (returnedKey string, value []byte, closer io.Closer, err error) {
450-
opts := newIterOptions(p.keyEncoder, itOpts)
451-
opts.UpperBound = key
452-
it, err := p.db.NewIter(opts)
449+
it, err := p.db.NewIter(newIterOptions(p.keyEncoder, itOpts, nil, key))
453450
if err != nil {
454451
return "", nil, nil, err
455452
}
453+
skipper := newInternalRegionSkipper(p.keyEncoder, itOpts)
456454

457-
if !it.Last() {
455+
// Backwards, the internal region is just as costly to step through: a floor
456+
// probe above it would walk the whole backlog in reverse.
457+
if !it.Last() || !skipper.backward(it) {
458458
return "", nil, nil, multierr.Combine(it.Close(), pebble.ErrNotFound)
459459
}
460460

@@ -464,21 +464,20 @@ func (p *Pebble) getLower(key []byte, itOpts IteratorOpts) (returnedKey string,
464464
}
465465

466466
func (p *Pebble) getHigher(key []byte, itOpts IteratorOpts) (returnedKey string, value []byte, closer io.Closer, err error) {
467-
opts := newIterOptions(p.keyEncoder, itOpts)
468-
opts.LowerBound = key
469-
it, err := p.db.NewIter(opts)
467+
it, err := p.db.NewIter(newIterOptions(p.keyEncoder, itOpts, key, nil))
470468
if err != nil {
471469
return "", nil, nil, err
472470
}
471+
skipper := newInternalRegionSkipper(p.keyEncoder, itOpts)
473472

474-
if !it.First() {
473+
if !it.First() || !skipper.forward(it) {
475474
return "", nil, nil, multierr.Combine(it.Close(), pebble.ErrNotFound)
476475
}
477476

478477
// The lower bound is inclusive, so the iterator may be positioned exactly on
479478
// the key. We are looking for a strict `x > y`, so step over it.
480479
if bytes.Equal(it.Key(), key) {
481-
if !it.Next() {
480+
if !it.Next() || !skipper.forward(it) {
482481
return "", nil, nil, multierr.Combine(it.Close(), pebble.ErrNotFound)
483482
}
484483
}
@@ -521,62 +520,52 @@ func (p *Pebble) KeyRangeScan(lowerBound, upperBound string, opts IteratorOpts)
521520
}
522521

523522
func (p *Pebble) KeyIterator(itOpts IteratorOpts) (KeyIterator, error) {
524-
opts := &pebble.IterOptions{}
525-
if !itOpts.IncludeInternalKeys {
526-
opts.SkipPoint = func(encodedKey []byte) bool {
527-
return p.keyEncoder.IsInternalKey(encodedKey)
528-
}
529-
}
530-
pbit, err := p.db.NewIter(opts)
523+
pbit, err := p.db.NewIter(newIterOptions(p.keyEncoder, itOpts, nil, nil))
531524
if err != nil {
532525
return nil, err
533526
}
534527

535-
return &PebbleIterator{p, pbit}, nil
528+
return &PebbleIterator{p, pbit, newInternalRegionSkipper(p.keyEncoder, itOpts)}, nil
536529
}
537530

538531
func (p *Pebble) KeyRangeScanReverse(lowerBound, upperBound string, itOpts IteratorOpts) (ReverseKeyIterator, error) {
539-
opts := &pebble.IterOptions{}
540-
if !itOpts.IncludeInternalKeys {
541-
opts.SkipPoint = func(encodedKey []byte) bool {
542-
return p.keyEncoder.IsInternalKey(encodedKey)
543-
}
544-
}
532+
var lb, ub []byte
545533
if lowerBound != "" {
546-
opts.LowerBound = p.keyEncoder.Encode(lowerBound)
534+
lb = p.keyEncoder.Encode(lowerBound)
547535
}
548536
if upperBound != "" {
549-
opts.UpperBound = p.keyEncoder.Encode(upperBound)
537+
ub = p.keyEncoder.Encode(upperBound)
550538
}
551-
pbit, err := p.db.NewIter(opts)
539+
pbit, err := p.db.NewIter(newIterOptions(p.keyEncoder, itOpts, lb, ub))
552540
if err != nil {
553541
return nil, err
554542
}
555-
pbit.Last()
556-
return &PebbleReverseIterator{p, pbit}, nil
543+
skipper := newInternalRegionSkipper(p.keyEncoder, itOpts)
544+
if pbit.Last() {
545+
skipper.backward(pbit)
546+
}
547+
return &PebbleReverseIterator{p, pbit, skipper}, nil
557548
}
558549

559550
func (p *Pebble) RangeScan(lowerBound, upperBound string, itOpts IteratorOpts) (KeyValueIterator, error) {
560-
opts := &pebble.IterOptions{}
561-
if !itOpts.IncludeInternalKeys {
562-
opts.SkipPoint = func(encodedKey []byte) bool {
563-
return p.keyEncoder.IsInternalKey(encodedKey)
564-
}
565-
}
551+
var lb, ub []byte
566552
if lowerBound != "" {
567-
opts.LowerBound = p.keyEncoder.Encode(lowerBound)
553+
lb = p.keyEncoder.Encode(lowerBound)
568554
}
569555
if upperBound != "" {
570-
opts.UpperBound = p.keyEncoder.Encode(upperBound)
556+
ub = p.keyEncoder.Encode(upperBound)
571557
}
572558

573-
pbit, err := p.db.NewIter(opts)
559+
pbit, err := p.db.NewIter(newIterOptions(p.keyEncoder, itOpts, lb, ub))
574560
if err != nil {
575561
return nil, err
576562
}
577563

578-
pbit.First()
579-
return &PebbleIterator{p, pbit}, nil
564+
skipper := newInternalRegionSkipper(p.keyEncoder, itOpts)
565+
if pbit.First() {
566+
skipper.forward(pbit)
567+
}
568+
return &PebbleIterator{p, pbit, skipper}, nil
580569
}
581570

582571
func (p *Pebble) Snapshot() (Snapshot, error) {
@@ -620,7 +609,7 @@ func (b *PebbleBatch) RangeScan(lowerBound, upperBound string) (KeyValueIterator
620609
return nil, err
621610
}
622611
pbit.SeekGE(lb)
623-
return &PebbleIterator{b.p, pbit}, nil
612+
return &PebbleIterator{b.p, pbit, internalRegionSkipper{}}, nil
624613
}
625614

626615
func (b *PebbleBatch) Close() error {
@@ -714,8 +703,9 @@ func (b *PebbleBatch) Commit() error {
714703
// Iterator wrapper methods
715704

716705
type PebbleIterator struct {
717-
p *Pebble
718-
pi *pebble.Iterator
706+
p *Pebble
707+
pi *pebble.Iterator
708+
skipper internalRegionSkipper
719709
}
720710

721711
func (p *PebbleIterator) Close() error {
@@ -731,19 +721,19 @@ func (p *PebbleIterator) Key() string {
731721
}
732722

733723
func (p *PebbleIterator) Next() bool {
734-
return p.pi.Next()
724+
return p.pi.Next() && p.skipper.forward(p.pi)
735725
}
736726

737727
func (p *PebbleIterator) Prev() bool {
738-
return p.pi.Prev()
728+
return p.pi.Prev() && p.skipper.backward(p.pi)
739729
}
740730

741731
func (p *PebbleIterator) SeekGE(key string) bool {
742-
return p.pi.SeekGE(p.p.keyEncoder.Encode(key))
732+
return p.pi.SeekGE(p.p.keyEncoder.Encode(key)) && p.skipper.forward(p.pi)
743733
}
744734

745735
func (p *PebbleIterator) SeekLT(key string) bool {
746-
return p.pi.SeekLT(p.p.keyEncoder.Encode(key))
736+
return p.pi.SeekLT(p.p.keyEncoder.Encode(key)) && p.skipper.backward(p.pi)
747737
}
748738

749739
func (p *PebbleIterator) Value() ([]byte, error) {
@@ -757,8 +747,9 @@ func (p *PebbleIterator) Value() ([]byte, error) {
757747
// Iterator wrapper methods
758748

759749
type PebbleReverseIterator struct {
760-
p *Pebble
761-
pi *pebble.Iterator
750+
p *Pebble
751+
pi *pebble.Iterator
752+
skipper internalRegionSkipper
762753
}
763754

764755
func (p *PebbleReverseIterator) Close() error {
@@ -774,7 +765,7 @@ func (p *PebbleReverseIterator) Key() string {
774765
}
775766

776767
func (p *PebbleReverseIterator) Prev() bool {
777-
return p.pi.Prev()
768+
return p.pi.Prev() && p.skipper.backward(p.pi)
778769
}
779770

780771
func (p *PebbleReverseIterator) Value() ([]byte, error) {
@@ -855,12 +846,70 @@ func (sl *pebbleSnapshotLoader) Complete() {
855846
sl.complete = true
856847
}
857848

858-
func newIterOptions(keyEncoder compare.Encoder, itOpts IteratorOpts) *pebble.IterOptions {
859-
opts := &pebble.IterOptions{}
860-
if !itOpts.IncludeInternalKeys {
861-
opts.SkipPoint = func(encodedKey []byte) bool {
862-
return keyEncoder.IsInternalKey(encodedKey)
849+
// newIterOptions builds the Pebble iterator options for a scan that may have to
850+
// exclude the internal-key region.
851+
//
852+
// It deliberately does not use pebble's SkipPoint. SkipPoint is a *filter*: the
853+
// iterator still steps through — and loads the blocks of — every internal key it
854+
// passes, so a ceiling/higher lookup past the last user key, or a scan with no
855+
// upper bound, walks the entire live notification backlog before returning. It
856+
// also hides those keys from us, so we could not detect the region to seek past
857+
// it. Instead: when the internal region runs to the end of the keyspace it is
858+
// pruned outright with an upper bound, and otherwise internalRegionSkipper
859+
// jumps over it with a single seek.
860+
func newIterOptions(enc compare.Encoder, itOpts IteratorOpts, lowerBound, upperBound []byte) *pebble.IterOptions {
861+
opts := &pebble.IterOptions{LowerBound: lowerBound, UpperBound: upperBound}
862+
if itOpts.IncludeInternalKeys {
863+
return opts
864+
}
865+
if start, end := enc.InternalKeyRange(); end == nil {
866+
// No user key can sort after the internal keys: bound them away.
867+
if opts.UpperBound == nil || bytes.Compare(start, opts.UpperBound) < 0 {
868+
opts.UpperBound = start
863869
}
864870
}
865871
return opts
866872
}
873+
874+
// internalRegionSkipper keeps an iterator out of the contiguous internal-key
875+
// region, jumping over it in a single seek rather than stepping through it.
876+
//
877+
// It is a no-op when internal keys are wanted, and when newIterOptions already
878+
// pruned the region with an upper bound (hierarchical). It carries its weight
879+
// for the natural encoder, where a user key is stored raw and can sort *after*
880+
// the internal keys, so the region cannot be bounded away.
881+
type internalRegionSkipper struct {
882+
enc compare.Encoder
883+
start, end []byte
884+
}
885+
886+
func newInternalRegionSkipper(enc compare.Encoder, itOpts IteratorOpts) internalRegionSkipper {
887+
if itOpts.IncludeInternalKeys {
888+
return internalRegionSkipper{}
889+
}
890+
start, end := enc.InternalKeyRange()
891+
if end == nil {
892+
// Already pruned by the upper bound in newIterOptions.
893+
return internalRegionSkipper{}
894+
}
895+
return internalRegionSkipper{enc: enc, start: start, end: end}
896+
}
897+
898+
// forward moves the iterator past the internal region when it is positioned
899+
// inside it, and reports whether it is still valid. A single seek suffices: the
900+
// region is contiguous, so nothing at or after end is an internal key.
901+
func (s internalRegionSkipper) forward(it *pebble.Iterator) bool {
902+
if s.end == nil || !it.Valid() || !s.enc.IsInternalKey(it.Key()) {
903+
return it.Valid()
904+
}
905+
return it.SeekGE(s.end)
906+
}
907+
908+
// backward moves the iterator before the internal region when it is positioned
909+
// inside it, and reports whether it is still valid.
910+
func (s internalRegionSkipper) backward(it *pebble.Iterator) bool {
911+
if s.end == nil || !it.Valid() || !s.enc.IsInternalKey(it.Key()) {
912+
return it.Valid()
913+
}
914+
return it.SeekLT(s.start)
915+
}

0 commit comments

Comments
 (0)