Skip to content

Commit e1277c4

Browse files
committed
cache: separate hit/miss metrics by level
We further break down the block cache metrics by level. The level is plumbed through the level iterator. However, it is not currently plumbed for the initial open of a file, and it is not plumbed for blob value reads. I will investigate plumbing a level through `LazyFetcher` separately. The n/a level reflects accesses where the level is not known and any accesses on flushable ingests. Metrics after 30m of ycsb-A: ``` BLOCK CACHE: 592 entries (17MB) miss rate [percentage of total misses] since start level | | background sstdata sstval blobval filter index -------+------------+------------------------------------------------------------------------------ n/a | 2.3% [91%] | 9.6% [7.4%] 4.1% [81%] 0.2% [3.3%] L0 | 22% [4.6%] | 36% [1.3%] 24% [2%] 15% [1.3%] L6 | 0% [4.1%] | 0% [0%] 0% [2.8%] 0% [1.3%] total | 0.3% | 10% [8.7%] 0% [4.8%] 4.1% [81%] 0% [5.9%] ``` Benchmark (before = without the level AND the category changes). On a GCE worker, `--benchtime=10s`: ``` name old time/op new time/op delta CacheGet-24 52.0ns ± 1% 44.0ns ± 1% -15.40% (p=0.008 n=5+5) ```
1 parent bf48872 commit e1277c4

26 files changed

+384
-185
lines changed

compaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2953,7 +2953,7 @@ func (d *DB) runCopyCompaction(
29532953
// TODO(radu): plumb a ReadEnv to CopySpan (it could use the buffer pool
29542954
// or update category stats).
29552955
wrote, err = sstable.CopySpan(ctx,
2956-
src, r, d.opts.MakeReaderOptions(),
2956+
src, r, c.startLevel.level,
29572957
w, d.opts.MakeWriterOptions(c.outputLevel.level, d.TableFormat()),
29582958
start, end,
29592959
)

db_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,7 @@ func TestMemTableReservation(t *testing.T) {
858858
t.Fatalf("expected 2 refs, but found %d", refs)
859859
}
860860
// Verify the memtable reservation has caused our test block to be evicted.
861-
if cv := tmpHandle.Peek(base.DiskFileNum(0), 0, cache.CategoryBackground); cv != nil {
861+
if cv := tmpHandle.Peek(base.DiskFileNum(0), 0, cache.MakeLevel(0), cache.CategoryBackground); cv != nil {
862862
t.Fatalf("expected failure, but found success: %#v", cv)
863863
}
864864

file_cache.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,9 @@ func (h *fileCacheHandle) newIters(
590590
internalOpts.readEnv.Virtual = env.Virtual
591591
internalOpts.readEnv.IsSharedIngested = env.IsSharedIngested
592592
internalOpts.readEnv.InternalBounds = env.InternalBounds
593+
if opts != nil && opts.layer.IsSet() && !opts.layer.IsFlushableIngests() {
594+
internalOpts.readEnv.Block.Level = cache.MakeLevel(opts.layer.Level())
595+
}
593596

594597
var iters iterSet
595598
if kinds.RangeKey() && file.HasRangeKeys {

internal/cache/cache.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -252,19 +252,23 @@ func (c *Handle) Cache() *Cache {
252252
//
253253
// Peek supports the special CategoryHidden category, in which case the hit or
254254
// miss is not recorded in metrics.
255-
func (c *Handle) Peek(fileNum base.DiskFileNum, offset uint64, category Category) *Value {
255+
func (c *Handle) Peek(
256+
fileNum base.DiskFileNum, offset uint64, level Level, category Category,
257+
) *Value {
256258
k := makeKey(c.id, fileNum, offset)
257-
return c.cache.getShard(k).get(k, category, true /* peekOnly */)
259+
return c.cache.getShard(k).get(k, level, category, true /* peekOnly */)
258260
}
259261

260262
// CategoryHidden can be used with Peek to avoid recording a cache hit or miss.
261263
const CategoryHidden Category = -1
262264

263265
// Get retrieves the cache value for the specified file and offset, returning
264266
// nil if no value is present.
265-
func (c *Handle) Get(fileNum base.DiskFileNum, offset uint64, category Category) *Value {
267+
func (c *Handle) Get(
268+
fileNum base.DiskFileNum, offset uint64, level Level, category Category,
269+
) *Value {
266270
k := makeKey(c.id, fileNum, offset)
267-
return c.cache.getShard(k).get(k, category, false /* peekOnly */)
271+
return c.cache.getShard(k).get(k, level, category, false /* peekOnly */)
268272
}
269273

270274
// GetWithReadHandle retrieves the cache value for the specified handleID, fileNum
@@ -291,7 +295,7 @@ func (c *Handle) Get(fileNum base.DiskFileNum, offset uint64, category Category)
291295
// While waiting, someone else may successfully read the value, which results
292296
// in a valid Handle being returned. This is a case where cacheHit=false.
293297
func (c *Handle) GetWithReadHandle(
294-
ctx context.Context, fileNum base.DiskFileNum, offset uint64, category Category,
298+
ctx context.Context, fileNum base.DiskFileNum, offset uint64, level Level, category Category,
295299
) (
296300
cv *Value,
297301
rh ReadHandle,
@@ -301,7 +305,7 @@ func (c *Handle) GetWithReadHandle(
301305
err error,
302306
) {
303307
k := makeKey(c.id, fileNum, offset)
304-
cv, re := c.cache.getShard(k).getWithReadEntry(k, category)
308+
cv, re := c.cache.getShard(k).getWithReadEntry(k, level, category)
305309
if cv != nil {
306310
return cv, ReadHandle{}, 0, 0, true, nil
307311
}

internal/cache/cache_test.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestCache(t *testing.T) {
4141
wantHit := fields[1][0] == 'h'
4242

4343
var hit bool
44-
cv := h.Get(base.DiskFileNum(key), 0, CategorySSTableData)
44+
cv := h.Get(base.DiskFileNum(key), 0, MakeLevel(0), CategorySSTableData)
4545
if cv == nil {
4646
cv = Alloc(1)
4747
cv.RawBuffer()[0] = fields[0][0]
@@ -81,14 +81,14 @@ func TestCachePeek(t *testing.T) {
8181
setTestValue(h, 0, uint64(i), "a", 1)
8282
}
8383
for i := range size / 2 {
84-
v := h.Get(base.DiskFileNum(0), uint64(i), CategoryBackground)
84+
v := h.Get(base.DiskFileNum(0), uint64(i), MakeLevel(0), CategoryBackground)
8585
if v == nil {
8686
t.Fatalf("expected to find block %d", i)
8787
}
8888
v.Release()
8989
}
9090
for i := size / 2; i < size; i++ {
91-
v := h.Peek(base.DiskFileNum(0), uint64(i), CategoryBackground)
91+
v := h.Peek(base.DiskFileNum(0), uint64(i), MakeLevel(0), CategoryBackground)
9292
if v == nil {
9393
t.Fatalf("expected to find block %d", i)
9494
}
@@ -100,7 +100,7 @@ func TestCachePeek(t *testing.T) {
100100
}
101101
// Verify that the Gets still find their values, despite the Peeks.
102102
for i := range size / 2 {
103-
v := h.Get(base.DiskFileNum(0), uint64(i), CategoryBackground)
103+
v := h.Get(base.DiskFileNum(0), uint64(i), MakeLevel(0), CategoryBackground)
104104
if v == nil {
105105
t.Fatalf("expected to find block %d", i)
106106
}
@@ -124,12 +124,12 @@ func TestCacheDelete(t *testing.T) {
124124
if expected, size := int64(10), cache.Size(); expected != size {
125125
t.Fatalf("expected cache size %d, but found %d", expected, size)
126126
}
127-
if v := h.Get(base.DiskFileNum(0), 0, CategorySSTableData); v == nil {
127+
if v := h.Get(base.DiskFileNum(0), 0, MakeLevel(0), CategorySSTableData); v == nil {
128128
t.Fatalf("expected to find block 0/0")
129129
} else {
130130
v.Release()
131131
}
132-
if v := h.Get(base.DiskFileNum(1), 0, CategorySSTableData); v != nil {
132+
if v := h.Get(base.DiskFileNum(1), 0, MakeLevel(0), CategorySSTableData); v != nil {
133133
t.Fatalf("expected to not find block 1/0")
134134
}
135135
// Deleting a non-existing block does nothing.
@@ -196,11 +196,11 @@ func TestMultipleDBs(t *testing.T) {
196196
if expected, size := int64(5), cache.Size(); expected != size {
197197
t.Fatalf("expected cache size %d, but found %d", expected, size)
198198
}
199-
v := h1.Get(base.DiskFileNum(0), 0, CategorySSTableData)
199+
v := h1.Get(base.DiskFileNum(0), 0, MakeLevel(0), CategorySSTableData)
200200
if v != nil {
201201
t.Fatalf("expected not present, but found %#v", v)
202202
}
203-
v = h2.Get(base.DiskFileNum(0), 0, CategorySSTableData)
203+
v = h2.Get(base.DiskFileNum(0), 0, MakeLevel(0), CategorySSTableData)
204204
if v := v.RawBuffer(); string(v) != "bbbbb" {
205205
t.Fatalf("expected bbbbb, but found %s", v)
206206
}
@@ -308,8 +308,9 @@ func BenchmarkCacheGet(b *testing.B) {
308308
for pb.Next() {
309309
randVal := pcg.Uint64()
310310
offset := randVal % size
311-
category := Category((randVal >> 32) % uint64(NumCategories))
312-
v := h.Get(base.DiskFileNum(0), offset, category)
311+
level := Level{levelPlusOne: int8((randVal >> 32) % NumLevels)}
312+
category := Category((randVal >> 48) % uint64(NumCategories))
313+
v := h.Get(base.DiskFileNum(0), offset, level, category)
313314
if v == nil {
314315
b.Fatal("failed to look up value")
315316
}

internal/cache/clockpro.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (k key) String() string {
7676
return fmt.Sprintf("%d/%d/%d", k.id, k.fileNum, k.offset)
7777
}
7878

79-
type counters [NumCategories]struct {
79+
type counters [NumLevels][NumCategories]struct {
8080
hits atomic.Int64
8181
misses atomic.Int64
8282
}
@@ -138,7 +138,7 @@ func (c *shard) init(maxSize int64) {
138138
//
139139
// If peekOnly is true, the state of the cache is not modified to reflect the
140140
// access.
141-
func (c *shard) get(k key, category Category, peekOnly bool) *Value {
141+
func (c *shard) get(k key, level Level, category Category, peekOnly bool) *Value {
142142
c.mu.RLock()
143143
if e, _ := c.blocks.Get(k); e != nil {
144144
if value := e.acquireValue(); value != nil {
@@ -148,14 +148,14 @@ func (c *shard) get(k key, category Category, peekOnly bool) *Value {
148148
}
149149
c.mu.RUnlock()
150150
if category != CategoryHidden {
151-
c.counters[category].hits.Add(1)
151+
c.counters[level.index()][category].hits.Add(1)
152152
}
153153
return value
154154
}
155155
}
156156
c.mu.RUnlock()
157157
if category != CategoryHidden {
158-
c.counters[category].misses.Add(1)
158+
c.counters[level.index()][category].misses.Add(1)
159159
}
160160
return nil
161161
}
@@ -165,7 +165,7 @@ func (c *shard) get(k key, category Category, peekOnly bool) *Value {
165165
// is not in the cache (nil Value), a non-nil readEntry is returned (in which
166166
// case the caller is responsible to dereference the entry, via one of
167167
// unrefAndTryRemoveFromMap(), setReadValue(), setReadError()).
168-
func (c *shard) getWithReadEntry(k key, category Category) (*Value, *readEntry) {
168+
func (c *shard) getWithReadEntry(k key, level Level, category Category) (*Value, *readEntry) {
169169
c.mu.RLock()
170170
if e, _ := c.blocks.Get(k); e != nil {
171171
if value := e.acquireValue(); value != nil {
@@ -174,13 +174,13 @@ func (c *shard) getWithReadEntry(k key, category Category) (*Value, *readEntry)
174174
e.referenced.Store(true)
175175
}
176176
c.mu.RUnlock()
177-
c.counters[category].hits.Add(1)
177+
c.counters[level.index()][category].hits.Add(1)
178178
return value, nil
179179
}
180180
}
181181
re := c.readShard.acquireReadEntry(k)
182182
c.mu.RUnlock()
183-
c.counters[category].misses.Add(1)
183+
c.counters[level.index()][category].misses.Add(1)
184184
return nil, re
185185
}
186186

internal/cache/metrics.go

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,54 @@ package cache
66

77
import (
88
"fmt"
9+
"iter"
910

1011
"github.com/cockroachdb/crlib/crtime"
12+
"github.com/cockroachdb/errors"
13+
"github.com/cockroachdb/pebble/internal/invariants"
1114
)
1215

16+
// Level is the LSM level associated with an accessed block. Used to maintain
17+
// granular cache hit/miss statistics.
18+
//
19+
// The zero value indicates that there is no level (e.g. flushable ingests) or
20+
// it is unknown.
21+
type Level struct {
22+
levelPlusOne int8
23+
}
24+
25+
func (l Level) String() string {
26+
if l.levelPlusOne <= 0 {
27+
return "n/a"
28+
}
29+
return fmt.Sprintf("L%d", l.levelPlusOne-1)
30+
}
31+
32+
// index returns a value between [0, NumLevels).
33+
func (l Level) index() int8 {
34+
return l.levelPlusOne
35+
}
36+
37+
func MakeLevel(l int) Level {
38+
if invariants.Enabled && (l < 0 || l >= NumLevels-1) {
39+
panic(errors.AssertionFailedf("invalid level: %d", l))
40+
}
41+
return Level{levelPlusOne: int8(l + 1)}
42+
}
43+
44+
const NumLevels = 1 /* unknown level */ + 7
45+
46+
// Levels is an iter.Seq[Level].
47+
func Levels(yield func(l Level) bool) {
48+
for i := range NumLevels {
49+
if !yield(Level{levelPlusOne: int8(i)}) {
50+
return
51+
}
52+
}
53+
}
54+
55+
var _ iter.Seq[Level] = Levels
56+
1357
// Category is used to maintain granular cache hit/miss statistics.
1458
type Category int8
1559

@@ -68,16 +112,51 @@ type Metrics struct {
68112

69113
// HitsAndMisses contains the number of cache hits and misses across a period of
70114
// time.
71-
type HitsAndMisses [NumCategories]struct {
115+
type HitsAndMisses [NumLevels][NumCategories]struct {
72116
Hits int64
73117
Misses int64
74118
}
75119

76-
// Aggregate returns the total hits and misses across all categories.
120+
func (hm *HitsAndMisses) Get(level Level, category Category) (hits, misses int64) {
121+
v := hm[level.index()][category]
122+
return v.Hits, v.Misses
123+
}
124+
125+
func (hm *HitsAndMisses) Hits(level Level, category Category) int64 {
126+
return hm[level.index()][category].Hits
127+
}
128+
129+
func (hm *HitsAndMisses) Misses(level Level, category Category) int64 {
130+
return hm[level.index()][category].Misses
131+
}
132+
133+
// Aggregate returns the total hits and misses across all categories and levels.
77134
func (hm *HitsAndMisses) Aggregate() (hits, misses int64) {
78-
for i := range *hm {
79-
hits += hm[i].Hits
80-
misses += hm[i].Misses
135+
for i := range hm {
136+
for j := range hm[i] {
137+
hits += hm[i][j].Hits
138+
misses += hm[i][j].Misses
139+
}
140+
}
141+
return hits, misses
142+
}
143+
144+
// AggregateLevel returns the total hits and misses for a specific level (across
145+
// all categories).
146+
func (hm *HitsAndMisses) AggregateLevel(level Level) (hits, misses int64) {
147+
for _, v := range hm[level.index()] {
148+
hits += v.Hits
149+
misses += v.Misses
150+
}
151+
return hits, misses
152+
}
153+
154+
// AggregateCategory returns the total hits and misses for a specific category
155+
// (across all levels).
156+
func (hm *HitsAndMisses) AggregateCategory(category Category) (hits, misses int64) {
157+
for i := range hm {
158+
hits += hm[i][category].Hits
159+
misses += hm[i][category].Misses
81160
}
82161
return hits, misses
83162
}
@@ -86,9 +165,11 @@ func (hm *HitsAndMisses) Aggregate() (hits, misses int64) {
86165
// current metrics.
87166
// At a high level, hm.ToRecent(current) means hm = current - hm.
88167
func (hm *HitsAndMisses) ToRecent(current *HitsAndMisses) {
89-
for i := range *hm {
90-
hm[i].Hits = current[i].Hits - hm[i].Hits
91-
hm[i].Misses = current[i].Misses - hm[i].Misses
168+
for i := range hm {
169+
for j := range hm[i] {
170+
hm[i][j].Hits = current[i][j].Hits - hm[i][j].Hits
171+
hm[i][j].Misses = current[i][j].Misses - hm[i][j].Misses
172+
}
92173
}
93174
}
94175

@@ -116,8 +197,10 @@ func (c *Cache) hitsAndMisses() HitsAndMisses {
116197
for i := range c.shards {
117198
shardCounters := &c.shards[i].counters
118199
for j := range hm {
119-
hm[j].Hits += shardCounters[j].hits.Load()
120-
hm[j].Misses += shardCounters[j].misses.Load()
200+
for k := range hm[j] {
201+
hm[j][k].Hits += shardCounters[j][k].hits.Load()
202+
hm[j][k].Misses += shardCounters[j][k].misses.Load()
203+
}
121204
}
122205
}
123206
return hm

internal/cache/read_shard_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func newTestReader(
5050
}
5151

5252
func (r *testReader) getAsync(shard *shard) *string {
53-
v, re := shard.getWithReadEntry(r.key, CategorySSTableData)
53+
v, re := shard.getWithReadEntry(r.key, MakeLevel(0), CategorySSTableData)
5454
if v != nil {
5555
str := string(v.RawBuffer())
5656
v.Release()
@@ -285,7 +285,7 @@ func TestReadShardConcurrent(t *testing.T) {
285285
for _, r := range differentReaders {
286286
for j := 0; j < r.numReaders; j++ {
287287
go func(r *testSyncReaders, index int) {
288-
v, rh, _, _, _, err := r.handle.GetWithReadHandle(context.Background(), r.fileNum, r.offset, CategorySSTableData)
288+
v, rh, _, _, _, err := r.handle.GetWithReadHandle(context.Background(), r.fileNum, r.offset, MakeLevel(0), CategorySSTableData)
289289
require.NoError(t, err)
290290
if v != nil {
291291
require.Equal(t, r.val, v.RawBuffer())

level_checker.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,11 +694,15 @@ type valuesInfo struct {
694694
// each blob.BlockID's referenced blob.BlockValueID for the `i`th blob reference.
695695
func gatherBlobHandles(
696696
ctx context.Context,
697+
readEnv block.ReadEnv,
697698
r *sstable.Reader,
698699
blobRefs manifest.BlobReferences,
699700
valueFetcher base.ValueFetcher,
700701
) ([]map[blob.BlockID]valuesInfo, error) {
701702
iter, err := r.NewPointIter(ctx, sstable.IterOptions{
703+
Env: sstable.ReadEnv{
704+
Block: readEnv,
705+
},
702706
BlobContext: sstable.TableBlobContext{
703707
ValueFetcher: valueFetcher,
704708
References: &blobRefs,
@@ -797,7 +801,7 @@ func validateBlobValueLiveness(
797801
// For this sstable, gather all the blob handles -- tracking
798802
// each base.BlobReferenceID + blob.BlockID's referenced
799803
// blob.BlockValueIDs.
800-
referenced, err := gatherBlobHandles(ctx, r, t.BlobReferences, valueFetcher)
804+
referenced, err := gatherBlobHandles(ctx, readEnv.Block, r, t.BlobReferences, valueFetcher)
801805
if err != nil {
802806
return err
803807
}

0 commit comments

Comments
 (0)