Skip to content

Commit 33ce02a

Browse files
committed
fix(kvblock): simplify Redis clear cleanup
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
1 parent 2aff0bf commit 33ce02a

5 files changed

Lines changed: 83 additions & 43 deletions

File tree

pkg/kvcache/kvblock/redis.go

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -154,29 +154,6 @@ var pruneRequestKeyScript = redis.NewScript(`
154154
return 0
155155
`)
156156

157-
// clearPodEntriesScript removes one batch of reverse-index members, deletes
158-
// matching request-key hash fields, and prunes request-key hashes that become
159-
// empty. Malformed members are only removed from the reverse-index set.
160-
var clearPodEntriesScript = redis.NewScript(`
161-
local removed = 0
162-
for i = 1, #ARGV do
163-
local member = ARGV[i]
164-
redis.call('SREM', KEYS[1], member)
165-
local sep = string.find(member, string.char(0), 1, true)
166-
if sep ~= nil then
167-
local requestKey = string.sub(member, 1, sep - 1)
168-
local field = string.sub(member, sep + 1)
169-
if requestKey ~= '' and field ~= '' then
170-
removed = removed + redis.call('HDEL', requestKey, field)
171-
if redis.call('HLEN', requestKey) == 0 then
172-
redis.call('DEL', requestKey)
173-
end
174-
end
175-
end
176-
end
177-
return removed
178-
`)
179-
180157
// pruneEngineKeyScript atomically deletes an engine key mapping only if all
181158
// associated request key hashes are empty. This prevents a TOCTOU race where a
182159
// concurrent Add could insert into a request key between checking and deleting.
@@ -441,6 +418,19 @@ func redisPodEntryMember(requestKey, field string) string {
441418
return requestKey + "\x00" + field
442419
}
443420

421+
type redisPodEntryRef struct {
422+
requestKey string
423+
field string
424+
}
425+
426+
func parseRedisPodEntryMember(member string) (redisPodEntryRef, bool) {
427+
requestKey, field, ok := strings.Cut(member, "\x00")
428+
if !ok || requestKey == "" || field == "" {
429+
return redisPodEntryRef{}, false
430+
}
431+
return redisPodEntryRef{requestKey: requestKey, field: field}, true
432+
}
433+
444434
// Clear removes every reverse-indexed field for the pod across all request-key
445435
// hashes and device tiers. Add maintains an exact per-pod set of
446436
// "<requestKey>\x00<encodedPodField>" entries, so Clear only touches keys this
@@ -450,18 +440,17 @@ func (r *RedisIndex) Clear(ctx context.Context, podIdentifier string) error {
450440
podEntriesKey := redisPodEntriesKey(podIdentifier)
451441

452442
const scanBatch int64 = 1024
453-
removed := 0
443+
processed := 0
454444
var cursor uint64
455445
for {
456446
members, next, err := r.RedisClient.SScan(ctx, podEntriesKey, cursor, "", scanBatch).Result()
457447
if err != nil {
458448
return fmt.Errorf("clear reverse-index scan failed: %w", err)
459449
}
460-
count, err := r.clearRedisPodEntryMembers(ctx, podEntriesKey, members)
461-
if err != nil {
450+
if err := r.clearRedisPodEntryMembers(ctx, podEntriesKey, members); err != nil {
462451
return err
463452
}
464-
removed += count
453+
processed += len(members)
465454

466455
if cursor = next; cursor != 0 {
467456
continue
@@ -475,22 +464,25 @@ func (r *RedisIndex) Clear(ctx context.Context, podIdentifier string) error {
475464
}
476465
}
477466

478-
logger.Info("cleared pod from index", "pod", podIdentifier, "removed", removed)
467+
logger.Info("cleared pod from index", "pod", podIdentifier, "members", processed)
479468
return nil
480469
}
481470

482-
func (r *RedisIndex) clearRedisPodEntryMembers(ctx context.Context, podEntriesKey string, members []string) (int, error) {
471+
func (r *RedisIndex) clearRedisPodEntryMembers(ctx context.Context, podEntriesKey string, members []string) error {
483472
if len(members) == 0 {
484-
return 0, nil
473+
return nil
485474
}
486475

487-
args := make([]interface{}, len(members))
488-
for i, member := range members {
489-
args[i] = member
476+
pipe := r.RedisClient.Pipeline()
477+
for _, member := range members {
478+
ref, ok := parseRedisPodEntryMember(member)
479+
if ok {
480+
pipe.HDel(ctx, ref.requestKey, ref.field)
481+
}
482+
pipe.SRem(ctx, podEntriesKey, member)
490483
}
491-
removed, err := clearPodEntriesScript.Run(ctx, r.RedisClient, []string{podEntriesKey}, args...).Int()
492-
if err != nil && !errors.Is(err, redis.Nil) {
493-
return 0, fmt.Errorf("clear reverse-index batch failed: %w", err)
484+
if _, err := pipe.Exec(ctx); err != nil {
485+
return fmt.Errorf("clear reverse-index pipeline failed: %w", err)
494486
}
495-
return removed, nil
487+
return nil
496488
}

pkg/kvcache/kvblock/redis_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ func TestRedisEvictPrunesReverseIndex(t *testing.T) {
106106
require.Zero(t, count)
107107
}
108108

109+
func TestRedisClearDropsMalformedReverseEntry(t *testing.T) {
110+
index := createRedisIndexForInspection(t)
111+
ctx := t.Context()
112+
podEntriesKey := "kvblock:pod:pod-malformed:entries"
113+
114+
require.NoError(t, index.RedisClient.SAdd(ctx, podEntriesKey, "malformed").Err())
115+
require.NoError(t, index.Clear(ctx, "pod-malformed"))
116+
117+
count, err := index.RedisClient.SCard(ctx, podEntriesKey).Result()
118+
require.NoError(t, err)
119+
require.Zero(t, count)
120+
}
121+
109122
func redisTestPodField(t *testing.T, entry PodEntry) string {
110123
t.Helper()
111124
value, err := json.Marshal(entry)

tests/profiling/kv_cache_index/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ In another shell:
6767
KV_CACHE_BENCH_REDIS_ADDR='redis://127.0.0.1:6380' \
6868
go test -tags redis_real_bench ./tests/profiling/kv_cache_index \
6969
-run '^$' \
70-
-bench 'BenchmarkRedisClearReal(SharedKeyspace|PodFanout)' \
70+
-bench 'BenchmarkRedisClearReal(Scale|SharedKeyspace|PodFanout)' \
7171
-benchmem \
7272
-benchtime=3x \
7373
-count=1

tests/profiling/kv_cache_index/redis_clear_benchmark_test.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import (
3030
"sigs.k8s.io/controller-runtime/pkg/log"
3131
)
3232

33-
const redisClearTargetPod = "pod-0"
33+
const (
34+
redisClearTargetPod = "pod-0"
35+
redisClearSeedBatchSize = 1024
36+
)
3437

3538
type redisClearBenchmarkCase struct {
3639
name string
@@ -166,8 +169,11 @@ func seedRedisClearReverseIndexData(
166169
}
167170
for pod := 0; pod < tc.podCount; pod++ {
168171
entry := kvblock.PodEntry{PodIdentifier: fmt.Sprintf("pod-%d", pod), DeviceTier: "gpu"}
169-
if err := index.Add(ctx, nil, requestKeys, []kvblock.PodEntry{entry}); err != nil {
170-
b.Fatalf("failed to seed redis data through Add: %v", err)
172+
for start := 0; start < len(requestKeys); start += redisClearSeedBatchSize {
173+
end := min(start+redisClearSeedBatchSize, len(requestKeys))
174+
if err := index.Add(ctx, nil, requestKeys[start:end], []kvblock.PodEntry{entry}); err != nil {
175+
b.Fatalf("failed to seed redis data through Add: %v", err)
176+
}
171177
}
172178
}
173179
}
@@ -180,16 +186,23 @@ func seedRedisClearLegacyData(
180186
) {
181187
b.Helper()
182188
pipe := index.RedisClient.Pipeline()
189+
pending := 0
183190
for key := 0; key < tc.requestKeys; key++ {
184191
redisKey := redisClearBlockHash(b, key+1).String()
185192
for pod := 0; pod < tc.podCount; pod++ {
186193
entry := kvblock.PodEntry{PodIdentifier: fmt.Sprintf("pod-%d", pod), DeviceTier: "gpu"}
187194
field := redisClearPodField(b, entry)
188195
pipe.HSet(ctx, redisKey, field, "")
196+
pending++
197+
if pending == redisClearSeedBatchSize {
198+
execRedisClearSeedPipeline(b, ctx, pipe)
199+
pipe = index.RedisClient.Pipeline()
200+
pending = 0
201+
}
189202
}
190203
}
191-
if _, err := pipe.Exec(ctx); err != nil {
192-
b.Fatalf("failed to seed redis data: %v", err)
204+
if pending > 0 {
205+
execRedisClearSeedPipeline(b, ctx, pipe)
193206
}
194207
}
195208

@@ -199,10 +212,24 @@ func seedUnrelatedRedisData(b *testing.B, ctx context.Context, index *kvblock.Re
199212
return
200213
}
201214
pipe := index.RedisClient.Pipeline()
215+
pending := 0
202216
for key := 0; key < count; key++ {
203217
field := redisClearPodField(b, kvblock.PodEntry{PodIdentifier: "other", DeviceTier: "gpu"})
204218
pipe.HSet(ctx, fmt.Sprintf("unrelated:%d", key), field, "")
219+
pending++
220+
if pending == redisClearSeedBatchSize {
221+
execRedisClearSeedPipeline(b, ctx, pipe)
222+
pipe = index.RedisClient.Pipeline()
223+
pending = 0
224+
}
225+
}
226+
if pending > 0 {
227+
execRedisClearSeedPipeline(b, ctx, pipe)
205228
}
229+
}
230+
231+
func execRedisClearSeedPipeline(b *testing.B, ctx context.Context, pipe redis.Pipeliner) {
232+
b.Helper()
206233
if _, err := pipe.Exec(ctx); err != nil {
207234
b.Fatalf("failed to seed redis data: %v", err)
208235
}

tests/profiling/kv_cache_index/redis_clear_real_benchmark_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ import (
2525

2626
const redisClearRealAddressEnv = "KV_CACHE_BENCH_REDIS_ADDR"
2727

28+
func BenchmarkRedisClearRealScaleLegacyFullScan(b *testing.B) {
29+
benchmarkRedisClearRealCases(b, false, redisClearScaleCases)
30+
}
31+
32+
func BenchmarkRedisClearRealScaleReverseIndex(b *testing.B) {
33+
benchmarkRedisClearRealCases(b, true, redisClearScaleCases)
34+
}
35+
2836
func BenchmarkRedisClearRealSharedKeyspaceLegacyFullScan(b *testing.B) {
2937
benchmarkRedisClearRealCases(b, false, redisClearSharedKeyspaceCases)
3038
}

0 commit comments

Comments
 (0)