Skip to content

Commit dbe01da

Browse files
committed
[manager/meta] optimize ReportEvent and host cache hot paths
Optimize the pure-local ReportEvent pipeline end to end: parse large HTTP bodies in request-owned buffers, use SIMD ASCII and NUL scans, flatten and intern request mutations, canonicalize versioned URIs without repeated parsing, inline the dominant one-spec task shape, and remove redundant temporary allocations. Fuse exact single-location local RMW while preserving capacity, key-count, lifecycle, partial-failure and non-local backend semantics. Reuse LRU handles and bounded scratch, align metadata and local-LRU shard hashing, retire immutable values outside locks, and retain strict validation for every generic caller. Accelerate GetHostCacheState with a bounded 4096-key probe and 16384-key parallel suffix, monotonic cancellation, ordinary and Mamba bitsets, request-scoped visibility parsing, serving-only filtering and strict non-NOENT error propagation. Preserve full materialization for P2P semantics and fail closed for malformed recovered EventReport URIs. Add parser, URI, backend, capacity, lifecycle, concurrency, randomized differential, cancellation and ordered-error coverage. Document performance results, design invariants and rollback boundaries. Preserve the latest mu-main ReportEvent metrics commit unchanged. Release validation on the merged mu-main tree passes 106 executable Bazel tests with one GPU-only skip. Pure-local measurements include about 106.32ms p50 for a 1M-key all-hit query with 4 workers, 84.38ms with 8 workers, and about 0.46ms for first-window cancellation; shared-host results are regression data, not an online SLA.
1 parent 334a991 commit dbe01da

44 files changed

Lines changed: 7285 additions & 636 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/api/report_event.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,7 @@ heartbeat/grace 短时序测试使用独立 storage/instance group,不得缩
822822
| D-12 | KVCM 只追加一个合法 s_version,不改变客户端 URI 其他部分 | 基础集成 `_assert_profile_specs_in_locations`;snapshot 集成 `_assert_reporter_scope` |
823823
| D-13 | 第一条 delta 已创建 generation、metadata 写失败时准确报错,重试复用 generation | `TestReportEventFirstDeltaMetadataFailureReportsFailureAndReusesGeneration` |
824824
| D-14 | 同一 spec 的折叠事件共享最终 metadata 写入失败结果 | `TestReportEventFoldedDeltaEventsShareFinalWriteFailure` |
825+
| D-15 | 大于 32 KiB 的部分失败批次保持逐项结果与输入索引严格对齐,只重试失败项后复用 generation 并最终收敛 | snapshot 集成 `test_34_large_partial_batch_preserves_item_alignment_and_retry` |
825826
| S-01 | snapshot 跨 medium 完整上报、响应返回 generation | `TestReportEventSnapshotReplacesCompleteSpecSetPerBlock`;snapshot 集成 `test_17/22` |
826827
| S-02 | 同 block 跨 medium 合法,同 block+medium 重复非法 | `TestReportEventRejectsCanonicalDuplicateSnapshotKeysButAllowsDifferentMedia`;snapshot 集成 `test_27_*` |
827828
| S-03 | snapshot block 内重复 spec name 被拒绝 | `TestReportEventRejectsDuplicateSpecNamesWithinSnapshotBlock` |

docs/design/report_event_performance.md

Lines changed: 939 additions & 4 deletions
Large diffs are not rendered by default.

docs/prometheus-en_US.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ every `kvcm.metrics.report_interval_ms`, default 20s).
133133
| `manager.get_cache_location_hit_block_counter` | counter | Total blocks hit via GetCacheLocation (cumulative) |
134134
| `manager.prefix_match_time_us` | gauge | Outer total latency for GetHostCacheState-style prefix matching (us) |
135135
| `meta_searcher.indexer_get_time_us` | gauge | Wall time spent reading metadata through MetaIndexer (us) |
136-
| `meta_indexer.get_io_time_us` | gauge | Metadata-backend wall time; local mode includes LRU/locks/copies and does not imply Redis I/O |
137-
| `meta_searcher.host_projection_time_us` | gauge | GetHostCacheState visibility checks and host/spec projection time (us) |
136+
| `meta_indexer.get_io_time_us` | gauge | Wall-time union of metadata-backend call intervals; local mode includes LRU/locks/copies, excludes parallel projection, and does not imply Redis I/O |
137+
| `meta_searcher.host_projection_time_us` | gauge | Wall-time union of GetHostCacheState visibility and host/spec projection callback intervals (us) |
138138
| `meta_searcher.host_prefix_reduce_time_us` | gauge | GetHostCacheState normal/Mamba host-prefix reduction time (us) |
139139
| `meta_indexer.search_cache_hit_ratio` | gauge | Search cache hit ratio |
140140
| `data_storage.create_keys_counter` | counter | Total created keys |
@@ -158,8 +158,9 @@ The full list depends on the active `MetricsReporter` type. The
158158
The GetHostCacheState phase metrics above are nested:
159159
`meta_indexer.get_io_time_us` is inside `meta_searcher.indexer_get_time_us`, and
160160
the indexer/projection/reduction phases are inside `manager.prefix_match_time_us`.
161-
Do not add them together. `get_io_time_us` is a historical name; with
162-
`storage_type=local` it contains no Redis network operation.
161+
Progressive local queries pipeline backend reads with projection, so these
162+
intervals can overlap and must not be added together. `get_io_time_us` is a
163+
historical name; with `storage_type=local` it contains no Redis network operation.
163164

164165
## Mapping to KMonitor Metrics
165166

docs/prometheus-zh_CN.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ kvcm_data_storage_storage_usage_ratio{type="nfs",unique_name="store_02"} 0.3
130130
| `manager.get_cache_location_hit_block_counter` | counter | GetCacheLocation 命中的 Block 总数(累计) |
131131
| `manager.prefix_match_time_us` | gauge | GetHostCacheState 等前缀匹配的外层总延迟(微秒) |
132132
| `meta_searcher.indexer_get_time_us` | gauge | MetaSearcher 调用 MetaIndexer 读取 metadata 的墙钟时间(微秒) |
133-
| `meta_indexer.get_io_time_us` | gauge | metadata backend 调用墙钟时间;local 模式也包含 LRU/锁/复制,并不表示 Redis I/O |
134-
| `meta_searcher.host_projection_time_us` | gauge | GetHostCacheState location 可见性检查及 host/spec 投影时间(微秒) |
133+
| `meta_indexer.get_io_time_us` | gauge | metadata backend 调用区间的墙钟并集;local 模式包含 LRU/锁/复制,不包含并行 projection,也不表示 Redis I/O |
134+
| `meta_searcher.host_projection_time_us` | gauge | GetHostCacheState location 可见性及 host/spec projection 回调区间的墙钟并集(微秒) |
135135
| `meta_searcher.host_prefix_reduce_time_us` | gauge | GetHostCacheState 普通/Mamba host 前缀归约时间(微秒) |
136136
| `meta_indexer.search_cache_hit_ratio` | gauge | 搜索缓存命中率 |
137137
| `data_storage.create_keys_counter` | counter | 已创建 key 总数 |
@@ -154,8 +154,8 @@ kvcm_data_storage_storage_usage_ratio{type="nfs",unique_name="store_02"} 0.3
154154

155155
上述 GetHostCacheState 分段指标是嵌套关系:`meta_indexer.get_io_time_us` 位于
156156
`meta_searcher.indexer_get_time_us` 内,后者与 projection/reduce 又位于
157-
`manager.prefix_match_time_us`,排障时不能把它们相加。`get_io_time_us` 是历史命名;当实例使用
158-
`storage_type=local` 时没有 Redis 网络调用。
157+
`manager.prefix_match_time_us`。渐进 local 查询会流水化 backend read 与 projection,两者可能重叠,排障时
158+
不能把它们相加。`get_io_time_us` 是历史命名;当实例使用 `storage_type=local` 时没有 Redis 网络调用。
159159

160160
## 与 KMonitor 指标对照
161161

integration_test/meta_service/test_report_event_snapshot.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3970,6 +3970,132 @@ def assert_single_invalid(body):
39703970
baseline_version,
39713971
)
39723972

3973+
def test_34_large_partial_batch_preserves_item_alignment_and_retry(self):
3974+
host = f"large-partial-{time.time_ns()}:8080"
3975+
base_key = 34_000_000_000 + time.time_ns() % 1_000_000_000
3976+
event_count = 512
3977+
invalid_indices = set(range(0, event_count, 17))
3978+
3979+
self.client.report_event(
3980+
_make_request(
3981+
self.instance_id,
3982+
host,
3983+
[_ev_node_register(["mem"])],
3984+
trace_id="t34_register",
3985+
)
3986+
)
3987+
3988+
events = []
3989+
raw_uris = {}
3990+
for index in range(event_count):
3991+
block_key = base_key + index
3992+
raw_uri = _build_event_report_uri(
3993+
host,
3994+
"mem",
3995+
{"size": str(index + 1), "source": f"large_{index}"},
3996+
)
3997+
raw_uris[index] = raw_uri
3998+
events.append(
3999+
_ev_block_add(
4000+
block_key,
4001+
"mem",
4002+
_make_single_spec(
4003+
"tp0",
4004+
"not-a-uri" if index in invalid_indices else raw_uri,
4005+
),
4006+
)
4007+
)
4008+
4009+
payload = _make_request(
4010+
self.instance_id,
4011+
host,
4012+
events,
4013+
trace_id="t34_large_partial",
4014+
)
4015+
self.assertGreater(len(json.dumps(payload)), 32 * 1024)
4016+
partial = self.client.report_event(payload, check_ok=False)
4017+
self.assertEqual(
4018+
partial.get("header", {}).get("status", {}).get("code"),
4019+
"INVALID_ARGUMENT",
4020+
partial,
4021+
)
4022+
self.assertEqual(
4023+
partial.get("item_results"),
4024+
[
4025+
"INVALID_ARGUMENT" if index in invalid_indices else "OK"
4026+
for index in range(event_count)
4027+
],
4028+
)
4029+
generation = partial.get("committed_snapshot_version", "")
4030+
self.assertEqual(len(generation), 32)
4031+
self.assertTrue(partial.get("snapshot_required"))
4032+
4033+
valid_samples = [1, event_count // 2 + 1, event_count - 1]
4034+
invalid_samples = [0, 17, max(invalid_indices)]
4035+
for index in valid_samples:
4036+
specs = _query_block_specs(
4037+
self.client,
4038+
self.instance_id,
4039+
base_key + index,
4040+
f"t34_valid_{index}",
4041+
)
4042+
self.assertEqual(len(specs), 1)
4043+
self.assertEqual(specs[0].get("name"), "tp0")
4044+
self.assertEqual(
4045+
_uri_identity_without_snapshot_version(specs[0]["uri"]),
4046+
_uri_identity_without_snapshot_version(raw_uris[index]),
4047+
)
4048+
self.assertEqual(
4049+
_snapshot_version_from_uri(self, specs[0]["uri"]),
4050+
generation,
4051+
)
4052+
for index in invalid_samples:
4053+
self.assertEqual(
4054+
_query_block_specs(
4055+
self.client,
4056+
self.instance_id,
4057+
base_key + index,
4058+
f"t34_invalid_absent_{index}",
4059+
),
4060+
[],
4061+
)
4062+
4063+
retry_events = [
4064+
_ev_block_add(
4065+
base_key + index,
4066+
"mem",
4067+
_make_single_spec("tp0", raw_uris[index]),
4068+
)
4069+
for index in sorted(invalid_indices)
4070+
]
4071+
retry = self.client.report_event(
4072+
_make_request(
4073+
self.instance_id,
4074+
host,
4075+
retry_events,
4076+
trace_id="t34_retry_failed_only",
4077+
)
4078+
)
4079+
self.assertEqual(retry.get("item_results", []), [])
4080+
self.assertEqual(retry.get("committed_snapshot_version"), generation)
4081+
self.assertFalse(retry.get("snapshot_required"))
4082+
for index in invalid_samples:
4083+
specs = _query_block_specs(
4084+
self.client,
4085+
self.instance_id,
4086+
base_key + index,
4087+
f"t34_retry_visible_{index}",
4088+
)
4089+
self.assertEqual(len(specs), 1)
4090+
self.assertEqual(
4091+
_uri_identity_without_snapshot_version(specs[0]["uri"]),
4092+
_uri_identity_without_snapshot_version(raw_uris[index]),
4093+
)
4094+
self.assertEqual(
4095+
_snapshot_version_from_uri(self, specs[0]["uri"]),
4096+
generation,
4097+
)
4098+
39734099
# ---------------------------------------------------------------------------
39744100
# Bench tests
39754101
# ---------------------------------------------------------------------------

kv_cache_manager/common/cache/advanced_cache.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <memory>
1414
#include <string>
1515
#include <string_view>
16+
#include <vector>
1617

1718
#include "kv_cache_manager/common/cache/cache.h"
1819

@@ -43,6 +44,16 @@ class Cache {
4344
// Opaque handle to an entry stored in the cache.
4445
struct Handle {};
4546

47+
// Caller-owned workspace for allocation-free repeated batch lookup and
48+
// release. The fields are intentionally generic storage owned by Cache;
49+
// callers only prepare and pass the object back to the same cache.
50+
struct BatchOperationScratch {
51+
std::vector<uint32_t> hashes;
52+
std::vector<size_t> shard_offsets;
53+
std::vector<size_t> cursors;
54+
std::vector<size_t> ordered_indices;
55+
};
56+
4657
public: // types hidden from Cache implementation
4758
// Pointer to cached object of unspecified type. (This type alias is
4859
// provided for clarity, not really for type checking.)
@@ -310,6 +321,21 @@ class Cache {
310321
}
311322
}
312323

324+
// Reserve implementation-specific batch workspace before entering a
325+
// caller's critical section. The default backend needs no workspace.
326+
virtual void PrepareBatchOperationScratch(size_t /*max_count*/, BatchOperationScratch * /*scratch*/) {}
327+
328+
// Scratch-aware variants preserve the ordinary Handle lifetime contract.
329+
// Backends without a specialized implementation delegate to the existing
330+
// methods, so decorators and alternate cache implementations keep their
331+
// behavior unchanged.
332+
virtual void LookupBatchWithScratch(const std::string_view *keys,
333+
size_t count,
334+
Handle **out_handles,
335+
BatchOperationScratch * /*scratch*/) {
336+
LookupBatch(keys, count, out_handles);
337+
}
338+
313339
// Convenience wrapper when secondary cache not supported
314340
inline Handle *BasicLookup(const std::string_view &key, Statistics *stats) {
315341
return Lookup(key, nullptr, nullptr, Priority::LOW, stats);
@@ -344,6 +370,10 @@ class Cache {
344370
}
345371
}
346372

373+
virtual void ReleaseBatchWithScratch(Handle *const *handles, size_t count, BatchOperationScratch * /*scratch*/) {
374+
ReleaseBatch(handles, count);
375+
}
376+
347377
// Return the object assiciated with a handle returned by a successful
348378
// Lookup(). For historical reasons, this is also known at the "value"
349379
// associated with the key.
@@ -664,6 +694,17 @@ class CacheWrapper : public Cache {
664694
target_->LookupBatch(keys, count, out_handles);
665695
}
666696

697+
void PrepareBatchOperationScratch(size_t max_count, BatchOperationScratch *scratch) override {
698+
target_->PrepareBatchOperationScratch(max_count, scratch);
699+
}
700+
701+
void LookupBatchWithScratch(const std::string_view *keys,
702+
size_t count,
703+
Handle **out_handles,
704+
BatchOperationScratch *scratch) override {
705+
target_->LookupBatchWithScratch(keys, count, out_handles, scratch);
706+
}
707+
667708
bool Ref(Handle *handle) override { return target_->Ref(handle); }
668709

669710
using Cache::Release;
@@ -673,6 +714,10 @@ class CacheWrapper : public Cache {
673714

674715
void ReleaseBatch(Handle *const *handles, size_t count) override { target_->ReleaseBatch(handles, count); }
675716

717+
void ReleaseBatchWithScratch(Handle *const *handles, size_t count, BatchOperationScratch *scratch) override {
718+
target_->ReleaseBatchWithScratch(handles, count, scratch);
719+
}
720+
676721
ObjectPtr Value(Handle *handle) override { return target_->Value(handle); }
677722

678723
bool Exists(const std::string_view &key) override { return target_->Exists(key); }

kv_cache_manager/common/cache/lru_cache.cc

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -842,14 +842,40 @@ const Cache::CacheItemHelper *LRUCache::GetCacheItemHelper(Handle *handle) const
842842
}
843843

844844
void LRUCache::LookupBatch(const std::string_view *keys, size_t count, Handle **out_handles) {
845+
BatchOperationScratch scratch;
846+
PrepareBatchOperationScratch(count, &scratch);
847+
LookupBatchWithScratch(keys, count, out_handles, &scratch);
848+
}
849+
850+
void LRUCache::PrepareBatchOperationScratch(size_t max_count, BatchOperationScratch *scratch) {
851+
if (!scratch) {
852+
return;
853+
}
854+
scratch->hashes.reserve(max_count);
855+
scratch->ordered_indices.reserve(max_count);
856+
const size_t shard_slots = GetNumShards() + 1;
857+
scratch->shard_offsets.reserve(shard_slots);
858+
scratch->cursors.reserve(shard_slots);
859+
}
860+
861+
void LRUCache::LookupBatchWithScratch(const std::string_view *keys,
862+
size_t count,
863+
Handle **out_handles,
864+
BatchOperationScratch *scratch) {
845865
if (count == 0) {
846866
return;
847867
}
868+
if (!scratch) {
869+
LookupBatch(keys, count, out_handles);
870+
return;
871+
}
848872
std::fill(out_handles, out_handles + count, nullptr);
849873

850874
const size_t shard_count = GetNumShards();
851-
std::vector<uint32_t> hashes(count);
852-
std::vector<size_t> shard_offsets(shard_count + 1, 0);
875+
scratch->hashes.resize(count);
876+
scratch->shard_offsets.assign(shard_count + 1, 0);
877+
auto &hashes = scratch->hashes;
878+
auto &shard_offsets = scratch->shard_offsets;
853879
for (size_t i = 0; i < count; ++i) {
854880
hashes[i] = LRUCacheShard::ComputeHash(keys[i], hash_seed_);
855881
const size_t shard = LRUCacheShard::HashPieceForSharding(hashes[i]) & shard_mask_;
@@ -859,8 +885,10 @@ void LRUCache::LookupBatch(const std::string_view *keys, size_t count, Handle **
859885
shard_offsets[shard + 1] += shard_offsets[shard];
860886
}
861887

862-
std::vector<size_t> cursors = shard_offsets;
863-
std::vector<size_t> ordered_indices(count);
888+
scratch->cursors.assign(shard_offsets.begin(), shard_offsets.end());
889+
scratch->ordered_indices.resize(count);
890+
auto &cursors = scratch->cursors;
891+
auto &ordered_indices = scratch->ordered_indices;
864892
for (size_t i = 0; i < count; ++i) {
865893
const size_t shard = LRUCacheShard::HashPieceForSharding(hashes[i]) & shard_mask_;
866894
ordered_indices[cursors[shard]++] = i;
@@ -877,12 +905,23 @@ void LRUCache::LookupBatch(const std::string_view *keys, size_t count, Handle **
877905
}
878906

879907
void LRUCache::ReleaseBatch(Handle *const *handles, size_t count) {
908+
BatchOperationScratch scratch;
909+
PrepareBatchOperationScratch(count, &scratch);
910+
ReleaseBatchWithScratch(handles, count, &scratch);
911+
}
912+
913+
void LRUCache::ReleaseBatchWithScratch(Handle *const *handles, size_t count, BatchOperationScratch *scratch) {
880914
if (count == 0) {
881915
return;
882916
}
917+
if (!scratch) {
918+
ReleaseBatch(handles, count);
919+
return;
920+
}
883921

884922
const size_t shard_count = GetNumShards();
885-
std::vector<size_t> shard_offsets(shard_count + 1, 0);
923+
scratch->shard_offsets.assign(shard_count + 1, 0);
924+
auto &shard_offsets = scratch->shard_offsets;
886925
size_t non_null_count = 0;
887926
for (size_t i = 0; i < count; ++i) {
888927
if (handles[i] == nullptr) {
@@ -897,8 +936,10 @@ void LRUCache::ReleaseBatch(Handle *const *handles, size_t count) {
897936
shard_offsets[shard + 1] += shard_offsets[shard];
898937
}
899938

900-
std::vector<size_t> cursors = shard_offsets;
901-
std::vector<size_t> ordered_indices(non_null_count);
939+
scratch->cursors.assign(shard_offsets.begin(), shard_offsets.end());
940+
scratch->ordered_indices.resize(non_null_count);
941+
auto &cursors = scratch->cursors;
942+
auto &ordered_indices = scratch->ordered_indices;
902943
for (size_t i = 0; i < count; ++i) {
903944
if (handles[i] == nullptr) {
904945
continue;

kv_cache_manager/common/cache/lru_cache.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,13 @@ class LRUCache
511511
const CacheItemHelper *GetCacheItemHelper(Handle *handle) const override;
512512

513513
void LookupBatch(const std::string_view *keys, size_t count, Handle **out_handles) override;
514+
void PrepareBatchOperationScratch(size_t max_count, BatchOperationScratch *scratch) override;
515+
void LookupBatchWithScratch(const std::string_view *keys,
516+
size_t count,
517+
Handle **out_handles,
518+
BatchOperationScratch *scratch) override;
514519
void ReleaseBatch(Handle *const *handles, size_t count) override;
520+
void ReleaseBatchWithScratch(Handle *const *handles, size_t count, BatchOperationScratch *scratch) override;
515521

516522
void ApplyToHandle(Cache *cache,
517523
Handle *handle,

kv_cache_manager/data_storage/event_report_backend.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,13 @@ ErrorCode EventReportBackend::Close() {
161161
retired_.store(true, std::memory_order_release);
162162
SetOpen(false);
163163
SetAvailable(false);
164-
liveness_checker_running_.store(false, std::memory_order_release);
164+
// Serialize the predicate update with wait_for(). An atomic predicate is
165+
// not sufficient to prevent a lost notification when the waiter is
166+
// between checking the predicate and actually sleeping.
167+
{
168+
std::lock_guard<std::mutex> wait_guard(liveness_wait_mutex_);
169+
liveness_checker_running_.store(false, std::memory_order_release);
170+
}
165171
liveness_wait_cv_.notify_all();
166172
if (liveness_checker_thread_.joinable()) {
167173
liveness_checker_thread_.join();

0 commit comments

Comments
 (0)