diff --git a/docs/analyze/offload_failure_log_matrix.html b/docs/analyze/offload_failure_log_matrix.html new file mode 100644 index 0000000000..8e8cb5d47e --- /dev/null +++ b/docs/analyze/offload_failure_log_matrix.html @@ -0,0 +1,165 @@ + + + + + + Mooncake Offload Failure Log Matrix + + + +

enable_offload=true, offload_on_evict=false 场景下 offload 失败路径与日志矩阵

+

说明:“是否有 ERROR 日志”按代码里的 LOG(ERROR) / MC_LOG(ERROR) 统计;WARNING/INFO 在备注中单独标注。

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
阶段失败路径 / 条件错误码 / 结果是否有 ERROR 日志日志位置 / 备注
Client 初始化RegisterLocalMemory() 失败返回底层错误FileStorage::Init() 打印 Failed to register local memory
Client 初始化storage backend Init() 失败返回底层错误FileStorage::Init() 打印 Failed to init storage backend
Client 初始化IsEnableOffloading() 返回错误返回底层错误FileStorage::Init() 打印 Failed to get enable persist result
Client 初始化IsEnableOffloading() 返回 false挂载成功但 enable_offloading_=false无 ERROR只有 INFO:IsEnableOffloading result: false
Client 初始化MountLocalDiskSegment() 失败返回底层错误Client 打印 Failed to mount file storage;Master 未启用 offload 时也有 ERROR
PutEnd 入队MEMORY replica 没有 segment namePushOffloadingQueue() 返回 OK 但不入队静默跳过
PutEnd 入队segment name 找不到 clientSEGMENT_NOT_FOUNDPushOffloadingQueue() 打印 Segment ... not foundPutEnd() 不再额外打印
PutEnd 入队client local disk segment 不存在UNABLE_OFFLOADINGPutEnd() 忽略返回值
PutEnd 入队local disk segment enable_offloading=falseUNABLE_OFFLOADINGPutEnd() 忽略返回值
PutEnd 入队offloading queue 达到 offloading_queue_limit_KEYS_ULTRA_LIMITPutEnd() 忽略返回值
PutEnd 入队同一个 tenant/key 已在 queue 中OBJECT_ALREADY_EXISTSPutEnd() 忽略返回值
Heartbeat 下发Master 找不到 client local disk segmentSEGMENT_NOT_FOUNDMaster 打印 Local disk segment not found;Client 先 WARNING 并尝试 remount
Heartbeat 下发remount 后 heartbeat 仍失败返回 heartbeat 错误Client 打印 Heartbeat failed after re-registration
Heartbeat 下发remount local disk segment 失败返回 remount 错误Client 打印 Failed to re-register local disk segment
Heartbeat 下发Client 传 enable_offloading=falseMaster 清空 pending queue无 ERROR代码路径无 ERROR;会清理 task/refcnt
Heartbeat 下发RPC / Master 其他错误返回底层错误Client 打印 Failed to send heartbeat with error
源数据查询BatchQuery() 返回空INVALID_REPLICA上层有本函数无 ERROR;调用方打印 BatchQuerySlices failed
源数据查询key 不存在 / metadata 查询失败原始错误码BatchQuerySegmentSlices() 打印 Key not found;调用方也打印 ERROR
源数据查询找不到本地 MEMORY replicaINVALID_KEYBatchQuerySegmentSlices() 打印 Key not found;调用方也打印 ERROR
Bucket 分组object size 大于 bucket_size_limit跳过该 objectGroupOffloadingKeysByBucket() 打印 Object size exceeds bucket size limit
Bucket 分组IsExist() 检查失败记录错误但继续打印 Failed to check existence in storage backend
Bucket 分组key 已存在于 SSD backend跳过静默跳过
Bucket 分组凑不满 bucket放入 ungrouped_offloading_objects_无 ERROR只有 VLOG
Bucket 分组AllocateOffloadingBuckets() 返回错误返回错误OffloadObjects() 打印 AllocateOffloadingBuckets failed
GPU 源数据device slice D2H copy 失败跳过该 objectOffloadObjects() 打印 D2H staging failed for key
GPU 源数据D2H 后整批为空调用 backend 后可能 INVALID_KEYbackend 打印 empty batch;Client 打印 Failed to store objects
Backend 通用BatchOffload() 收到空 batchINVALID_KEY各 backend 均有 empty batch ERROR
Backend 通用IsEnableOffloading() 出错返回底层错误视 backendBucket 有 Failed to get store metadata;Offset 未初始化有 ERROR;File-per-key meta 未加载有 ERROR
Backend 容量IsEnableOffloading() 返回 falseKEYS_ULTRA_LIMIT上层有backend 本身无 ERROR;Client 打印 Failed to store objects with error,并置 enable_offloading_=false
Bucket backendbackend 未初始化INTERNAL_ERRORStorage backend is not initialized
Bucket backendBuildBucket() object slice 为空INVALID_KEYFailed to create bucket, object is empty;调用方再打印 Failed to build bucket
Bucket backend获取 bucket data path 失败INTERNAL_ERRORFailed to get bucket data path
Bucket backend打开 bucket 文件失败FILE_OPEN_FAILFailed to open file for bucket writing
Bucket backendaligned buffer 分配失败INTERNAL_ERRORFailed to allocate aligned buffer for WriteBucket
Bucket backendwrite_aligned() 失败底层错误码write_aligned failed
Bucket backendvector_write() 失败底层错误码vector_write failed
Bucket backend写入字节数不匹配FILE_WRITE_FAILWrite size mismatch
Bucket backenddatasync() 失败FILE_WRITE_FAILdatasync failed for bucket
Bucket backend写 bucket metadata 失败FILE_WRITE_FAILFailed to store bucket metadata;清理 orphan 失败也会 ERROR
Bucket backendNotifyOffloadSuccess() 失败Master 错误码Client 打印 NotifyOffloadSuccess failed;backend 打印 Complete handler failed
Bucket backendcommit metadata 前发现 duplicate keyOBJECT_ALREADY_EXISTS无 ERROR只有 WARNING:Duplicate key detected
File-per-key backend单 key StoreObject() 失败跳过该 keyFailed to store object for key
File-per-key backendcomplete handler 失败Master 错误码Complete handler failed
File-per-key backendtest failure predicate 命中跳过该 key无 ERROR只有 INFO:[TEST] Injecting failure
Offset allocator backendbackend 未初始化INTERNAL_ERRORStorage backend is not initialized
Offset allocator backendslice 为空跳过该 key静默跳过
Offset allocator backendtest failure predicate 命中跳过该 key无 ERROR只有 INFO
Offset allocator backendallocator 分配空间失败停止处理本 batchFailed to allocate ... bytes for key
Offset allocator backendvector_write() 失败跳过该 keyFailed to write record for key
Offset allocator backend写入字节数不匹配跳过该 keyWrite size mismatch for key
Offset allocator backendcomplete handler 失败Master 错误码Complete handler failed
Notify Mastertasks.size() != metadatas.size()INVALID_PARAMS上层有Master 本函数无 ERROR;Client complete handler 打印 NotifyOffloadSuccess failed
Notify MasterAddReplica() 失败,且不是 OBJECT_NOT_FOUND返回 AddReplica 错误Master 打印 Failed to add replica
Notify Masterobject 已不存在OBJECT_NOT_FOUND 被忽略Master 显式忽略该错误
任务超时offloading_tasks 超过 put_start_release_timeout_sec_清理 task 并 dec refcnt无 ERROR只有 WARNING:Offloading task expired for key
Client 失活Master 清理 stale handles / unmount local disk segmentmetadata / offloading task 被清理视具体路径相关清理路径不一定有 ERROR;后续 heartbeat 找不到 segment 时会有 ERROR
+
+ + diff --git a/docs/offload-mechanism.md b/docs/offload-mechanism.md new file mode 100644 index 0000000000..5fe72e2db3 --- /dev/null +++ b/docs/offload-mechanism.md @@ -0,0 +1,331 @@ +# Mooncake SSD Offload 机制 + +## 1. 核心概念 + +Offload 是 Mooncake 将数据从 **DRAM(MEMORY副本)** 迁移到 **本地 SSD(LOCAL_DISK副本)** 的过程。与 Eviction(直接丢弃)不同,Offload 将数据持久化到磁盘,后续可通过 Load 路径读回。 + +``` +MEMORY副本 ──Offload──→ LOCAL_DISK副本 ──Promotion──→ MEMORY副本 + │ │ + └──Eviction(丢弃) └──Disk Eviction(丢弃) +``` + +## 2. 核心数据流 + +### 2.1 Offload(内存 → SSD) + +```mermaid +sequenceDiagram + participant FS as FileStorage (Client) + participant M as MasterService + + loop 每隔 heartbeat_interval (默认10s) + FS->>M: OffloadObjectHeartbeat(client_id, enable_offloading) + M-->>FS: 返回 offloading_objects {key→size} + end + + Note over FS: 执行 OffloadObjects() + FS->>FS: BatchQuerySegmentSlices() 从内存读数据 + FS->>FS: StorageBackend::BatchOffload() 写入SSD + FS->>M: NotifyOffloadSuccess(keys, metadatas) + Note over M: 释放MEMORY副本refcnt
添加LOCAL_DISK副本(COMPLETE) +``` + +### 2.2 Load(SSD → 请求方) + +```mermaid +sequenceDiagram + participant RC as 请求方Client + participant M as MasterService + participant TC as 目标Client (FileStorage) + + RC->>M: Get/BatchGet(keys) + M-->>RC: 返回 LOCAL_DISK 副本位置 + RC->>TC: batch_get_offload_object(keys) + TC->>TC: 从SSD读取到ClientBuffer + TC-->>RC: 返回 batch_id + RDMA地址 + RC->>RC: TransferEngine RDMA零拷贝拉取 + RC->>TC: release_offload_buffer(batch_id) +``` + +## 3. 触发时机与 Key 选取 + +系统有两种 offload 触发模式,由 `offload_on_evict` 开关控制: + +### 模式 A:PutEnd 即入队(默认,`offload_on_evict=false`) + +```mermaid +flowchart TD + A[Client 调用 PutEnd] --> B{enable_offload?
!offload_on_evict?} + B -->|Yes| C[将该对象的所有已完成
MEMORY副本加入 offloading_queue] + B -->|No| D[不做任何offload操作] + C --> E[副本 refcnt++ 防止被evict] + E --> F[等心跳线程取出执行] +``` + +- **选取标准**:所有 PutEnd 完成的对象**无差别入队** +- **无筛选逻辑**:不区分冷热,全部 offload + +### 模式 B:Eviction 时入队(`offload_on_evict=true`) + +```mermaid +flowchart TD + A[内存使用率 > eviction_high_watermark] --> B[BatchEvict 开始淘汰] + B --> C{遍历候选对象} + C --> D{已有 LOCAL_DISK 副本?} + D -->|Yes| E[安全,直接evict MEMORY副本] + D -->|No| F{offload队列达到上限?} + F -->|No| G[PushOffloadingQueue
refcnt++ 保护] + F -->|Yes| H{offload_force_evict?} + H -->|Yes| I[强制evict,数据丢失] + H -->|No| J[跳过,保留数据] + G --> K[等心跳线程取出执行] +``` + +- **选取标准**:由 `BatchEvict` 决定候选对象,基于 **lease_timeout 时间排序**(近似 LRU) +- **两轮扫描**:第一轮淘汰无 soft pin 的对象,第二轮淘汰有 soft pin 的对象(需 `allow_evict_soft_pinned_objects=true`) +- **保护机制**:入队时 `refcnt++` 防止 offload 期间被 evict + +### Master 端 Eviction 流程 + +```mermaid +flowchart TD + A[EvictionThreadFunc 后台线程] --> B{内存使用率 >
eviction_high_watermark?} + B -->|No| C[休眠,继续监测] + B -->|Yes| D[计算本次evict目标量] + D --> E[BatchEvict
按lease_timeout排序选候选] + E --> F{offload_on_evict模式?} + F -->|No| G[直接evict MEMORY副本] + F -->|Yes| H[尝试先offload再evict] +``` + +## 4. Offload 与 Eviction 的关系 + +| 维度 | Offload | Eviction | +|------|---------|----------| +| 目的 | 将数据持久化到 SSD | 释放内存空间 | +| 数据去向 | 本地 SSD 文件 | 丢弃 | +| 数据可恢复 | 是(通过 Load/Promotion) | 否 | +| 触发者 | 心跳线程(定时) | Eviction 后台线程(水位触发) | +| 副本变化 | MEMORY → LOCAL_DISK | MEMORY → 删除 | + +**协同关系**: +- Offload 是 Eviction 的**前置安全网**——先持久化再释放,避免数据丢失 +- `offload_on_evict=true` 时二者紧密耦合:eviction 候选先尝试 offload,成功后才释放内存 +- `offload_on_evict=false` 时二者独立:PutEnd 时入 offload 队列,eviction 按自己逻辑运行 + +## 5. 四种配置组合 + +| 组合 | enable_offload | offload_on_evict | offload_force_evict | 行为 | +|------|:-:|:-:|:-:|------| +| A(默认) | true | false | false | PutEnd 立即入 offload 队列,eviction 独立运行 | +| B | true | true | false | eviction 时才尝试 offload,失败则跳过(保留数据) | +| C | true | true | true | eviction 时先 offload,队列满则强制 evict(数据丢失) | +| D | true | false | true | 等同 A(force_evict 无效) | + +## 6. Promotion(SSD → 内存热提升) + +当 `promotion_on_hit=true` 时,频繁访问的 LOCAL_DISK 数据自动提升回内存: + +```mermaid +flowchart TD + A[Get 命中 LOCAL_DISK 副本] --> B[TryPushPromotionQueue] + B --> C{准入检查} + C -->|频率 >= threshold| D{内存水位 < 高水位?} + C -->|频率不足| Z[跳过] + D -->|Yes| E{去重:无MEMORY副本且无进行中任务?} + D -->|No| Z + E -->|Yes| F{队列 < promotion_queue_limit?} + E -->|No| Z + F -->|Yes| G[加入promotion队列] + F -->|No| Z + G --> H[心跳线程取出
分配MEMORY副本→SSD读取→RDMA写入] +``` + +- **频率统计**:Count-Min Sketch,阈值 `promotion_admission_threshold`(默认 2) +- **每次心跳限 1 个** promotion 任务(`kMaxPerHeartbeat=1`) + +## 7. 关键代码索引 + +### 7.1 Master 端(`mooncake-store/src/master_service.cpp`) + +| 函数 | 行号 | 职责 | +|------|------|------| +| `EvictionThreadFunc()` | :3160 | 后台线程,监测内存水位,触发 `BatchEvict` | +| `BatchEvict()` | :4466 | 核心淘汰逻辑,按 lease_timeout 选候选对象,内部定义 `try_evict_or_offload` lambda(:4515) 处理 offload/evict 分支 | +| `OffloadObjectHeartbeat()` | :2618 | 客户端心跳入口,返回 `offloading_objects` 队列给客户端 | +| `NotifyOffloadSuccess()` | :2705 | 处理客户端 offload 完成通知:释放 MEMORY 副本 refcnt,添加 LOCAL_DISK 副本 | +| `PushOffloadingQueue()` | :2744 | 将 key 入 offload 队列,根据副本的 segment 名称定位目标客户端 | +| `TryPushPromotionQueue()` | :2823 | Get 命中 LOCAL_DISK 时调用,经四重准入检查后将 key 加入 promotion 队列 | +| `PromotionObjectHeartbeat()` | :2919 | 返回待 promotion 任务(每次心跳限 1 个) | +| `PromotionAllocStart()` | :2948 | 为 promotion 分配 MEMORY 副本(PROCESSING 状态) | +| `NotifyPromotionSuccess()` | :3041 | 确认 promotion 完成:标记 MEMORY 副本 COMPLETE,释放 LOCAL_DISK refcnt | + +**PutEnd 中的 offload 触发**(:1390): + +```cpp +if (enable_offload_ && !offload_on_evict_) { + metadata.VisitReplicas(/* MEMORY + COMPLETED */, [&](Replica& replica) { + auto result = PushOffloadingQueue(key, replica); + if (result) { + replica.inc_refcnt(); // 防止 offload 期间被 evict + } + }); +} +``` + +**BatchEvict 中的 try_evict_or_offload**(:4515): + +```cpp +auto try_evict_or_offload = [&](const std::string& key, ObjectMetadata& metadata, ...) { + if (!offload_on_evict_) return metadata.size * evict_replicas(metadata); // 直接淘汰 + + if (has_local_disk_replica(metadata)) + return metadata.size * evict_replicas(metadata); // 已有SSD副本,安全淘汰 + + if (offload_force_evict_ && offload_queued >= offload_cap) + return metadata.size * evict_replicas(metadata); // 队列满,强制淘汰 + + // 尝试入 offload 队列 + auto result = PushOffloadingQueue(key, replica); + if (result) { replica.inc_refcnt(); /* 保护 */ return ...; } + + if (offload_force_evict_) return metadata.size * evict_replicas(metadata); // 入队失败,强制淘汰 + return 0; // 跳过,保留数据 +}; +``` + +### 7.2 Client 端(`mooncake-store/src/file_storage.cpp`) + +| 函数 | 行号 | 职责 | +|------|------|------| +| `Heartbeat()` | :495 | 心跳主循环:拉取 offload 任务 → `OffloadObjects()` → `ProcessPromotionTasks()` | +| `OffloadObjects()` | :341 | 执行 offload:从内存读数据 → 写 SSD → 通知 Master | +| `BatchGet()` | :300 | Load 路径:从 SSD 读到 ClientBuffer,返回 RDMA 可访问地址 | +| `ProcessPromotionTasks()` | :534 | 驱动 promotion:拉取任务 → 分配 MEMORY → SSD 读取 → RDMA 写入 | + +### 7.3 RPC 通信层(`mooncake-store/src/master_client.cpp`) + +| 函数 | 行号 | 职责 | +|------|------|------| +| `OffloadObjectHeartbeat()` | :941 | RPC 封装:客户端 → Master 拉取 offload 任务 | +| `NotifyOffloadSuccess()` | :963 | RPC 封装:客户端 → Master 确认 offload 完成 | +| `PromotionObjectHeartbeat()` | :977 | RPC 封装:客户端 → Master 拉取 promotion 任务 | +| `PromotionAllocStart()` | :985 | RPC 封装:客户端 → Master 请求分配 promotion 的 MEMORY 副本 | +| `NotifyPromotionSuccess()` | :998 | RPC 封装:客户端 → Master 确认 promotion 完成 | + +## 8. 控制开关与环境变量详解 + +### 8.1 Master 端开关 + +配置文件:`mooncake-store/include/master_config.h`,可通过 `master.yaml` 或命令行参数设置。 + +#### `enable_offload`(默认 false) + +- **false**:SSD offload 完全禁用。客户端调用 `OffloadObjectHeartbeat(enable_offloading=false)` 时,Master 清空该客户端的 offload 队列并释放所有 refcnt。对象只有 MEMORY 副本,内存不足时直接 eviction 丢弃。 +- **true**:启用 offload。客户端 `FileStorage` 初始化时注册 LOCAL_DISK segment(`MountLocalDiskSegment`),心跳线程开始工作。PutEnd 或 eviction 时对象可被加入 offload 队列。 + +#### `offload_on_evict`(默认 false) + +- **false(模式 A)**:PutEnd 完成后**立即**将该对象的所有 MEMORY 副本加入 offload 队列。意味着所有写入的数据都会尽快下沉到 SSD,内存中的副本仅作为 RDMA 访问源存在,直到 offload 完成后由 Master 释放。 +- **true(模式 B/C)**:PutEnd 时不做任何 offload 操作。只有当内存使用率超过 `eviction_high_watermark_ratio` 触发 `BatchEvict` 时,才将候选淘汰对象入 offload 队列。**区别**:模式下 B 对象在内存充裕时不会被 offload,仅在被选中淘汰时才持久化到 SSD。 + +#### `offload_force_evict`(默认 false) + +- **false**:在 `offload_on_evict=true` 模式下,如果 offload 队列已满(达到 `offloading_queue_limit_ * kOffloadCapRatio` = 25000),超出上限的候选对象**跳过淘汰**,数据保留在内存中。这会导致本轮 eviction 无法释放足够内存,Master 会打印 WARNING。 +- **true**:在 `offload_on_evict=true` 模式下,offload 队列满时不再跳过,而是**强制 eviction 丢弃数据**。适用于宁可丢失数据也要保证内存可用的场景。 +- **注意**:此开关仅在 `offload_on_evict=true` 时生效。单独设置(模式 D)无任何效果。 + +#### `promotion_on_hit`(默认 false) + +- **false**:LOCAL_DISK 副本被 Get 命中后,后续访问始终走 SSD Load 路径(读磁盘 → staging buffer → RDMA 传输)。 +- **true**:Get 命中 LOCAL_DISK 副本时,`TryPushPromotionQueue()` 被调用,通过 Count-Min Sketch 统计访问频率。频率达到 `promotion_admission_threshold` 的 key 被加入 promotion 队列,心跳线程将其从 SSD 提升回 MEMORY 副本。**效果**:热点数据自动回到内存,后续访问走 RDMA 零拷贝路径,避免 SSD I/O 延迟。 + +#### `promotion_admission_threshold`(默认 2) + +- Count-Min Sketch 的频率阈值。key 被访问的次数(近似)达到此值才有资格 promotion。 +- **设为 1**:任何被访问一次的 LOCAL_DISK key 立即被加入 promotion 队列。适合缓存空间充裕的场景。 +- **设为更大值(如 5)**:需要多次访问才 promotion。避免一次性访问冷数据占用 promotion 资源。 + +#### `promotion_queue_limit`(默认 50000) + +- 全局(所有 shard 共享)的待 promotion 任务上限。 +- **达到上限**:`TryPushPromotionQueue` 中的容量门控拒绝新任务,热 key 暂时留在 SSD。 +- 此上限同时控制 `promotion_in_flight_` 计数器,防止 promotion 占用过多内存。 + +#### `eviction_high_watermark_ratio`(默认 0.85) + +- 内存使用率触发 eviction 的阈值。 +- **设高(如 0.95)**:容忍更高的内存使用率,eviction 触发更晚,留给 offload 的时间窗口更短。 +- **设低(如 0.70)**:更早触发 eviction,内存更充裕,但在 `offload_on_evict=true` 模式下会提前开始 offload。 + +#### `eviction_ratio`(默认 0.05) + +- 每轮 `BatchEvict` 的目标回收比例(相对于总内存)。 +- **设大(如 0.10)**:每轮淘汰更多对象,eviction 频率更低但每轮耗时更长。 +- **设小(如 0.02)**:每轮淘汰少量对象,更平滑但 eviction 线程更频繁工作。 + +### 8.2 Client 端环境变量 + +#### `MOONCAKE_OFFLOAD_FILE_STORAGE_PATH`(默认 `/data/file_storage`) + +- SSD 上的存储目录路径。BucketStorageBackend 在此目录下创建 `.bucket` 和 `.meta` 文件。 +- **不设置**:使用默认路径,需确保该目录存在且有写入权限。 +- **建议**:设置为 NVMe SSD 挂载点,如 `/nvme/mooncake_offload`。 + +#### `MOONCAKE_OFFLOAD_STORAGE_BACKEND_DESCRIPTOR`(默认 `bucket_storage_backend`) + +- `bucket_storage_backend`:默认推荐。多对象合入桶文件(256MB/桶,500 key/桶),支持 FIFO/LRU 淘汰,支持重启恢复。 +- `file_per_key_storage_backend`:每个对象一个文件。适合调试,大规模场景下文件数爆炸。 +- `offset_allocator_storage_backend`:单文件 + 偏移分配器,1024 分片元数据。高并发性能好,但**不支持重启恢复**(启动时 truncates)。 + +#### `MOONCAKE_OFFLOAD_BUCKET_SIZE_LIMIT_BYTES`(默认 256MB) + +- BucketStorageBackend 单个桶文件的大小上限。配置定义在 `mooncake-store/include/storage_backend.h` 的 `BucketBackendConfig::bucket_size_limit`(:181-182)。 +- **分组逻辑**(`GroupOffloadingKeysByBucket()`,`storage_backend.cpp:1880`):心跳返回的 offload 对象按此大小打包分组。对象被依次加入当前桶,直到桶数据量达到 256MB 或 500 个 key 为止。凑不满一桶的剩余对象暂存在 `ungrouped_offloading_objects_` 中,等下次心跳凑满再写入。 +- **设小(如 64MB)**:桶更小更密集,淘汰粒度更细(LRU/FIFO 淘汰时整桶删除,浪费空间更少),但文件数量增多。 +- **设大(如 512MB)**:减少文件数,但淘汰时整桶删除可能浪费更多有效数据。 +- **注意**:单个对象大小超过此限制时会被跳过(:1911 打印 ERROR 日志)。 + +#### `MOONCAKE_OFFLOAD_BUCKET_KEYS_LIMIT`(默认 500) + +- BucketStorageBackend 单个桶文件的 key 数量上限。配置定义在 `BucketBackendConfig::bucket_keys_limit`(:184)。 +- 与 `bucket_size_limit` 共同控制分组,任一条件先达到即封桶。 + +#### `MOONCAKE_OFFLOAD_BUCKET_EVICTION_POLICY`(默认 `none`) + +- BucketStorageBackend 的 SSD 空间淘汰策略。配置定义在 `BucketBackendConfig::eviction_policy`。 +- `none`:不淘汰。SSD 写满后 offload 失败。 +- `fifo`:淘汰最早创建的桶。 +- `lru`:淘汰最久未被读取的桶(通过 `last_access_ns_` 原子计数器追踪)。 + +#### `MOONCAKE_OFFLOAD_LOCAL_BUFFER_SIZE_BYTES`(默认 1280MB) + +- Load 路径的 staging buffer 大小。从 SSD 读取数据时先写入此 buffer,再通过 RDMA 传输。 +- **设小**:并发 Load 能力受限,大对象可能需要排队等待 buffer 槽位。 +- **设大**:支持更多并发 Load,但占用更多 Host 内存。 +- 此 buffer 会被注册到 Transfer Engine 用于 RDMA 访问。 + +#### `MOONCAKE_OFFLOAD_TOTAL_SIZE_LIMIT_BYTES`(默认 2TB) + +- SSD 磁盘使用上限。达到上限后 BucketStorageBackend 触发淘汰(如有 eviction policy)。 +- **设为 0**:BucketBackend 默认使用磁盘物理容量的 90%。 + +#### `MOONCAKE_OFFLOAD_HEARTBEAT_INTERVAL_SECONDS`(默认 10) + +- 客户端心跳线程的间隔。每次心跳执行:(1) 拉取 offload 任务 (2) 执行 OffloadObjects (3) 执行 ProcessPromotionTasks。 +- **设小(如 3)**:offload/promotion 响应更快,但 Master RPC 压力增大。 +- **设大(如 30)**:减少 RPC 开销,但数据在内存中停留更久,promotion 延迟更高。 + +#### `MOONCAKE_OFFLOAD_USE_URING`(默认 false) + +- **false**:使用标准 POSIX I/O(pread/pwrite)。 +- **true**:使用 Linux io_uring 异步 I/O。每个线程拥有独立的 io_uring ring(无锁),ClientBuffer 注册为 fixed buffer 避免 mmap 开销,配合 O_DIRECT 绕过页缓存。**仅 Linux 可用**。 + +### 8.3 内部硬编码常量 + +| 常量 | 值 | 说明 | +|------|----|------| +| `offloading_queue_limit_` | 50000 | 单客户端 offload 队列最大长度(`master_service.h`) | +| `kOffloadCapRatio` | 0.5 | `offload_force_evict` 的触发阈值 = `offloading_queue_limit_ * 0.5` = 25000 | +| `kMaxPerHeartbeat` | 1 | 每次心跳最多返回 1 个 promotion 任务,防止阻塞 | diff --git a/docs/offload-push-mode.md b/docs/offload-push-mode.md new file mode 100644 index 0000000000..2118501bf2 --- /dev/null +++ b/docs/offload-push-mode.md @@ -0,0 +1,374 @@ +# Mooncake SSD Offload —— Push 读取模式(URMA write 为例) + +> 本文档描述 `feature/offloadrpc` 分支引入的 **Push 读取模式**,作为 [offload-mechanism.md](offload-mechanism.md) 第 2.2 节「Load(SSD → 请求方)」的扩展。 +> 传输层以 **UB / URMA** 为例(`URMA_OPC_WRITE` 单边写);RDMA 等其它 transport 走同一套抽象,不单独展开。 +> 关联开关:`MC_OFFLOAD_PUSH`。 + +--- + +## 1. 背景与动机 + +当一个对象只在远端节点的 SSD 上时,请求方需要把它从对端磁盘读回本地内存。原有实现(下称 **Pull 模式**)由**请求方主动发起**,一次 Load 要 **3 次 RPC / 单边操作**;**Push 模式**把单边传输的方向对调,由**数据持有方(owner)主动 URMA write**,一次 Load 只需 **1 次 RPC 往返**。 + +### 1.1 Pull vs Push 流程图 + +**Pull 模式(请求方主动拉,3 次往返)** + +```mermaid +sequenceDiagram + autonumber + participant R as 请求方 + participant O as 对端 owner + R->>O: ① RPC batch_get_offload_object(keys) + Note over O: BatchGet:SSD → ClientBuffer + O-->>R: pointers + gc_ttl + R->>O: ② URMA READ:从对端 ClientBuffer 拉数据 + Note over O: (被动,数据被读走) + R->>O: ③ RPC release_offload_buffer(batch_id) + Note over O: ReleaseBuffer +``` + +**Push 模式(持有方主动推,1 次往返)** + +```mermaid +sequenceDiagram + autonumber + participant R as 请求方 + participant O as 对端 owner + R->>O: ① RPC batch_get_offload_object_push
(keys + 自身 TE 端点 + dst_slices) + Note over O: BatchGet:SSD → ClientBuffer + O->>R: ② URMA WRITE:ClientBuffer → 请求方内存 + Note over O: ReleaseBuffer(本地,写完即放) + O-->>R: error_code(数据已落在请求方内存) +``` + +**收益**:消除请求方侧的 URMA READ 往返,以及单独的 `release_offload_buffer` RPC —— 由对端写完后就地释放 buffer。 + +**不变的部分**:对端仍必须先把 SSD 数据读进**已注册的 ClientBuffer**(`FileStorage::BatchGet`)。URMA 单边写的源必须是注册过的内存段(`urma_register_seg` 得到的 `urma_target_seg_t`),SSD 上的数据无法绕过这块中转直接走网卡。Push 省的是后续步骤,不是这次 SSD→内存的拷贝。 + +--- + +## 2. 设计要点 + +### 2.1 方向对调(Pull READ ↔ Push WRITE) + +两条传输函数互为镜像,只差三个字段;最终都落到 URMA 的同一套 `urma_jfs_wr_t`,仅 `opcode` 与 SGE 方向不同: + +| | Pull `submit_batch_get_offload_object` | Push `submit_batch_push_offload_object` | +|---|---|---| +| `TransferRequest::opcode` | `READ` | `WRITE` | +| `openSegment` 的对象 | 对端(owner)的 segment | **请求方**的 segment | +| `source`(本地) | 请求方目标分片 `slice.ptr` | **对端 ClientBuffer** `src_pointer` | +| `target_offset`(远端) | 对端 ClientBuffer 地址 | **请求方目标分片地址** `dst.addr` | +| URMA `wr.opcode` | `URMA_OPC_READ` | `URMA_OPC_WRITE` | +| 发起方 | 请求方 | 对端 | + +### 2.2 成立前提:两端内存都注册为 URMA segment + +- **对端 ClientBuffer**:`FileStorage::RegisterLocalMemory()` 注册到对端 transfer engine,底层经 `urma_register_seg` 得到本地段 `l_seg`,可作为 WRITE 的源(local SGE)。 +- **请求方目标内存**:应用 GET 时传入的 `objects` 分片本就注册过;对端 `openSegment(requester_te_addr)` 把它**导入**为远端段 `r_seg`(remote SGE)+ 远端 `tjetty`,WRITE 才能寻址过去。 + +### 2.3 非连续目标分片 + +对端某个 key 的数据是**一整块连续** ClientBuffer;请求方接收内存可能是**多段非连续**分片(GPU 显存常见)。Push 按 `dst.size` 累加 `offset`,把连续源切到各目标分片,逐段生成一条 `TransferRequest` → 一条 `urma_jfs_wr_t`。 + +### 2.4 ub / urma 适配 + +Push 改动全部位于 `TransferSubmitter::submit_*` 层,只改 `opcode/source/target_offset`,**未触碰任何具体 transport**。`MultiTransport` 按端点自动选到 `UbTransport`;`urma_endpoint.cpp` 已支持 `URMA_OPC_WRITE` 且 SGE 方向自动反转,注册时 access 已开 `READ|WRITE|ATOMIC`。Push 无需为 ub/urma 写第二份逻辑。 + +--- + +## 3. RPC 数据结构(`mooncake-store/include/rpc_types.h`) + +```cpp +// 请求方一个目标分片:对端 URMA write 时写入的目的地址 + 长度 +struct OffloadDstSlice { + uint64_t addr; // 请求方目标内存虚拟地址 + uint64_t size; // 该分片字节数 +}; +YLT_REFL(OffloadDstSlice, addr, size); + +// Push 请求体 +struct BatchGetOffloadObjectPushRequest { + std::vector keys; // 租户作用域 storage key + std::vector sizes; // 每个 key 总字节数 + std::string requester_te_addr; // 请求方 transfer engine 端点 + std::vector> dst_slices; // 每个 key 一组目标分片 +}; +YLT_REFL(BatchGetOffloadObjectPushRequest, keys, sizes, requester_te_addr, dst_slices); + +// Push 响应体(数据已落在请求方内存,仅回状态码) +struct BatchGetOffloadObjectPushResponse { + ErrorCode error_code; +}; +YLT_REFL(BatchGetOffloadObjectPushResponse, error_code); +``` + +- `YLT_REFL` 是序列化反射宏;不加它,struct_pack 无法在 RPC 中编解码这些结构。 +- **不变量**:`keys`、`sizes`、`dst_slices` 是三个**平行数组**,长度必须相等,下标 `i` 描述同一个 key。handler 在进入任何按下标循环前先校验等长,坏请求直接 `INVALID_PARAMS` 拒绝(fail-fast,防越界/错位)。 + +--- + +## 4. 改动清单(逐文件) + +| 文件 | 改动 | +|---|---| +| `include/rpc_types.h` | 新增 `OffloadDstSlice` / `BatchGetOffloadObjectPushRequest` / `…PushResponse` | +| `include/transfer_task.h`、`src/transfer_task.cpp` | 新增 `submit_batch_push_offload_object`(WRITE 版传输);header 增加 `#include "rpc_types.h"` | +| `include/client_service.h`、`src/client_service.cpp` | 新增 `Client::BatchPushOffloadObject`(提交传输 + 等 future 完成) | +| `include/real_client.h`、`src/real_client.cpp` | 新增对端 handler `batch_get_offload_object_push`;请求方 `batch_get_into_offload_object_internal` 增加 `MC_OFFLOAD_PUSH` 分支 | +| `include/pyclient.h`、`src/real_client.cpp` | 新增 `ClientRequester::batch_get_offload_object_push`(invoke_rpc 封装) | +| `src/real_client.cpp`、`src/real_client_main.cpp` | 两处 server 各 `register_handler` 新 handler | + +> ⚠️ **handler 必须两处都注册**:内嵌 server(`real_client.cpp` 的 `offload_rpc_server_`)和独立进程 server(`real_client_main.cpp`)。漏一处,对应部署形态下 push 会因「RPC 未注册」失败。 + +--- + +## 5. 调用链总览 + +```mermaid +flowchart TB + subgraph REQ[请求方 本端] + A["batch_get_into_offload_object_internal"] + B["ClientRequester::batch_get_offload_object_push"] + C["invoke_rpc 模板 (&RealClient::batch_get_offload_object_push)"] + D["coro_rpc_client.send_request
struct_pack 序列化"] + A --> B --> C --> D + end + subgraph OWN[对端 owner] + E["RealClient::batch_get_offload_object_push"] + F["co_await coro_io::post(lambda)"] + G["FileStorage::BatchGet
SSD → ClientBuffer"] + G2["BucketStorageBackend::BatchLoad
preadv / io_uring"] + H["Client::BatchPushOffloadObject
URMA write"] + I["TransferSubmitter::submit_batch_push_offload_object
openSegment + submitTransfer"] + J["UbTransport::submitTransferTask"] + K["UrmaEndpoint::submitPostSend"] + L["urma_post_jetty_send_wr ★ URMA_OPC_WRITE"] + M["FileStorage::ReleaseBuffer
写完即释放"] + E --> F + F --> G --> G2 + F --> H --> I --> J --> K --> L + F --> M + end + D -- "网络 RPC" --> E + E -. "co_return error_code" .-> D +``` + +--- + +## 6. 关键代码解读 + +### 6.1 请求方入口:`batch_get_into_offload_object_internal`(`src/real_client.cpp`) + +收集目标地址,按 `MC_OFFLOAD_PUSH` 分流到 Push 或保留 Pull: + +```cpp +std::vector> dst_slices; +for (const auto &object_it : objects) { + storage_keys.emplace_back(MakeTenantScopedStorageKey(...)); + int64_t total = 0; + std::vector key_dst; + for (const auto &s : object_it.second) { + total += s.size; + key_dst.emplace_back(reinterpret_cast(s.ptr), s.size); // 应用目标内存地址+长度 + } + sizes.emplace_back(total); + dst_slices.emplace_back(std::move(key_dst)); // 与 storage_keys 对齐 +} + +static const bool kOffloadPush = []() { // 只读一次环境变量并缓存 + const char *v = std::getenv("MC_OFFLOAD_PUSH"); + return v && (std::string_view(v) == "true" || std::string_view(v) == "1"); +}(); +if (kOffloadPush) { + BatchGetOffloadObjectPushRequest push_req; + push_req.keys = storage_keys; + push_req.sizes = sizes; + push_req.requester_te_addr = client_->GetSegmentEndpoint(); // 自身 TE 端点 + push_req.dst_slices = std::move(dst_slices); + auto pushResp = client_requester_->batch_get_offload_object_push(target_rpc_service_addr, push_req); + if (!pushResp) { return tl::make_unexpected(pushResp.error()); } // RPC 层失败 + if (pushResp->error_code != ErrorCode::OK) { return tl::make_unexpected(pushResp->error_code); } + return {}; // ★ Push 到此结束:无 READ、无 release +} +// 否则走下方原有 Pull 链路(完全保留) +``` + +`s.ptr` 是应用 GET 时给定的目标内存(已注册段),转成 `uint64_t` 即 `OffloadDstSlice::addr`,与 URMA `r_sge.addr` 语义一致。 + +### 6.2 对端 handler:`RealClient::batch_get_offload_object_push`(`src/real_client.cpp`) + +读盘 + URMA write + 释放,在一个线程池任务里完成: + +```cpp +async_simple::coro::Lazy> +RealClient::batch_get_offload_object_push(const BatchGetOffloadObjectPushRequest &req) { + if (!file_storage_) { co_return tl::make_unexpected(ErrorCode::INVALID_PARAMS); } + if (req.keys.size() != req.sizes.size() || + req.keys.size() != req.dst_slices.size()) { co_return ... INVALID_PARAMS; } // 平行数组校验 + + struct CallState { req; file_storage; client; }; // 堆上打包,lambda 只捕获裸指针 + auto state = std::make_unique(); ... + auto *s = state.get(); + + auto try_result = co_await coro_io::post([s]() -> tl::expected { + auto result = s->file_storage->BatchGet(s->req.keys, s->req.sizes); // ① SSD → ClientBuffer + if (!result) { return tl::make_unexpected(result.error()); } + const uint64_t batch_id = result.value().batch_id; + auto write_result = s->client->BatchPushOffloadObject( // ② URMA write → 请求方内存 + s->req.requester_te_addr, s->req.keys, + result.value().pointers, // 对端 ClientBuffer 每个 key 的地址 = URMA write 的源 + s->req.dst_slices); + s->file_storage->ReleaseBuffer(batch_id); // ③ 写完即释放 + return write_result; + }); + + auto pushed = try_result.value(); + if (!pushed) { co_return tl::make_unexpected(pushed.error()); } + co_return BatchGetOffloadObjectPushResponse(ErrorCode::OK); +} +``` + +`coro_io::post` 把「读盘 + 等 URMA write 完成 + 释放」这些会阻塞的慢操作提交到阻塞线程池,`co_await` 让出 coro_rpc 的 IO 线程,使其继续处理 ping 等其它 RPC。 + +### 6.3 Client 封装:`Client::BatchPushOffloadObject`(`src/client_service.cpp`) + +```cpp +auto future = transfer_submitter_->submit_batch_push_offload_object(...); +if (!future) { return tl::make_unexpected(ErrorCode::TRANSFER_FAIL); } +auto result = future->get(); // ★ 阻塞到 URMA write 完成(jfc 收到完成事件) +if (result != ErrorCode::OK) { return tl::make_unexpected(result); } +return {}; +``` + +`future->get()` 阻塞到 URMA write 完成,这是对端能安全释放 ClientBuffer 的前提(否则源 buffer 在传输中途被回收会损坏数据)。 + +### 6.4 传输层:`submit_batch_push_offload_object`(`src/transfer_task.cpp`) + +把「一块连续源 → 多个目标分片」翻译成 transfer engine 的 WRITE 请求: + +```cpp +SegmentHandle seg = engine_.openSegment(requester_te_addr); // 打开/导入“请求方”的 segment(只开一次) +for (size_t i = 0; i < keys.size(); ++i) { + const uint64_t src = src_pointers[i]; // 这个 key 在对端 ClientBuffer 里的连续起始地址 + uint64_t offset = 0; + for (const auto& dst : dst_slices[i]) { + TransferRequest request; + request.opcode = TransferRequest::WRITE; // ★ WRITE + request.source = reinterpret_cast(src + offset); // 源 = 对端本地 buffer + request.target_id = seg; // 目标 = 请求方 segment + request.target_offset = dst.addr; // 目标地址 = 请求方分片地址 + request.length = dst.size; + requests.emplace_back(request); + offset += dst.size; + } +} +return submitTransfer(requests); +``` + +### 6.5 落到 URMA:`TransferRequest` → `urma_jfs_wr_t` + +`submitTransfer` → `UbTransport::submitTransferTask` 把每个 `TransferRequest` 切成 `Slice`(`ub_transport.cpp`): + +```cpp +slice->opcode = request.opcode; // WRITE 透传 +slice->source_addr = request.source; // 本地源(对端 ClientBuffer) +slice->ub.dest_addr = request.target_offset + offset; // 远端目标(请求方分片地址) +``` + +再由 `UrmaEndpoint::submitPostSend`(`urma_endpoint.cpp`)组装成 URMA 工作请求并提交: + +```cpp +// 本地 SGE(源):对端 ClientBuffer +l_sge.addr = (uint64_t)slice->source_addr; +l_sge.tseg = slice->ub.l_seg; // 本地注册段(urma_register_seg) +// 远端 SGE(目标):请求方内存 +r_sge.addr = slice->ub.dest_addr; +r_sge.tseg = slice->ub.r_seg; // openSegment 导入的远端段 + +wr.opcode = (slice->opcode == READ) ? URMA_OPC_READ : URMA_OPC_WRITE; // ★ 本路径 = URMA_OPC_WRITE +wr.rw.src.sge = (READ) ? &r_sge : &l_sge; // WRITE:源 = 本地 l_sge +wr.rw.dst.sge = (READ) ? &l_sge : &r_sge; // WRITE:目标 = 远端 r_sge +wr.tjetty = imported_jetty_map_[jetty]; // 导入的远端 jetty + +urma_post_jetty_send_wr(jetty_list_[jetty_index], wr_list, &bad_wr); // 提交,完成经 jfc 通知 +``` + +> URMA 术语对照:`jetty` ≈ 收发队列(类 QP),`urma_target_seg_t` ≈ 注册内存段(类 MR),`jfc` ≈ 完成队列(类 CQ)。Push 路径就是构造 `URMA_OPC_WRITE` 的 `jfs_wr`,源 SGE 指向对端 ClientBuffer,目标 SGE 指向请求方导入段。 + +### 6.6 请求方 RPC 封装:`ClientRequester::batch_get_offload_object_push` + +```cpp +auto result = invoke_rpc<&RealClient::batch_get_offload_object_push, + BatchGetOffloadObjectPushResponse>(client_addr, req); +``` + +`invoke_rpc(addr, 参数...)` 用成员函数指针 `&RealClient::batch_get_offload_object_push` 作为「调对端哪个 handler」的编译期 ID;对端 `register_handler<同一个指针>` 把该 ID 映射回 handler。 + +--- + +## 7. 时序图(含线程模型) + +```mermaid +sequenceDiagram + autonumber + participant R as 请求方线程
(syncAwait 阻塞) + participant IO as 对端 IO 线程
(server 仅 1 条) + participant W as 对端 worker 线程
(coro_io 阻塞池) + participant U as URMA / 网卡 + R->>IO: send_request(请求) + Note over IO: 反序列化 req + 平行数组校验 + IO->>W: co_await coro_io::post + Note over IO: IO 线程让出,去收别的 RPC + Note over W: FileStorage::BatchGet
preadv 读盘 → ClientBuffer + W->>U: submitPostSend / urma_post_jetty_send_wr
(URMA_OPC_WRITE) + Note over U: ClientBuffer → 请求方内存 + U-->>W: jfc 完成事件(future->get 返回) + Note over W: FileStorage::ReleaseBuffer + W-->>IO: lambda 完成,协程恢复 + IO-->>R: 响应(error_code) +``` + +- **IO 线程**:coro_rpc server 事件循环,只做收包/反序列化/分发/回包等快操作,**绝不阻塞**;内嵌 offload server 仅 1 条(`coro_rpc_server(1, 0, ...)`)。 +- **worker 线程**:coro_io 共享阻塞线程池,跑 `coro_io::post` 提交的慢活(读盘、等 URMA write、释放)。 +- coro_rpc **默认不会**自动给每个请求分配工作线程 —— handler 默认就在 IO 线程跑。任务挪到 worker 线程,是 handler **主动 `coro_io::post`** 的结果。这也是 offload server 只配 1 条 IO 线程也不会被读盘/传输拖死的原因。 + +--- + +## 8. 开关:`MC_OFFLOAD_PUSH` + +| 取值 | 行为 | +|---|---| +| 未设置 / 其它 | **Pull 模式**(默认),原 3 步链路完全保留 | +| `true` 或 `1` | **Push 模式** | + +- 在 `batch_get_into_offload_object_internal` 中通过 `static const bool` + lambda 读取一次并缓存,之后分支零开销。 +- Pull / Push **互斥**:一次运行只走其中一条路径。 +- 没有单边 WRITE 能力的 transport 应保持 Pull(回退路径已保留)。 + +--- + +## 9. 关键代码索引 + +| 角色 | 位置 | +|---|---| +| RPC 数据结构 | `mooncake-store/include/rpc_types.h` | +| 请求方入口/分支 | `RealClient::batch_get_into_offload_object_internal`(`src/real_client.cpp`) | +| 请求方 RPC 封装 | `ClientRequester::batch_get_offload_object_push` / `invoke_rpc`(`src/real_client.cpp`) | +| 对端 handler | `RealClient::batch_get_offload_object_push`(`src/real_client.cpp`) | +| Client 封装 | `Client::BatchPushOffloadObject`(`src/client_service.cpp`) | +| Push 传输(WRITE) | `TransferSubmitter::submit_batch_push_offload_object`(`src/transfer_task.cpp`) | +| Slice 构建 | `UbTransport::submitTransferTask`(`mooncake-transfer-engine/.../ub_transport.cpp`) | +| URMA 提交 | `UrmaEndpoint::submitPostSend` → `urma_post_jetty_send_wr`(`.../urma/urma_endpoint.cpp`) | +| handler 注册 | `RealClient::setup_internal` 内嵌 server / `RegisterClientRpcService`(`src/real_client_main.cpp`) | + +--- + +## 10. 限制与待办 + +1. **响应粒度**:`BatchGetOffloadObjectPushResponse` 目前只回整体 `error_code`,未支持逐 key 部分失败。如需,可扩成 `std::vector status`。 +2. **gc_ttl 语义**:Pull 路径中「`elapsed >= gc_ttl → OBJECT_HAS_LEASE`」的判定在 Push 下不再需要(对端写完即释放),已省略。 +3. **传输回退**:Push 仅在确认 transport 支持单边 WRITE 时启用;TCP/共享内存等应继续走 Pull。当前由 `MC_OFFLOAD_PUSH` 手动控制,尚未做 transport 能力自动探测。 +4. **构建/测试**:改动尚未在 Linux 目标上编译验证与端到端压测;Windows 开发机无法构建 Mooncake(依赖 RDMA/etcd 等)。 +``` diff --git a/docs/source/deployment/mooncake-store-deployment-guide.md b/docs/source/deployment/mooncake-store-deployment-guide.md index 640aec3c2e..6b8f15620b 100644 --- a/docs/source/deployment/mooncake-store-deployment-guide.md +++ b/docs/source/deployment/mooncake-store-deployment-guide.md @@ -1062,6 +1062,9 @@ the deferred direct-mmap path is desired while the arena is otherwise enabled. ```bash export MC_YLT_LOG_LEVEL=info +export MC_YLT_LOG_PATH=logs/rpc.log +export MC_YLT_LOG_MAX_FILE_SIZE=1048576000 +export MC_YLT_LOG_MAX_FILES=3 ``` Available: `trace`, `debug`, `info`, `warn` (or `warning`), `error`, `critical`. When unset (or set to an unrecognized value), the level defaults to `warn`. diff --git a/docs/source/design/distributed-metadata-plane-lightweight-coordinator.html b/docs/source/design/distributed-metadata-plane-lightweight-coordinator.html new file mode 100644 index 0000000000..fb8d33464b --- /dev/null +++ b/docs/source/design/distributed-metadata-plane-lightweight-coordinator.html @@ -0,0 +1,2066 @@ + + + + + Mooncake 分布式元数据管理面轻量 Coordinator 架构设计 + + + +

Mooncake 分布式元数据管理面轻量 Coordinator 架构设计

+ +

1. 设计摘要

+
+ 推荐方案: + 采用 Redis Cluster 风格的固定 slot 分片和客户端直连路由,同时引入一个不在前台请求路径上的轻量 Coordinator。 + Coordinator 只负责集群视图、slot 迁移计划、ownership version 与 fencing 发布、资源摘要聚合和后台任务预算调节;GetReplicaListPutStartPutEnd 等热路径请求仍由客户端直接访问 slot 所属 MasterGroup 的 current primary。 +
+ +

1.1 术语与层次

+ + + + + + + + + + +
术语本文中的唯一含义
MasterNode一个可寻址的 Master 进程实例;是部署、租约和故障域单位。
MasterGroup一个复制组;是日志提交、主备切换和一致性单位,包含一个 current primary 和若干 replica。
Slot由 key hash 得到的固定路由编号。
SlotGroup一组连续或离散 slot;是负载统计和组间迁移单位。
Slot owner拥有 SlotGroup 的 MasterGroup,而不是某个永久固定的 MasterNode。
Group primaryMasterGroup 当前接收强一致前台请求的 MasterNode,随 term 变化。
+

+ 下文若使用“Master 服务”,仅泛指 MasterNode 上运行的服务。路由、所有权或 failover 描述必须明确使用 + MasterGroup、Group primary 或 MasterNode,避免“Master Shard”同时指进程、复制组和 slot 分片。 +

+ +

1.2 Generation、Term、Revision 与 Fencing

+

+ 本设计不再把所有版本都称为 epoch,而是只区分三种语义:generation/term 是 fencing token, + 用于隔离旧 owner;revision 只用于快照发布、CAS 和增量 watch;sequence 只用于同一数据流内的样本排序。 + 只有 generation/term 会决定请求是否有权改变状态,revision 和 sequence 都不能授予写权限。 +

+ +
+初始状态:SlotGroup S belongs to MasterGroup G1, assignment_generation = 7
+
+Client C1 cached: S -> G1, generation 7
+Coordinator migrates S: G1 -> G2, generation becomes 8
+
+C1 sends Put(key, owner=G1, assignment_generation=7)
+G1/G2 compares request generation with durable assignment:
+  7 < 8  -> reject STALE_ASSIGNMENT or MOVED(S, G2, 8)
+
+Without assignment_generation:
+  delayed C1 or old G1 might continue modifying S after cutover,
+  creating two owners and divergent object metadata.
+  
+ +
+ Generation/term 的作用不是发现故障,而是隔离旧状态。 + lease/heartbeat 用于判断参与者可能失活;generation/term 用于在发生切换后证明请求仍属于当前一代。 + 仅检测到旧 primary 断连是不够的,因为它可能仍在运行,只是发生了网络分区。 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
字段类别作用域何时递增防止的问题旧值处理
view_revisionRevision整个 ClusterView任意已发布集群视图发生变化增量更新乱序、旧 view 覆盖新 view客户端忽略旧 view;watch 缺口时重新读取全量 view
node_incarnation_idIncarnation UUID一个 node_id 的进程 incarnationMasterNode 每次启动生成新的 UUID重启前残留 lease/heartbeat 覆盖新进程状态拒绝旧 incarnation 的续租和上报
termFencing term一个 MasterGroup组内选出或授权新的 primary旧 primary 在网络分区后继续提交写返回 STALE_TERMNOT_PRIMARY
assignment_generationFencing generation一个 SlotGroupSlotGroup 从一个 MasterGroup 迁到另一个 MasterGroup迁移后 source group 或旧客户端继续修改对象元数据返回 STALE_ASSIGNMENTMOVED
control_generationFencing generation一个 Storage Client 的控制归属client control owner 从一个 MasterGroup 迁到另一个 group两个 group 同时接收 segment heartbeat、mount/unmount 或下发任务拒绝旧 heartbeat/控制写并返回新 control owner
segment_incarnationFencing incarnation一个物理 segment/allocator incarnationsegment 重新注册、重建 allocator 或不可兼容地 remount旧 reservation/descriptor 写入已经重建的物理空间reservation 失败,重新查询 segment 并分配
resource_revisionRevision一版聚合 ResourceViewCoordinator 发布新的资源摘要资源视图乱序覆盖忽略旧摘要;它不单独提供空间分配正确性
sample_seqSequence一个 segment 的遥测流同一 segment_incarnation 每发布一个样本递增延迟到达的容量/带宽样本覆盖新样本丢弃旧样本并结合 timestamp 判断 freshness
budget_revisionRevision一版 BackgroundBudgetCoordinator 调整后台任务预算旧预算重新放大迁移或 offload 流量忽略旧预算;当前预算过期则使用保守默认值
+ +

+ 四个 fencing scope 是 group termslot assignment generationclient control generation + 和 segment incarnation。前三者可共享 FencingToken{scope_id, generation} 的实现;segment 使用 UUID incarnation token。 + 它们属于不同状态机,不能跨 scope 比较。 + revision/sequence 使用无符号 64 位单调计数器;incarnation 使用启动或重建时生成的 UUID,避免依赖旧进程持久化计数器。 +

+
+// 通用比较规则,不表示存在一个全局 generation。
+struct FencingToken {
+  string scope_id;
+  uint64_t generation;
+}
+
+SnapshotRevision = uint64_t;   // view/resource/budget,只用于排序、watch 和 CAS
+IncarnationId = UUID;          // node/segment,每次重启或重建生成新值
+StreamSequence = uint64_t;     // telemetry,在同一 incarnation 内单调递增
+  
+ +

1.2.1 Term 为什么单独命名

+

+ term 本质上是 MasterGroup 的领导权 generation,但保留 term 这个名称是为了和 Raft 等复制协议一致。 + QUORUM 模式由组选举产生更高 term;ASYNC_STANDBY 模式由 Coordinator 在取得独占 fencing token 后授权更高 term。 + 无论采用哪种模式,数据写入权威方都必须检查 term。只把新 primary 地址写入服务发现,而不在提交点检查 term,不能防止双主。 +

+ +

1.2.2 Fencing 是什么

+

+ Fencing(隔离旧持有者)是“在最终写入点拒绝旧 token”的机制,generation/term/incarnation 是本文的 fencing token。 + 检查必须发生在真正改变状态的地方,例如 MasterGroup 日志提交、segment allocator reserve/free、HA Backend CAS, + 而不能只在客户端或入口 RPC 检查一次。否则请求通过检查后发生切主,延迟请求仍可能写入。 +

+ +

1.3 一致性与故障恢复基础概念

+ + + + + + + + + + + + + + + + + + + + + + + + + +
概念解释及本文中的用途
Lease有到期时间的临时所有权/存活声明,持有者必须续租。用于故障检测和资源自动回收,但不能代替 fencing。
CASCompare-And-Swap:仅当持久化值仍等于预期版本时才更新。用于保证两个 Coordinator 或迁移流程不能同时提交冲突 owner。
Quorum复制组的多数派。3 voter 中至少 2 个确认后提交,可容忍 1 个故障;失去多数派时宁可拒绝写,也不能产生两个合法 primary。
OpLog按顺序记录元数据状态变化的操作日志。replica 和迁移 target 先加载 snapshot,再 replay 后续 OpLog。
Snapshot某个一致性位置上的完整状态快照,用于避免从第一条 OpLog 开始恢复。snapshot 必须携带其对应的 log/commit position。
Replay lagtarget/replica 已应用位置与 source committed position 的差距。只有追到 cutover position 才能安全接管。
RPORecovery Point Objective,可接受的数据丢失范围。QUORUM 已提交写目标是 RPO=0;异步主备的 RPO 由尚未 replay 的日志决定。
RTORecovery Time Objective,从故障发生到恢复服务允许的时间;受故障检测、选主和 replay lag 影响。
Idempotency key同一逻辑操作重试时保持不变的 ID,使重复请求只产生一次状态变化,用于 reservation 和后台 Task。
ResourceView容量、带宽、IOPS 和健康状态的近实时只读快照,用于选候选;它可能过期,因此最终 reserve 仍需 allocator 原子确认。
Reservation带 TTL 的临时资源承诺。PutStart 预留、PutEnd 提交、PutRevoke/超时释放,以避免并发请求超卖。
Replica descriptor描述一个对象副本所在 segment、offset/length、介质类型和访问参数的元数据;它不是对象数据本身。
Failure domain共享故障风险的边界,例如进程、机器、机架或 AZ。副本跨域放置是为了避免一次故障同时损坏全部副本。
Locality调用方与 segment 之间的 NUMA、主机、机架、AZ 或网络 fabric 距离,用于估算访问时延和网络成本。
Rendezvous hash对“实体、候选节点”组合计算稳定分数并选最高者;候选集合变化时只移动较少实体。本文用于生成初始 control-owner 建议,最终映射仍由 Coordinator 显式提交。
Hard constraint不满足就绝不能选择的条件,例如容量不足、介质能力不符或副本处于同一故障域;与可权衡的软评分分开处理。
Dominant resource某候选节点上占用比例最高的资源维度。例如容量只占 20% 但带宽占 90%,其主导资源是带宽,不能因容量充足继续放置。
Hysteresis进入和退出某状态使用不同阈值或连续观测窗口,避免热点副本、预算和读路由在临界值附近频繁来回切换。
P99.999.9% 请求时延不超过的值,即每 1,000 个请求约有 1 个更慢;用于约束极端尾延迟,而不仅是平均性能。
MOVEDSlotGroup 已稳定归属于另一个 MasterGroup;客户端应更新本地长期路由并重试。
ASKSlotGroup 正在迁移,客户端仅对本次请求访问 importing group,不永久覆盖本地 owner。
+ +

1.4 一次 Put 请求如何使用这些版本号

+
+1. Client 从 ClusterView(view_revision=100) 得到:
+     slot 42 -> SlotGroup S7(assignment_generation=8)
+             -> MasterGroup G2(term=12)
+             -> primary endpoint B
+
+2. Client 向 B 发送 PutStart:
+     {group=G2, term=12, slot=42, assignment_generation=8, request_id=R}
+
+3. B 在写入 PROCESSING 元数据前检查:
+     a. 自己仍是 G2 term 12 的 primary;否则 NOT_PRIMARY/STALE_TERM
+     b. slot 42 仍属于 G2 且 generation=8;否则 MOVED/STALE_ASSIGNMENT
+     c. request_id R 是否已处理;若是则返回相同结果
+
+4. B 使用本地 ResourceView 选择目标存储节点,并从本组的 DelegatedExtentPool
+   分配已由后台租入的空间和资源预算。常态 Put 不同步访问 Coordinator 或远端 allocator。
+
+5. B 写入 PROCESSING 元数据,将变更异步复制给 standby,然后返回 replica descriptors。
+
+6. Client 写数据并发送 PutEnd(R)。B 再次检查 term、assignment_generation 和 request_id,
+   然后把对象从 PROCESSING 转为 COMPLETE,并异步复制该变更。
+  
+

+ view_revision 只说明客户端使用哪一版路由快照,不授予写权限;真正保护写正确性的是对应作用域的 + termassignment_generationsegment_incarnation 以及幂等 request_id。即使客户端 view 不是最新, + 服务端也能拒绝危险写,并通过错误码引导客户端刷新。 +

+ +
+ 和 Redis 的关系: + Redis Cluster 没有独立 Coordinator,slot ownership 和 failover 信息由节点 gossip 传播,reshard 通常由外部工具驱动。 + Mooncake 的不同点在于 Master 还承担物理空间管理、冷热分级、租约、client liveness 和后台迁移任务。 + 因此引入轻量 Coordinator 是为了管理 Mooncake 特有的全局控制问题,而不是替代 Redis Cluster 的去中心化热路径。 +
+ +
+ P99.9 原则: + Coordinator 不能成为 P99.9 热路径依赖。客户端在正常读写时不访问 Coordinator;Coordinator 故障只影响扩缩容、rebalance、跨组迁移、视图发布和预算调节,不影响已有稳定 group 的前台读写或 quorum 组内选主。 +
+ +

2. 设计目标与非目标

+

2.1 设计目标

+
    +
  • 将对象元数据按固定 slot 拆分到多个 MasterGroup,解除单 MasterNode 的 CPU、锁和 RPC 瓶颈。
  • +
  • 保持前台请求路径短:客户端计算 slot,直接访问对应 MasterGroup 的 current primary。
  • +
  • 在扩缩容、slot 迁移、failover、冷热分级、资源压力下保持 P99.9 时延稳定。
  • +
  • 用轻量 Coordinator 管理 slot view、资源摘要和后台预算,但不代理元数据请求。
  • +
  • 支持兼容式演进:单 Master 先暴露全量 slot view,再逐步引入多 Master 和迁移能力。
  • +
+ +

2.2 非目标

+
    +
  • 不把 Coordinator 做成所有元数据 RPC 的代理。
  • +
  • 不要求跨 slot Batch 操作具备全局原子性;默认提供逐 key 成功/失败结果。
  • +
  • 不要求第一阶段实现完整自动 rebalance;可以先由管理命令触发迁移计划。
  • +
  • 不把物理数据迁移放入 Master 进程内同步执行;Master 只负责元数据状态机和任务编排。
  • +
+ +

3. 顶层架构

+
++---------------------------+
+| Mooncake Client / SDK     |
+| - slot(key)               |
+| - slot map cache          |
+| - MOVED/ASK retry         |
+| - batch regroup           |
++-------------+-------------+
+              |
+              | foreground metadata RPC
+              v
++-------------------+   +-------------------+   +-------------------+
+| Group G1 primary  |   | Group G2 primary  |   | Group G3 primary  |
+| serves slot groups|   | serves slot groups|   | serves slot groups|
+| object metadata   |   | object metadata   |   | object metadata   |
+| lease / tasks     |   | lease / tasks     |   | lease / tasks     |
++---------+---------+   +---------+---------+   +---------+---------+
+          |                       |                       |
+          | resource summary, latency stats, migration state
+          v                       v                       v
++---------------------------------------------------------+
+| Lightweight Coordinator                                 |
+| - cluster view publisher                                |
+| - slot ownership / generation                           |
+| - migration planner                                     |
+| - group placement / fencing publisher                   |
+| - resource summary aggregator                           |
+| - background budget controller                          |
++---------------------------+-----------------------------+
+                            |
+                            | durable cluster metadata
+                            v
++---------------------------------------------------------+
+| HA Backend                                               |
+| etcd / Redis / K8s Lease / persistent catalog / OpLog    |
++---------------------------------------------------------+
+
++---------------------------------------------------------+
+| Storage Clients / Segments                               |
+| - memory / local disk / NoF SSD / CXL                    |
+| - heartbeat / capacity / health                          |
+| - execute copy/offload/promotion tasks                   |
++---------------------------------------------------------+
+  
+ +

3.1 核心原则

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
原则含义
热路径去中心化客户端通过本地 slot map 直连 owner MasterGroup 的 current primary,不经过 Coordinator。
控制路径集中但轻量Coordinator 管理 view、跨组迁移、group placement、fencing 发布和预算,不保存每个对象的热路径状态。
对象状态单 owner同一个 key 的 Put/Get/Evict/Offload/Promotion 只由当前 slot owner MasterGroup 提交。
所有权带 fencing token组内 primary 用 term,slot 改属用 assignment_generation,segment 控制权改属用 control_generation;三者不能混用。
后台任务可退让迁移、snapshot、eviction、offload、promotion 必须受预算控制,P99.9 超阈值时自动降速。
+ +

4. 组件职责

+

4.1 Mooncake Client / SDK

+
    +
  • 维护本地 ClusterView 和 slot map 缓存。
  • +
  • 计算 slot = hash(tenant_id, key) % 16384
  • +
  • 按 slot owner MasterGroup 解析 current primary endpoint 并直连。
  • +
  • 处理 MOVEDASKNOT_PRIMARYSTALE_TERMTRYAGAINSTALE_ASSIGNMENT
  • +
  • 对 Batch 请求按 owner regroup,并按原始 key 顺序合并响应。
  • +
  • 设置 per-RPC deadline,避免单个 Master 慢请求拖垮 P99.9。
  • +
+ +

4.2 MasterGroup 与其 current primary

+
    +
  • MasterGroup 负责一组 SlotGroup 的对象元数据;current primary 对外服务,replica 按复制模式同步状态。
  • +
  • 执行对象生命周期状态机:PutStartPutEndPutRevokeRemoveEvict
  • +
  • 维护对象租约、hard pin、soft pin 和访问热度统计。
  • +
  • 生成和消费对象级后台任务,例如 copy、move、offload、promotion。
  • +
  • 由 current primary 向 Coordinator 上报前台时延、队列深度、slot 负载、资源使用摘要和迁移状态。
  • +
  • 订阅 Coordinator 发布的 cluster view、resource view 和 background budget。
  • +
+ +

4.3 Lightweight Coordinator

+
    +
  • 维护 MasterNode incarnation、MasterGroup membership、slot ownership、assignment generation 和 view revision。
  • +
  • 生成扩缩容和 rebalance 计划。
  • +
  • 协调 slot group 迁移状态机,但不搬运前台请求。
  • +
  • 汇总 segment resource summary,发布近实时 ResourceView。
  • +
  • 根据 P99.9、队列深度、CPU、网络、replay lag 动态调节后台任务预算。
  • +
  • 观察组内选主结果并发布带 term 的新 primary endpoint;通过 HA Backend fencing 防止旧 primary 继续写。
  • +
+ +

4.4 HA Backend

+
    +
  • 持久化 cluster view、slot ownership、membership lease 和 coordinator lease。
  • +
  • 保存 MasterGroup 的 OpLog、snapshot catalog、commit position 或恢复指针。
  • +
  • 在 Coordinator 故障时支持新 Coordinator 接管。
  • +
+ +

4.5 Storage Client / Segment

+
    +
  • 向 Master 或 Resource Registry 上报 segment 容量、水位、健康状态。
  • +
  • 执行数据路径操作,例如 RDMA 写、SSD offload、本地 promotion、replica clear。
  • +
  • 通过 task heartbeat 获取 offload/promotion/copy/move 任务。
  • +
+ +

4.6 Segment 与 MasterGroup 的归属关系

+
+ 结论: + key 的 slot owner 和 segment 的 control owner 是两套独立映射。 + slot owner 管对象及其 replica 状态;segment control owner 管 segment 注册、心跳、健康状态和客户端级任务通道。 + 一个对象可以被放到任意健康 segment 上,因此对象的 slot owner 不要求等于该 segment 的 control owner。 +
+ +

+ 推荐按 client_id 而不是按 segment_id 分配 segment control owner:同一个 Storage Client + 注册的 MEMORY、LOCAL_DISK、NOF_SSD 等全部 segment 由同一个 control-owner MasterGroup 管理,heartbeat 发往该组 current primary。 + 这样 client liveness、一次 heartbeat 中携带的多个 segment 状态以及下发给该 client 的任务队列都落在同一处, + 不需要拆分一次 heartbeat。Coordinator 在 ClusterView 中持久化该映射;初次加入时可用 + rendezvous hash 选择 control-owner MasterGroup,并在负载不均衡时显式迁移,而不能由各 MasterNode 独立计算后直接生效。 +

+ +
+对象控制面:hash(tenant_id, key) -> slot -> slot owner MasterGroup -> current primary
+Segment 控制面:client_id -> client control-owner MasterGroup -> current primary -> 全部 segments
+
+例:
+  Group G1 owns slots [0, 4095]       and controls Client X -> [Segment X.mem, X.ssd]
+  Group G2 owns slots [4096, 8191]    and controls Client Y -> [Segment Y.mem]
+  Group G3 owns slots [8192, 16383]   and controls Client Z -> [Segment Z.mem, Z.nof]
+
+  key K belongs to a slot owned by G2
+  K replicas may be placed on X.mem and Z.nof
+  G2 owns K metadata and decides create/remove/offload/promotion
+  G1 controls X.mem health; G3 controls Z.nof health
+  Storage Client X/Z remains the authority that atomically reserves/frees physical extents
+  
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态或动作唯一写入者/权威方其他 Master 如何使用
key、replica descriptor、对象状态机key 的 slot owner MasterGroup非 owner 返回 MOVED,不得修改
client lease、segment mount/unmount、health、watermarkclient control-owner MasterGroup通过只读 ResourceView 获取摘要
物理 extent 的 reserve/freeStorage Client 的 allocator,使用 lease/CAS 和幂等 request_idslot owner 直接请求 allocator;control owner 不在每次 Put 的同步链路中
offload/promotion/copy/move 的对象决策对象 slot owner MasterGroup任务经 client control owner 的 heartbeat 通道投递,完成后回报 slot owner
segment control owner 映射Coordinator + HA Backend CAS所有 Master 订阅同一版本化映射
+ +

+ 因而“G1 管理 Segment X”只表示 G1 是 X 所属 client 的控制面 owner,并不表示只有 G1 的 slot + 才能在 X 上放 replica。所有 slot-owner group 都可依据 ResourceView 选择 X,但最终空间分配必须由 X 所在 + Storage Client 的 allocator 原子确认,避免多个 Master 根据过期容量摘要重复分配。 +

+ +

4.6.1 Owner 选择、故障与迁移规则

+
    +
  • 初始分配:Coordinator 对 client_id 和 ACTIVE MasterGroup 集合做 rendezvous hash,选择一个 control-owner group;组内 primary/replica 由该组成员关系决定。hash 可叠加连接数、segment 数和 heartbeat QPS 权重。
  • +
  • 稳定性:MasterNode 加入或退出时不自动重算全部 client;只有 Coordinator 生成并提交的新 group 映射才改变 owner,避免大规模心跳抖动。
  • +
  • 故障切换:current primary 失效后由组内复制协议选出新 primary 并递增 term;Coordinator 发布 endpoint。只有 client 从一个 control-owner group 迁到另一个 group 时才递增 control_generation。Storage Client 同时校验 term 和 control_generation,旧 primary 必须被 fencing。
  • +
  • 主动迁移:先让 target group 导入 client/segment 快照并接收增量状态,再递增 control_generation 切换 heartbeat,最后清理 source group;迁移期间对象 slot ownership 不变。
  • +
  • 故障域:同一 MasterGroup 的 replicas 尽量跨节点、机架或 AZ;同一 client 的 segment 不拆给多个 control-owner group,除非未来 heartbeat 协议支持按 segment 独立分流。
  • +
+ +
+ 不要混淆三种变化: + MasterGroup 组内切主只提升 term;SlotGroup 跨组迁移改变对象元数据归属并提升 assignment_generation; + client control owner 跨组迁移改变 segment 控制归属并提升 control_generation。任何一种变化都不应隐式触发另外两种。 +
+ +

4.6.2 跨组任务投递

+

+ 当对象 slot owner 与目标 segment control owner 不同,不能依赖两个 group 的内存队列或分布式事务。 + 对象 owner 先在本组 OpLog 中提交 TaskIntent,再以至少一次语义投递到 control-owner group;后者通过 + Storage Client heartbeat 下发。完成通知可重复发送,由对象 owner 根据 task_id 幂等提交最终 replica 状态。 +

+
+TaskEnvelope {
+  UUID task_id;
+  string object_owner_group_id;
+  uint64_t object_group_term;
+  uint32_t slot_id;
+  uint64_t assignment_generation;
+  string client_control_group_id;
+  uint64_t control_generation;
+  UUID target_segment_id;
+  TaskType type;
+}
+  
+

+ 任一 fencing generation/term 不匹配时不得盲目执行:投递方刷新 ClusterView 后重新路由,执行方使用 + task_id 去重。这样组内切主、SlotGroup 迁移或 client control owner 迁移都不会造成任务丢失或重复修改对象状态。 +

+ +

4.7 多 Master 的分组:MasterGroup 与 MasterNode

+
+ 推荐模型: + 用 MasterGroup(也可称 ReplicaSet)表达主备复制关系,用不同 MasterGroup 之间的 slot + 分配表达 shard 分担关系。MasterNode 只是进程/机器实例,不应被永久标记成全局 primary、standby 或 shard。 +
+ +
+                         Cluster
+                            |
+          +-----------------+-----------------+
+          |                                   |
+    MasterGroup G1                       MasterGroup G2
+    owns slot groups 0..3                owns slot groups 4..7
+    (shard relation with G2)             (shard relation with G1)
+          |                                   |
+    primary: Master A                    primary: Master C
+    standby: Master B                    standby: Master D
+    (A/B are replication peers)          (C/D are replication peers)
+
+Client route:
+  key -> slot -> slot_group -> master_group -> current primary endpoint
+  
+ +

+ 因此,“A 和 B 是主备”表示它们是同一个 MasterGroup 的复制成员;“A 和 C 分担 shard”更准确地说是 + A 所在的 G1 与 C 所在的 G2 分别拥有不同 slot group。主备关系发生在组内,分片关系发生在组间, + 两者不应使用同一个 role 字段表达。 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
抽象职责所有权/版本
MasterNode描述进程地址、故障域、容量、存活租约;可承载一个或多个 group replicanode_id + node_incarnation_id
MasterGroup对象元数据的一致性与故障切换单元,包含一个 primary 和若干 standbygroup_id + term
GroupMember描述某个 node 在某个 group 内的 PRIMARY、VOTER、LEARNER 等角色角色只在 group 内有效
SlotGroupAssignment把一段 slot 映射到一个 MasterGroup;扩缩容时迁移该映射及其状态assignment_generation
+ +

4.7.1 两种部署方式

+

方式一:节点独占 group,建议作为第一阶段。

+
+G1 = {A primary, B standby}
+G2 = {C primary, D standby}
+  
+
    +
  • 关系直观,故障和容量边界容易分析,运维简单。
  • +
  • standby 正常情况下资源利用率较低,可提供只读诊断,但不应承接强一致前台写。
  • +
  • 两副本只能实现异步或同步主备,无法在网络分区下形成安全多数派;需要 quorum 强一致时,每组至少放置 3 个跨故障域 voter。
  • +
+ +

方式二:一个节点承载多个 group replica,用于提高利用率。

+
+G1 = {A primary, B standby, C standby}
+G2 = {B primary, C standby, A standby}
+G3 = {C primary, A standby, B standby}
+  
+
    +
  • A、B、C 都承担一部分 primary shard,同时又互为其他 group 的 replica;“standby”是非 primary replica 的统称,QUORUM 模式对应 VOTER,ASYNC_STANDBY 模式对应 FOLLOWER。
  • +
  • 调度必须限制同一 node 的 primary 数、总 QPS、metadata bytes、replay 带宽和故障恢复负载。
  • +
  • 同一 group 的两个 replica 不能落在同一进程或同一故障域;否则节点或机架故障会同时丢失主备。
  • +
  • 一个 node 故障后,不能让其所有 standby 同时提升到同一个剩余 node,需要预先校验 failover capacity。
  • +
+ +

4.7.2 分组与迁移规则

+
    +
  • 组内切主:只改变 MasterGroup.term 和 primary member,不改变该组拥有的 slot;客户端刷新 endpoint 即可。
  • +
  • 组间迁移:把 SlotGroup 从 source MasterGroup 复制到 target MasterGroup,完成 snapshot/replay 后递增 assignment_generation 并切换归属。
  • +
  • 成员变更:采用 learner 加入、追平、转 voter、移除旧 member 的联合配置流程,不能直接覆盖成员列表。
  • +
  • 分组粒度:MasterGroup 数量应多于物理节点数,才能细粒度均衡;但每组都有 OpLog、snapshot 和 heartbeat 成本,应使用固定数量的虚拟 group,而不是每个 slot 一个复制组。
  • +
  • Coordinator 边界:Coordinator 决定 group membership 和 slot-to-group assignment;组内复制协议决定日志提交和 primary,不让 Coordinator 进入每次元数据写的提交路径。
  • +
  • Segment 控制归属:ClientControlOwner 也指向 MasterGroup,而不是裸 node;heartbeat 访问该组当前 primary,组内切主不改变 client 的逻辑归属。
  • +
+ +
+ 关键约束: + 如果没有真正的组内复制协议和提交法定人数,只能称为 primary/standby snapshot failover,不能宣称强一致 HA。 + 此时 primary 故障可能丢失尚未同步的 OpLog,ClusterView 必须暴露相应的 RPO 状态。 +
+ +

5. 数据模型

+

5.1 ClusterView

+
+struct ClusterView {
+  string cluster_id;
+  uint64_t view_revision;
+  uint32_t slot_count;          // default: 16384
+  vector<MasterNode> nodes;
+  vector<MasterGroup> master_groups;
+  vector<SlotGroupOwner> slot_groups;
+  vector<ClientControlOwner> client_control_owners;
+}
+
+struct MasterNode {
+  string node_id;
+  string rpc_address;
+  NodeStatus status;
+  FailureDomain failure_domain;
+  UUID node_incarnation_id;
+}
+
+struct MasterGroup {
+  string group_id;
+  uint64_t term;
+  vector<GroupMember> members;
+  string primary_node_id;       // cached routing endpoint for current term
+  ReplicationMode mode;         // QUORUM / ASYNC_STANDBY
+  CommitPosition committed;
+}
+
+struct GroupMember {
+  string node_id;
+  GroupRole role;               // PRIMARY / VOTER / FOLLOWER / LEARNER
+  CommitPosition replayed;
+}
+
+struct SlotGroupOwner {
+  uint32_t slot_group_id;
+  uint32_t slot_begin;
+  uint32_t slot_end;
+  string owner_master_group_id;
+  uint64_t assignment_generation;
+  SlotGroupState state;
+}
+
+struct ClientControlOwner {
+  UUID client_id;               // applies to all segments of this client
+  string owner_master_group_id;
+  uint64_t control_generation;  // independent from slot assignment
+  ControlOwnerState state;
+}
+  
+ +

5.2 ResourceView

+
+struct ResourceView {
+  uint64_t resource_revision;
+  vector<SegmentResource> segments;
+  ClusterPressure pressure;
+}
+
+struct SegmentResource {
+  UUID segment_id;
+  UUID client_id;
+  uint64_t control_generation;
+  UUID segment_incarnation;
+  string segment_name;
+  ReplicaType type;             // MEMORY / LOCAL_DISK / NOF_SSD / CXL
+  ResourceCapability capability;
+  ResourceTelemetry telemetry;
+  FailureDomain failure_domain;
+  NetworkLocality locality;
+  SegmentHealth health;
+}
+  
+ +

5.3 多维资源、工作负载与放置抽象

+
+ 设计原则: + 不再用单一 used_bytes / capacity_bytes 判断节点优劣,而把“节点能提供什么”、 + “对象需要什么”和“当前还承诺了多少资源”分开建模。容量大但带宽小的 segment 可承载冷数据, + 容量小但带宽大的 segment 可承载热点副本;同一对象的持久放置与实时读流量也可以由不同策略处理。 +
+ +

建议增加以下六类稳定抽象:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
抽象回答的问题更新频率与归属
ResourceCapabilitysegment 的容量、顺序/随机读写带宽、IOPS、基础时延、并发度和介质能力上限慢变化;注册或基准测试时由 Storage Client 上报
ResourceTelemetry当前空闲容量、可用带宽、队列深度、P99 时延、错误率和热度快变化;control owner 聚合后发布带时间戳的摘要
FailureDomain / NetworkLocality节点、机架、AZ、NUMA、RDMA fabric 与访问方之间的故障隔离和网络代价低频变化;Coordinator 维护
WorkloadIntent对象预期大小、读写比、目标吞吐/P99、热度、生命周期、持久性和副本约束写入时显式提供,缺省值由历史观测推断
ResourceReservation已被并发 Put 或流量调度承诺、但尚未反映到 telemetry 的 bytes、bandwidth 和 IOPS实时;allocator/traffic admission 以 lease + segment incarnation 管理
PlacementPolicy + ScoreBreakdown如何执行硬约束过滤、软目标打分,以及为什么选择某个 segment版本化策略;Master 本地执行并记录可解释结果
+ +
+struct ResourceVector {
+  uint64_t capacity_bytes;
+  double read_bandwidth_Bps;
+  double write_bandwidth_Bps;
+  double read_iops;
+  double write_iops;
+  double concurrency;
+}
+
+struct ResourceCapability {
+  ResourceVector limit;         // calibrated sustainable limit, not link peak
+  LatencyProfile baseline;      // p50 / p99 by operation and object-size bucket
+  set<Feature> features;       // RDMA_READ / PERSISTENT / RANDOM_WRITE / ...
+}
+
+struct ResourceTelemetry {
+  ResourceVector used;
+  ResourceVector reserved;
+  LatencyProfile observed;
+  double queue_depth;
+  double error_rate;
+  uint64_t sample_timestamp_ms;
+  uint64_t sample_seq;          // monotonic within one segment_incarnation
+}
+
+struct WorkloadIntent {
+  uint64_t object_size_bytes;
+  AccessClass access_class;     // HOT / WARM / COLD, only a hint
+  double expected_read_Bps;
+  double expected_write_Bps;
+  double expected_iops;
+  uint64_t target_p99_us;
+  DurabilityClass durability;
+  uint32_t replica_num;
+  FailureDomainPolicy isolation;
+  optional<string> consumer_locality;
+}
+
+struct ResourceReservation {
+  UUID reservation_id;
+  UUID segment_id;
+  ResourceVector amount;
+  uint64_t expire_at_ms;
+  UUID segment_incarnation;
+  string idempotency_key;
+}
+
+struct ScoreBreakdown {
+  double capacity_headroom;
+  double bandwidth_headroom;
+  double latency_cost;
+  double locality_cost;
+  double failure_correlation_cost;
+  double migration_cost;
+  double total_score;
+  string policy_version;
+}
+  
+ +

5.3.1 放置和读路由必须解耦

+
    +
  • Replica Placement:决定数据存在哪些 segment,主要优化容量、持久性、写入代价和故障域。
  • +
  • Replica Read Selection:在已有 replica 中按实时带宽、队列、时延和调用方 locality 选择读源;不因为一次流量变化立即搬数据。
  • +
  • Tiering / Rebalance:根据较长时间窗口的热度和资源价格决定复制、迁移、offload 或 promotion,带 hysteresis 和冷却时间。
  • +
+

+ 例如,大容量低带宽节点可保存完整冷副本,小容量高带宽节点只保存工作集热点副本;读请求优先访问高带宽副本, + 高带宽层容量不足时淘汰热点副本不会删除低带宽层的持久副本。这样“容量归属”和“流量归属”不再被一个 placement 决策绑死。 +

+ +

5.3.2 可演进的算法接口

+
+PlacementResult Place(PlacementContext ctx) {
+  candidates = FilterByHardConstraints(
+      ctx.resource_view, ctx.intent, ctx.excluded_failure_domains);
+
+  candidates = CheckMultiResourceFeasibility(
+      candidates, capability - telemetry.used - telemetry.reserved);
+
+  ranked = policy.Score(candidates, ctx.intent, ctx.policy_version);
+  reservation = TryReserve(ranked, bytes + bandwidth + iops, bounded_attempts);
+
+  return {reservation, ranked.winner.score_breakdown};
+}
+  
+
    +
  • 硬约束和软评分分离;容量不足、目标 P99 不满足、故障域冲突必须先过滤,不能靠低分表达。
  • +
  • 采用归一化 headroom 或 Dominant Resource Fairness 思路检查多维可行性,避免容量充足却把带宽耗尽。
  • +
  • PlacementPolicy 是版本化、无状态接口;后续可从加权打分演进为 bin-packing、min-cost flow 或学习型策略,而不改变 Put 状态机。
  • +
  • 策略输入只使用版本化快照,输出保存 policy_versionScoreBreakdown,便于回放、灰度和比较新旧算法。
  • +
  • 在线请求只对 Top-K 候选做有界 reserve 尝试;复杂的全局优化在 Coordinator 后台生成 policy 参数或迁移计划,不能进入 Put 热路径。
  • +
+ +

5.3.3 指标语义要求

+
+ 避免错误建模: + 带宽不能只保存一个静态数值。至少区分读/写、可持续能力/当前使用量/已预留量,并按对象大小桶记录有效吞吐和时延; + 否则 100 Gbit/s 网卡、SSD 顺序吞吐和 4 KiB 随机访问会被错误地视为同一种资源。 +
+ +

5.4 BackgroundBudget

+
+struct BackgroundBudget {
+  uint64_t budget_revision;
+  uint32_t migration_max_inflight_groups;
+  uint32_t migration_qps_budget;
+  uint64_t snapshot_bytes_per_sec;
+  uint32_t eviction_keys_per_round;
+  uint32_t offload_tasks_per_heartbeat;
+  uint32_t promotion_tasks_per_heartbeat;
+}
+  
+ +

6. 热路径流程分析

+

6.1 GetReplicaList

+
+Client
+  -> calculate slot(key)
+  -> find owner from local ClusterView
+  -> RPC GetReplicaList(key, tenant_id, master_group_id, group_term,
+                        slot_id, assignment_generation)
+
+Owner MasterGroup current primary
+  -> validate master_group_id, term, slot ownership and assignment_generation
+  -> lookup object metadata in local slot group
+  -> refresh lease / hotness counter
+  -> rank readable replicas by current latency / bandwidth / caller locality
+  -> if promotion-on-hit eligible: enqueue promotion task only
+  -> return replica list
+
+Client
+  -> if OK: use replica descriptors for data path
+  -> if MOVED/STALE_ASSIGNMENT: refresh slot map and retry
+  -> if TRYAGAIN: short backoff within request deadline
+  
+ +

+ P99.9 控制点:读路径只访问一个 MasterGroup primary;promotion 不在前台执行;slot view 或 group term 过期通过重定向快速修正;RPC 有 deadline。 +

+ +

6.2 PutStart / PutEnd

+
+PutStart:
+Client -> owner MasterGroup current primary
+  -> validate group term and assignment generation
+  -> build WorkloadIntent from request policy and observed history
+  -> filter hard constraints and score candidates from local ResourceView
+  -> allocate from the local DelegatedExtentPool
+  -> write object PROCESSING metadata
+  -> enqueue asynchronous standby replication
+  -> return replica descriptors
+
+Data Path:
+Client writes object data to selected replicas
+
+PutEnd:
+Client -> owner MasterGroup current primary
+  -> validate group term and assignment generation
+  -> transition PROCESSING -> COMPLETE
+  -> enqueue asynchronous standby replication
+  -> optionally enqueue offload task
+  -> return OK
+  
+ +

+ P99.9 控制点:PutStart 不向 Coordinator 或远端空间分配服务同步请求资源;ResourceView 和 DelegatedExtentPool 都在本地。 + 本地 extent 不足时快速失败并触发后台补充,禁止把远端 refill 或长时间候选扫描退化到当前请求中。 +

+ +

6.3 Batch 请求

+
+Client input keys: [k1, k2, k3, k4, k5]
+
+Regroup by owner:
+  Master A: [k1, k4]
+  Master B: [k2, k5]
+  Master C: [k3]
+
+Parallel RPC:
+  send sub-batches concurrently with per-shard deadline
+
+Merge:
+  output results in original key order
+  
+ +

+ P99.9 控制点:子批次并行,单个慢 shard 不阻塞其他 shard 的结果;业务可选择 fail-fast、partial success 或 bounded retry。 +

+ +

6.4 时序图与当前 Mooncake 对比

+

+ 本节只统计正常稳定状态,不把首次建连、ClusterView watch、MOVED/STALE_TERM 重试和故障恢复计入常态热路径。 + 当前实现以代码中的单个全局 MasterService 为基线:客户端 Get 调用一次 + GetReplicaList;Put 调用 PutStart、执行数据传输、再调用 PutEnd; + segment allocator 由 Master 进程内的 segment_manager_ 访问。当前 1024 个 metadata shard 是进程内锁分片, + 不是分布式 MasterGroup。对应代码入口为 client_service.cpp::Get/Put、 + master_client.cpp::GetReplicaList/PutStart/PutEndmaster_service.cpp::GetReplicaList/PutStart/PutEnd。 + 图中的当前基线是默认非 HA 模式;当前 HA 仍是单个 active Master,客户端 RPC 形状不变,leader watch 在后台执行。 + 若某种 HA 配置把 OpLog 外部持久化同步放入请求路径,应把该耗时作为单独基线实测,不能与默认模式混合比较。 +

+

+ 本节时序图使用 Mermaid 11 sequenceDiagram 绘制;HTML 通过官方推荐的 ESM 方式从 jsDelivr 加载。 + 无网络环境仍会保留 Mermaid 源码,可在文档构建阶段改为 vendored Mermaid 或预渲染 SVG。 +

+ +

6.4.1 计数口径

+ + + + + + + + +
符号含义
G一个 Batch 中涉及的 owner MasterGroup 数量。
RPC 次数一次 request/response 算一个 RPC;若统计单向网络消息,会在表中单独说明。
串行网络轮次关键路径上必须前后等待的网络 round trip 数。多个并行 RPC 虽然增加消息数,但只增加一个串行轮次。
数据传输Transfer Engine/RDMA/NoF 写读,不等同于 metadata RPC;单独计数。
+ +

6.4.2 Get:当前实现

+
+sequenceDiagram
+    autonumber
+    actor App as Application
+    participant Client as Mooncake Client
+    participant Master as Global Master
+    participant Segment as Storage Segment
+    App->>Client: Get(key)
+    Client->>Master: GetReplicaList(key)
+    activate Master
+    Master->>Master: local metadata lookup
grant read lease + Master-->>Client: replica descriptor + deactivate Master + Client->>Segment: TransferRead / RDMA read + Segment-->>Client: value bytes + Client-->>App: value + Note over Client,Master: Metadata RPC = 1, synchronous inter-Master RPC = 0 +
+ +

6.4.3 Get:新设计

+
+sequenceDiagram
+    autonumber
+    actor App as Application
+    participant Client as Routed Client
+    participant Primary as Owner Group Primary
+    participant Segment as Selected Segment
+    participant Async as Async Aggregator
+    App->>Client: Get(key)
+    Client->>Client: slot hash + cached ClusterView lookup
+    Client->>Primary: GetReplicaList(term, assignment_generation)
+    activate Primary
+    Primary->>Primary: validate routing + local lookup
+    Primary-->>Client: ranked replica descriptors
+    Primary-)Async: aggregate lease and hotness update
+    deactivate Primary
+    Client->>Segment: TransferRead / RDMA read
+    Segment-->>Client: value bytes
+    Client-->>App: value
+    Note over Client,Primary: Metadata RPC = 1, Coordinator RPC = 0, quorum RTT = 0
+  
+ +

+ 正常 Get 的通信次数与当前实现相同,新增 slot hash、view lookup、term/generation 比较均为本地 CPU 操作。 + 但当前 Get 会刷新对象 read lease;如果新设计把每次 lease/hotness 更新同步复制到 quorum,Get 将额外增加一个 group commit RTT, + 形成明显劣化。推荐把访问热度和 lease heartbeat 做本地聚合、异步批量复制,并规定新 primary 在提升后至少一个 + max_read_lease_ttl 窗口内禁止回收旧 replica。这样 failover 期间保守占用空间,但正常 Get 不增加网络轮次。 +

+ +

6.4.4 Put:当前实现

+
+sequenceDiagram
+    autonumber
+    actor App as Application
+    participant Client as Mooncake Client
+    participant Master as Global Master
+    participant Storage as Target Storage Nodes
+    App->>Client: Put(key, value)
+    Client->>Master: PutStart
+    activate Master
+    Master->>Master: local placement + local allocation
+    Master-->>Client: replica descriptors
+    deactivate Master
+    Client->>Storage: TransferWrite to configured replicas
+    Storage-->>Client: transfer completion
+    Client->>Master: PutEnd
+    activate Master
+    Master->>Master: mark COMPLETE
+    Master-->>Client: OK
+    deactivate Master
+    Client-->>App: OK
+    Note over Client,Master: Metadata RPC = 2, allocator RPC = 0, group commit RTT = 0
+  
+ +

6.4.5 Put:新设计快路径

+

+ Put 只采用这一条前台路径。为避免每次 PutStart 同步访问空间分配服务,增加 DelegatedExtentLease:Storage Client allocator + 预先把互不重叠的 extent range、bytes/bandwidth/IOPS budget 租给 MasterGroup。Group primary 在租约范围内本地切分, + 后台低水位时批量续租。lease 必须携带 segment_incarnation + control_generation + owner_group_id + group_term + expire_at, + Storage Client 不得把同一 extent 同时租给两个 group。 +

+
+sequenceDiagram
+    autonumber
+    actor App as Application
+    participant Client as Mooncake Client
+    participant Primary as Owner Group Primary
+    participant LocalPool as Local Delegated Extent Pool
+    participant Standby as Standby Replicas
+    participant Storage as Target Storage Nodes
+    participant Allocator as Background Allocation Service
+    rect rgb(239, 246, 255)
+        Note over Primary,Allocator: Background refill, outside per-object critical path
+        Primary-)Allocator: LeaseExtentBatch at low watermark
+        Allocator-)Primary: delegated ranges + budgets
+    end
+    rect rgb(236, 253, 245)
+        Note over App,Storage: Per-object low-latency path
+        App->>Client: Put(key, value)
+        Client->>Primary: PutStart(term, assignment_generation)
+        Primary->>LocalPool: local allocate
+        LocalPool-->>Primary: descriptors
+        Primary-)Standby: async replicate PROCESSING
+        Primary-->>Client: replica descriptors
+        Client->>Storage: TransferWrite to configured replicas
+        Storage-->>Client: transfer completion
+        Client->>Primary: PutEnd
+        Primary-)Standby: async replicate COMPLETE
+        Primary-->>Client: OK
+        Client-->>App: OK
+    end
+    Note over Client,Primary: Same synchronous metadata RPC count as current = 2
+    Note over Primary,Allocator: Normal PutStart allocator RPC = 0, synchronous replication RTT = 0
+  
+

+ 该快路径在同步通信形状上与当前实现一致:2 次 metadata RPC、按配置副本数执行数据写、0 次逐对象 allocator RPC、0 次同步复制 RTT。 + 代价是 PROCESSING/COMPLETE 在 follower 确认前已经向客户端返回,因此 ASYNC_STANDBY 存在非零 RPO。 + 因而本设计的 Put 语义必须明确公布这一 RPO,不能把异步复制描述为 RPO=0 的强一致提交。 +

+

+ primary 切换后,新 term 不得继续本地切分旧 term 的 delegated range;旧 range 进入 quarantine,等待 lease TTL + 到期或 allocator 明确回收,新 primary 获取新 range。这样即使旧 primary 暂时仍可访问数据网络,也只可能在隔离范围内产生孤儿写, + 不会与新 primary 重复分配同一 extent。该策略用临时容量占用换取正常 PutStart 的零 allocator RTT。 +

+ +

6.4.6 Batch:当前实现与新设计

+
+sequenceDiagram
+    autonumber
+    participant Client
+    participant Current as Current Global Master
+    participant G1 as Group G1 Primary
+    participant G2 as Group G2 Primary
+    participant G3 as Group G3 Primary
+    rect rgb(249, 250, 251)
+        Note over Client,Current: Current implementation
+        Client->>Current: BatchGet / BatchPutStart(all keys)
+        Current->>Current: process keys in one Master process
+        Current-->>Client: per-key results
+    end
+    rect rgb(239, 246, 255)
+        Note over Client,G3: New design
+        Client->>Client: local regroup by owner group
+        par one parallel network round
+            Client->>G1: sub-batch 1
+            G1-->>Client: results 1
+        and
+            Client->>G2: sub-batch 2
+            G2-->>Client: results 2
+        and
+            Client->>G3: sub-batch 3
+            G3-->>Client: results 3
+        end
+        Client->>Client: merge in original key order
+    end
+  
+

+ 当前跨任意 key 的 Batch 只发 1 个 Master RPC;新设计涉及 G 个 owner 时发送 G 个并行 RPC。 + 消息数从 2 个单向消息增加为 2G,但理想情况下仍是 1 个串行网络轮次。它提高总吞吐和故障隔离, + 但整批完成时延变为所有 sub-batch 的最大值;因此必须支持 per-group deadline、partial result,以及限制最大并发 group 数。 +

+ +

6.4.7 Storage heartbeat 与后台任务

+
+sequenceDiagram
+    autonumber
+    participant Storage as Storage Client
+    participant Current as Current Global Master
+    participant Object as Object-owner Group
+    participant Control as Control-owner Group
+    rect rgb(249, 250, 251)
+        Note over Storage,Current: Current implementation
+        Storage->>Current: Ping / Offload / Promotion heartbeat
+        Current-->>Storage: status / tasks
+    end
+    rect rgb(239, 246, 255)
+        Note over Storage,Control: New design
+        Object-)Control: batched TaskEnvelope
+        Storage->>Control: heartbeat(term, control_generation)
+        Control-->>Storage: task batch
+        Storage-)Control: batched completion
+        Control-)Object: idempotent completion batch
+    end
+    Note over Storage,Control: Per heartbeat RPC count unchanged, cross-group messages are asynchronous
+  
+

+ 每类 heartbeat 的前台 RPC 次数不变,Coordinator 不参与。新增的跨组 TaskEnvelope/Completion 是后台批量通信, + 不进入应用 Get/Put 的同步关键路径;如果实现成每对象同步转发,则会放大 RPC,违反本设计。 +

+ +

6.4.8 通信次数与性能结论

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
操作当前实现新设计常态路径串行网络轮次变化性能判断
Get metadata1 client-Master RPC1 client-group-primary RPC;本地路由;无 Coordinator0基本不劣化;多 group 可降低锁和 CPU 排队。禁止每 Get 同步复制 lease/hotness。
Get data1 replica data read1 replica data read0不劣化;动态 read selection 可能改善 locality 和尾延迟。
Put metadata(新设计快路径)PutStart + PutEnd = 2 RPC2 client RPC;delegated extent 本地分配;replication 异步0通信形状与当前一致,目标为基本不劣化;代价是明确的非零 RPO。
Put data按配置副本数写入目标存储节点按相同副本数写入目标存储节点0不因元数据分片增加;placement/locality 可能改善或恶化,需基准验证。
跨组 Batch metadata1 Batch RPC,2 个单向消息G 个并行 sub-RPC,2G 个单向消息理想值 0;仍为 1 round消息数增加;吞吐可扩展,但整批 P99.9 受最慢 group 影响。
Storage heartbeat每类 heartbeat 1 RPC每类 heartbeat 1 RPC 到 control-owner primary0不劣化;跨组任务必须批量异步。
稳定状态 CoordinatorHA client watch backend周期/watch 更新 ClusterView 和 budget0(不在请求路径)不影响单请求时延,但增加低频控制面流量。
+ +
+ 结论: + Put 采用 delegated extent 和 ASYNC replication 的唯一快路径,并配合并行 sub-batch、异步 lease/hotness 和后台任务批量化后, + Get、单 key Put 和 heartbeat 的同步 RPC/网络轮次可以与当前实现保持一致,因此设计目标可以设为“基本不劣化”。 + 该 Put 路径采用 ASYNC_STANDBY,允许非零 RPO;接口、监控和验收结果必须明确披露这一语义。 +
+ +

6.4.9 必须通过的性能验收

+
    +
  • 相同硬件、对象大小、replica 数和并发度下,对比当前单 Master 与采用 ASYNC_STANDBY 的新设计快路径。
  • +
  • 分别测试 4 KiB、64 KiB、1 MiB、16 MiB;报告 metadata-only 与 end-to-end 的吞吐、P50、P99、P99.9。
  • +
  • Get:单副本 MasterGroup 相比当前实现在无 CPU 饱和时 P99.9 增幅目标不超过 5%;超出则必须给出 route/serialization/queue breakdown。
  • +
  • Put:ASYNC + delegated extent 快路径相比当前实现在无 CPU 饱和时 metadata P99.9 增幅目标不超过 5%;分别报告 local allocation、async enqueue 和 data transfer。
  • +
  • Batch:固定总 key 数,扫描 G=1/2/4/8/16;报告消息数、最慢 group latency 和 partial-result 时间。
  • +
  • 验证 Coordinator 停止 60 秒时稳定 Get/Put 的 RPC 数和时延分布不发生变化。
  • +
  • 验证 lease/hotness 更新、TaskEnvelope 和 ResourceView 上报不出现在应用请求 trace 的同步 critical path。
  • +
+ +

7. 控制路径流程分析

+

7.1 MasterNode 加入集群

+
+1. New MasterNode starts with node_id and rpc_address.
+2. It registers membership lease in HA Backend.
+3. Coordinator observes the new node and marks it JOINING.
+4. Coordinator publishes a new ClusterView including the node but assigns no MasterGroup replica yet.
+5. After health check and warmup pass, node becomes ACTIVE.
+6. Coordinator adds it to selected MasterGroups as LEARNER.
+7. After snapshot/replay catches up, each group promotes the learner to VOTER or PRIMARY candidate.
+8. Coordinator may then generate a plan to move SlotGroups between MasterGroups.
+  
+ +

+ 加入过程不影响已有 slot 的前台读写。只有当 slot group 迁移进入 cutover 时,相关 slot 的客户端会看到短暂 MOVED/ASK。 +

+ +

7.2 MasterNode 优雅下线

+
+1. Operator marks MasterNode DRAINING.
+2. Coordinator stops placing new group replicas or primaries on this node.
+3. For every affected MasterGroup, add and catch up a replacement learner.
+4. Transfer primary leadership away, then remove this node from group membership.
+5. Only if group-level load remains imbalanced, move SlotGroups between groups in small batches.
+6. When the node carries no group replica, mark it INACTIVE and stop the process.
+  
+ +

+ P99.9 控制点:drain 以小批量迁移执行;当前台 P99.9 超阈值时迁移自动暂停;不做全节点一次性冻结。 +

+ +

7.3 扩容 Rebalance

+
+1. Coordinator collects load:
+   - per slot group QPS
+   - key count / metadata bytes
+   - foreground P99.9
+   - migration backlog
+   - resource pressure
+
+2. Coordinator selects candidate slot groups:
+   - avoid hottest slot groups first
+   - prefer large imbalance but low active QPS groups
+   - limit concurrent groups
+
+3. Coordinator starts migration:
+   source -> snapshot copy -> incremental replay -> short cutover -> cleanup
+
+4. Coordinator observes P99.9 guard:
+   if P99.9 or queue depth exceeds threshold: reduce budget or pause migration
+
+5. Repeat until target balance reached.
+  
+ +

7.4 缩容 Rebalance

+
+1. Operator marks nodes to remove as DRAINING.
+2. Coordinator computes target placement excluding DRAINING nodes.
+3. Slot groups migrate away in priority order:
+   - cold / low QPS slot groups first
+   - hot slot groups during low-traffic windows
+4. If remaining capacity is insufficient, Coordinator rejects shrink plan.
+5. After migration completes, DRAINING node is removed.
+  
+ +

+ 缩容比扩容更容易造成尾延迟,因为目标节点承接更多负载。必须先做容量校验和 P99.9 预算校验,不能只看 slot 数是否均衡。 +

+ +

7.5 Slot Group 迁移

+
+Plan:
+  Coordinator chooses source MasterGroup, target MasterGroup, slot_group_id, migration_budget.
+
+Prepare:
+  Target group primary creates empty slot group in IMPORTING state.
+  Source group primary marks slot group MIGRATING_SOURCE and records start_oplog_offset.
+
+Snapshot Copy:
+  Source scans metadata in bounded batches.
+  Target group replicates and commits imported batches but does not serve normal requests yet.
+
+Incremental Replay:
+  Source streams OpLog after start_oplog_offset.
+  Target group commits replay and reports committed replay_lag.
+
+Cutover:
+  Source briefly blocks writes for the slot group.
+  Source commits the final OpLog tail and emits a cutover position.
+  Target proves that the cutover position is committed by its replication mode.
+  Coordinator CAS-updates owner_master_group_id and increments assignment_generation with a fencing token.
+  Source group returns MOVED for new requests.
+  Target group serves requests with the new assignment generation.
+
+Cleanup:
+  Source keeps tombstone for late requests, then releases old metadata.
+  
+ +

7.6 MasterNode 故障与 MasterGroup Failover

+
+1. HA Backend lease expires or health checks fail.
+2. Coordinator marks node SUSPECT, then UNAVAILABLE after quorum/lease confirmation.
+3. QUORUM group elects a replica holding the committed prefix; ASYNC_STANDBY group requires Coordinator-authorized promotion and reports possible RPO loss.
+4. New primary proves its commit/replay position and obtains a higher group term/fencing token.
+5. Coordinator publishes the new primary endpoint and term; slot owner and assignment_generation remain unchanged.
+6. Clients receive NOT_PRIMARY/STALE_TERM or refresh view after RPC deadline.
+7. New primary serves foreground traffic; groups without quorum remain unavailable instead of accepting split-brain writes.
+  
+ +

+ P99.9 控制点:故障检测不等待长 RPC;客户端使用短 deadline;failover 以 MasterGroup 为粒度并可并行;replica 持续 replay,避免切换后大规模恢复。组内切主不递增 assignment_generation,只有 SlotGroup 改属另一个 MasterGroup 时才递增。 +

+ +

7.7 Coordinator 故障

+
+1. Active Coordinator lease expires in HA Backend.
+2. Standby Coordinator competes for coordinator lease.
+3. New Coordinator reads persisted ClusterView and migration states.
+4. It reconciles Master reports.
+5. Incomplete migration is either resumed or rolled back.
+6. Foreground metadata requests continue using last stable ClusterView.
+  
+ +

+ Coordinator 故障不应影响稳定 slot 的读写,也不阻止具备 quorum 的 MasterGroup 完成组内选主。受影响的是新的扩缩容计划、跨组迁移、primary endpoint 发布和后台预算更新。 +

+ +

7.8 资源压力与 Admission Control

+
+1. Storage clients report segment usage and health.
+2. MasterGroup primaries report allocation failures and queue depth.
+3. Coordinator computes cluster pressure level.
+4. Coordinator publishes ResourceView and BackgroundBudget.
+5. MasterGroup primaries adjust placement, eviction/offload speed, and admission policy.
+6. If resources remain insufficient, PutStart fails fast with a precise error.
+  
+ +

+ P99.9 控制点:资源不足时不能让 PutStart 在 Master 内反复扫描和等待;应快速失败、降级 replica 数,或交给上层重试。 +

+ +

7.9 冷热分级任务

+
+Get path:
+  GetReplicaList observes LOCAL_DISK-only hot key
+  -> enqueue promotion task
+  -> return current readable replica immediately
+
+Background path:
+  Storage client heartbeat pulls promotion/offload tasks
+  -> executes data movement
+  -> NotifyPromotionSuccess / NotifyOffloadSuccess
+  -> slot owner commits metadata transition
+  
+ +

+ P99.9 控制点:冷热任务必须异步化;前台读不等待 promotion 完成;Coordinator 只调节任务预算,不参与单对象决策。 +

+ +

8. P99.9 稳定性设计

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
设计点要求原因
Coordinator 非热路径正常 Get/Put 不访问 Coordinator避免把 Coordinator 变成新的尾延迟来源
前台 RPC 优先级MasterNode 按 group 区分 foreground queue 和 background queue迁移和扫描不能挤占客户请求
短锁和分片锁对象状态锁限定在 slot group 或 key 粒度避免全 shard 锁导致 P99.9 抖动
迁移预算限制同时迁移 slot group、snapshot 带宽和 replay QPS扩缩容期间稳定前台时延
快速失败资源不足、fencing token 过期、owner 错误时快速返回明确错误避免请求排队等待不可满足条件
客户端 deadline每个 Master RPC 设置 deadline 和 bounded retry控制跨 shard Batch 的尾部等待
自动保护闭环当 P99.9 超阈值时自动降低后台预算把客户时延优先级置于扩缩容速度之上
+ +

9. 4+1 视图

+ +

9.1 逻辑视图

+

+ 逻辑视图描述系统的核心抽象和职责边界。 +

+
+Object Key Space
+  -> Slot
+  -> Slot Group
+  -> Owner MasterGroup
+  -> Current Primary MasterNode
+
+MasterGroup owns:
+  - ObjectMetadata
+  - LeaseState
+  - ReplicaState
+  - ObjectTaskState
+  - HotnessState
+  - SlotGroupOpLog
+
+Coordinator owns:
+  - ClusterView
+  - SlotOwnership
+  - MigrationPlan
+  - ResourceSummary
+  - BackgroundBudget
+  - GroupPlacement / SlotAssignment
+  - FencingPublication
+
+HA Backend persists:
+  - ClusterView records
+  - Membership leases
+  - Slot assignment generation records
+  - Snapshot catalog
+  - OpLog pointers
+  
+ +

9.2 开发视图

+

+ 开发视图描述代码模块拆分建议。 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模块职责可能落点
ClusterViewClient客户端获取、缓存、刷新 slot mapmooncake-store/include/master_client.h 附近新增路由层
ShardRouter计算 slot,处理 MOVED/ASK,Batch regroupMasterClient 内部组件
MasterNodeService承载一个或多个 MasterGroup replica 的服务进程从现有 MasterService 演进
SlotOwnershipManager校验 master_group_id、term、slot ownership 和 assignment_generationMasterNode 内部
CoordinatorService集群视图、跨组迁移、fencing 发布、预算控制新增 coordinator 模块
ResourceRegistry汇总 segment 容量、水位、健康状态Coordinator 子模块或独立库
MigrationManagerslot group snapshot、replay、cutoverCoordinator + source/target MasterGroup 协作
LatencyGuard根据 P99.9 和队列深度调节后台预算Coordinator 子模块
+ +

9.3 进程视图

+

+ 进程视图描述运行时并发关系和通信路径。 +

+
+Client Process
+  - foreground application threads
+  - MasterClient routing cache
+  - RPC pools per MasterGroup primary
+
+MasterNode Process
+  - foreground RPC workers
+  - background task workers
+  - one or more MasterGroup replicas
+  - per-group slot state store
+  - per-group OpLog append/replay worker
+  - metrics reporter
+  - migration sender/receiver
+
+Coordinator Process
+  - membership watcher
+  - cluster view publisher
+  - migration planner
+  - group placement / fencing publisher
+  - resource aggregator
+  - latency guard loop
+
+HA Backend
+  - lease/session service
+  - durable key-value records
+  - watch/notify stream
+  
+ +

+ 前台通信路径:Client -> owner MasterGroup current primary。控制通信路径:MasterNode -> Coordinator -> HA Backend,或 Coordinator -> MasterNode。 + 两者必须使用独立 RPC 队列或至少独立优先级,防止控制面流量影响客户请求。 +

+ +

9.4 物理视图

+

+ 物理视图描述部署拓扑。 +

+
+Rack / AZ A:
+  MasterNode A: G1 primary, G2 voter, G3 voter
+  Storage Clients
+
+Rack / AZ B:
+  MasterNode B: G2 primary, G1 voter, G3 voter
+  Storage Clients
+
+Rack / AZ C:
+  MasterNode C: G3 primary, G1 voter, G2 voter
+  Storage Clients
+
+Coordinator:
+  active + standby deployment
+  backed by HA Backend lease
+
+HA Backend:
+  odd-number quorum deployment if using etcd-like backend
+  
+ +

+ 同一 MasterGroup 的 replicas 应跨故障域部署,并为任一节点故障后的 primary 提升预留容量。Coordinator active/standby 也应跨节点部署,但 Coordinator 不承载热路径,因此其资源规格可以小于 MasterNode。 +

+ +

9.5 场景视图

+

+ 场景视图对应架构的 +1,用关键用例验证其他四个视图是否闭合。 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
场景参与组件关键判断
正常 GetClient、owner MasterGroup primary不访问 Coordinator;单 group primary 完成并按复制模式提交。
正常 PutClient、owner MasterGroup primary、Storage ClientMaster 使用本地 ResourceView;不阻塞等待 Coordinator。
扩容Coordinator、Source Group、Target Group、Client迁移按预算执行;cutover 通过 assignment generation 和 MOVED 完成。
缩容Coordinator、Draining MasterNode、affected MasterGroups先容量校验,再低 QPS slot group 优先迁移。
MasterNode 故障affected MasterGroups、Coordinator、HA Backend、Client各组独立提升 term 和 primary;slot owner/assignment_generation 不变。
Coordinator 故障HA Backend、Standby Coordinator、MasterGroups稳定 slot 前台读写继续;迁移编排暂停后恢复。
P99.9 抖动MasterGroups、CoordinatorLatencyGuard 降低后台任务预算,必要时暂停迁移。
+ +

10. API 与协议建议

+

10.1 客户端路由 API

+
+GetClusterView() -> ClusterView
+WatchClusterView(view_revision) -> ClusterViewDelta
+
+RequestContext {
+  string tenant_id;
+  uint32_t slot_id;
+  string master_group_id;
+  uint64_t group_term;
+  uint64_t assignment_generation;
+  string request_id;
+}
+  
+ +

10.2 MasterNode / MasterGroup 上报 API

+
+ReportShardStats(ShardStats) -> OK
+ReportMigrationState(MigrationState) -> OK
+ReportResourceUsage(ResourceUsage) -> OK
+  
+ +

10.3 Coordinator 控制 API

+
+SubmitRebalancePlan(RebalancePlan) -> PlanId
+PauseMigration(PlanId) -> OK
+ResumeMigration(PlanId) -> OK
+DrainNode(node_id) -> OK
+UpdateBackgroundBudget(BackgroundBudget) -> OK
+  
+ +

10.4 路由错误码

+
+MOVED(slot_id, target_group_id, target_endpoint, assignment_generation, group_term)
+ASK(slot_id, target_group_id, target_endpoint, assignment_generation, group_term)
+NOT_PRIMARY(master_group_id, primary_endpoint, current_term)
+STALE_TERM(master_group_id, current_term)
+TRYAGAIN(slot_id, reason)
+STALE_ASSIGNMENT(slot_id, current_generation)
+SLOT_NOT_OWNED(slot_id, owner_master_group_id)
+MASTER_READONLY(node_id)
+  
+ +

11. 新开发特性与独立验收清单

+

+ 本节是可直接进入需求和测试系统的 feature backlog。每个特性必须能单独部署或使用 fake/mock 依赖进行验收; + “代码已合入”“接口已定义”不算验收完成。除特别说明外,所有故障用例都必须有自动化测试,所有状态变化都必须暴露指标和结构化日志。 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID / 特性交付边界独立验收标准
F01
稳定 Slot Hash
定义 tenant-aware hash、16384 slots、跨语言测试向量和版本号;不包含远程路由。至少 C++/Python 对 10,000 个固定输入产生完全一致的 slot;进程重启、编译优化级别变化后结果不变;非法 hash version 明确报错。
F02
ClusterView 数据模型与持久化
序列化 MasterNode、MasterGroup、SlotGroupOwner、ClientControlOwner 及各自 fencing 字段;提供内存和 HA Backend adapter。view 可 round-trip;CAS 拒绝旧 view_revision;进程重启恢复结果一致;未知字段可前向兼容;损坏记录不被静默接受。
F03
ClusterView Watch
提供全量读取、增量 watch、断线续传和 compact 后回退全量读取。连续提交 1,000 次 view 更新,watcher 不丢失且顺序一致;任意位置断线重连后收敛到最终 view;慢 watcher 不阻塞发布者。
F04
客户端 Group 路由
实现 key -> slot -> SlotGroup -> MasterGroup -> primary endpoint,校验 group term 和 assignment_generation。使用 fake view 将全量 slot 路由到预期 endpoint;收到 NOT_PRIMARYSTALE_TERMMOVED 后在 bounded retry 内刷新并成功;Coordinator 不出现在正常 Get/Put 调用链。
F05
Batch Regroup
按 MasterGroup 并行拆分 Batch,并按输入顺序合并逐 key 结果。混合至少 3 个 group、重复 key 和部分失败时,输出顺序及错误一一对应;一个 group 超时不取消已完成 group;总等待不超过配置 deadline。
F06
MasterNode Membership
实现 node register、lease renew、SUSPECT、UNAVAILABLE、DRAINING、INACTIVE 状态机。fake clock 下验证每个合法转换;lease 到期在规定窗口内标记 UNAVAILABLE;旧 node_incarnation_id 无法续租或覆盖重启后的新实例;非法跳转被拒绝并记录原因。
F07
MasterGroup 单副本运行时
一个 MasterNode 承载多个逻辑 group;每组隔离元数据、OpLog、snapshot、指标和 RPC queue。单进程启动至少 32 个 group 并写入相同 key,各组数据互不串扰;单组 snapshot/restart 恢复不影响其他组;可按 group 查询 CPU、队列和 metadata bytes。
F08
Group Term 与写 Fencing
所有写请求携带 master_group_id + group_term;旧 primary 无法提交写。将 term 从 N 提升到 N+1 后,term=N 的 Put/Remove/任务完成通知全部返回 STALE_TERM;并发压力下旧 term 成功提交数为 0;读写错误包含 current term。
F09
QUORUM Group 复制与选主
实现 3+ voter 的日志复制、commit index、leader election 和 learner catch-up。3 节点组在任意 1 节点故障后已确认写不丢失并恢复服务;隔离旧 primary 后不能形成双写;失去多数派时拒绝写;learner 追平后状态 hash 与 leader 一致。
F10
ASYNC_STANDBY 模式
实现异步 OpLog replay、replay position、显式提升和 RPO 暴露;不宣称 quorum 语义。注入 replay lag 后指标准确反映差值;提升时返回可能丢失的 position/bytes;未取得 fencing token 的 follower 不可提升;文档/API 明确返回当前 ReplicationMode。
F11
Group Placement 与成员变更
Coordinator 依据故障域和容量放置 replicas,采用 learner-add/catch-up/promote/remove 流程。输入多机架拓扑时同组 replicas 不共故障域;目标不满足约束时计划被拒绝;成员变更中断后可恢复或回滚;任一时刻不产生两个有效 primary。
F12
SlotGroup 静态分配
把 16384 slots 完整且无重叠地分配给多个 MasterGroup,并在服务端强制校验归属。覆盖检查证明每个 slot 恰好一个 owner;非 owner 请求返回包含目标 group 的 MOVED;旧 assignment_generation 返回 STALE_ASSIGNMENT;组内切主不改变 assignment_generation。
F13
SlotGroup 在线迁移
实现 IMPORTING、snapshot copy、committed replay、cutover fencing、MOVED 和 source cleanup。持续并发 Put/Get 时迁移一个 SlotGroup,cutover 后 source/target 对象状态 hash 一致、已确认写零丢失、同一 key 无双 owner;任一阶段 kill/restart 均能恢复或安全回滚。
F14
Segment Control-Owner 路由
按 client_id 将同一 Storage Client 的全部 segments 绑定到一个 MasterGroup,支持 control_generation 和 heartbeat redirect。同一 client 的不同介质 segment 始终落到同组;组内切主只改变 endpoint/term;跨组迁移只提升 control_generation;旧 control owner 无法接受新 heartbeat 或控制写。
F15
跨组任务投递
对象 owner 提交 TaskIntent,至少一次投递到 segment control-owner group,并以 task_id 幂等完成。在投递前、投递后、执行后分别注入崩溃,最终任务恰好产生一次对象状态变化;重复消息不重复分配/释放;任一 term/generation 过期时刷新路由而不是执行。
F16
Resource Capability 与 Telemetry
上报容量、读写带宽、IOPS、并发度、延迟桶、实时使用量、reserved、队列及采样时间。fake Storage Client 上报后 ResourceView 在时限内可见;同一 segment_incarnation 下较小 sample_seq 的样本不会覆盖新样本;超过 freshness 阈值的 segment 被标为 stale;读/写及不同对象大小桶不被合并。
F17
多维 Reservation
以 lease/CAS 原子预留 bytes、bandwidth、IOPS,支持 TTL、幂等键、commit 和 revoke。100 个并发请求不会使任一资源维度超卖;相同幂等键只产生一份 reservation;TTL 后资源可回收;commit/revoke 重复调用结果稳定。
F18
版本化 PlacementPolicy
实现硬约束过滤、可插拔评分、Top-K 有界 reserve 和 ScoreBreakdown;首版使用确定性加权评分。容量不足、故障域冲突和能力不匹配的候选永不入选;固定快照与 policy version 输出完全可重放;每次决策可解释各分项;reserve 尝试不超过配置 K。
F24
Delegated Extent Lease
Storage Client allocator 批量租出互不重叠的 extent range 和多维预算;MasterGroup 在租约内本地分配、后台续租和归还。并发向两个 group 发放租约时 extent 零重叠;旧 segment_incarnation/control_generation 或过期 lease 无法 commit;正常 PutStart 不产生 allocator RPC;耗尽时有界触发后台批量 refill,当前请求快速失败而不无限等待。
F19
Replica Read Selection
在已有 replicas 中根据健康、实时队列、带宽、时延和 locality 排序,不改变持久 placement。注入慢副本后新读流量在观测窗口内转移,恢复后按 hysteresis 回切;无健康副本时返回明确错误;选择变化不产生数据迁移或 metadata ownership 变化。
F20
BackgroundBudget 与 LatencyGuard
分别限制迁移、snapshot、offload、promotion、eviction;根据前台 P99.9 和队列闭环降速/恢复。压测使 P99.9 超阈值后一个控制窗口内降低预算,连续健康窗口后渐进恢复;前台流量不经过 Coordinator;预算过期时采用保守默认值。
F21
Coordinator HA
active/standby lease、fencing token、持久计划恢复;不承担 MasterGroup 日志提交。双实例竞争时最多一个可提交 view;kill active 后 standby 恢复未完成迁移状态;切换期间稳定 group 的 Get/Put 持续成功;旧 Coordinator CAS 全部失败。
F22
可观测性与一致性审计
提供 per-node/group/slot/client/segment 指标、结构化事件和只读审计 API。自动审计能检测 slot 重叠/缺口、同 group 双 primary、replica 跨域违规、stale fencing token 和 reservation 泄漏;健康集群误报为 0;每个异常可定位到实体 ID 和 view revision。
F23
故障注入与兼容性测试框架
可注入进程退出、网络分区、延迟、丢包、磁盘错误、时钟推进和旧版本客户端;输出确定性报告。CI 可独立复现指定 seed;至少覆盖旧 primary 隔离、迁移中断、watch 断线、重复任务、过期 ResourceView;失败报告包含 seed、时间线和最终不变量检查。
+ +

11.1 通用 Definition of Done

+
    +
  • 每项特性有独立 feature flag 或明确的启用条件,关闭时不改变现有单 Master 行为。
  • +
  • 新增持久化结构必须包含 schema/version,并至少验证一次旧版本读取新版本中已知字段的兼容路径。
  • +
  • 所有重试均有 deadline、最大次数和幂等键;验收不得依赖无限等待或人工查看日志。
  • +
  • 性能相关特性必须同时报告吞吐、P50、P99、P99.9 和资源占用,并保存可比较的基线。
  • +
  • 验收报告必须记录代码版本、配置、拓扑、随机 seed、开始/结束时间和不变量检查结果。
  • +
+ +

11.2 依赖关系与并行开发边界

+
+基础契约: F01 -> F02 -> F03
+客户端:   F01 + F02 -> F04 -> F05
+Group:    F02 -> F07 -> F08 -> {F09 | F10} -> F11
+Slot:     F04 + F07 -> F12 -> F13
+Segment:  F02 + F07 -> F14 -> F15
+资源调度: F16 -> F17 -> {F18, F24}; F18 + F24 -> F19
+保护闭环: F16 -> F20
+系统 HA:  F03 + F11 + F13 -> F21
+质量体系: F22、F23 从第一阶段开始,并为其他特性提供验收 harness
+  
+

+ 箭头表示生产集成依赖,不表示验收必须串行。例如 F04 可用 fake ClusterView 验收,F15 可用两个内存 MasterGroup + 和 fake heartbeat 验收,F18 可使用固定 ResourceView 快照验收。这样各团队可以并行开发,同时保持每项交付可独立判定成功或失败。 +

+ +

12. 演进计划

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
阶段内容收益
阶段一单 Master 暴露 16384 slot 的 ClusterView;客户端实现 slot 计算和路由缓存。不改变部署即可验证客户端路由逻辑。
阶段二多个单副本 MasterGroup 静态分配 SlotGroup;Batch regroup;MOVED/STALE_ASSIGNMENT。核心元数据请求横向扩展。
阶段三引入轻量 Coordinator,管理 view、resource summary 和后台预算。为扩缩容、P99.9 保护和资源协同打基础。
阶段四实现 slot group 在线迁移、snapshot copy、incremental replay、ASK/MOVED cutover。支持在线扩缩容和 rebalance。
阶段五为 MasterGroup 增加 replica、term、组内选主与 fencing;实现 Coordinator active/standby。降低故障影响范围,提高可用性。
+ +

13. 风险与约束

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
风险影响缓解措施
Coordinator 被误用到热路径成为新的 P99.9 瓶颈接口约束:Get/Put 不调用 Coordinator;客户端只周期性或按错误刷新 view。
ResourceView 过期PutStart 选择到高水位或不可用 segmentallocator lease/CAS 二次确认;失败快速换候选或返回错误。
迁移 replay lag 过大cutover 窗口变长lag 超阈值时暂停新迁移,降低前台写入冻结时间。
跨 slot Batch 尾延迟最慢 shard 拉高整批响应per-shard deadline、partial result、bounded retry。
热点 slot group单 shard 局部热点rebalance 避免迁移最热 group;必要时支持 hash tag 约束和 key-level split。
Coordinator split-brain生成冲突 view 或迁移计划Coordinator lease + fencing token;所有 view 更新通过 HA Backend CAS。
term、assignment_generation、control_generation 混用普通切主触发无谓 slot 迁移,或旧 primary/旧 control owner 未被正确隔离分别校验组内领导权、SlotGroup 归属和 client 控制归属;协议字段与错误码独立。
多个 group replicas 共置于同一故障域单节点或单机架故障同时失去 quorumgroup placement 使用反亲和约束,并在部署前做 N-1 failover capacity 校验。
异步主备被当作强一致复制故障提升后丢失已确认写或产生分叉ClusterView 明确暴露 ReplicationMode、commit/replay position 和可承诺的 RPO;需要 RPO=0 时使用 quorum 提交。
+ +

14. 最终建议

+
+

+ 轻量 Coordinator 路线是 Mooncake 分布式元数据面的推荐落地方案:它保留 Redis Cluster 风格的客户端直连和 slot redirect,避免中心化热路径;同时补足 Mooncake 相比 Redis 多出来的物理资源管理、冷热分级、slot 迁移限速和 P99.9 自动保护能力。 +

+
+ +

+ 实施时应坚持四条红线:Coordinator 不代理前台请求;slot owner 是 MasterGroup 而不是裸 MasterNode; + term、assignment_generation、control_generation 分别保护组内领导权、slot 归属和 segment 控制归属;segment_incarnation 隔离重建前后的物理空间;所有迁移动作都必须带 fencing 和可观测的 P99.9 保护阈值。 + 只要这些边界成立,该设计既能横向扩展元数据吞吐,也能在扩缩容和故障场景下保持尾延迟可控。 +

+ + + diff --git a/docs/source/design/ssd-balance-allocation.md b/docs/source/design/ssd-balance-allocation.md new file mode 100644 index 0000000000..5b314c8563 --- /dev/null +++ b/docs/source/design/ssd-balance-allocation.md @@ -0,0 +1,264 @@ +# SSD负载均衡分配策略设计文档 + +## 1. 概述 + +### 1.1 问题背景 + +现有 `FreeRatioFirstAllocationStrategy` 在选择segment时只考虑DDR空闲比例,忽略了SSD水位。这导致以下问题: + +- 一个segment的DDR空闲但SSD已满时,数据仍被分配到该segment +- 后续eviction时无法offload到SSD(因为SSD已满),DDR产生backpressure +- 最终DDR被填满,整个节点无法接受新写入 + +### 1.2 解决方案 + +新增 `SsdBalanceAllocationStrategy`,按SSD空闲比例做负载均衡: + +- 默认只看SSD水位(alpha=0),优先选择SSD空闲的节点 +- SSD达到高水位时禁止向该节点写入,但不驱逐SSD数据 +- DDR达到驱逐水位时临时禁止写入,水位下降后自动恢复 + +### 1.3 适用场景 + +多节点集群中每个节点有DDR+本地SSD的分层存储环境。 + +## 2. 设计目标 + +| 目标 | 说明 | +|------|------| +| SSD比例均衡 | 按SSD空闲比例选择segment,优先写入SSD空闲的节点 | +| SSD驱逐保护 | SSD达到高水位时禁止写入,绝不驱逐SSD数据(避免数据丢失) | +| DDR准入控制 | 每个segment的DDR达到准入水位时禁止向该segment分配,自动fallback到其他segment | +| 全满暂停 | 所有节点DDR都满时暂停所有put,返回 DDR_ADMISSION_REJECTED(-201),不触发eviction | + +## 3. 核心算法 + +### 3.1 SSD比例计算 + +``` +ssd_free_ratio = (ssd_total_capacity - ssd_used_bytes) / ssd_total_capacity +``` + +- 无SSD信息的segment:`ssd_free_ratio = 1.0`(不约束) +- `ssd_used_bytes` 通过 `std::atomic` 跟踪,在offload成功时递增,磁盘驱逐时递减 + +### 3.2 候选采样与排序 + +``` +1. 采样 min(6 * replica_num, total_segments) 个候选segment +2. 排除SSD使用率 >= ssd_high_watermark_ratio 的segment +3. 按ssd_free_ratio降序排序 +4. 从top-N候选中尝试分配 +5. 如果replica_num未满足,fallback到随机分配 +``` + +### 3.3 SSD高水位保护 + +当segment的SSD使用率 >= `ssd_high_watermark_ratio`(默认0.90)时: + +- **禁止**向该segment分配新数据 +- **绝不驱逐**SSD上的已有数据(驱逐意味着数据不可恢复丢失) +- SSD数据只能通过以下方式释放: + - 正常promotion(访问命中后提升回DDR) + - TTL过期(软pin到期后自动清理) +- SSD水位下降后,节点自动恢复可写状态 + +### 3.4 DDR写入准入控制(per-segment) + +通过 `--ddr_admission_watermark_ratio`(默认 0.0,即禁用)设定每个 segment 的 DDR 准入水位。 +当 segment 的 DDR 使用率 >= 该水位时: + +- 分配策略**跳过**该 segment,尝试分配到其他 segment +- 所有 segment 都被跳过时,返回 `DDR_ADMISSION_REJECTED`(-201) +- **不设置** `need_mem_eviction_`(避免触发 eviction 驱逐已有数据,DDR 数据零丢失) +- 其他 segment DDR 下降(eviction 释放空间或 offload 完成)后自动恢复 + +与 eviction 的关系: +- `ddr_admission_watermark_ratio`(如 0.90)应设得**低于** `eviction_high_watermark_ratio`(0.95) +- 准入阻写先于 eviction 驱逐发生,保护 DDR 数据不被驱逐 +- 如果所有 segment 都超过准入水位也无 eviction 触发,put 暂停直到有 segment 释放空间 + +使用方式: + +```bash +./mooncake_master --allocation_strategy=ssd_balance \ + --ddr_admission_watermark_ratio=0.90 +``` + +## 4. 决策流程 + +### 4.1 AllocateAndInsertMetadata流程 + +``` +AllocateAndInsertMetadata() +│ +├── 获取AllocatorManager和SsdMetricsProvider +│ +├── 调用 SsdBalanceAllocationStrategy::Allocate() +│ │ +│ ├── 处理preferred segments +│ │ ├── 检查SSD水位,跳过高水位segment +│ │ └── 检查DDR准入水位,跳过超标segment +│ │ +│ ├── 候选采样 + SSD比例排序 +│ │ ├── 排除excluded/used segments +│ │ ├── 排除SSD高水位segments +│ │ ├── 排除DDR准入水位超标的segments +│ │ └── 按ssd_free_ratio降序排序,取top-N +│ │ +│ ├── Fallback随机分配 +│ │ └── 同样排除SSD高水位和DDR准入超标segments +│ │ +│ └── 返回结果 +│ ├── 有可用segment → replicas +│ ├── 被DDR准入拒绝 → DDR_ADMISSION_REJECTED(-201) +│ │ └── 不设need_mem_eviction_,保护DDR数据 +│ └── 其他原因失败 → NO_AVAILABLE_HANDLE(-200) +│ └── 设need_mem_eviction_,触发eviction释放空间 +│ +└── 返回结果给客户端 +``` + +### 4.2 SSD水位检查 + +``` +isSsdHighWatermark(segment_name) +│ +├── 查询SsdMetricsProvider +│ ├── total = getSsdTotalCapacity(segment_name) +│ └── used = getSsdUsedBytes(segment_name) +│ +├── total <= 0? +│ └── 返回false(无SSD信息,不阻塞) +│ +└── used/total >= ssd_high_watermark_ratio? + ├── YES → 排除该segment + └── NO → 允许分配 +``` + +### 4.3 DDR准入水位检查 + +``` +isDdrHighWatermark(segment_name) +│ +├── ddr_admission_watermark_ <= 0.0? +│ └── 返回false(未启用DDR准入) +│ +├── ddr_admission_watermark_ >= 1.0? +│ └── 返回false(显式禁用) +│ +├── 查询SsdMetricsProvider +│ └── ratio = getDdrUsedRatio(segment_name) +│ └── MasterMetricManager.get_segment_mem_used_ratio() +│ +└── ratio >= ddr_admission_watermark_? + ├── YES → 排除该segment + └── NO → 允许分配 +``` + +## 5. SSD使用量追踪 + +### 5.1 数据结构 + +`LocalDiskSegment` 新增字段: + +```cpp +std::atomic ssd_used_bytes{0}; +``` + +### 5.2 更新时机 + +| 事件 | 操作 | 触发位置 | +|------|------|----------| +| offload成功 | `ssd_used_bytes += data_size` | `NotifyOffloadSuccess` | +| 磁盘replica被驱逐 | `ssd_used_bytes -= object_size` | `EvictDiskReplica` | + +### 5.3 暴露接口 + +通过 `SsdMetricsProvider` 接口: + +```cpp +class SsdMetricsProvider { + virtual int64_t getSsdTotalCapacity(const std::string& segment_name) const = 0; + virtual int64_t getSsdUsedBytes(const std::string& segment_name) const = 0; + virtual double getDdrUsedRatio(const std::string& segment_name) const { + return 0.0; // 默认不检查DDR + } +}; +``` + +`ScopedLocalDiskSegmentAccess` 实现该接口,通过 segment_name → client_id → LocalDiskSegment 查找。 + +## 6. 配置参数 + +### 6.1 Master 启动参数 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `--allocation_strategy` | `random` | 设为 `ssd_balance` 启用本策略 | +| `--ssd_high_watermark_ratio` | `0.90` | SSD使用率上限,超过则禁止向该节点写入 | +| `--ddr_admission_watermark_ratio` | `0.0` | DDR准入水位(0.0 = 禁用),低于此值则禁止向该segment分配 | + +### 6.2 环境变量(存储后端驱逐保护) + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `MOONCAKE_OFFLOAD_DISABLE_SSD_EVICTION` | `false` | 强制禁止SSD驱逐,即使 eviction_policy 非 NONE 也不驱逐 | + +### 6.3 错误码 + +| 错误码 | 值 | 触发条件 | +|--------|-----|----------| +| `NO_AVAILABLE_HANDLE` | -200 | 分配失败(段满或其他原因),触发 eviction | +| `DDR_ADMISSION_REJECTED` | -201 | DDR准入水位拒绝分配,**不触发** eviction | + +启用方式: + +```bash +./mooncake_master --allocation_strategy=ssd_balance \ + --ssd_high_watermark_ratio=0.90 \ + --ddr_admission_watermark_ratio=0.90 +``` + +## 7. 代码结构 + +### 7.1 新增/修改文件 + +| 文件 | 变更类型 | 说明 | +|------|----------|------| +| `include/allocation_strategy.h` | 修改 | 新增 `SsdMetricsProvider` 接口(含 `getDdrUsedRatio`)、`SsdBalanceAllocationStrategy` 类(含 `isDdrHighWatermark`)、更新工厂函数 | +| `include/types.h` | 修改 | `AllocationStrategyType` 枚举新增 `SSD_BALANCE`;新增 `DDR_ADMISSION_REJECTED` 错误码 | +| `include/segment.h` | 修改 | `LocalDiskSegment` 新增 `ssd_used_bytes`;`ScopedLocalDiskSegmentAccess` 实现 `SsdMetricsProvider`(含 `getDdrUsedRatio`) | +| `src/segment.cpp` | 修改 | 实现 `getSsdTotalCapacity`、`getSsdUsedBytes`、`getDdrUsedRatio` | +| `include/master_config.h` | 修改 | 新增 `ssd_high_watermark_ratio`、`ddr_admission_watermark_ratio` 配置字段 | +| `src/master.cpp` | 修改 | 新增 `--ssd_high_watermark_ratio`、`--ddr_admission_watermark_ratio` gflag | +| `include/master_service.h` | 修改 | 新增 `ssd_high_watermark_ratio_` 成员 | +| `src/master_service.cpp` | 修改 | 分配策略传 SSD/DDR provider、SSD使用量追踪、分发 DDR_ADMISSION_REJECTED(不触发 eviction) | +| `src/client_service.cpp` | 修改 | 处理 `DDR_ADMISSION_REJECTED` 错误码(日志 + 重试) | +| `include/storage_backend.h` | 修改 | `BucketBackendConfig` 新增 `disable_ssd_eviction` 字段 | +| `src/storage_backend.cpp` | 修改 | `PrepareEviction` 检查 `disable_ssd_eviction`;`IsEnableOffloading` 跳过eviction分支 | + +### 7.2 类继承关系 + +``` +AllocationStrategy (抽象基类) +├── RandomAllocationStrategy +│ └── FreeRatioFirstAllocationStrategy +│ └── SsdBalanceAllocationStrategy ← 新增 +└── CxlAllocationStrategy + +SsdMetricsProvider (抽象接口) +└── ScopedLocalDiskSegmentAccess ← 新增实现 +``` + +## 8. 验证方案 + +详见 `mooncake-wheel/tests/verify_ssd_balance.py` 和 `tests/ssd_balance_test_guide.md`。 + +| 测试 | 验证内容 | +|------|----------| +| `load_balancing` | 2个Client不对称SSD,验证数据按SSD空闲比例分布 | +| `ssd_high_watermark_blocking` | SSD达到90%高水位后offload完成,验证新分配被拒绝 + 初始数据可读 | +| `ssd_eviction_protection` | 启用FIFO驱逐+`MOONCAKE_OFFLOAD_DISABLE_SSD_EVICTION=true`,验证已有SSD数据不被驱逐 | +| `ddr_admission` | 设置 `--ddr_admission_watermark_ratio=0.90`,DDR满时拒绝写入,不触发eviction | +| `all_ssd_full` | 所有节点SSD满后全局拒绝,释放后恢复 | diff --git a/docs/spdiag_integration_guide.md b/docs/spdiag_integration_guide.md new file mode 100644 index 0000000000..0c37fda692 --- /dev/null +++ b/docs/spdiag_integration_guide.md @@ -0,0 +1,205 @@ +# Mooncake SpDiag 两层集成使用指南 + +Mooncake 在配置阶段提供两个互斥的 SpDiag 编译层。构建完成后不能在运行时切换层级;如需切换,应重新配置并编译。 + +| 层 | 配置 | SpDiag 来源 | 生成结果 | +|---|---|---|---| +| Layer 0:Mock | `MOONCAKE_ENABLE_SPDIAG=OFF` | Mooncake 拉取固定源码头文件 | PerfPoint 为空实现,无 SpDiag 运行时依赖 | +| Layer 1:System | `MOONCAKE_ENABLE_SPDIAG=ON` | 用户预装的 SpDiag RPM | Mooncake 链接系统 `.so`,并可将同版本 CLI/`.so` 打入单一 RPM | + +## 1. Layer 0:默认 Mock + +### 1.1 配置与编译 + +```bash +cmake -S . -B build \ + -DMOONCAKE_ENABLE_SPDIAG=OFF +cmake --build build --parallel +``` + +`MOONCAKE_ENABLE_SPDIAG` 默认值为 `OFF`,因此该参数可以省略。 + +Mooncake 使用 FetchContent 拉取固定修订的 SpDiag 源码,只消费 `include/spdiag` 公共头文件,并通过统一目标 `SpDiag::spdiag_lib` 传播 `SPDIAG_DISABLE`。 + +SpDiag 头文件中的 `SPDIAG_DISABLE` 分支将 PerfPoint 构造、`Start()`、`End()` 和 `Abandon()` 编译为空实现。因此: + +- Mooncake 打点源码保持不变; +- Mooncake ELF 不依赖 `libspdiag.so`; +- 不生成或打包 SpDiag CLI; +- 不创建 SpDiag SHM; +- 系统中已安装的 SpDiag 不参与该构建。 + +离线环境可以指定已准备好的 SpDiag 源码目录: + +```bash +cmake -S . -B build \ + -DMOONCAKE_ENABLE_SPDIAG=OFF \ + -DMOONCAKE_SPDIAG_SOURCE_DIR=/opt/src/spdiag +``` + +## 2. Layer 1:系统 SpDiag + +### 2.1 前置条件 + +Layer 1 不下载或编译真实 SpDiag。配置 Mooncake 前,用户必须先安装完整的 SpDiag 运行时 RPM 和开发 RPM。安装结果必须同时提供: + +- `SpDiagConfig.cmake`; +- 导入目标 `SpDiag::spdiag_lib`; +- 共享库 `libspdiag.so`; +- 可被系统找到的 `spdiag` CLI; +- `SPDIAG_ENABLE_PERCENTILE`; +- `SPDIAG_ENABLE_PERFLOG`。 + +SpDiag RPM 构建时应至少启用: + +```text +SPDIAG_BUILD_SHARED=ON +ENABLE_PERCENTILE=ON +ENABLE_PERFLOG=ON +``` + +Mooncake 不固定 Layer 1 的 SpDiag 版本或源码 SHA。它通过 RPM 数据库分别查询 `libspdiag.so` 与 CLI 的 `VERSION-RELEASE.ARCH`,并要求二者完全一致。SpDiag RPM 中其他功能的启用情况和功能正确性由 SpDiag 发布包负责。 + +### 2.2 配置与编译 + +标准系统路径: + +```bash +cmake -S . -B build-spdiag \ + -DMOONCAKE_ENABLE_SPDIAG=ON +cmake --build build-spdiag --parallel +``` + +自定义 RPM 安装前缀: + +```bash +cmake -S . -B build-spdiag \ + -DMOONCAKE_ENABLE_SPDIAG=ON \ + -DCMAKE_PREFIX_PATH=/opt/spdiag \ + -DCMAKE_PROGRAM_PATH=/opt/spdiag/bin +``` + +也可以直接指定 CMake package 和 CLI: + +```bash +cmake -S . -B build-spdiag \ + -DMOONCAKE_ENABLE_SPDIAG=ON \ + -DSpDiag_DIR=/opt/spdiag/lib64/cmake/SpDiag \ + -DMOONCAKE_SPDIAG_SYSTEM_CLI=/opt/spdiag/bin/spdiag +``` + +### 2.3 配置检查 + +ON 模式依次确认: + +1. 找到 SpDiag CMake package 和 `SpDiag::spdiag_lib`; +2. package 导出 `SHARED_LIBRARY` 目标; +3. target 传播 P99 与 PerfLog 能力宏; +4. 找到 `spdiag` CLI; +5. `.so` 与 CLI 均由已安装的 RPM 提供; +6. 两者的 `VERSION-RELEASE.ARCH` 完全一致。 + +任一条件不满足都会停止配置。Mooncake 不会回退到源码构建;应先修复或重新安装 SpDiag RPM,再重新配置 Mooncake。 + +## 3. 构建 Mooncake RPM + +### 3.1 Mock RPM + +```bash +bash scripts/build_rpm.sh build rpm-output "$(uname -m)" +rpm -qlp rpm-output/mooncake-*.rpm +``` + +Mock RPM 包含 Mooncake 二进制,但不包含: + +```text +/usr/bin/spdiag +/usr/lib64/libspdiag.so* +/etc/spdiag/spdiag.conf +``` + +### 3.2 System RPM + +```bash +bash scripts/build_rpm.sh build-spdiag rpm-output "$(uname -m)" +rpm -qlp rpm-output/mooncake-*.rpm +``` + +System RPM 包含: + +```text +/usr/bin/mooncake_master +/usr/bin/mooncake_client +/usr/bin/spdiag +/usr/lib64/libspdiag.so* +/etc/spdiag/spdiag.conf # 系统安装提供该配置时 +``` + +打包脚本读取 `build-spdiag/mooncake_spdiag.env`,并复制 CMake 配置阶段已经选中的 CLI、共享库及其符号链接。CLI 与共享库的版本一致性在 CMake 配置阶段完成检查;如果构建机上的 SpDiag RPM 随后发生变化,应重新配置后再打包。 + +## 4. 安装与运行 + +Mooncake RPM 使用标准系统路径安装 Mooncake 和 SpDiag 运行时: + +```text +/usr/bin/mooncake_master +/usr/bin/mooncake_client +/usr/bin/spdiag +/usr/lib64/libspdiag.so* +/etc/spdiag/spdiag.conf +``` + +安装和基础运行命令: + +```bash +sudo rpm -Uvh rpm-output/mooncake-*.rpm +sudo ldconfig + +spdiag --version +spdiag start +spdiag status + +# 启动 Mooncake master/client 并执行实际读写负载 + +spdiag show +spdiag show --detail +``` + +PerfLog、P99、历史数据和 CSV 参数以所安装 SpDiag CLI 的帮助为准: + +```bash +spdiag --help +spdiag show --help +``` + +## 5. 常见错误 + +### `.so` 或 CLI 不受 RPM 管理 + +Layer 1 面向系统 SpDiag RPM,不接受手工复制的散装文件。请安装完整的 SpDiag 运行时 RPM 和开发 RPM 后重新配置 Mooncake。 + +### `.so` 与 CLI 的 RPM 版本不同 + +卸载冲突版本,并安装同一发布批次的 SpDiag RPM。Mooncake 比较 `VERSION-RELEASE.ARCH`,不通过文件 SHA 判断。 + +### 找到静态库 + +重新构建 SpDiag RPM,并设置 `SPDIAG_BUILD_SHARED=ON`。 + +### 缺少 P99 或 PerfLog + +重新构建 SpDiag RPM,并设置: + +```text +ENABLE_PERCENTILE=ON +ENABLE_PERFLOG=ON +``` + +### 从 Mock 切换到 System + +推荐使用新的构建目录: + +```bash +cmake -S . -B build-spdiag \ + -DMOONCAKE_ENABLE_SPDIAG=ON +``` diff --git a/docs/yh/log-reference.md b/docs/yh/log-reference.md new file mode 100644 index 0000000000..2bcc96b6d3 --- /dev/null +++ b/docs/yh/log-reference.md @@ -0,0 +1,1153 @@ +# Mooncake Store 日志参考手册 + +本文档描述 `get` / `get_batch` / `get_into` / `batch_get_into` / `put` / `put_batch` 六个操作的全链路日志输出,以及传输任务层、URMA 建连层、Client 创建等新增日志。 + +--- + +## 0. 日志系统概览 + +### 0.1 两套日志系统 + +当前代码中存在两套日志系统: + +| 系统 | 使用文件 | 宏 | trace_id 前缀 | 输出方式 | +|------|---------|-----|--------------|---------| +| **MC_LOG 系统** | `real_client.cpp`、`client_service.cpp`、`transfer_task.cpp` | `MC_LOG` / `MC_VLOG` | 是 | 异步队列 | +| **原生 glog** | `store_py.cpp`(batch 操作)、`urma_endpoint.cpp` | `LOG` / `VLOG` | 否 | 同步直接输出 | + +MC_LOG 系统的每条日志自动带有 `trace_id[xxx] ` 前缀(无 trace 上下文时为 `trace_id[none] `)。原生 glog 日志无此前缀。 + +**注意**:当两层日志混合输出时(如 `get_batch`),原生 glog 日志与 MC_LOG 日志可能因异步队列导致顺序不完全一致。 + +### 0.2 MC_LOG 异步队列 + +MC_LOG 通过 `AsyncLogMessage` 临时对象在析构时将日志条目入队,后台单线程消费并调用 glog 输出。队列容量 8192 条,满时阻塞写入线程。FATAL 级别绕过队列直接同步输出。进程退出时通过 `atexit` 处理器刷出剩余日志。 + +实现文件:`mooncake-common/include/mooncake_logging.h`、`mooncake-common/src/mooncake_logging.cpp`。 + +### 0.3 TraceId 系统 + +- **生成**:`NewTraceId()` 使用 `(PID << 48) ^ (steady_clock_ns & 0x0000FFFFFFFF0000) ^ atomic_counter++` 生成全局唯一 ID +- **线程传递**:`ScopedTraceId` 通过 `thread_local` 保存/恢复当前线程的 trace_id +- **同步调用链传递**:`RealClient` 入口创建 `ScopedTraceId(NewTraceId())` 后,同线程下游函数通过 `CurrentTraceId()` 读取同一个 trace_id +- **异步任务传播**:提交线程用 `CurrentTraceId()` 捕获当前 trace_id,并写入 `MemcpyTask`、`FilereadTask` 或线程池 lambda;工作线程执行时通过 `ScopedTraceId` 恢复,确保异步路径日志可追踪 +- **上下文恢复**:`ScopedTraceId` 析构时恢复旧值,避免线程池复用、嵌套调用或提前返回后把上一个请求的 trace_id 带到后续日志 + +--- + +## 1. `get` 日志链路 + +> Python 绑定层(`store_py.cpp`)的单 key `get` 生命周期日志(`get start/complete/slow`)已移除,改为由 `real_client.cpp` 输出慢操作告警。 + +正常路径日志按调用顺序: + +``` +real_client::get_buffer + ├ real_client::get_buffer_internal + │ ├ query_success + │ ├ replica_selected + │ ├ [SSD 路径] ssd_read_detail + │ ├ get_breakdown + │ └ [慢操作] get_buffer_slow + ├ client_service::Get + │ └ transfer_read_completed + ├ client_service::TransferData + │ └ transfer_data op[READ] + └ [慢操作] get_buffer_slow(在 real_client::get_buffer 层) +``` + +### 1.1 核心逻辑层 — `real_client.cpp::get_buffer_internal` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `query_success` | INFO | `query_success key[{key}] replicas[{n}]` | Master 查询成功,返回 n 个副本 | +| `replica_selected` | INFO | `replica_selected key[{key}] type[{type}] endpoint[{ip:port}] size[{bytes}]` | Memory/LocalDisk 副本选中,含 endpoint | +| `replica_selected` | INFO | `replica_selected key[{key}] type[{type}] file_path[{path}] size[{bytes}]` | Disk 副本选中,含文件路径 | +| `get_breakdown` | INFO | `get_breakdown key[{key}] query_us[{t1}] select_us[{t2}] alloc_us[{t3}] read_us[{t4}] total_us[{total}] type[{type}] status[{status}]` | 分阶段耗时汇总 | + +**`get_breakdown` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `query_us` | Master 查询耗时(微秒) | +| `select_us` | 副本选择耗时 | +| `alloc_us` | 缓冲区分配耗时 | +| `read_us` | 数据读取耗时(RDMA/文件IO/SSD RPC) | +| `total_us` | 总耗时 | +| `type` | 副本类型:`memory_local` / `memory_remote` / `local_disk_local` / `local_disk_remote` / `disk` | +| `status` | 结果:`read_ok` / `read_fail` / `ssd_ok` / `ssd_fail` | + +### 1.2 传输服务层 — `client_service.cpp::Get` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `transfer_read_completed` | INFO | `transfer_read_completed key[{key}] elapsed_us[{us}] data_size[{bytes}] cache_hit[{0/1}]` | RDMA/文件传输完成 | +| `transfer_read_failed` | ERROR | `transfer_read_failed key={key}` | 传输失败 | +| `lease_expired_before_data_transfer_completed` | WARNING | `lease_expired_before_data_transfer_completed key={key}` | 租约过期 | + +**`transfer_read_completed` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `elapsed_us` | 传输总耗时(微秒) | +| `data_size` | 传输数据大小(字节) | +| `cache_hit` | 是否命中热缓存:`1` 命中,`0` 未命中 | + +### 1.3 传输引擎层 — `client_service.cpp::TransferData` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `transfer_data` | INFO | `transfer_data first_transfer_data[{0/1}] op[{READ/WRITE}] strategy[{int}] submit_us[{t1}] wait_us[{t2}] result[{code}]` | 传输耗时拆分 | + +**字段说明:** + +| 字段 | 含义 | +|------|------| +| `first_transfer_data` | 进程首次传输数据输出 `1`,后续输出 `0`(用于区分首次建连开销) | +| `op` | 操作类型:`READ` 或 `WRITE` | +| `strategy` | `TransferFuture` 传输策略整数值(对应传输引擎内部策略枚举) | +| `submit_us` | 提交传输请求耗时(微秒) | +| `wait_us` | 等待传输完成耗时(微秒) | +| `result` | 传输结果,`OK` 表示成功 | + +### 1.4 SSD Offload 路径 — `real_client.cpp::batch_get_into_offload_object_internal` + +仅当副本类型为 `local_disk`(远端 SSD)时触发。 + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `ssd_read_detail` | INFO | `ssd_read_detail endpoint[{ip:port}] num_keys[{n}] total_size[{bytes}] elapsed_ms[{ms}] batch_id[{id}]` | SSD RPC 读取详情 | + +**`ssd_read_detail` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `endpoint` | SSD offload RPC 服务端地址 | +| `num_keys` | 本批次读取的 key 数量 | +| `total_size` | 本批次读取的总字节数 | +| `elapsed_ms` | 整批 RPC 耗时(毫秒) | +| `batch_id` | 批次 ID | + +--- + +## 2. `get_batch` 日志链路 + +> **注意**:Python 绑定层(`store_py.cpp`)的 batch 操作仍使用原生 `LOG()`,无 `trace_id` 前缀。下层(real_client/client_service)使用 `MC_LOG`,带 `trace_id` 前缀。两层日志混合输出时可能因异步队列导致顺序不完全一致。 + +``` +store_py::get_batch + ├ get_batch start + ├ real_client::batch_get_buffer_internal + │ ├ batch_query_result + │ ├ [逐 key] replica_selected (无此日志,batch 不逐 key 输出) + │ ├ [SSD 路径] ssd_read_detail + │ ├ batch_get_breakdown + │ └ [慢操作] batch_get_buffer_slow + ├ client_service::BatchGet + │ └ batch_get_transfer_complete + ├ client_service::TransferData (多次) + │ └ transfer_data op[READ] + └ get_batch complete +``` + +### 2.1 Python 绑定层 — `store_py.cpp::get_batch` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `get_batch start` | INFO | `get_batch start num_keys[{n}]` | 操作开始 | +| `get_batch complete` | INFO | `get_batch complete num_keys[{n}] success[{s}] rc[0] elapsed_us[{us}]` | 操作成功完成 | +| `get_batch complete` | INFO | `get_batch complete num_keys[{n}] rc[-1] elapsed_us[{us}]` | 操作失败 | +| `get_batch_slow` | WARNING | `get_batch_slow num_keys[{n}] elapsed_us[{us}]` | 耗时超过 3ms 触发慢操作告警 | + +### 2.2 核心逻辑层 — `real_client.cpp::batch_get_buffer_internal` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `batch_query_result` | INFO | `batch_query_result num_keys[{n}] num_found[{f}]` | 批量查询结果,f 为找到的 key 数 | +| `batch_get_breakdown` | INFO | `batch_get_breakdown num_keys[{n}] query_us[{t1}] prep_us[{t2}] read_us[{t3}] total_us[{total}] batch_get_ops[{m}] ssd_offload_ops[{s}] success[{ok}]` | 分阶段耗时汇总 | + +**`batch_get_breakdown` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `query_us` | 批量 Master 查询耗时 | +| `prep_us` | 准备阶段耗时(副本选择 + 缓冲区分配,逐 key 循环) | +| `read_us` | 数据读取耗时(BatchGet + SSD RPC) | +| `total_us` | 总耗时 | +| `batch_get_ops` | 走 BatchGet 的 key 数(MEMORY + DISK 副本) | +| `ssd_offload_ops` | 走 SSD RPC 的 key 数(LOCAL_DISK 副本) | +| `success` | 成功读取的 key 数 | + +### 2.3 传输服务层 — `client_service.cpp::BatchGet` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `batch_get_transfer_complete` | INFO | `batch_get_transfer_complete num_keys[{n}] success[{s}] elapsed_us[{us}] pending_count[{c}]` | 批量传输完成 | + +**字段说明:** + +| 字段 | 含义 | +|------|------| +| `num_keys` | 批量传输的 key 总数 | +| `success` | 成功传输的 key 数 | +| `elapsed_us` | 传输总耗时(微秒) | +| `pending_count` | 总传输任务数(提交的 TransferFuture 数量) | + +### 2.4 传输引擎层 — 同第 1 节的 `transfer_data` + +### 2.5 SSD Offload 路径 — 同第 1 节的 `ssd_read_detail` + +--- + +## 3. `get_into` 日志链路 + +`get_into` 将对象直接读入调用方提供的 buffer,核心日志来自 `real_client.cpp`。 + +``` +real_client::get_into + ├ real_client::resolve_ranged_read_metadata + │ ├ query_success + │ └ replica_selected + ├ real_client::execute_ranged_read + │ ├ [MEMORY/DISK] client_service::Get + │ ├ [LOCAL_DISK] ssd_read_detail + │ └ [失败] SSD/DISK/scatter/Get error + ├ get_into_breakdown + └ [慢操作] get_into_slow +``` + +### 3.1 核心逻辑层 — `real_client.cpp::get_into_range_internal` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `query_success` | INFO | `query_success key[{key}] replicas[{n}]` | Master 查询成功,返回 n 个副本 | +| `replica_selected` | INFO | `replica_selected key[{key}] type[{type}] endpoint[{ip:port}] size[{bytes}]` | Memory/LocalDisk 副本选中,含 endpoint | +| `replica_selected` | INFO | `replica_selected key[{key}] type[{type}] file_path[{path}] size[{bytes}]` | Disk 副本选中,含文件路径 | +| `get_into_breakdown` | INFO | `get_into_breakdown key[{key}] query_us[{t1}] select_us[{t2}] read_us[{t3}] total_us[{total}] type[{type}] mode[{mode}] status[{status}]` | 分阶段耗时汇总 | +| `get_into_breakdown` | INFO | `get_into_breakdown key[{key}] query_us[0] select_us[0] read_us[0] total_us[{total}] type[unknown] mode[unknown] status[{error}]` | metadata 查询/选副本失败时的汇总 | + +**`get_into_breakdown` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `query_us` | Master 查询耗时(微秒) | +| `select_us` | 副本选择耗时 | +| `read_us` | 数据读取耗时(RDMA/文件IO/SSD RPC/scatter) | +| `total_us` | 总耗时 | +| `type` | 副本类型:`memory_local` / `memory_remote` / `local_disk_local` / `local_disk_remote` / `disk` / `unknown` | +| `mode` | 读取模式:`full` 完整对象读取,`range` 部分读取,`unknown` 表示 metadata 阶段失败 | +| `status` | 结果:`read_ok` / `mem_fail` / `disk_fail` / `ssd_fail` / 错误码字符串 | + +### 3.2 读取失败日志 — `real_client.cpp::execute_ranged_read` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `SSD read failed` | ERROR | `SSD read failed for key '{key}': {error}` | LOCAL_DISK 完整读取失败 | +| `Ranged SSD read failed` | ERROR | `Ranged SSD read failed for key '{key}': {error}` | LOCAL_DISK 范围读取失败 | +| `DISK Get failed` | ERROR | `DISK Get failed for key: {key} with error: {error}` | DISK 文件读取失败 | +| `DISK full read scatter failed` | ERROR | `DISK full read scatter failed for key '{key}': {error}` | DISK 完整读取后 scatter 到用户 buffer 失败 | +| `Ranged disk read scatter failed` | ERROR | `Ranged disk read scatter failed for key '{key}': {error}` | DISK/LOCAL_DISK 范围读取后 scatter 失败 | +| `Ranged Get failed` | ERROR | `Ranged Get failed for key: {key} with error: {error}` | MEMORY 范围读取失败 | + +### 3.3 下层日志 + +- MEMORY / DISK 通过 `Client::Get` 时,会继续产生 `transfer_read_completed` / `transfer_data op[READ]`。 +- LOCAL_DISK 通过 SSD offload 时,会继续产生 `ssd_read_detail`。 + +--- + +## 4. `batch_get_into` 日志链路 + +`batch_get_into` 将多个对象分别读入调用方提供的 buffers,批量成功项不逐 key 打 INFO,失败项仍逐 key 打 ERROR。 + +``` +real_client::batch_get_into + ├ batch_get_into_query_result + ├ [逐 key 失败] Query/Select/Buffer/DISK error + ├ [MEMORY] client_service::BatchGet + ├ [DISK] client_service::BatchGet + scatter + ├ [LOCAL_DISK] ssd_read_detail + ├ batch_get_into_breakdown + └ [慢操作] batch_get_into_slow +``` + +### 4.1 核心逻辑层 — `real_client.cpp::batch_get_into_internal` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `batch_get_into_query_result` | INFO | `batch_get_into_query_result num_keys[{n}] num_found[{f}]` | 批量查询结果,f 为找到的 key 数 | +| `batch_get_into_breakdown` | INFO | `batch_get_into_breakdown num_keys[{n}] query_us[{t1}] prep_us[{t2}] read_us[{t3}] total_us[{total}] mem_ops[{m}] disk_ops[{d}] ssd_offload_ops[{s}] success[{ok}]` | 分阶段耗时汇总 | + +**`batch_get_into_breakdown` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `query_us` | 批量 Master 查询耗时 | +| `prep_us` | 准备阶段耗时(逐 key 副本选择、容量校验、分类、slice 准备) | +| `read_us` | 数据读取耗时(MEMORY BatchGet + DISK BatchGet/scatter + SSD RPC) | +| `total_us` | 总耗时 | +| `mem_ops` | 走 MEMORY `BatchGet` 的 key 数 | +| `disk_ops` | 走 DISK 临时 buffer + `BatchGet` + scatter 的 key 数 | +| `ssd_offload_ops` | 实际提交到 SSD offload 的 key 数(LOCAL_DISK) | +| `success` | 成功读取且返回正数字节数的 key 数 | + +### 4.2 逐 key / endpoint 失败日志 + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `Query failed` | ERROR | `Query failed for key '{key}': {error}` | 单个 key 查询失败(非 NOT_FOUND/NOT_READY) | +| `Empty replica list` | ERROR | `Empty replica list for key: {key}` | 查询结果没有 replica | +| `No usable replica` | ERROR | `No usable replica for key: {key}` | 无可用 COMPLETE 副本 | +| `Buffer too small` | ERROR | `Buffer too small for key '{key}': required={r}, available={a}` | 用户 buffer 容量不足 | +| `BatchGet failed` | ERROR | `BatchGet failed for key '{key}': {error}` | MEMORY BatchGet 失败 | +| `DISK BatchGet failed` | ERROR | `DISK BatchGet failed for key '{key}': {error}` | DISK BatchGet 失败 | +| `DISK scatter failed` | ERROR | `DISK scatter failed for key '{key}': {error}` | DISK 临时 buffer scatter 到用户 buffer 失败 | +| `Batch get store object failed` | ERROR | `Batch get store object failed endpoint[{endpoint}] objects[{n}] error[{error}]` | LOCAL_DISK endpoint 级 offload 失败 | + +### 4.3 下层日志 + +- MEMORY / DISK 批量读取会继续产生 `batch_get_transfer_complete` / `transfer_data op[READ]`。 +- LOCAL_DISK offload 会继续产生 `ssd_read_detail`。 + +--- + +## 5. `put` 日志链路 + +> Python 绑定层(`store_py.cpp`)的单 key `put` 生命周期日志(`put start/complete/slow`)已移除,改为由 `real_client.cpp` 输出慢操作告警。 + +``` +real_client::put_internal + ├ put_result + └ [慢操作] put_slow +client_service::Put + ├ put_start_success (或 OBJECT_ALREADY_EXISTS) + └ put_end_success +client_service::TransferData + └ transfer_data op[WRITE] +``` + +### 5.1 核心逻辑层 — `real_client.cpp::put_internal` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `put_result` | INFO | `put_result key[{key}] rc[0] size[{bytes}]` | Put 成功 | +| `put_result` | INFO | `put_result key[{key}] rc[{code}] size[{bytes}]` | Put 失败,code 为错误码 | + +### 5.2 传输服务层 — `client_service.cpp::Put` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `put_start` | INFO | `put_start key[{key}] rc[OBJECT_ALREADY_EXISTS]` | 对象已存在,直接返回成功 | +| `put_start_success` | INFO | `put_start_success key[{key}] replicas[{n}]` | Master 分配 replica 成功 | +| `put_end_success` | INFO | `put_end_success key[{key}] transfer_us[{us}] data_size[{bytes}]` | Put 完成,数据写入成功 | + +**`put_end_success` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `transfer_us` | 传输阶段总耗时(含磁盘写入 + RDMA 传输) | +| `data_size` | 写入数据大小 | + +### 5.3 传输引擎层 — `client_service.cpp::TransferData` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `transfer_data` | INFO | `transfer_data first_transfer_data[{0/1}] op[WRITE] strategy[{int}] submit_us[{t1}] wait_us[{t2}] result[{code}]` | 传输耗时拆分 | + +字段含义同第 1.3 节 `transfer_data`。 + +--- + +## 6. `put_batch` 日志链路 + +> **注意**:Python 绑定层(`store_py.cpp`)的 batch 操作仍使用原生 `LOG()`,无 `trace_id` 前缀。下层使用 `MC_LOG`,带 `trace_id` 前缀。 + +``` +store_py::put_batch + ├ put_batch start + ├ real_client::put_batch_internal + │ └ batch_put_result + ├ client_service::BatchPut + │ ├ batch_put start + │ └ batch_put complete + ├ client_service::TransferData (多次) + │ └ transfer_data op[WRITE] + └ put_batch complete +``` + +### 6.1 Python 绑定层 — `store_py.cpp::put_batch` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `put_batch start` | INFO | `put_batch start num_keys[{n}] total_size[{bytes}]` | 操作开始 | +| `put_batch complete` | INFO | `put_batch complete num_keys[{n}] rc[{ret}] elapsed_us[{us}]` | 操作完成,rc=0 成功 | +| `put_batch_slow` | WARNING | `put_batch_slow num_keys[{n}] elapsed_us[{us}]` | 耗时超过 10ms 触发慢操作告警 | + +### 6.2 核心逻辑层 — `real_client.cpp::put_batch_internal` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `batch_put_result` | INFO | `batch_put_result num_keys[{n}] num_failed[{f}]` | 批量 Put 结果 | + +### 6.3 传输服务层 — `client_service.cpp::BatchPut` + +| 关键字 | 级别 | 格式 | 说明 | +|--------|------|------|------| +| `batch_put start` | INFO | `batch_put start num_keys[{n}]` | 批量 Put 传输开始 | +| `batch_put complete` | INFO | `batch_put complete num_keys[{n}] num_failed[{f}] transfer_us[{us}] total_size[{bytes}]` | 批量 Put 完成(正常路径) | +| `batch_put complete` | INFO | `batch_put complete num_keys[{n}] num_failed[{f}] total_size[{bytes}]` | 批量 Put 完成(prefer_same_node 路径,无 transfer_us) | + +**`batch_put complete` 字段说明:** + +| 字段 | 含义 | +|------|------| +| `num_keys` | 批量写入的 key 数量 | +| `num_failed` | 失败的 key 数量 | +| `transfer_us` | 传输阶段总耗时(微秒,prefer_same_node 路径无此字段) | +| `total_size` | 写入的总数据大小(字节) | + +### 6.4 传输引擎层 — 同第 5 节的 `transfer_data` + +--- + +## 7. 附录:PerfPoint 打点与日志对照表 + +PerfPoint 定义在 `mooncake-integration/store/mooncake_perf_points.def`。 +使用 `spdiag show` 可查看实时性能数据,配合日志进行交叉分析。 + +### 7.0 `get_into` / `batch_get_into` 与 `get_buffer` / `batch_get_buffer` 是否一致 + +结论:**单 key 的 `get_into` 与 `get_buffer` 基本一致,批量的 `batch_get_into` 与 `batch_get_buffer` 还不完全一致。** + +单 key 路径中,`get_into_breakdown.type` 已经和 `get_breakdown.type` 一样细分为 `memory_local` / `memory_remote` / `local_disk_local` / `local_disk_remote` / `disk`。如果只看到 `memory` / `local_disk` / `disk` 三类,那是旧文档口径,不是当前源码口径。 + +| 接口 | 入口日志 | 入口 PerfPoint | 汇总日志 | 子步骤对齐情况 | +|------|----------|----------------|----------|----------------| +| `get_buffer` | `get_buffer_start` | `GET_BUFFER_INTERNAL_FULL` | `get_breakdown` | `type` 细分 local/remote;有 `alloc_us` | +| `get_into` | `get_into_start` | `GET_INTO_INTERNAL` | `get_into_breakdown` | `type` 细分 local/remote;没有独立 `alloc_us`,DISK/范围读临时 buffer 分配计入 `read_us` | +| `batch_get_buffer` | `batch_get_buffer_start` | `GET_BATCH_BUFFER_INTERNAL_FULL` | `batch_get_breakdown` | `batch_get_ops` 合并 MEMORY + DISK,`ssd_offload_ops` 表示 LOCAL_DISK | +| `batch_get_into` | `batch_get_into_start` | `GET_BATCH_INTO_INTERNAL` | `batch_get_into_breakdown` | `mem_ops` / `disk_ops` / `ssd_offload_ops` 拆开统计,比 `batch_get_buffer` 更细 | + +差异点: + +- `get_into` 的 replica type 现在与 `get_buffer` 一致,都是 local/remote 细分;文档已同步修正。 +- `get_into_breakdown` 没有 `alloc_us` 字段,因为它通常直接写入调用方 buffer;只有 DISK 或 range 读需要临时 CPU buffer,这部分耗时归入 `read_us`。 +- `batch_get_into_breakdown` 比 `batch_get_breakdown` 多拆了 `mem_ops` 和 `disk_ops`;而 `batch_get_breakdown` 用 `batch_get_ops` 合并 MEMORY + DISK。所以这两者目前不是完全一致口径。 + +### GET 侧 + +| PerfPoint 名称 | 定义位置 | 标签 | 对应日志关键字 | +|----------------|---------|------|---------------| +| `GET_STORE_PY_GET` | store_py.cpp::get | Get | —(已移除) | +| `GET_BUFFER_INTERNAL` | store_py.cpp::get | GetBuffer | `get_breakdown` | +| `GET_BUFFER_INTERNAL_FULL` | real_client.cpp::get_buffer | GetBufferInternal | `get_breakdown` | +| `GET_INTERNAL_QUERY` | real_client.cpp::get_buffer_internal | Query | `query_success` | +| `GET_INTERNAL_SELECT_REPLICA` | real_client.cpp::get_buffer_internal | SelectReplica | `replica_selected` | +| `GET_INTERNAL_ALLOC_BUFFER` | real_client.cpp::get_buffer_internal | AllocBuffer | `get_breakdown` alloc_us | +| `GET_INTERNAL_SSD_READ` | real_client.cpp::get_buffer_internal | SSDRead | `ssd_read_detail` | +| `GET_INTERNAL_MEM_READ` | real_client.cpp::get_buffer_internal | MemRead | `transfer_read_completed` | +| `GET_INTERNAL_DISK_READ` | real_client.cpp::get_buffer_internal | DiskRead | `transfer_read_completed` | +| `GET_SSD_OFFLOAD_RPC` | real_client.cpp::batch_get_into_offload_object_internal | OffloadRpc | `ssd_read_detail` | +| `GET_SSD_TRANSFER_DATA` | real_client.cpp::batch_get_into_offload_object_internal | TransferData | `ssd_read_detail` | +| `GET_SSD_RELEASE_BUFFER` | real_client.cpp::batch_get_into_offload_object_internal | ReleaseBuffer | — | +| `GET_SINGLE_FIND_REPLICA` | client_service.cpp::Get | FindReplica | `transfer_read_completed` | +| `GET_SINGLE_HOT_CACHE` | client_service.cpp::Get | HotCache | `transfer_read_completed` cache_hit | +| `GET_SINGLE_TRANSFER_READ` | client_service.cpp::Get | TransferRead | `transfer_read_completed` | +| `GET_SINGLE_RELEASE_CACHE` | client_service.cpp::Get | ReleaseCache | — | +| `GET_SINGLE_ASYNC_CACHE` | client_service.cpp::Get | AsyncCache | — | +| `GET_SINGLE_TRANSFER_FULL` | client_service.cpp::TransferData | TransferData | `transfer_data op[READ]` | +| `GET_SINGLE_TRANSFER_SUBMIT` | client_service.cpp::TransferData | Submit | `transfer_data` submit_us | +| `GET_SINGLE_TRANSFER_WAIT` | client_service.cpp::TransferData | Wait | `transfer_data` wait_us | + +### GET INTO 侧 + +| PerfPoint 名称 | 定义位置 | 标签 | 对应日志关键字 | +|----------------|---------|------|---------------| +| `GET_INTO_INTERNAL` | real_client.cpp::get_into | GetIntoInternal | `get_into_breakdown` | +| `GET_INTO_INTERNAL_QUERY` | real_client.cpp::get_into_internal | Query | `query_success` | +| `GET_INTO_INTERNAL_SELECT_REPLICA` | real_client.cpp::get_into_internal | SelectReplica | `replica_selected` | +| `GET_INTO_INTERNAL_ALLOC_BUFFER` | real_client.cpp::get_into_internal | AllocBuffer | `get_into_breakdown` read_us | +| `GET_INTO_INTERNAL_SSD_READ` | real_client.cpp::get_into_internal | SSDRead | `ssd_read_detail` / `SSD read failed` | +| `GET_INTO_INTERNAL_MEM_READ` | real_client.cpp::get_into_internal | MemRead | `transfer_read_completed` / `get_into_breakdown` | +| `GET_INTO_INTERNAL_DISK_READ` | real_client.cpp::get_into_internal | DiskRead | `transfer_read_completed` / `get_into_breakdown` | + +### GET BATCH 侧 + +| PerfPoint 名称 | 定义位置 | 标签 | 对应日志关键字 | +|----------------|---------|------|---------------| +| `GET_STORE_PY_GET_BATCH` | store_py.cpp::get_batch | GetBatch | `get_batch start` / `get_batch complete` | +| `GET_BATCH_BUFFER_INTERNAL` | store_py.cpp::get_batch | BatchGetBuffer | `batch_get_breakdown` | +| `GET_BATCH_BUFFER_INTERNAL_FULL` | real_client.cpp::batch_get_buffer | BatchGetBufferInternal | `batch_get_breakdown` | +| `GET_BATCH_INTERNAL_QUERY` | real_client.cpp::batch_get_buffer_internal | BatchQuery | `batch_query_result` | +| `GET_BATCH_INTERNAL_PREPARATION` | real_client.cpp::batch_get_buffer_internal | Preparation | —(仅定义,当前未在源码中使用) | +| `GET_BATCH_INTERNAL_SELECT_REPLICA` | real_client.cpp::batch_get_buffer_internal | SelectReplica | `batch_get_breakdown` prep_us(逐 key 汇总) | +| `GET_BATCH_INTERNAL_ALLOC_BUFFER` | real_client.cpp::batch_get_buffer_internal | AllocBuffer | `batch_get_breakdown` prep_us(逐 key 汇总) | +| `GET_BATCH_INTERNAL_SSD_READ` | real_client.cpp::batch_get_buffer_internal | SSDRead | `ssd_read_detail` | +| `GET_BATCH_INTERNAL_MEMDISH_READ` | real_client.cpp::batch_get_buffer_internal | MemDiskRead | `batch_get_transfer_complete` | +| `GET_BATCH_FULL` | client_service.cpp::BatchGet | TransferBatchGet | `batch_get_transfer_complete` | +| `GET_BATCH_FIND_REPLICA` | client_service.cpp::BatchGet | FindReplica | — | +| `GET_BATCH_HOT_CACHE` | client_service.cpp::BatchGet | HotCache | — | +| `GET_BATCH_SUBMIT` | client_service.cpp::BatchGet | Submit | — | +| `GET_BATCH_WAIT` | client_service.cpp::BatchGet | Wait | — | +| `GET_BATCH_RELEASE_CACHE` | client_service.cpp::BatchGet | ReleaseCache | — | +| `GET_BATCH_ASYNC_CACHE` | client_service.cpp::BatchGet | AsyncCache | — | + +### BATCH GET INTO 侧 + +| PerfPoint 名称 | 定义位置 | 标签 | 对应日志关键字 | +|----------------|---------|------|---------------| +| `GET_BATCH_INTO_INTERNAL` | real_client.cpp::batch_get_into | BatchGetIntoInternal | `batch_get_into_breakdown` | +| `GET_BATCH_INTO_INTERNAL_QUERY` | real_client.cpp::batch_get_into_internal | BatchQuery | `batch_get_into_query_result` | +| `GET_BATCH_INTO_INTERNAL_SELECT_REPLICA` | real_client.cpp::batch_get_into_internal | SelectReplica | `batch_get_into_breakdown` prep_us(逐 key 汇总) | +| `GET_BATCH_INTO_INTERNAL_ALLOC_BUFFER` | real_client.cpp::batch_get_into_internal | AllocBuffer | `batch_get_into_breakdown` read_us(仅 DISK 临时 buffer) | +| `GET_BATCH_INTO_INTERNAL_MEM_READ` | real_client.cpp::batch_get_into_internal | MemRead | `batch_get_transfer_complete` / `batch_get_into_breakdown` | +| `GET_BATCH_INTO_INTERNAL_DISK_READ` | real_client.cpp::batch_get_into_internal | DiskRead | `DISK BatchGet failed` / `DISK scatter failed` | +| `GET_BATCH_INTO_INTERNAL_SSD_READ` | real_client.cpp::batch_get_into_internal | SSDRead | `ssd_read_detail` / `Batch get store object failed` | + +### PUT 侧 + +| PerfPoint 名称 | 定义位置 | 标签 | 对应日志关键字 | +|----------------|---------|------|---------------| +| `PUT_STORE_PY_PUT` | store_py.cpp::put | Put | —(已移除) | +| `PUT_INTERNAL_FULL` | store_py.cpp::put | PutBuffer | `put_result` | +| `PUT_INTERNAL_ALLOC_BUFFER` | real_client.cpp::put_internal | AllocBuffer | — | +| `PUT_INTERNAL_MEM_COPY` | real_client.cpp::put_internal | MemCopy | — | +| `PUT_INTERNAL_SPLIT_SLICES` | real_client.cpp::put_internal | SplitSlices | — | +| `PUT_SINGLE_FULL` | client_service.cpp::Put | TransferPut | `put_end_success` | +| `PUT_SINGLE_PUT_START` | client_service.cpp::Put | PutStart | `put_start_success` | +| `PUT_SINGLE_DISK_WRITE` | client_service.cpp::Put | DiskWrite | `put_end_success` | +| `PUT_SINGLE_TRANSFER_WRITE` | client_service.cpp::Put | TransferWrite | `put_end_success` | +| `PUT_SINGLE_PUT_END` | client_service.cpp::Put | PutEnd | `put_end_success` | +| `PUT_SINGLE_PUT_REVOKE` | client_service.cpp::Put | PutRevoke | — | +| `PUT_SINGLE_TRANSFER_FULL` | client_service.cpp::TransferData | TransferData | `transfer_data op[WRITE]` | +| `PUT_SINGLE_TRANSFER_SUBMIT` | client_service.cpp::TransferData | Submit | `transfer_data` submit_us | +| `PUT_SINGLE_TRANSFER_WAIT` | client_service.cpp::TransferData | Wait | `transfer_data` wait_us | + +### PUT BATCH 侧 + +| PerfPoint 名称 | 定义位置 | 标签 | 对应日志关键字 | +|----------------|---------|------|---------------| +| `PUT_STORE_PY_PUT_BATCH` | store_py.cpp::put_batch | PutBatch | `put_batch start` / `put_batch complete` | +| `PUT_BATCH_INTERNAL_FULL` | store_py.cpp::put_batch | BatchPutBuffer | `batch_put_result` | +| `PUT_BATCH_INTERNAL_ALLOC_BUFFER` | real_client.cpp::put_batch_internal | AllocBuffer | — | +| `PUT_BATCH_INTERNAL_MEM_COPY` | real_client.cpp::put_batch_internal | MemCopy | — | +| `PUT_BATCH_INTERNAL_SPLIT_SLICES` | real_client.cpp::put_batch_internal | SplitSlices | — | +| `PUT_BATCH_FULL` | client_service.cpp::BatchPut | TransferBatchPut | `batch_put complete` | +| `PUT_BATCH_CREATE_OPS` | client_service.cpp::BatchPut | CreateOps | — | +| `PUT_BATCH_PUT_START` | client_service.cpp::StartBatchPut | PutStart | — | +| `PUT_BATCH_SUBMIT` | client_service.cpp::SubmitTransfers | Submit | — | +| `PUT_BATCH_DISK_WRITE` | client_service.cpp::SubmitTransfers | DiskWrite | — | +| `PUT_BATCH_WAIT` | client_service.cpp::WaitForTransfers | Wait | — | +| `PUT_BATCH_PUT_END` | client_service.cpp::FinalizeBatchPut | PutEnd | — | +| `PUT_BATCH_PUT_REVOKE` | client_service.cpp::FinalizeBatchPut | PutRevoke | — | +| `PUT_BATCH_COLLECT_RESULTS` | client_service.cpp::BatchPut | CollectResults | — | + +### UB/URMA 建连侧 + +| PerfPoint 名称 | 定义位置 | 标签 | 对应日志关键字 | +|----------------|---------|------|---------------| +| `UB_HANDSHAKE_ENCODE` | transfer_metadata.cpp::encode | Encode | — | +| `UB_HANDSHAKE_DECODE` | transfer_metadata.cpp::decode | Decode | — | +| `UB_ENDPOINT_CONSTRUCT` | urma_endpoint.cpp::construct | Construct | `urma_endpoint_construct_breakdown` | +| `UB_ENDPOINT_CREATE_JETTY` | urma_endpoint.cpp::construct | CreateJetty | `urma_create_jetty_breakdown` | +| `UB_ENDPOINT_ACTIVE_SETUP` | urma_endpoint.cpp::setupConnectionsByActive | ActiveSetup | `urma_active_setup_breakdown` | +| `UB_ENDPOINT_ACTIVE_HANDSHAKE` | urma_endpoint.cpp::setupConnectionsByActive | SendHandshake | — | +| `UB_ENDPOINT_PASSIVE_SETUP` | urma_endpoint.cpp::setupConnectionsByPassive | PassiveSetup | `urma_passive_setup_breakdown` | +| `UB_ENDPOINT_DO_SETUP_ALL` | urma_endpoint.cpp::doSetupConnection | DoSetupAll | `urma_do_setup_all_breakdown` | +| `UB_ENDPOINT_IMPORT_JETTY` | urma_endpoint.cpp::doSetupConnection | ImportJetty | `urma_import_jetty_breakdown` | +| `UB_ENDPOINT_BIND_JETTY` | urma_endpoint.cpp::doSetupConnection | BindJetty | `urma_bind_jetty_breakdown` | + +--- + +## 8. 日志配置 + +Mooncake 使用 glog 作为日志库,通过环境变量控制日志级别、输出位置和开关。 + +### 8.1 环境变量 + +| 变量 | 默认值 | 说明 | +|------|-------|------| +| `MC_LOG_LEVEL` | `INFO` | 日志输出级别 | +| `MC_LOG_DIR` | 空(stderr) | 日志文件输出目录 | +| `MC_LOG_ENABLE` | 禁用 | 日志总开关 | +| `MC_HIFREQ_LOG_SAMPLE_RATE` | `0.1` | 高频 breakdown 日志采样率,详见 §8.6 | + +代码来源:`mooncake-transfer-engine/src/config.cpp`(MC_LOG_LEVEL)、`mooncake-common/src/mooncake_logging.cpp`(MC_LOG_ENABLE、MC_HIFREQ_LOG_SAMPLE_RATE) + +### 8.2 设置日志级别 — `MC_LOG_LEVEL` + +可选值: + +| 值 | 效果 | +|----|------| +| `TRACE` | 最详细,输出 INFO/WARNING/ERROR,额外启用 trace 标志 | +| `INFO` | 默认,输出 INFO/WARNING/ERROR | +| `WARNING` | 只输出 WARNING/ERROR | +| `ERROR` | 只输出 ERROR | + +**操作方法:** + +```bash +# 在启动 Mooncake 服务前设置 +export MC_LOG_LEVEL=WARNING +./mooncake_master + +# 或在 Python 端(import mooncake 前) +import os +os.environ['MC_LOG_LEVEL'] = 'WARNING' +import mooncake +``` + +**注意:** 设置日志级别只影响日志输出,不影响时间记录代码(`steady_clock::now()` 调用仍会执行)。 + +### 8.3 设置日志输出位置 — `MC_LOG_DIR` + +| 情况 | 行为 | +|------|------| +| 未设置或为空 | 日志输出到 stderr(终端) | +| 目录不存在 | 输出 WARNING,回退到 stderr | +| 目录不可写 | 输出 WARNING,回退到 stderr | +| 目录存在且可写 | 日志写入该目录下的文件 | + +**操作方法:** + +```bash +# 输出到指定目录 +export MC_LOG_DIR=/var/log/mooncake +./mooncake_master + +# 需要先确保目录存在且可写 +mkdir -p /var/log/mooncake +chmod 755 /var/log/mooncake +``` + +### 8.4 设置日志总开关 — `MC_LOG_ENABLE` + +控制 Mooncake 日志输出开关。未显式设置时默认关闭;显式启用后,`MC_LOG`/`MC_VLOG` 按此开关输出,Transfer Engine 初始化时也会按此开关设置 glog 的最低输出级别。 + +| 值 | 效果 | +|----|------| +| 未设置 | 默认禁用 | +| `off` / `0` / `false` / `no` | 关闭日志(不区分大小写) | +| 其他值 | 启用 | + +**注意:** +- FATAL 级别日志不受此开关影响,始终输出 +- 此开关与 `MC_LOG_LEVEL` 独立:两者都允许时日志才输出 +- 仅使用原生 `LOG()` 且未经过 Transfer Engine 配置初始化的进程,仍可能受 glog 自身 flag 控制 + +**操作方法:** + +```bash +# 启用日志 +export MC_LOG_ENABLE=on +./mooncake_master +``` + +### 8.5 常用配置场景 + +| 场景 | 配置 | +|------|------| +| 生产环境 | `export MC_LOG_ENABLE=on && export MC_LOG_LEVEL=WARNING && export MC_LOG_DIR=/var/log/mooncake` | +| 调试排查 | `export MC_LOG_ENABLE=on && export MC_LOG_LEVEL=INFO`(输出到 stderr,除非设置 `MC_LOG_DIR`) | +| 性能测试(减少日志) | `export MC_LOG_ENABLE=on && export MC_LOG_LEVEL=ERROR` | +| 启用日志输出 | `export MC_LOG_ENABLE=on` | + +### 8.6 高频 breakdown 日志采样 — `MC_HIFREQ_LOG_SAMPLE_RATE` + +`get_buffer` / `batch_get_buffer` / `get_into` / `batch_get_into` 四条链路精简后,每个请求只剩一条 +`*_breakdown` 汇总日志(原生 `LOG(INFO)`)。由于 get 类操作调用极频繁,这条"每请求一条"的日志本身即为高频 +日志,可用本变量按概率采样。 + +| 取值 | 效果 | +|------|------| +| `1.0` | 每个请求都输出其 breakdown(100%) | +| `0.1`(默认) | 每个请求 10% 概率输出 breakdown | +| `0` | 完全不输出 breakdown(全部静默) | +| 非法/越界 | 非数值回退 `0.1`;`<0` 截断为 `0`;`>1` 截断为 `1` | + +**实现要点:** +- 每个请求只掷一次骰子(`mooncake::logging::ShouldSampleHiFreqLog()`,线程本地无锁 RNG);命中才**既输出 + 日志又记录其 steady/system clock 计时**,未命中则跳过计时与输出,开销接近零。 +- 仅作用于 breakdown 这一条文本日志。**SpDiag `PerfPoint` 打点始终记录,不受采样影响**;`ERROR`/`WARNING` + 也照常每次输出。 +- breakdown 是原生 `LOG(INFO)`,可见性由本变量控制,**与 `MC_LOG_ENABLE` 无关**。 +- 解析与缓存:`mooncake-common/src/mooncake_logging.cpp::ParseHiFreqLogSampleRate`(进程内只解析一次)。 + +```bash +# 全量输出 breakdown(排查/对账时用) +export MC_HIFREQ_LOG_SAMPLE_RATE=1.0 +# 默认 10% 采样,无需设置;完全关闭: +export MC_HIFREQ_LOG_SAMPLE_RATE=0 +``` + +#### Correlated latency diagnostics + +High-frequency diagnostics now use deterministic `trace_id` sampling. A +sampled request emits correlated `*_breakdown`, `transfer_diag`, +`storage_read_breakdown`, `BatchGetReplicaListItem`, and +`urma_queue_depth` records across worker threads and processes. SSD offload +RPCs propagate the trace id to the storage owner. Calls without a trace id +retain random local sampling. + +- `transfer_diag.submit_us`: validation, segment lookup, request construction, + and submission until a future is returned. +- `transfer_diag.wait_us`: time blocked in `future.get()`. +- `local_endpoints_us`: time spent locking and copying the locally mounted + Transfer Engine endpoint set. +- `select_replica_us`: time spent scanning the returned replicas and choosing + the preferred complete replica. +- Transfer-layer records intentionally do not carry object keys; correlate + them with Store records through the existing trace id. +- `storage_read_breakdown.alloc_us`: owner ClientBuffer batch allocation. +- `storage_read_breakdown.plan_us`: metadata lookup and read-plan construction. +- `storage_read_breakdown.file_open_us`: path resolution and file open time. +- `storage_read_breakdown.disk_read_us`: cumulative time inside actual + `read_aligned`/`vector_read` calls. +- `storage_read_breakdown.total_us`: wall time of the owner `BatchGet`. + +Batch endpoint records are emitted as `batch_transfer_item`, one record per +key. Transfer failure ERROR records are never sampled and include the key. + +--- + +## 9. 异步日志与 TraceId + +### 9.1 异步日志架构 + +实现文件:`mooncake-common/include/mooncake_logging.h`、`mooncake-common/src/mooncake_logging.cpp` + +**流程:** + +``` +调用 MC_LOG(severity) << "message" + → 创建 AsyncLogMessage 临时对象 + → 捕获当前 trace_id(CurrentTraceId()) + → 构造日志消息到 ostringstream + → 析构时判断: + - FATAL:直接同步调用 glog 输出(带 trace_id 前缀) + - 其他:构造 LogEntry{file, line, severity, trace_id, message} 入队 + → AsyncLogQueue 后台线程消费 + → WriteSync() 调用 google::LogMessage 输出(自动带 trace_id 前缀) +``` + +**队列参数:** + +| 参数 | 值 | +|------|-----| +| 最大队列长度 | 8192 条 | +| 队列满策略 | 阻塞写入线程(condition_variable wait) | +| 后台线程数 | 1 | +| 退出处理 | `atexit` 注册 `Stop()`,刷出剩余日志 | + +### 9.2 TraceId 系统 + +**ID 生成算法:** + +``` +process_seed = (PID << 48) ^ (steady_clock_ns & 0x0000FFFFFFFF0000) +trace_id = process_seed ^ atomic_counter++ +``` + +PID 保证跨进程唯一,steady_clock_ns 保证同进程每次启动不同,atomic_counter 保证同进程内递增唯一。 + +**线程传递机制:** + +- `thread_local uint64_t current_trace_id` 存储当前线程的 trace_id +- `ScopedTraceId` 在构造时保存旧值、设置新值,析构时恢复旧值 +- `AsyncLogMessage` 构造时读取 `CurrentTraceId()`,并把该值固化到日志条目中;日志之后即使由后台线程异步落盘,也不会丢失原始 trace_id +- RAII 模式,支持嵌套、提前返回和异常退出 + +恢复旧值的原因是 `current_trace_id` 绑定在线程上,而 Mooncake 中大量使用线程池和后台 worker。线程处理完一个请求或任务后会继续处理其他工作;如果不恢复,后续不属于该请求的日志可能仍然带着旧 trace_id,导致排查时误以为它们属于同一条调用链。 + +**函数间传递链路:** + +1. 入口函数生成 trace:`real_client.cpp` 中 `get_buffer`、`get_into`、`batch_get_into`、`put`、`put_batch` 等公开入口创建 `ScopedTraceId trace(NewTraceId())`,从这里开始一次用户操作拥有独立 trace_id。 +2. 同步函数调用自动继承:入口函数继续调用 `*_internal`、`client_service`、`TransferSubmitter` 等下游函数时,只要仍在同一线程执行,下游 `MC_LOG` 会通过 `CurrentTraceId()` 读到同一个 thread-local trace_id,不需要把 trace_id 作为函数参数层层传递。 +3. MC_LOG 捕获当前 trace:每条 `MC_LOG` / `MC_VLOG` 在构造 `AsyncLogMessage` 时立即捕获当前 trace_id,并随 `LogEntry` 放入异步日志队列。后台日志线程只负责输出已经捕获好的 trace_id。 +4. 跨线程提交前显式捕获:当执行流要进入线程池或 worker 队列时,提交线程先调用 `CurrentTraceId()` 取出当前 trace_id,并把它放进任务对象或 lambda 捕获列表。 +5. 工作线程恢复上下文:worker 取出任务后创建 `ScopedTraceId trace(task.trace_id)` 或 `ScopedTraceId trace(trace_id)`,让该任务执行期间的所有 `MC_LOG` 都继续带原始请求的 trace_id。 +6. 任务结束自动恢复:worker 任务作用域结束后 `ScopedTraceId` 析构,恢复该线程之前的 trace_id,通常恢复为 `0`,后续空闲、清理或下一任务日志不会串到刚完成的请求上。 + +典型同步链路: + +``` +RealClient::get_buffer + -> ScopedTraceId(NewTraceId()) + -> RealClient::get_buffer_internal + -> Client::Get / TransferSubmitter + -> MC_LOG 捕获 CurrentTraceId() +``` + +典型异步链路: + +``` +RealClient::put + -> ScopedTraceId(NewTraceId()) + -> client_service 提交异步任务前 CurrentTraceId() + -> write_thread_pool_.enqueue(..., trace_id) + -> worker lambda 内 ScopedTraceId(trace_id) + -> StoreObject / PutEnd 相关 MC_LOG 继续使用同一 trace_id +``` + +典型传输任务链路: + +``` +TransferSubmitter::submitTransfer + -> MemcpyTask(..., CurrentTraceId()) + -> MemcpyWorkerPool::workerThread + -> ScopedTraceId(task.trace_id) + -> memcpy / GPU copy 相关 MC_LOG 使用原始 trace_id + +TransferSubmitter::submitFileReadOperation + -> FilereadTask(..., CurrentTraceId()) + -> FilereadWorkerPool::workerThread + -> ScopedTraceId(task.trace_id) + -> LoadObject 相关 MC_LOG 使用原始 trace_id +``` + +**异步任务传播:** + +- `MemcpyTask` 和 `FilereadTask` 携带 `trace_id` 字段 +- `client_service.cpp` 中异步 `StoreObject + PutEnd` 的线程池 lambda 通过捕获列表携带 `trace_id` +- 工作线程取出任务后通过 `ScopedTraceId trace(task.trace_id)` 或 `ScopedTraceId trace(trace_id)` 恢复上下文 +- 确保异步路径的日志可关联到原始操作 + +**日志格式:** + +``` +I0527 14:30:00.123456 12345 real_client.cpp:2682] trace_id[123456789abcdef0] get_breakdown key[k1] ... +I0527 14:30:00.123789 12345 real_client.cpp:3600] trace_id[none] Cleaning up ... +``` + +### 9.3 FlushAsyncLogs + +调用 `mooncake::logging::FlushAsyncLogs()` 可手动刷出异步队列中所有待输出日志,内部会等待队列为空且无活跃写入后调用 `google::FlushLogFiles(google::INFO)`。 + +--- + +## 10. `client_create_breakdown` 日志 + +来源:`client_service.cpp::Client::Create` + +记录 Client 对象创建过程各阶段耗时。有两种变体: + +### 10.1 RPC-only 模式(`protocol == "rpc_only"`) + +``` +client_create_breakdown protocol[rpc_only] connect_master_us[{t1}] storage_config_us[{t2}] init_transfer_engine_us[0] init_transfer_submitter_us[0] total_us[{t5}] +``` + +### 10.2 完整模式 + +``` +client_create_breakdown protocol[{p}] connect_master_us[{t1}] storage_config_us[{t2}] init_transfer_engine_us[{t3}] init_transfer_submitter_us[{t4}] total_us[{t5}] +``` + +### 字段说明 + +| 字段 | 含义 | +|------|------| +| `protocol` | 传输协议:`rdma` / `tcp` / `ub` / `ascend` / `cxl` / `rpc_only` 等 | +| `connect_master_us` | 连接 Master 服务耗时(微秒),含 HA 视图读取(如启用) | +| `storage_config_us` | 获取存储配置耗时(微秒),含 GetStorageConfig 或回退 GetFsdir | +| `init_transfer_engine_us` | 初始化传输引擎耗时(微秒),rpc_only 模式为 0 | +| `init_transfer_submitter_us` | 初始化传输提交器耗时(微秒),rpc_only 模式为 0 | +| `total_us` | `Client::Create` 总耗时(微秒),从函数入口到返回 | + +--- + +## 11. 传输任务层日志 + +来源:`mooncake-store/src/transfer_task.cpp` + +记录传输任务执行过程中的关键步骤耗时。 + +### 11.1 `transfer_future_wait` + +``` +transfer_future_wait first_wait[{0/1}] ready_before_wait[{0/1}] strategy[{int}] wait_us[{us}] result[{code}] +``` + +记录等待传输 Future 完成的过程。 + +| 字段 | 含义 | +|------|------| +| `first_wait` | 是否为进程首次调用 wait:`1` 首次,`0` 后续(用于区分首次建连开销) | +| `ready_before_wait` | wait 前传输是否已完成:`1` 已完成(无需等待),`0` 需要等待 | +| `strategy` | `TransferFuture` 的传输策略整数值 | +| `wait_us` | 等待耗时(微秒) | +| `result` | 传输结果:`OK` 表示成功,其他为错误码 | + +### 11.2 `open_segment_breakdown` + +``` +open_segment_breakdown endpoint[{addr}] open_segment_us[{us}] status[{0/-1}] +``` + +记录打开远端 Segment(建立传输连接)的耗时。 + +| 字段 | 含义 | +|------|------| +| `endpoint` | 传输引擎端点地址字符串 | +| `open_segment_us` | `openSegment()` 调用耗时(微秒) | +| `status` | 结果:`0` 成功,`-1` 失败(`ERR_INVALID_ARGUMENT`) | + +### 11.3 `submit_transfer_breakdown` + +``` +submit_transfer_breakdown first_transfer[{0/1}] batch_id[{id}] request_count[{n}] alloc_batch_id_us[{us}] submit_transfer_us[{us}] status[{0/err}] +``` + +记录提交批量传输请求的耗时拆分。 + +| 字段 | 含义 | +|------|------| +| `first_transfer` | 是否为首次提交传输:`1` 首次,`0` 后续 | +| `batch_id` | 批量传输 ID | +| `request_count` | 本次提交的传输请求数量 | +| `alloc_batch_id_us` | 分配 batch ID 耗时(微秒) | +| `submit_transfer_us` | 提交传输请求耗时(微秒) | +| `status` | 提交结果:`0` 成功,其他为 gRPC 状态码 | + +--- + +## 12. URMA 端建连日志(第一次建立连接) + +来源:`mooncake-transfer-engine/src/transport/kunpeng_transport/urma/urma_endpoint.cpp` + +> **注意**:本节的 `*_breakdown` 日志用的是 `MC_LOG(INFO)`,**会带 `trace_id` 前缀**——主动端在握手时通过 `local_desc.trace_id = CurrentTraceId()` 把 trace_id 传给对端,所以一次建连的主动端和被动端日志可以用同一个 `trace_id` 串起来。少量纯状态日志(如 `"Connection has been established"`)仍是原生 `LOG()`,无 trace_id。 + +记录 UB/URMA 传输端点的构建和建连过程各步骤耗时。 + +### 12.0 先搞清楚:连接是什么时候建的、日志按什么顺序打 + +**何时触发**:URMA 连接是**懒建立**的——`openSegment()` 只拿元数据,并不建连;直到第一次真正往某个 `peer_nic_path`(远端节点+网卡)提交传输时,worker 线程发现这个 endpoint 还没 connected,才触发建连。这也是为什么进程第一次传输某个对端时会有一段额外开销(对应 `first_transfer` / `first_wait` 字段的 `1`)。 + +**建连分主动端(发起方)和被动端(响应方)两侧**,各自打不同的 breakdown 日志。把它们按发生顺序串起来,就是下面这张图——读真实日志时照着对号入座即可: + +``` +主动端 Node A 被动端 Node B +(worker 发现 endpoint 未连) +setupConnectionsByActive() ← 计时起点 t0 + │ + ├─[情况①: peer_nic == 本机自己] 同节点直连,跳过握手,直接 doSetupConnection + │ → urma_active_setup_breakdown ... local_peer[1] handshake_us[0] ... (§12.3 变体1) + │ + └─[情况②: 跨节点] 需要握手 + sendHandshake(local_desc{nic,jetty_num,trace_id}) ──RPC──▶ onSetupConnections() + setupConnectionsByPassive() + doSetupConnection() ← 被动端也建 + 每个 jetty: import + bind + → urma_import_jetty_breakdown (§12.6) + → urma_bind_jetty_breakdown (§12.7) + → urma_do_setup_all_breakdown (§12.5) + ◀──返回 peer_desc{eid,jetty_num}── + → urma_passive_setup_breakdown (§12.4) + ├─ 握手失败 → urma_active_setup_breakdown ... handshake_us[..] do_setup_us[0] status[err] (§12.3 变体2) + └─ 握手成功 → doSetupConnection(peer_eid, peer_jetty_num) ← 主动端建 + 每个 jetty: import + bind + → urma_import_jetty_breakdown (§12.6) + → urma_bind_jetty_breakdown (§12.7) + → urma_do_setup_all_breakdown (§12.5) + → urma_active_setup_breakdown ... local_peer[0] handshake_us[..] do_setup_us[..] status[0] (§12.3 变体3) +``` + +**各日志在排查里的分工**(拿到一条慢建连日志时这样定位): + +- `urma_active_setup_breakdown` 是**主动端的总账**:先看 `total_us` 判断这次建连慢不慢,再看是 `handshake_us`(握手 RPC 慢,多半是网络/对端响应慢)还是 `do_setup_us`(本地 import+bind 慢)占大头。 +- `urma_passive_setup_breakdown` 是**被动端的总账**:和主动端用同一 `trace_id` 配对,看对端响应那一侧花了多久。 +- `urma_do_setup_all_breakdown` 把一次 `doSetupConnection` 里**所有 jetty 的 import+bind 循环**汇总;想进一步拆到单个 jetty,再看 `urma_import_jetty_breakdown` / `urma_bind_jetty_breakdown`。 +- `urma_endpoint_construct_breakdown` / `urma_create_jetty_breakdown` 属于**更早的端点构建期**(创建 jetty 资源,发生在 init/首次用到该 context 时),不在每次建连路径上,但首次开销分析要一并看。 + +> 想从代码角度理解这套握手/Jetty 绑定流程(`setupConnectionsByActive` → `sendHandshake` → `doSetupConnection` → `import/bind jetty`),见 `transfer-engine-deep-dive.md` 第 8 站与 `urma-transfer-engine-flow.md` 第 5 站。对应的 PerfPoint 打点见本手册 §545「UB/URMA 建连侧」(`UB_ENDPOINT_ACTIVE_SETUP` / `ACTIVE_HANDSHAKE` / `PASSIVE_SETUP` 等)。 + +下面是每条日志的逐字段参考。 + +### 12.1 `urma_endpoint_construct_breakdown` + +``` +urma_endpoint_construct_breakdown local_nic[{nic}] jetty_count[{n}] construct_us[{us}] status[0] +``` + +记录 UrmaEndpoint 构建过程总耗时。 + +| 字段 | 含义 | +|------|------| +| `local_nic` | 本端 NIC 路径标识 | +| `jetty_count` | 创建的 jetty 数量(即 `num_jetty_per_ep` 配置值) | +| `construct_us` | 整个 construct 过程耗时(微秒),含所有 jetty 创建 | +| `status` | 结果:`0` 成功 | + +### 12.2 `urma_create_jetty_breakdown` + +**成功时:** +``` +urma_create_jetty_breakdown index[{i}] jetty_id[{id}] jfc_id[{id}] create_us[{us}] status[0] +``` + +**失败时:** +``` +urma_create_jetty_breakdown index[{i}] create_us[{us}] status[-1] +``` + +记录单个 jetty 创建耗时。 + +| 字段 | 含义 | +|------|------| +| `index` | jetty 在 jetty_list 中的索引 | +| `jetty_id` | 创建成功后的 jetty ID | +| `jfc_id` | jetty 关联的 JFC(Jetty Flow Control)ID | +| `create_us` | `urma_create_jetty()` 调用耗时(微秒) | +| `status` | 结果:`0` 成功,`-1` 失败 | + +### 12.3 `urma_active_setup_breakdown` + +记录主动建连(active side)过程耗时。有三种变体: + +**本端对端(local_peer=1,同节点通信):** +``` +urma_active_setup_breakdown local_nic[{nic}] peer_nic[{nic}] local_peer[1] handshake_us[0] do_setup_us[{us}] total_us[{us}] status[{rc}] +``` + +**握手失败:** +``` +urma_active_setup_breakdown local_nic[{nic}] peer_nic[{nic}] local_peer[0] handshake_us[{us}] do_setup_us[0] total_us[{us}] status[{rc}] +``` + +**成功路径:** +``` +urma_active_setup_breakdown local_nic[{nic}] peer_nic[{nic}] local_peer[0] handshake_us[{us}] do_setup_us[{us}] total_us[{us}] status[{rc}] +``` + +| 字段 | 含义 | +|------|------| +| `local_nic` | 本端 NIC 路径标识 | +| `peer_nic` | 对端 NIC 路径标识 | +| `local_peer` | 是否为本节点内通信:`1` 是(无需握手,直接 doSetup),`0` 否(需跨节点握手) | +| `handshake_us` | 握手耗时(微秒),本端对端时为 `0` | +| `do_setup_us` | `doSetupConnection()` 耗时(微秒),握手失败时为 `0` | +| `total_us` | 主动建连总耗时(微秒) | +| `status` | 结果:`0` 成功,其他为错误码(如 `ERR_DEVICE_NOT_FOUND`、`ERR_REJECT_HANDSHAKE`) | + +### 12.4 `urma_passive_setup_breakdown` + +``` +urma_passive_setup_breakdown local_nic[{nic}] peer_nic[{nic}] total_us[{us}] status[{rc}] +``` + +记录被动建连(passive side,响应握手请求)过程耗时。 + +| 字段 | 含义 | +|------|------| +| `local_nic` | 本端 NIC 路径标识 | +| `peer_nic` | 对端 NIC 路径标识 | +| `total_us` | 被动建连总耗时(微秒),含 `doSetupConnection()` | +| `status` | 结果:`0` 成功,其他为错误码 | + +### 12.5 `urma_do_setup_all_breakdown` + +``` +urma_do_setup_all_breakdown local_nic[{nic}] peer_nic[{nic}] jetty_count[{n}] total_us[{us}] status[0] +``` + +记录 `doSetupConnection()` 整体耗时(包含所有 jetty 的 import + bind 循环)。 + +| 字段 | 含义 | +|------|------| +| `local_nic` | 本端 NIC 路径标识 | +| `peer_nic` | 对端 NIC 路径标识 | +| `jetty_count` | 设置的 jetty 数量(本端与对端匹配) | +| `total_us` | 全部 jetty 设置总耗时(微秒) | +| `status` | 结果:`0` 成功 | + +### 12.6 `urma_import_jetty_breakdown` + +``` +urma_import_jetty_breakdown index[{i}] peer_jetty_id[{id}] import_us[{us}] status[{0/-1}] +``` + +记录单个 jetty 的 `urma_import_jetty()` 调用耗时。 + +| 字段 | 含义 | +|------|------| +| `index` | jetty 在 jetty_list 中的索引 | +| `peer_jetty_id` | 对端 jetty ID | +| `import_us` | `urma_import_jetty()` 调用耗时(微秒) | +| `status` | 结果:`0` 成功,`-1` 失败 | + +### 12.7 `urma_bind_jetty_breakdown` + +``` +urma_bind_jetty_breakdown index[{i}] local_jetty_id[{id}] peer_jetty_id[{id}] bind_us[{us}] status[0] +``` + +记录单个 jetty 的 `urma_bind_jetty()` 调用耗时。 + +| 字段 | 含义 | +|------|------| +| `index` | jetty 在 jetty_list 中的索引 | +| `local_jetty_id` | 本端 jetty ID | +| `peer_jetty_id` | 对端 jetty ID(即 imported jetty) | +| `bind_us` | `urma_bind_jetty()` 调用耗时(微秒) | +| `status` | 结果:`0` 成功 | + +--- + +## 13. 慢操作告警汇总 + +来源:`mooncake-store/src/real_client.cpp` + +慢操作告警统一由 `real_client.cpp` 输出,阈值为 **3000us(3ms)**。仅当操作耗时超过阈值时才输出 WARNING 级别日志。 + +> **注意**:这些告警日志由 MC_LOG 输出,自动带 `trace_id` 前缀。 + +### 13.1 单 key 操作 + +``` +trace_id[xxx] {op}_slow key[{key}] size[{size}] elapsed_us[{us}] rc[{rc}] +``` + +| 字段 | 含义 | +|------|------| +| `{op}` | 操作名:`get_buffer` / `get_into` / `put` / `put_parts` / `put_from` | +| `key` | 对象 key | +| `size` | 数据大小(字节) | +| `elapsed_us` | 操作总耗时(微秒) | +| `rc` | 返回码:`0` 成功(但慢),`-1` 失败 | + +### 13.2 批量操作(带 success 计数) + +``` +trace_id[xxx] {op}_slow num_keys[{n}] size[{size}] elapsed_us[{us}] success[{s}] +``` + +| 字段 | 含义 | +|------|------| +| `{op}` | 操作名:`batch_get_buffer` / `batch_get_into` / `batch_put_from` / `batch_put_from_multi_buffers` | +| `num_keys` | 批量操作的 key 数量 | +| `size` | 批量操作的总数据大小(字节) | +| `elapsed_us` | 操作总耗时(微秒) | +| `success` | 成功的 key 数量 | + +### 13.3 批量操作(带 rc 返回码) + +``` +trace_id[xxx] {op}_slow num_keys[{n}] size[{size}] elapsed_us[{us}] rc[{rc}] +``` + +| 字段 | 含义 | +|------|------| +| `{op}` | 操作名:`put_batch` | +| `num_keys` | 批量操作的 key 数量 | +| `size` | 批量操作的总数据大小(字节) | +| `elapsed_us` | 操作总耗时(微秒) | +| `rc` | 返回码:`0` 成功(但慢),其他为错误码 | + +### 13.4 与旧版 Python 层慢日志的关系 + +旧版 `store_py.cpp` 的 `get_slow` / `put_slow` 已移除。旧版 `get_batch_slow` / `put_batch_slow` 仍在 `store_py.cpp` 中保留(使用原生 `LOG()`,无 trace_id)。 + +当操作超过 3ms 时: +- 单 key `get`/`put`:仅 real_client.cpp 输出慢日志 +- 批量 `get_batch`/`put_batch`:可能同时出现 `store_py.cpp` 的慢日志(无 trace_id)和 `real_client.cpp` 的慢日志(有 trace_id) diff --git a/docs/yh/pipline.md b/docs/yh/pipline.md new file mode 100644 index 0000000000..f0d5a0d0f5 --- /dev/null +++ b/docs/yh/pipline.md @@ -0,0 +1,317 @@ +## 打点流程图 + +### `get` 流程 + +```mermaid +flowchart TB + Start["store_py::get(key)
Python入口,释放GIL,调用get_buffer,返回结果
🔑 store_py.cpp::get/Get"] --> GetBufferCall["store_->get_buffer(key)
🔑 store_py.cpp::get/GetBuffer"] + + GetBufferCall --> Internal["get_buffer_internal(key, allocator)
核心逻辑:查询→选副本→分配→读取
"] + + Internal --> QueryPart["部分1: client_->Query(key)
向Master查询对象副本元数据
🔑 real_client.cpp::get_buffer_internal/Query"] + QueryPart --> SelectPart["部分2: SelectBestReplica
从副本列表中选择最优副本
🔑 real_client.cpp::get_buffer_internal/SelectReplica"] + SelectPart --> AllocPart["部分3: allocator->allocate
分配本地缓冲区
🔑 real_client.cpp::get_buffer_internal/AllocBuffer"] + + AllocPart --> CheckDisk{is_local_disk_replica?} + + CheckDisk -->|Yes| SSDPart["部分4a: batch_get_into_offload_object_internal
通过RPC从远端SSD读取数据
🔑 real_client.cpp::get_buffer_internal/SSDRead"] + SSDPart --> SSDRpc["步骤1: batch_get_offload_object()
RPC到远端节点,远端从SSD读数据到buffer
🔑 real_client.cpp::batch_get_into_offload_object_internal/OffloadRpc"] + SSDRpc --> SSDTransfer["步骤2: BatchGetOffloadObject()
Transfer Engine零拷贝搬数据到本地
🔑 real_client.cpp::batch_get_into_offload_object_internal/TransferData"] + SSDTransfer --> SSDRelease["步骤3: release_offload_buffer()
通知远端释放buffer(fire-and-forget)
🔑 real_client.cpp::batch_get_into_offload_object_internal/ReleaseBuffer"] + SSDRelease --> Done["返回"] + + CheckDisk -->|No| ReadType{is_memory_replica?} + + ReadType -->|Yes| MemRead["部分4b-Memory: client_->Get(key, filtered_qr, slices)
内存副本RDMA读取
🔑 real_client.cpp::get_buffer_internal/MemRead"] + ReadType -->|No| DiskRead["部分4b-Disk: client_->Get(key, filtered_qr, slices)
磁盘副本文件I/O读取
🔑 real_client.cpp::get_buffer_internal/DiskRead"] + + MemRead --> ClientGetSub["Client::Get内部子步骤
"] + DiskRead --> ClientGetSub + + ClientGetSub --> FindReplica["子步骤1: FindFirstCompleteReplica
🔑 client_service.cpp::Get/FindReplica"] + FindReplica --> HotCache["子步骤2: RedirectToHotCache
🔑 client_service.cpp::Get/HotCache"] + HotCache --> TransferRead["子步骤3: TransferRead → TransferData
🔑 client_service.cpp::Get/TransferRead"] + TransferRead --> TransferDetail["TransferData内部
🔑 client_service.cpp::TransferData/TransferData
├ submit → client_service.cpp::TransferData/Submit
└ future.get() → client_service.cpp::TransferData/Wait"] + TransferDetail --> ReleaseCache["子步骤4: ReleaseHotKey
🔑 client_service.cpp::Get/ReleaseCache"] + ReleaseCache --> AsyncUpdate["子步骤5: ProcessSlicesAsync
🔑 client_service.cpp::Get/AsyncCache"] + AsyncUpdate --> Done + + style Start fill:#e8f5e9 + style GetBufferCall fill:#c8e6c9 + style Internal fill:#e3f2fd + style QueryPart fill:#fff3e0 + style SelectPart fill:#fff3e0 + style AllocPart fill:#fff3e0 + style SSDPart fill:#fce4ec + style SSDRpc fill:#fce4ec + style SSDTransfer fill:#fce4ec + style SSDRelease fill:#fce4ec + style MemRead fill:#bbdefb + style DiskRead fill:#ffccbc + style ClientGetSub fill:#e3f2fd + style FindReplica fill:#f3e5f5 + style HotCache fill:#f3e5f5 + style TransferRead fill:#f3e5f5 + style TransferDetail fill:#e0f2f1 + style ReleaseCache fill:#f3e5f5 + style AsyncUpdate fill:#f3e5f5 +``` + +### `get_batch` 流程 + +```mermaid +flowchart TB + Start["store_py::get_batch(keys)
Python入口,释放GIL,调用batch_get_buffer,返回结果
🔑 store_py.cpp::get_batch/GetBatch"] --> BatchGetBufferCall["store_->batch_get_buffer(keys)
🔑 store_py.cpp::get_batch/BatchGetBuffer"] + + BatchGetBufferCall --> Internal["batch_get_buffer_internal(keys, allocator)
核心逻辑:批量查询→选副本→分配→读取
"] + + Internal --> QueryPart["部分1: client_->BatchQuery(keys)
批量向Master查询副本元数据
🔑 real_client.cpp::batch_get_buffer_internal/BatchQuery"] + QueryPart --> LoopPart["部分2: 循环逐key处理
├ SelectBestReplica → real_client.cpp::batch_get_buffer_internal/SelectReplica
└ allocator->allocate → real_client.cpp::batch_get_buffer_internal/AllocBuffer"] + + LoopPart --> CheckDisk{有 LOCAL_DISK 副本?} + + CheckDisk -->|Yes| SSDPart["部分3a: batch_get_into_offload_object_internal
🔑 real_client.cpp::batch_get_buffer_internal/SSDRead"] + SSDPart --> SSDRpc["步骤1: batch_get_offload_object()
🔑 real_client.cpp::batch_get_into_offload_object_internal/OffloadRpc"] + SSDRpc --> SSDTransfer["步骤2: BatchGetOffloadObject()
🔑 real_client.cpp::batch_get_into_offload_object_internal/TransferData"] + SSDTransfer --> SSDRelease["步骤3: release_offload_buffer()
🔑 real_client.cpp::batch_get_into_offload_object_internal/ReleaseBuffer"] + + CheckDisk -->|No| MemDiskRead["部分3b: client_->BatchGet(keys, query_results, slices)
批量读取内存/磁盘副本
🔑 real_client.cpp::batch_get_buffer_internal/MemDiskRead"] + + MemDiskRead --> BatchGetSub["Client::BatchGet内部子步骤
"] + + BatchGetSub --> SubmitLoop["提交阶段 [循环]
├ FindFirstCompleteReplica → client_service.cpp::BatchGet/FindReplica
├ RedirectToHotCache → client_service.cpp::BatchGet/HotCache
└ submit → client_service.cpp::BatchGet/Submit"] + SubmitLoop --> WaitLoop["等待阶段 [循环]
├ future.get() → client_service.cpp::BatchGet/Wait
├ ReleaseHotKey → client_service.cpp::BatchGet/ReleaseCache
└ ProcessSlicesAsync → client_service.cpp::BatchGet/AsyncCache"] + + SSDRelease --> Done["返回"] + WaitLoop --> Done + + style Start fill:#e8f5e9 + style BatchGetBufferCall fill:#c8e6c9 + style Internal fill:#e3f2fd + style QueryPart fill:#fff3e0 + style LoopPart fill:#fff3e0 + style SSDPart fill:#fce4ec + style SSDRpc fill:#fce4ec + style SSDTransfer fill:#fce4ec + style SSDRelease fill:#fce4ec + style MemDiskRead fill:#bbdefb + style BatchGetSub fill:#e3f2fd + style SubmitLoop fill:#f3e5f5 + style WaitLoop fill:#f3e5f5 +``` + +### `get_into` 流程 + +```mermaid +flowchart TB + Start["store_->get_into(key, buffer, size)
用户提供目标buffer,返回读取字节数或错误码
🔑 real_client.cpp::get_into/GetIntoInternal"] --> RangeInternal["get_into_range_internal(key, buffer, 0, 0, size, true)
完整对象读取,size表示目标buffer容量"] + + RangeInternal --> Metadata["resolve_ranged_read_metadata(key)
查询元数据并选择最优副本"] + Metadata --> QueryPart["部分1: client_->Query(key)
向Master查询对象副本元数据
🔑 real_client.cpp::get_into_internal/Query"] + QueryPart --> SelectPart["部分2: SelectBestReplica
优先级:本地MEMORY → 远端MEMORY → LOCAL_DISK → DISK
🔑 real_client.cpp::get_into_internal/SelectReplica"] + SelectPart --> ExecuteRead["execute_ranged_read(key, buffer, offsets, size, metadata)
根据副本类型和范围执行读取"] + + ExecuteRead --> FullRead{完整对象读取?} + FullRead -->|Yes| ReplicaType{副本类型} + FullRead -->|No| PartialRead["范围读取
MEMORY可按src_offset直接读
DISK/LOCAL_DISK先读临时buffer再scatter"] + + ReplicaType -->|LOCAL_DISK| SSDRead["部分3a: batch_get_into_offload_object_internal
远端SSD读取后写入用户buffer
🔑 real_client.cpp::get_into_internal/SSDRead"] + SSDRead --> SSDRpc["步骤1: batch_get_offload_object()
RPC到远端节点从SSD读入offload buffer
🔑 real_client.cpp::batch_get_into_offload_object_internal/OffloadRpc"] + SSDRpc --> SSDTransfer["步骤2: BatchGetOffloadObject()
Transfer Engine将数据搬到用户buffer
🔑 real_client.cpp::batch_get_into_offload_object_internal/TransferData"] + SSDTransfer --> SSDRelease["步骤3: release_offload_buffer()
通知远端释放buffer(fire-and-forget)
🔑 real_client.cpp::batch_get_into_offload_object_internal/ReleaseBuffer"] + + ReplicaType -->|DISK| DiskAlloc["部分3b: client_buffer_allocator_->allocate
分配CPU临时buffer,避免文件I/O直接写GPU buffer
🔑 real_client.cpp::get_into_internal/AllocBuffer"] + DiskAlloc --> DiskRead["client_->Get(key, filtered_qr, tmp_slices)
本地磁盘文件I/O读入临时buffer
🔑 real_client.cpp::get_into_internal/DiskRead"] + DiskRead --> Scatter["scatter_host_to_maybe_device
从CPU临时buffer拷贝/搬运到用户buffer"] + + ReplicaType -->|MEMORY| MemRead["部分3c: client_->Get(key, filtered_qr, slices)
内存副本直接读入用户buffer
🔑 real_client.cpp::get_into_internal/MemRead"] + + PartialRead --> PartialType{副本类型} + PartialType -->|LOCAL_DISK| PartialSSD["读取[0, src_offset+size)到CPU临时buffer
再scatter目标范围
🔑 real_client.cpp::get_into_internal/SSDRead"] + PartialType -->|DISK| PartialDisk["读取完整对象到CPU临时buffer
再scatter目标范围
🔑 real_client.cpp::get_into_internal/DiskRead"] + PartialType -->|MEMORY| PartialMem["client_->Get(key, query_result, slices, src_offset)
从源offset直接读到用户buffer
🔑 real_client.cpp::get_into_internal/MemRead"] + + MemRead --> ClientGetSub["Client::Get内部子步骤
"] + DiskRead --> ClientGetSub + PartialMem --> ClientGetSub + PartialDisk --> Done["返回"] + PartialSSD --> Done + Scatter --> Done + SSDRelease --> Done + + ClientGetSub --> FindReplica["子步骤1: FindFirstCompleteReplica
🔑 client_service.cpp::Get/FindReplica"] + FindReplica --> HotCache["子步骤2: RedirectToHotCache
🔑 client_service.cpp::Get/HotCache"] + HotCache --> TransferRead["子步骤3: TransferRead → TransferData
🔑 client_service.cpp::Get/TransferRead"] + TransferRead --> TransferDetail["TransferData内部
🔑 client_service.cpp::TransferData/TransferData
├ submit → client_service.cpp::TransferData/Submit
└ future.get() → client_service.cpp::TransferData/Wait"] + TransferDetail --> ReleaseCache["子步骤4: ReleaseHotKey
🔑 client_service.cpp::Get/ReleaseCache"] + ReleaseCache --> AsyncUpdate["子步骤5: ProcessSlicesAsync
🔑 client_service.cpp::Get/AsyncCache"] + AsyncUpdate --> Done + + style Start fill:#e8f5e9 + style RangeInternal fill:#c8e6c9 + style Metadata fill:#e3f2fd + style QueryPart fill:#fff3e0 + style SelectPart fill:#fff3e0 + style ExecuteRead fill:#e3f2fd + style SSDRead fill:#fce4ec + style SSDRpc fill:#fce4ec + style SSDTransfer fill:#fce4ec + style SSDRelease fill:#fce4ec + style DiskAlloc fill:#fff3e0 + style DiskRead fill:#ffccbc + style Scatter fill:#ffccbc + style MemRead fill:#bbdefb + style PartialRead fill:#e3f2fd + style PartialSSD fill:#fce4ec + style PartialDisk fill:#ffccbc + style PartialMem fill:#bbdefb + style ClientGetSub fill:#e3f2fd + style FindReplica fill:#f3e5f5 + style HotCache fill:#f3e5f5 + style TransferRead fill:#f3e5f5 + style TransferDetail fill:#e0f2f1 + style ReleaseCache fill:#f3e5f5 + style AsyncUpdate fill:#f3e5f5 +``` + +### `batch_get_into` 流程 + +```mermaid +flowchart TB + Start["store_->batch_get_into(keys, buffers, sizes)
每个key写入对应用户buffer,逐项返回字节数或错误码
🔑 real_client.cpp::batch_get_into/BatchGetIntoInternal"] --> Internal["batch_get_into_internal(keys, buffers, sizes)
核心逻辑:批量查询→逐key分类→分路径批量读取"] + + Internal --> QueryPart["部分1: client_->BatchQuery(keys)
批量向Master查询副本元数据
🔑 real_client.cpp::batch_get_into_internal/BatchQuery"] + QueryPart --> LoopPart["部分2: 循环逐key处理
├ SelectBestReplica → real_client.cpp::batch_get_into_internal/SelectReplica
├ 校验sizes[i] >= total_size
└ 按副本类型分类为MEMORY/DISK/LOCAL_DISK"] + + LoopPart --> MemOps{有 MEMORY 副本?} + MemOps -->|Yes| MemRead["部分3a: client_->BatchGet(memory_keys, query_results, slices)
批量直接写入用户buffers
🔑 real_client.cpp::batch_get_into_internal/MemRead"] + MemOps -->|No| DiskOps + + MemRead --> BatchGetSub["Client::BatchGet内部子步骤
"] + BatchGetSub --> SubmitLoop["提交阶段 [循环]
├ FindFirstCompleteReplica → client_service.cpp::BatchGet/FindReplica
├ RedirectToHotCache → client_service.cpp::BatchGet/HotCache
└ submit → client_service.cpp::BatchGet/Submit"] + SubmitLoop --> WaitLoop["等待阶段 [循环]
├ future.get() → client_service.cpp::BatchGet/Wait
├ ReleaseHotKey → client_service.cpp::BatchGet/ReleaseCache
└ ProcessSlicesAsync → client_service.cpp::BatchGet/AsyncCache"] + + WaitLoop --> DiskOps{有 DISK 副本?} + DiskOps -->|Yes| DiskAlloc["部分3b-1: 为每个DISK key分配CPU临时buffer
文件I/O先写临时buffer
🔑 real_client.cpp::batch_get_into_internal/AllocBuffer"] + DiskAlloc --> DiskRead["部分3b-2: client_->BatchGet(disk_keys, qrs, temp_slices)
批量本地磁盘文件I/O
🔑 real_client.cpp::batch_get_into_internal/DiskRead"] + DiskRead --> Scatter["部分3b-3: scatter_host_to_maybe_device [循环]
将临时buffer搬运到对应用户buffer"] + DiskOps -->|No| SSDOps + + Scatter --> SSDOps{有 LOCAL_DISK 副本?} + SSDOps -->|Yes| GroupEndpoint["部分3c-1: 按transport_endpoint分组
构造offload_objects[endpoint][key] = slices"] + GroupEndpoint --> SSDRead["部分3c-2: batch_get_into_offload_object_internal(endpoint, objects)
远端SSD读取后写入用户buffers
🔑 real_client.cpp::batch_get_into_internal/SSDRead"] + SSDRead --> SSDRpc["步骤1: batch_get_offload_object()
🔑 real_client.cpp::batch_get_into_offload_object_internal/OffloadRpc"] + SSDRpc --> SSDTransfer["步骤2: BatchGetOffloadObject()
🔑 real_client.cpp::batch_get_into_offload_object_internal/TransferData"] + SSDTransfer --> SSDRelease["步骤3: release_offload_buffer()
🔑 real_client.cpp::batch_get_into_offload_object_internal/ReleaseBuffer"] + + SSDOps -->|No| Done["返回逐项结果"] + SSDRelease --> Done + + style Start fill:#e8f5e9 + style Internal fill:#e3f2fd + style QueryPart fill:#fff3e0 + style LoopPart fill:#fff3e0 + style MemRead fill:#bbdefb + style BatchGetSub fill:#e3f2fd + style SubmitLoop fill:#f3e5f5 + style WaitLoop fill:#f3e5f5 + style DiskAlloc fill:#fff3e0 + style DiskRead fill:#ffccbc + style Scatter fill:#ffccbc + style GroupEndpoint fill:#fff3e0 + style SSDRead fill:#fce4ec + style SSDRpc fill:#fce4ec + style SSDTransfer fill:#fce4ec + style SSDRelease fill:#fce4ec +``` + +### `put` 流程 + +```mermaid +flowchart TB + Start["store_py::put(key, value)
Python入口,释放GIL,调用store_->put()
🔑 store_py.cpp::put/Put"] --> PutCall["store_->put(key, value, config)
🔑 store_py.cpp::put/PutBuffer"] + + PutCall --> Internal["put_internal(key, value, config, allocator)
核心逻辑:分配→拷贝→切分→写入
"] + + Internal --> AllocPart["部分1: allocator->allocate
分配本地缓冲区(RDMA注册内存)
🔑 real_client.cpp::put_internal/AllocBuffer"] + AllocPart --> CopyPart["部分2: memcpy
将用户数据拷贝到分配的缓冲区
🔑 real_client.cpp::put_internal/MemCopy"] + CopyPart --> SplitPart["部分3: split_into_slices
按kMaxSliceSize切分为多个Slice
🔑 real_client.cpp::put_internal/SplitSlices"] + SplitPart --> ClientPutPart["部分4: client_->Put(key, slices, config)
🔑 client_service.cpp::Put/TransferPut"] + + ClientPutPart --> PutStart["子步骤1: master_client_.PutStart(key)
向Master申请分配replica handle
若返回OBJECT_ALREADY_EXISTS则直接返回成功
🔑 client_service.cpp::Put/PutStart"] + + PutStart --> CheckDisk{storage_backend_存在
且有磁盘副本?} + + CheckDisk -->|Yes| DiskWrite["子步骤2a: PutToLocalFile(key, slices, disk_descriptor)
将数据写入本地磁盘(仅处理一个磁盘副本)
🔑 client_service.cpp::Put/DiskWrite"] + + CheckDisk -->|No| MemReplicaLoop["子步骤2b: 遍历所有内存副本
对每个内存副本调用TransferWrite"] + + DiskWrite --> MemReplicaLoop + + MemReplicaLoop --> TransferWrite["TransferWrite → TransferData(replica, slices, WRITE)
🔑 client_service.cpp::Put/TransferWrite"] + + TransferWrite --> TransferDetail["TransferData内部
🔑 client_service.cpp::TransferData/TransferData
├ transfer_submitter_->submit() → client_service.cpp::TransferData/Submit
└ future->get() 阻塞等待传输完成 → client_service.cpp::TransferData/Wait"] + + TransferDetail --> CheckTransfer{传输是否成功?} + + CheckTransfer -->|失败| PutRevoke["子步骤3a: master_client_.PutRevoke(key, MEMORY)
撤销本次Put操作,释放已分配的replica
🔑 client_service.cpp::Put/PutRevoke"] + + CheckTransfer -->|成功| PutEnd["子步骤3b: master_client_.PutEnd(key, MEMORY)
确认Put完成,replica正式生效
🔑 client_service.cpp::Put/PutEnd"] + + PutRevoke --> Done["返回"] + PutEnd --> Done + + style Start fill:#e8f5e9 + style PutCall fill:#c8e6c9 + style Internal fill:#e3f2fd + style AllocPart fill:#fff3e0 + style CopyPart fill:#fff3e0 + style SplitPart fill:#fff3e0 + style ClientPutPart fill:#bbdefb + style PutStart fill:#f3e5f5 + style DiskWrite fill:#fce4ec + style MemReplicaLoop fill:#f3e5f5 + style TransferWrite fill:#f3e5f5 + style TransferDetail fill:#e0f2f1 + style PutRevoke fill:#ffcdd2 + style PutEnd fill:#c8e6c9 +``` + +### `put_batch` 流程 + +```mermaid +flowchart TB + Start["store_py::put_batch(keys, values)
Python入口,释放GIL,调用store_->put_batch()
🔑 store_py.cpp::put_batch/PutBatch"] --> PutBatchCall["store_->put_batch(keys, values, config)
🔑 store_py.cpp::put_batch/BatchPutBuffer"] + + PutBatchCall --> Internal["put_batch_internal(keys, values, config, allocator)
核心逻辑:逐key分配→拷贝→切分→批量写入
"] + + Internal --> LoopPart["部分1: 循环逐key处理
对每个key执行以下3步:
├ allocator->allocate → 🔑 real_client.cpp::put_batch_internal/AllocBuffer
│ (分配本地缓冲区,RDMA注册内存)
├ memcpy → 🔑 real_client.cpp::put_batch_internal/MemCopy
│ (将用户数据拷贝到分配的缓冲区)
└ split_into_slices → 🔑 real_client.cpp::put_batch_internal/SplitSlices
(按kMaxSliceSize切分为多个Slice)"] + + LoopPart --> BatchPutPart["部分2: client_->BatchPut(keys, batched_slices, config)
🔑 client_service.cpp::BatchPut/TransferBatchPut"] + + BatchPutPart --> CreateOps["子步骤1: CreatePutOperations(keys, batched_slices)
为每个key创建PutOperation对象,包含key和对应的slices
🔑 client_service.cpp::BatchPut/CreateOps"] + + CreateOps --> StartBatch["子步骤2: StartBatchPut(ops, config)
调用master_client_.BatchPutStart(keys, slice_lengths, config)
Master为每个key分配replica handle,返回到op.replicas中
分配失败的op标记错误,后续步骤跳过
🔑 client_service.cpp::StartBatchPut/PutStart"] + + StartBatch --> SubmitPhase["子步骤3: SubmitTransfers(ops)
对每个未失败的op,逐个提交传输任务:
├ 若storage_backend_存在且有磁盘副本:
│ 调用PutToLocalFile写入本地磁盘 → 🔑 client_service.cpp::SubmitTransfers/DiskWrite
├ 遍历op中所有内存副本:
│ 调用transfer_submitter_->submit(replica, slices, WRITE)
│ 返回TransferFuture存入op.pending_transfers → 🔑 client_service.cpp::SubmitTransfers/Submit
└ 若任一replica提交失败,标记op错误,清空pending_transfers"] + + SubmitPhase --> WaitPhase["子步骤4: WaitForTransfers(ops)
对每个有pending_transfers的op:
├ 遍历所有TransferFuture,调用future.get()阻塞等待传输完成 → 🔑 client_service.cpp::WaitForTransfers/Wait
└ 若任一传输失败,记录首个错误,标记op失败"] + + WaitPhase --> Finalize["子步骤5: FinalizeBatchPut(ops)
根据每个op的结果分类处理:
├ 传输成功的op: 调用master_client_.BatchPutEnd(keys) → 🔑 client_service.cpp::FinalizeBatchPut/PutEnd
│ 确认Put完成,replica正式生效,标记op成功
├ 传输失败但已分配replica的op: 调用master_client_.BatchPutRevoke(keys) → 🔑 client_service.cpp::FinalizeBatchPut/PutRevoke
│ 撤销Put操作,释放已分配的replica
└ 未分配replica的op(早期失败): 无需清理"] + + Finalize --> CollectResults["子步骤6: CollectResults(ops)
从每个PutOperation中收集结果
OBJECT_ALREADY_EXISTS视为成功
🔑 client_service.cpp::BatchPut/CollectResults"] + + CollectResults --> Done["返回"] + + style Start fill:#e8f5e9 + style PutBatchCall fill:#c8e6c9 + style Internal fill:#e3f2fd + style LoopPart fill:#fff3e0 + style BatchPutPart fill:#bbdefb + style CreateOps fill:#f3e5f5 + style StartBatch fill:#f3e5f5 + style SubmitPhase fill:#f3e5f5 + style WaitPhase fill:#f3e5f5 + style Finalize fill:#f3e5f5 + style CollectResults fill:#f3e5f5 +``` diff --git a/extern/yalantinglibs b/extern/yalantinglibs index 7801bc9ad9..b12fbfd86d 160000 --- a/extern/yalantinglibs +++ b/extern/yalantinglibs @@ -1 +1 @@ -Subproject commit 7801bc9ad9021781f15217552214e325a1cf7373 +Subproject commit b12fbfd86db241b9b79a696c373ffd3306e65369 diff --git a/mooncake-common/FindSpDiag.cmake b/mooncake-common/FindSpDiag.cmake new file mode 100644 index 0000000000..40094f3820 --- /dev/null +++ b/mooncake-common/FindSpDiag.cmake @@ -0,0 +1,184 @@ +# Two-layer SpDiag integration for Mooncake. +# +# Layer 0 (default) uses the SpDiag public headers with SPDIAG_DISABLE. Layer 1 +# consumes a shared SpDiag SDK and CLI installed from system RPMs. + +include_guard(GLOBAL) + +option(MOONCAKE_ENABLE_SPDIAG + "Use the system-installed SpDiag shared library and CLI" OFF) + +function(_mooncake_write_spdiag_manifest layer library cli config) + file( + WRITE "${CMAKE_BINARY_DIR}/mooncake_spdiag.env" + "MOONCAKE_SPDIAG_LAYER=${layer}\n" + "MOONCAKE_SPDIAG_SYSTEM_LIBRARY=${library}\n" + "MOONCAKE_SPDIAG_SYSTEM_CLI=${cli}\n" + "MOONCAKE_SPDIAG_SYSTEM_CONFIG=${config}\n") + set(MOONCAKE_SPDIAG_ACTIVE_LAYER + "${layer}" + CACHE INTERNAL "Active Mooncake SpDiag layer" FORCE) +endfunction() + +if(NOT MOONCAKE_ENABLE_SPDIAG) + set(MOONCAKE_SPDIAG_GIT_REPOSITORY + "https://gitcode.com/openeuler/spdiag.git" + CACHE STRING "SpDiag repository used by Layer 0") + set(MOONCAKE_SPDIAG_GIT_TAG + "46b7b84371a6935eaca93763fbb7cb3b4bccfbe2" + CACHE STRING "SpDiag revision used by Layer 0") + set(MOONCAKE_SPDIAG_SOURCE_DIR + "" + CACHE PATH "Local SpDiag source directory for offline builds") + + include(FetchContent) + if(MOONCAKE_SPDIAG_SOURCE_DIR) + set(spdiag_SOURCE_DIR "${MOONCAKE_SPDIAG_SOURCE_DIR}") + else() + FetchContent_Populate( + spdiag + GIT_REPOSITORY "${MOONCAKE_SPDIAG_GIT_REPOSITORY}" + GIT_TAG "${MOONCAKE_SPDIAG_GIT_TAG}") + endif() + + add_library(mooncake_spdiag_mock INTERFACE) + target_include_directories(mooncake_spdiag_mock + INTERFACE "${spdiag_SOURCE_DIR}/include") + target_compile_definitions(mooncake_spdiag_mock INTERFACE SPDIAG_DISABLE) + add_library(SpDiag::spdiag_lib ALIAS mooncake_spdiag_mock) + + set(MOONCAKE_SPDIAG_LIBRARY_DIR + "" + CACHE INTERNAL "System SpDiag library directory" FORCE) + _mooncake_write_spdiag_manifest("mock" "" "" "") + message(STATUS "SpDiag: Layer 0 SPDIAG_DISABLE headers") + return() +endif() + +find_package(SpDiag CONFIG QUIET) +if(NOT SpDiag_FOUND OR NOT TARGET SpDiag::spdiag_lib) + message( + FATAL_ERROR + "MOONCAKE_ENABLE_SPDIAG=ON requires the SpDiag runtime and development " + "RPMs. Install the RPMs that provide libspdiag.so, the spdiag CLI, and " + "SpDiagConfig.cmake before configuring Mooncake.") +endif() +set_property(TARGET SpDiag::spdiag_lib PROPERTY IMPORTED_GLOBAL TRUE) + +get_target_property(_MOONCAKE_SPDIAG_TARGET_TYPE SpDiag::spdiag_lib TYPE) +if(NOT _MOONCAKE_SPDIAG_TARGET_TYPE STREQUAL "SHARED_LIBRARY") + message( + FATAL_ERROR "MOONCAKE_ENABLE_SPDIAG=ON requires a shared libspdiag.so. " + "Reinstall SpDiag with SPDIAG_BUILD_SHARED=ON.") +endif() + +function(_mooncake_get_imported_location target output_variable) + get_target_property(_configs "${target}" IMPORTED_CONFIGURATIONS) + foreach(_config IN LISTS _configs) + string(TOUPPER "${_config}" _config_upper) + get_target_property(_location "${target}" + "IMPORTED_LOCATION_${_config_upper}") + if(_location) + set("${output_variable}" + "${_location}" + PARENT_SCOPE) + return() + endif() + endforeach() + + get_target_property(_location "${target}" IMPORTED_LOCATION) + set("${output_variable}" + "${_location}" + PARENT_SCOPE) +endfunction() + +_mooncake_get_imported_location(SpDiag::spdiag_lib + MOONCAKE_SPDIAG_SYSTEM_LIBRARY) +if(NOT MOONCAKE_SPDIAG_SYSTEM_LIBRARY) + message(FATAL_ERROR "The installed SpDiag package does not expose its " + "shared-library location.") +endif() +get_filename_component(MOONCAKE_SPDIAG_SYSTEM_LIBRARY + "${MOONCAKE_SPDIAG_SYSTEM_LIBRARY}" REALPATH) +get_filename_component(MOONCAKE_SPDIAG_LIBRARY_DIR + "${MOONCAKE_SPDIAG_SYSTEM_LIBRARY}" DIRECTORY) + +get_target_property(_MOONCAKE_SPDIAG_DEFINITIONS SpDiag::spdiag_lib + INTERFACE_COMPILE_DEFINITIONS) +foreach(_definition SPDIAG_ENABLE_PERCENTILE SPDIAG_ENABLE_PERFLOG) + if(NOT _definition IN_LIST _MOONCAKE_SPDIAG_DEFINITIONS) + message( + FATAL_ERROR + "The installed SpDiag package does not provide ${_definition}. " + "Rebuild and reinstall the SpDiag RPM with percentile and PerfLog " + "enabled.") + endif() +endforeach() + +get_filename_component(_MOONCAKE_SPDIAG_SYSTEM_PREFIX + "${MOONCAKE_SPDIAG_LIBRARY_DIR}" DIRECTORY) +unset(MOONCAKE_SPDIAG_SYSTEM_CLI) +unset(MOONCAKE_SPDIAG_SYSTEM_CLI CACHE) +find_program( + MOONCAKE_SPDIAG_SYSTEM_CLI + NAMES spdiag + PATHS "${_MOONCAKE_SPDIAG_SYSTEM_PREFIX}/bin" + NO_DEFAULT_PATH) +if(NOT MOONCAKE_SPDIAG_SYSTEM_CLI) + message( + FATAL_ERROR "The SpDiag SDK was found, but the spdiag CLI is missing. " + "Install the matching SpDiag runtime RPM.") +endif() + +find_program(_MOONCAKE_RPM_EXECUTABLE NAMES rpm) +if(NOT _MOONCAKE_RPM_EXECUTABLE) + message(FATAL_ERROR "The rpm command is required to compare the SpDiag " + "library and CLI versions.") +endif() + +function(_mooncake_get_rpm_identity file_path output_variable) + execute_process( + COMMAND "${_MOONCAKE_RPM_EXECUTABLE}" -qf --qf + "%{VERSION}-%{RELEASE}.%{ARCH}" "${file_path}" + RESULT_VARIABLE _result + OUTPUT_VARIABLE _identity + ERROR_VARIABLE _error + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE) + if(NOT _result EQUAL 0) + message(FATAL_ERROR "${file_path} is not provided by an installed RPM: " + "${_error}") + endif() + set("${output_variable}" + "${_identity}" + PARENT_SCOPE) +endfunction() + +_mooncake_get_rpm_identity("${MOONCAKE_SPDIAG_SYSTEM_LIBRARY}" + _MOONCAKE_SPDIAG_LIBRARY_IDENTITY) +_mooncake_get_rpm_identity("${MOONCAKE_SPDIAG_SYSTEM_CLI}" + _MOONCAKE_SPDIAG_CLI_IDENTITY) +if(NOT "${_MOONCAKE_SPDIAG_LIBRARY_IDENTITY}" STREQUAL + "${_MOONCAKE_SPDIAG_CLI_IDENTITY}") + message( + FATAL_ERROR + "libspdiag.so and the spdiag CLI come from different RPM versions: " + "${_MOONCAKE_SPDIAG_LIBRARY_IDENTITY} and " + "${_MOONCAKE_SPDIAG_CLI_IDENTITY}. Install one matching SpDiag RPM set.") +endif() + +if(EXISTS "/etc/spdiag/spdiag.conf") + set(MOONCAKE_SPDIAG_SYSTEM_CONFIG "/etc/spdiag/spdiag.conf") +else() + set(MOONCAKE_SPDIAG_SYSTEM_CONFIG "") +endif() + +set(MOONCAKE_SPDIAG_LIBRARY_DIR + "${MOONCAKE_SPDIAG_LIBRARY_DIR}" + CACHE INTERNAL "System SpDiag library directory" FORCE) +_mooncake_write_spdiag_manifest( + "system" "${MOONCAKE_SPDIAG_SYSTEM_LIBRARY}" "${MOONCAKE_SPDIAG_SYSTEM_CLI}" + "${MOONCAKE_SPDIAG_SYSTEM_CONFIG}") +message( + STATUS "SpDiag: Layer 1 system RPM ${_MOONCAKE_SPDIAG_LIBRARY_IDENTITY}; " + "library=${MOONCAKE_SPDIAG_SYSTEM_LIBRARY}; " + "CLI=${MOONCAKE_SPDIAG_SYSTEM_CLI}") diff --git a/mooncake-common/FindUrma.cmake b/mooncake-common/FindUrma.cmake index 0af8d1a7ca..6a1d08bfda 100644 --- a/mooncake-common/FindUrma.cmake +++ b/mooncake-common/FindUrma.cmake @@ -14,7 +14,7 @@ message(STATUS "URMA source dir: ${urma_SOURCE_DIR}") message(STATUS "URMA binary dir: ${urma_BINARY_DIR}") # 假设 UMDK 头文件在其 include 目录下 -set(urma_INCLUDE_DIR ${urma_SOURCE_DIR}/src/urma/lib/urma/core/include) +set(urma_INCLUDE_DIR ${urma_SOURCE_DIR}/src/urma/lib/urma/core/include ${urma_SOURCE_DIR}/src/urma/lib/urma/bond/include) # 添加到需要的目标 message(STATUS "urma_INCLUDE_DIR: ${urma_INCLUDE_DIR}") \ No newline at end of file diff --git a/mooncake-common/etcd/etcd_wrapper.go b/mooncake-common/etcd/etcd_wrapper.go index 4ac718072c..7cedada048 100644 --- a/mooncake-common/etcd/etcd_wrapper.go +++ b/mooncake-common/etcd/etcd_wrapper.go @@ -714,6 +714,26 @@ func EtcdStorePutWrapper(key *C.char, keySize C.int, value *C.char, valueSize C. return 0 } +//export EtcdStorePutWithLeaseWrapper +func EtcdStorePutWithLeaseWrapper(key *C.char, keySize C.int, value *C.char, valueSize C.int, + leaseId int64, errMsg **C.char) int { + cli := getStoreClient() + if cli == nil { + *errMsg = C.CString("etcd client not initialized") + return -1 + } + k := C.GoStringN(key, keySize) + v := C.GoStringN(value, valueSize) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, err := cli.Put(ctx, k, v, clientv3.WithLease(clientv3.LeaseID(leaseId))) + if err != nil { + *errMsg = C.CString(err.Error()) + return -1 + } + return 0 +} + // Create key if absent (CAS on CreateRevision==0). // Return: // - 0 on success diff --git a/mooncake-common/include/default_config.h b/mooncake-common/include/default_config.h index f072baf212..1c2149e239 100644 --- a/mooncake-common/include/default_config.h +++ b/mooncake-common/include/default_config.h @@ -7,8 +7,16 @@ #endif #include +#include +#include +#include #include +#include +#include +#include +#include #include +#include #include #include @@ -148,36 +156,63 @@ class DefaultConfig { std::unordered_map data_; }; -// usage: export MC_YLT_LOG_LEVEL=info or export MC_YLT_LOG_LEVEL=debug etc. +// Configure yalantinglibs logging with MC_YLT_LOG_LEVEL, MC_YLT_LOG_PATH, +// MC_YLT_LOG_MAX_FILE_SIZE (bytes), and MC_YLT_LOG_MAX_FILES. inline void init_ylt_log_level() { - const char* env_level = std::getenv("MC_YLT_LOG_LEVEL"); - if (!env_level || !*env_level) { - // default is WARN - easylog::set_min_severity(easylog::Severity::WARN); - return; - } - std::string level_str(env_level); - std::transform(level_str.begin(), level_str.end(), level_str.begin(), - [](unsigned char c) { return std::tolower(c); }); - easylog::Severity severity; - if (level_str == "trace") { - severity = easylog::Severity::TRACE; - } else if (level_str == "debug") { - severity = easylog::Severity::DEBUG; - } else if (level_str == "info") { - severity = easylog::Severity::INFO; - } else if (level_str == "warn" || level_str == "warning") { - severity = easylog::Severity::WARN; - } else if (level_str == "error") { - severity = easylog::Severity::ERROR; - } else if (level_str == "critical") { - severity = easylog::Severity::CRITICAL; - } else { - // rollback to WARN - severity = easylog::Severity::WARN; - } + static std::once_flag once; + std::call_once(once, [] { + easylog::Severity severity = easylog::Severity::WARN; + const char* env_level = std::getenv("MC_YLT_LOG_LEVEL"); + if (env_level && *env_level) { + std::string level_str(env_level); + std::transform(level_str.begin(), level_str.end(), + level_str.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (level_str == "trace") { + severity = easylog::Severity::TRACE; + } else if (level_str == "debug") { + severity = easylog::Severity::DEBUG; + } else if (level_str == "info") { + severity = easylog::Severity::INFO; + } else if (level_str == "warn" || level_str == "warning") { + severity = easylog::Severity::WARN; + } else if (level_str == "error") { + severity = easylog::Severity::ERROR; + } else if (level_str == "critical") { + severity = easylog::Severity::CRITICAL; + } + } + + std::string log_path = "logs/rpc.log"; + const char* env_log_path = std::getenv("MC_YLT_LOG_PATH"); + if (env_log_path && *env_log_path) { + log_path = env_log_path; + } - easylog::set_min_severity(severity); + auto get_size_env = [](const char* name, size_t default_value) { + const char* value = std::getenv(name); + if (!value || !*value) return default_value; + + uint64_t parsed = 0; + const char* end = value + std::strlen(value); + const auto result = std::from_chars(value, end, parsed); + if (result.ec != std::errc() || result.ptr != end || + parsed > std::numeric_limits::max()) { + return default_value; + } + return static_cast(parsed); + }; + + constexpr size_t kDefaultMaxFileSize = 1000ULL * 1024 * 1024; + constexpr size_t kDefaultMaxFiles = 3; + const size_t max_file_size = get_size_env( + "MC_YLT_LOG_MAX_FILE_SIZE", kDefaultMaxFileSize); + const size_t max_files = + get_size_env("MC_YLT_LOG_MAX_FILES", kDefaultMaxFiles); + + easylog::init_log(severity, log_path, true, false, max_file_size, + max_files, false); + }); } } // namespace mooncake diff --git a/mooncake-common/include/environ.h b/mooncake-common/include/environ.h index 3ff16e4723..197551161c 100644 --- a/mooncake-common/include/environ.h +++ b/mooncake-common/include/environ.h @@ -61,6 +61,13 @@ class Environ { bool GetPathRoundrobin() const { return path_roundrobin_; } bool GetWithNvidiaPeermem() const { return with_nvidia_peermem_; } int GetEfaCqThreads() const { return efa_cq_threads_; } + size_t GetOffloadRpcThreadNum(size_t default_value = 8) const; + uint32_t GetYltRpcPoolMaxConnection(uint32_t default_value = 100) const; + size_t GetYltRpcPoolIdleTimeoutMs(size_t default_value) const; + size_t GetYltRpcPoolShortIdleTimeoutMs(size_t default_value) const; + bool GetYltRpcPoolWarmupEnabled(bool default_value = true) const; + size_t GetYltRpcPoolWarmupConnections(size_t default_value) const; + bool GetStoreWarmupEnabled(bool default_value = false) const; bool GetStoreChecksumEnabled() const { return store_checksum_enabled_; } // AWS / S3 client configuration diff --git a/mooncake-common/include/mooncake_logging.h b/mooncake-common/include/mooncake_logging.h new file mode 100644 index 0000000000..df7ef39564 --- /dev/null +++ b/mooncake-common/include/mooncake_logging.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mooncake::logging { + +uint64_t NewTraceId(); +uint64_t CurrentTraceId(); +bool ShouldLog(google::LogSeverity severity); +bool ShouldVLog(int level); +void ApplyMooncakeLogEnableToGlog(); + +// High-frequency log sampling. Parses MC_HIFREQ_LOG_SAMPLE_RATE once (default +// 0.1, clamped to [0,1]) and caches it process-wide. ShouldSampleHiFreqLog() +// without an id rolls a thread-local RNG. The trace-id overload hashes the id +// so all layers handling the same request make the same sampling decision. +double HiFreqLogSampleRate(); +bool ShouldSampleHiFreqLog(); +// Deterministic variant used to correlate high-frequency logs across +// processes and worker threads. A zero trace id falls back to random sampling. +bool ShouldSampleHiFreqLog(uint64_t trace_id); + +class ScopedTraceId { + public: + explicit ScopedTraceId(uint64_t trace_id); + ~ScopedTraceId(); + + ScopedTraceId(const ScopedTraceId&) = delete; + ScopedTraceId& operator=(const ScopedTraceId&) = delete; + + private: + uint64_t previous_trace_id_; +}; + +class AsyncLogMessage { + public: + AsyncLogMessage(const char* file, int line, google::LogSeverity severity, + bool enabled); + ~AsyncLogMessage(); + + AsyncLogMessage(const AsyncLogMessage&) = delete; + AsyncLogMessage& operator=(const AsyncLogMessage&) = delete; + + std::ostream& stream(); + + private: + const char* file_; + int line_; + google::LogSeverity severity_; + bool enabled_; + uint64_t trace_id_; + std::ostringstream stream_; +}; + +void FlushAsyncLogs(); + +} // namespace mooncake::logging + +#define MC_LOG(severity) \ + mooncake::logging::AsyncLogMessage( \ + __FILE__, __LINE__, google::severity, \ + mooncake::logging::ShouldLog(google::severity)) \ + .stream() + +#define MC_VLOG(level) \ + mooncake::logging::AsyncLogMessage(__FILE__, __LINE__, google::INFO, \ + mooncake::logging::ShouldVLog(level)) \ + .stream() diff --git a/mooncake-common/src/CMakeLists.txt b/mooncake-common/src/CMakeLists.txt index 2730485cf1..fcfb77ab2a 100644 --- a/mooncake-common/src/CMakeLists.txt +++ b/mooncake-common/src/CMakeLists.txt @@ -1,7 +1,8 @@ find_package(yaml-cpp REQUIRED) set(MOONCAKE_COMMON_SOURCES crc_checksum.cpp default_config.cpp environ.cpp - rpc_client_io_context.cpp) + rpc_client_io_context.cpp mooncake_logging.cpp +) add_library(asio_shared SHARED asio_impl.cpp) @@ -40,7 +41,9 @@ target_include_directories( $) target_link_libraries(mooncake_common PUBLIC asio_shared yaml-cpp jsoncpp - yalantinglibs::yalantinglibs) + glog::glog + gflags::gflags + yalantinglibs::yalantinglibs) if(BUILD_SHARED_LIBS) install(TARGETS mooncake_common DESTINATION lib) diff --git a/mooncake-common/src/environ.cpp b/mooncake-common/src/environ.cpp index de6faec6d2..b6b5b4e9cf 100644 --- a/mooncake-common/src/environ.cpp +++ b/mooncake-common/src/environ.cpp @@ -111,6 +111,44 @@ uint32_t ResolveRpcClientIoThreads(const EnvironSource& source, } // namespace +size_t Environ::GetOffloadRpcThreadNum(size_t default_value) const { + const size_t value = GetSizeT("MC_OFFLOAD_RPC_THREAD_NUM", default_value); + return value == 0 ? default_value : value; +} + +uint32_t Environ::GetYltRpcPoolMaxConnection(uint32_t default_value) const { + const size_t value = + GetSizeT("MC_YLT_RPC_POOL_MAX_CONNECTION", default_value); + if (value == 0 || value > UINT32_MAX) { + std::cerr << "[Mooncake] Warning: invalid value for env " + << "MC_YLT_RPC_POOL_MAX_CONNECTION, using default " + << default_value << std::endl; + return default_value; + } + return static_cast(value); +} + +size_t Environ::GetYltRpcPoolIdleTimeoutMs(size_t default_value) const { + return GetSizeT("MC_YLT_RPC_POOL_IDLE_TIMEOUT_MS", default_value); +} + +size_t Environ::GetYltRpcPoolShortIdleTimeoutMs( + size_t default_value) const { + return GetSizeT("MC_YLT_RPC_POOL_SHORT_IDLE_TIMEOUT_MS", default_value); +} + +bool Environ::GetYltRpcPoolWarmupEnabled(bool default_value) const { + return GetBool("MC_YLT_RPC_POOL_WARMUP", default_value); +} + +size_t Environ::GetYltRpcPoolWarmupConnections(size_t default_value) const { + return GetSizeT("MC_YLT_RPC_POOL_WARMUP_CONNECTIONS", default_value); +} + +bool Environ::GetStoreWarmupEnabled(bool default_value) const { + return GetBool("MC_STORE_WARMUP", default_value); +} + Environ& Environ::Get() { static Environ instance(GetOsEnvironSource()); return instance; diff --git a/mooncake-common/src/mooncake_logging.cpp b/mooncake-common/src/mooncake_logging.cpp new file mode 100644 index 0000000000..e79b6cb27c --- /dev/null +++ b/mooncake-common/src/mooncake_logging.cpp @@ -0,0 +1,392 @@ +#include "mooncake_logging.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace mooncake::logging { +namespace { + +thread_local uint64_t current_trace_id = 0; + +uint64_t GetPidForTrace() { +#ifdef _WIN32 + return static_cast(_getpid()); +#else + return static_cast(getpid()); +#endif +} + +uint64_t SteadyClockNs() { + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +double ParseHiFreqLogSampleRate() { + const char* value = std::getenv("MC_HIFREQ_LOG_SAMPLE_RATE"); + if (value == nullptr || *value == '\0') + return 1.0; // default: full sampling + errno = 0; + char* end = nullptr; + double rate = std::strtod(value, &end); + if (end == value || errno != 0) return 1.0; // non-numeric / overflow + if (rate < 0.0) return 0.0; + if (rate > 1.0) return 1.0; + return rate; +} + +// Interval for the background periodic flush performed by the async worker +// thread. Default 1s; tunable via MC_LOG_FLUSH_SECS (accepts fractional +// seconds, e.g. "0.5"). Clamped to a 50ms floor to avoid pathological busy +// flushing. +std::chrono::milliseconds ParseFlushIntervalMs() { + double secs = 1.0; + const char* value = std::getenv("MC_LOG_FLUSH_SECS"); + if (value != nullptr && *value != '\0') { + errno = 0; + char* end = nullptr; + double parsed = std::strtod(value, &end); + if (end != value && errno == 0 && parsed > 0.0) secs = parsed; + } + if (secs < 0.05) secs = 0.05; + return std::chrono::milliseconds(static_cast(secs * 1000.0)); +} + +// Ring buffer capacity (messages). Default 65536; tunable via +// MC_LOG_QUEUE_SIZE. Floored at 64 so the buffer stays usable. +size_t ParseQueueSize() { + const char* value = std::getenv("MC_LOG_QUEUE_SIZE"); + if (value == nullptr || *value == '\0') return 65536; + long parsed = std::strtol(value, nullptr, 10); + if (parsed < 64) return 64; + return static_cast(parsed); +} + +// Number of background writer threads. Default 2; tunable via MC_LOG_WORKERS, +// clamped to [1, 16]. Note glog serializes the actual file write internally, +// so extra workers mostly overlap message formatting with IO and keep the ring +// drained rather than parallelizing disk writes. +size_t ParseWorkerCount() { + const char* value = std::getenv("MC_LOG_WORKERS"); + if (value == nullptr || *value == '\0') return 2; + long parsed = std::strtol(value, nullptr, 10); + if (parsed < 1) return 1; + if (parsed > 16) return 16; + return static_cast(parsed); +} + +struct LogEntry { + const char* file; + int line; + google::LogSeverity severity; + uint64_t trace_id; + std::string message; +}; + +// Async log pipeline backing MC_LOG(). +// +// Hot path (Enqueue): take the mutex, move the entry into a PRE-ALLOCATED ring +// buffer, release. No per-message heap allocation for queue nodes and no +// blocking: when the ring is full the OLDEST entry is overwritten (overrun) and +// a counter is bumped. This bounds producer latency -- a hot business thread +// is never stalled waiting on slow log IO -- at the cost of dropping the oldest +// pending lines under sustained overload. The dropped count is reported +// periodically as a WARNING so loss is never silent. +// +// Background: ParseWorkerCount() writer threads drain the ring via glog. One +// of them also performs the periodic FlushLogFiles (coordinated by an atomic so +// it runs once per interval regardless of worker count) and the overrun report +// -- both off the hot path. +class AsyncLogQueue { + public: + static AsyncLogQueue& Instance() { + static AsyncLogQueue* queue = new AsyncLogQueue(); + return *queue; + } + + void Enqueue(LogEntry entry) { + EnsureStarted(); + { + std::lock_guard lock(mutex_); + if (stopped_) { + WriteSync(entry); + return; + } + RingPush(std::move(entry)); // overruns oldest if full + } + queue_not_empty_.notify_one(); + } + + void Flush() { + EnsureStarted(); + { + std::unique_lock lock(mutex_); + queue_empty_.wait( + lock, [this] { return RingEmpty() && active_writes_ == 0; }); + } + google::FlushLogFiles(google::INFO); + } + + private: + AsyncLogQueue() + : capacity_(ParseQueueSize() + 1), // +1 slot reserved as full marker + worker_count_(ParseWorkerCount()), + flush_interval_(ParseFlushIntervalMs()), + ring_(capacity_) {} + + void EnsureStarted() { + std::call_once(start_once_, [this] { + for (size_t i = 0; i < worker_count_; ++i) { + workers_.emplace_back([this] { WorkerLoop(); }); + } + std::atexit([] { AsyncLogQueue::Instance().Stop(); }); + }); + } + + void Stop() { + { + std::lock_guard lock(mutex_); + stopped_ = true; + } + queue_not_empty_.notify_all(); + for (auto& w : workers_) { + if (w.joinable()) w.join(); + } + ReportOverruns(); + google::FlushLogFiles(google::INFO); + } + + void WorkerLoop() { + // The periodic flush runs here, off the hot path: log producers never + // pay a flush cost. google::FlushLogFiles drains glog's file buffer, + // covering BOTH the async MC_LOG output written below AND synchronous + // LOG()/MC_LOG emitted on business threads (they share the same glog + // file). This keeps the tail of the log (e.g. get_into_breakdown / + // put_result) from being lost when the host process is torn down + // without running atexit/static destructors (Go's os.Exit, SIGKILL + // under k8s). + while (true) { + LogEntry entry; + bool have_entry = false; + { + std::unique_lock lock(mutex_); + queue_not_empty_.wait_for(lock, flush_interval_, [this] { + return stopped_ || !RingEmpty(); + }); + if (!RingEmpty()) { + entry = std::move(ring_[head_]); + head_ = (head_ + 1) % capacity_; + ++active_writes_; + have_entry = true; + } else { + queue_empty_.notify_all(); + if (stopped_) return; + } + } + if (have_entry) { + WriteSync(entry); + std::lock_guard lock(mutex_); + --active_writes_; + if (RingEmpty() && active_writes_ == 0) { + queue_empty_.notify_all(); + } + } + MaybePeriodicFlush(); + } + } + + // Flush + overrun report, gated by an atomic so it runs at most once per + // interval across all workers. Bounds the worst-case loss window on a hard + // kill to one interval, with no per-message flush overhead. + void MaybePeriodicFlush() { + const int64_t now_ns = + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + const int64_t interval_ns = + std::chrono::duration_cast( + flush_interval_) + .count(); + int64_t last = last_flush_ns_.load(std::memory_order_relaxed); + if (now_ns - last < interval_ns) return; + if (!last_flush_ns_.compare_exchange_strong( + last, now_ns, std::memory_order_relaxed)) { + return; // another worker won this interval + } + google::FlushLogFiles(google::INFO); + ReportOverruns(); + } + + // Emit one synchronous WARNING if entries were overrun since the last + // report, so dropped logs are never silent. The counter is read+reset + // under the queue mutex; the WriteSync itself does not touch the ring. + void ReportOverruns() { + uint64_t dropped = 0; + { + std::lock_guard lock(mutex_); + dropped = overrun_count_; + overrun_count_ = 0; + } + if (dropped > 0) { + google::LogMessage(__FILE__, __LINE__, google::WARNING).stream() + << "trace_id[none] async log ring overran, dropped " << dropped + << " message(s); raise MC_LOG_QUEUE_SIZE or MC_LOG_WORKERS"; + } + } + + // --- ring buffer, all access guarded by mutex_ --- + bool RingEmpty() const { return head_ == tail_; } + + void RingPush(LogEntry&& entry) { + ring_[tail_] = std::move(entry); + tail_ = (tail_ + 1) % capacity_; + if (tail_ == head_) { // full: overwrite oldest + head_ = (head_ + 1) % capacity_; + ++overrun_count_; + } + } + + static void WriteSync(const LogEntry& entry) { + google::LogMessage log_message(entry.file, entry.line, entry.severity); + auto& stream = log_message.stream(); + if (entry.trace_id != 0) { + stream << "trace_id[" << entry.trace_id << "] "; + } else { + stream << "trace_id[none] "; + } + stream << entry.message; + if (entry.severity == google::FATAL) + google::FlushLogFiles(google::INFO); + } + + const size_t capacity_; + const size_t worker_count_; + const std::chrono::milliseconds flush_interval_; + + std::once_flag start_once_; + std::vector workers_; + + std::mutex mutex_; + std::condition_variable queue_not_empty_; + std::condition_variable queue_empty_; + + std::vector ring_; + size_t head_ = 0; + size_t tail_ = 0; + uint64_t overrun_count_ = 0; + size_t active_writes_ = 0; + bool stopped_ = false; + + std::atomic last_flush_ns_{0}; +}; + +} // namespace + +uint64_t NewTraceId() { + static const uint64_t process_seed = + (GetPidForTrace() << 48) ^ (SteadyClockNs() & 0x0000FFFFFFFF0000ULL); + static std::atomic counter{1}; + return process_seed ^ counter.fetch_add(1, std::memory_order_relaxed); +} + +uint64_t CurrentTraceId() { return current_trace_id; } + +double HiFreqLogSampleRate() { + static const double rate = ParseHiFreqLogSampleRate(); + return rate; +} + +bool ShouldSampleHiFreqLog() { + const double rate = HiFreqLogSampleRate(); + if (rate >= 1.0) return true; + if (rate <= 0.0) return false; + thread_local std::mt19937 rng(static_cast( + SteadyClockNs() ^ reinterpret_cast(&rng))); + thread_local std::uniform_real_distribution dist(0.0, 1.0); + return dist(rng) < rate; +} + +bool ShouldSampleHiFreqLog(uint64_t trace_id) { + if (trace_id == 0) return ShouldSampleHiFreqLog(); + const double rate = HiFreqLogSampleRate(); + if (rate >= 1.0) return true; + if (rate <= 0.0) return false; + + // SplitMix64 finalizer: stable, cheap, and sufficiently uniform for + // deterministic sampling. Use the top 53 bits to match double precision. + uint64_t value = trace_id + 0x9e3779b97f4a7c15ULL; + value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ULL; + value = (value ^ (value >> 27)) * 0x94d049bb133111ebULL; + value ^= value >> 31; + constexpr double kScale = 1.0 / static_cast(1ULL << 53); + return static_cast(value >> 11) * kScale < rate; +} + +bool ShouldLog(google::LogSeverity severity) { + // MC_LOG_ENABLE was removed: MC_LOG now behaves like plain glog LOG and is + // gated only by glog's own severity threshold (still async + trace_id). + if (severity == google::FATAL) return true; + return severity >= FLAGS_minloglevel; +} + +bool ShouldVLog(int level) { return VLOG_IS_ON(level); } + +void ApplyMooncakeLogEnableToGlog() { + // No-op retained for call-site compatibility (master/real_client main). + // MC_LOG_ENABLE was removed; nothing to apply. +} + +ScopedTraceId::ScopedTraceId(uint64_t trace_id) + : previous_trace_id_(current_trace_id) { + current_trace_id = trace_id; +} + +ScopedTraceId::~ScopedTraceId() { current_trace_id = previous_trace_id_; } + +AsyncLogMessage::AsyncLogMessage(const char* file, int line, + google::LogSeverity severity, bool enabled) + : file_(file), + line_(line), + severity_(severity), + enabled_(enabled), + trace_id_(CurrentTraceId()) {} + +AsyncLogMessage::~AsyncLogMessage() { + if (!enabled_) return; + if (severity_ == google::FATAL) { + google::LogMessage log_message(file_, line_, severity_); + auto& output = log_message.stream(); + if (trace_id_ != 0) { + output << "trace_id[" << trace_id_ << "] "; + } else { + output << "trace_id[none] "; + } + output << stream_.str(); + return; + } + AsyncLogQueue::Instance().Enqueue( + LogEntry{file_, line_, severity_, trace_id_, stream_.str()}); +} + +std::ostream& AsyncLogMessage::stream() { return stream_; } + +void FlushAsyncLogs() { AsyncLogQueue::Instance().Flush(); } + +} // namespace mooncake::logging diff --git a/mooncake-integration/CMakeLists.txt b/mooncake-integration/CMakeLists.txt index c7fe22818b..ba9767de27 100644 --- a/mooncake-integration/CMakeLists.txt +++ b/mooncake-integration/CMakeLists.txt @@ -113,6 +113,11 @@ if(WITH_STORE) store/engram_store_py.cpp integration_utils.h) set_target_properties(store PROPERTIES INSTALL_RPATH "$ORIGIN") + + include(${CMAKE_SOURCE_DIR}/mooncake-common/FindSpDiag.cmake) + target_include_directories(store PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/store) + target_link_libraries(store PRIVATE SpDiag::spdiag_lib) + if(USE_ASCEND_DIRECT) target_link_libraries( store PUBLIC ascendcl transfer_engine glog::glog gflags::gflags diff --git a/mooncake-integration/store/mooncake_perf_points.def b/mooncake-integration/store/mooncake_perf_points.def new file mode 100644 index 0000000000..eaf6dd992d --- /dev/null +++ b/mooncake-integration/store/mooncake_perf_points.def @@ -0,0 +1,216 @@ +// === Python绑定层 === +PERF_KEY_DEF(GET_STORE_PY_GET, "store_py.cpp::get", "Get") +PERF_KEY_DEF(GET_STORE_PY_GET_BATCH, "store_py.cpp::get_batch", "GetBatch") + +// === RealClient核心逻辑层 === +PERF_KEY_DEF(GET_BUFFER_INTERNAL, "store_py.cpp::get", "GetBuffer") +PERF_KEY_DEF(GET_BATCH_BUFFER_INTERNAL, "store_py.cpp::get_batch", "BatchGetBuffer") +PERF_KEY_DEF(GET_BUFFER_INTERNAL_FULL, "real_client.cpp::get_buffer", "GetBufferInternal") +PERF_KEY_DEF(GET_BATCH_BUFFER_INTERNAL_FULL, "real_client.cpp::batch_get_buffer", "BatchGetBufferInternal") +PERF_KEY_DEF(GET_INTO_INTERNAL, "real_client.cpp::get_into", "GetIntoInternal") +PERF_KEY_DEF(GET_BATCH_INTO_INTERNAL, "real_client.cpp::batch_get_into", "BatchGetIntoInternal") + +// === get_buffer_internal 子步骤 === +PERF_KEY_DEF(GET_INTERNAL_QUERY, "real_client.cpp::get_buffer_internal", "Query") +PERF_KEY_DEF(GET_INTERNAL_SELECT_REPLICA, "real_client.cpp::get_buffer_internal", "SelectReplica") +PERF_KEY_DEF(GET_INTERNAL_ALLOC_BUFFER, "real_client.cpp::get_buffer_internal", "AllocBuffer") +PERF_KEY_DEF(GET_INTERNAL_SSD_READ, "real_client.cpp::get_buffer_internal", "SSDRead") +PERF_KEY_DEF(GET_INTERNAL_MEM_READ, "real_client.cpp::get_buffer_internal", "MemRead") +PERF_KEY_DEF(GET_INTERNAL_DISK_READ, "real_client.cpp::get_buffer_internal", "DiskRead") + +// === batch_get_buffer_internal 子步骤 === +PERF_KEY_DEF(GET_BATCH_INTERNAL_QUERY, "real_client.cpp::batch_get_buffer_internal", "BatchQuery") +PERF_KEY_DEF(GET_BATCH_INTERNAL_PREPARATION, "real_client.cpp::batch_get_buffer_internal", "Preparation") +PERF_KEY_DEF(GET_BATCH_INTERNAL_SELECT_REPLICA, "real_client.cpp::batch_get_buffer_internal", "SelectReplica") +PERF_KEY_DEF(GET_BATCH_INTERNAL_ALLOC_BUFFER, "real_client.cpp::batch_get_buffer_internal", "AllocBuffer") +PERF_KEY_DEF(GET_BATCH_INTERNAL_SSD_READ, "real_client.cpp::batch_get_buffer_internal", "SSDRead") +PERF_KEY_DEF(GET_BATCH_INTERNAL_MEMDISH_READ,"real_client.cpp::batch_get_buffer_internal", "MemDiskRead") + +// === get_into_internal steps === +PERF_KEY_DEF(GET_INTO_INTERNAL_QUERY, "real_client.cpp::get_into_internal", "Query") +PERF_KEY_DEF(GET_INTO_INTERNAL_SELECT_REPLICA, "real_client.cpp::get_into_internal", "SelectReplica") +PERF_KEY_DEF(GET_INTO_INTERNAL_ALLOC_BUFFER, "real_client.cpp::get_into_internal", "AllocBuffer") +PERF_KEY_DEF(GET_INTO_INTERNAL_SSD_READ, "real_client.cpp::get_into_internal", "SSDRead") +PERF_KEY_DEF(GET_INTO_INTERNAL_MEM_READ, "real_client.cpp::get_into_internal", "MemRead") +PERF_KEY_DEF(GET_INTO_INTERNAL_DISK_READ, "real_client.cpp::get_into_internal", "DiskRead") + +// === batch_get_into_internal steps === +PERF_KEY_DEF(GET_BATCH_INTO_INTERNAL_QUERY, "real_client.cpp::batch_get_into_internal", "BatchQuery") +PERF_KEY_DEF(GET_BATCH_INTO_INTERNAL_SELECT_REPLICA, "real_client.cpp::batch_get_into_internal", "SelectReplica") +PERF_KEY_DEF(GET_BATCH_INTO_INTERNAL_ALLOC_BUFFER, "real_client.cpp::batch_get_into_internal", "AllocBuffer") +PERF_KEY_DEF(GET_BATCH_INTO_INTERNAL_MEM_READ, "real_client.cpp::batch_get_into_internal", "MemRead") +PERF_KEY_DEF(GET_BATCH_INTO_INTERNAL_DISK_READ, "real_client.cpp::batch_get_into_internal", "DiskRead") +PERF_KEY_DEF(GET_BATCH_INTO_INTERNAL_SSD_READ, "real_client.cpp::batch_get_into_internal", "SSDRead") + +// === Client::Get (单key) === +PERF_KEY_DEF(GET_SINGLE_FULL, "client_service.cpp::Get", "TransferGet") +PERF_KEY_DEF(GET_SINGLE_FIND_REPLICA, "client_service.cpp::Get", "FindReplica") +PERF_KEY_DEF(GET_SINGLE_HOT_CACHE, "client_service.cpp::Get", "HotCache") +PERF_KEY_DEF(GET_SINGLE_TRANSFER_READ, "client_service.cpp::Get", "TransferRead") +PERF_KEY_DEF(GET_SINGLE_RELEASE_CACHE, "client_service.cpp::Get", "ReleaseCache") +PERF_KEY_DEF(GET_SINGLE_ASYNC_CACHE, "client_service.cpp::Get", "AsyncCache") + +// === Client::Get 内 TransferData === +PERF_KEY_DEF(GET_SINGLE_TRANSFER_FULL, "client_service.cpp::TransferData", "TransferData[G]") +PERF_KEY_DEF(GET_SINGLE_TRANSFER_SUBMIT, "client_service.cpp::TransferData", "Submit[G]") +PERF_KEY_DEF(GET_SINGLE_TRANSFER_WAIT, "client_service.cpp::TransferData", "Wait[G]") + +// === Client::BatchGet (批量) === +PERF_KEY_DEF(GET_BATCH_FULL, "client_service.cpp::BatchGet", "TransferBatchGet") +PERF_KEY_DEF(GET_BATCH_FIND_REPLICA, "client_service.cpp::BatchGet", "FindReplica") +PERF_KEY_DEF(GET_BATCH_HOT_CACHE, "client_service.cpp::BatchGet", "HotCache") +PERF_KEY_DEF(GET_BATCH_SUBMIT, "client_service.cpp::BatchGet", "Submit") +PERF_KEY_DEF(GET_BATCH_WAIT, "client_service.cpp::BatchGet", "Wait") +PERF_KEY_DEF(GET_BATCH_RELEASE_CACHE, "client_service.cpp::BatchGet", "ReleaseCache") +PERF_KEY_DEF(GET_BATCH_ASYNC_CACHE, "client_service.cpp::BatchGet", "AsyncCache") + +// === Python绑定层 === +PERF_KEY_DEF(PUT_STORE_PY_PUT, "store_py.cpp::put", "Put") +PERF_KEY_DEF(PUT_STORE_PY_PUT_BATCH, "store_py.cpp::put_batch", "PutBatch") + +// === RealClient核心逻辑层 === +PERF_KEY_DEF(PUT_INTERNAL_FULL, "store_py.cpp::put", "PutBuffer") +PERF_KEY_DEF(PUT_INTERNAL_ALLOC_BUFFER, "real_client.cpp::put_internal", "AllocBuffer") +PERF_KEY_DEF(PUT_INTERNAL_MEM_COPY, "real_client.cpp::put_internal", "MemCopy") +PERF_KEY_DEF(PUT_INTERNAL_SPLIT_SLICES, "real_client.cpp::put_internal", "SplitSlices") + +PERF_KEY_DEF(PUT_BATCH_INTERNAL_FULL, "store_py.cpp::put_batch", "BatchPutBuffer") +PERF_KEY_DEF(PUT_BATCH_INTERNAL_ALLOC_BUFFER,"real_client.cpp::put_batch_internal", "AllocBuffer") +PERF_KEY_DEF(PUT_BATCH_INTERNAL_MEM_COPY, "real_client.cpp::put_batch_internal", "MemCopy") +PERF_KEY_DEF(PUT_BATCH_INTERNAL_SPLIT_SLICES,"real_client.cpp::put_batch_internal", "SplitSlices") + +// === Client::Put (单key) === +PERF_KEY_DEF(PUT_SINGLE_FULL, "client_service.cpp::Put", "TransferPut") +PERF_KEY_DEF(PUT_SINGLE_PUT_START, "client_service.cpp::Put", "PutStart") +PERF_KEY_DEF(PUT_SINGLE_DISK_WRITE, "client_service.cpp::Put", "DiskWrite") +PERF_KEY_DEF(PUT_SINGLE_TRANSFER_WRITE, "client_service.cpp::Put", "TransferWrite") +PERF_KEY_DEF(PUT_SINGLE_PUT_END, "client_service.cpp::Put", "PutEnd") +PERF_KEY_DEF(PUT_SINGLE_PUT_REVOKE, "client_service.cpp::Put", "PutRevoke") + +// === Client::Put 内 TransferData === +PERF_KEY_DEF(PUT_SINGLE_TRANSFER_FULL, "client_service.cpp::TransferData", "TransferData[W]") +PERF_KEY_DEF(PUT_SINGLE_TRANSFER_SUBMIT, "client_service.cpp::TransferData", "Submit[W]") +PERF_KEY_DEF(PUT_SINGLE_TRANSFER_WAIT, "client_service.cpp::TransferData", "Wait[W]") + +// === Client::BatchPut (批量) === +PERF_KEY_DEF(PUT_BATCH_FULL, "client_service.cpp::BatchPut", "TransferBatchPut") +PERF_KEY_DEF(PUT_BATCH_CREATE_OPS, "client_service.cpp::BatchPut", "CreateOps") +PERF_KEY_DEF(PUT_BATCH_PUT_START, "client_service.cpp::StartBatchPut", "PutStart") +PERF_KEY_DEF(PUT_BATCH_SUBMIT, "client_service.cpp::SubmitTransfers", "Submit") +PERF_KEY_DEF(PUT_BATCH_DISK_WRITE, "client_service.cpp::SubmitTransfers", "DiskWrite") +PERF_KEY_DEF(PUT_BATCH_WAIT, "client_service.cpp::WaitForTransfers", "Wait") +PERF_KEY_DEF(PUT_BATCH_PUT_END, "client_service.cpp::FinalizeBatchPut", "PutEnd") +PERF_KEY_DEF(PUT_BATCH_PUT_REVOKE, "client_service.cpp::FinalizeBatchPut", "PutRevoke") +PERF_KEY_DEF(PUT_BATCH_COLLECT_RESULTS, "client_service.cpp::BatchPut", "CollectResults") + +// === batch_get_into_offload_object_internal 子步骤 (SSD read) === +PERF_KEY_DEF(GET_SSD_OFFLOAD_RPC, "real_client.cpp::batch_get_into_offload_object_internal", "OffloadRpc") +PERF_KEY_DEF(GET_SSD_OFFLOAD_RPC_POOL, "real_client.cpp::ClientRequester::invoke_rpc", "RpcPoolLookup") +PERF_KEY_DEF(GET_SSD_OFFLOAD_RPC_CALL, "real_client.cpp::ClientRequester::invoke_rpc", "RpcCall") +PERF_KEY_DEF(GET_SSD_OFFLOAD_RPC_RESULT_GET, "real_client.cpp::ClientRequester::invoke_rpc", "RpcResultGet") +PERF_KEY_DEF(GET_SSD_TRANSFER_DATA, "real_client.cpp::batch_get_into_offload_object_internal", "TransferData") +PERF_KEY_DEF(GET_SSD_RELEASE_BUFFER, "real_client.cpp::batch_get_into_offload_object_internal", "ReleaseBuffer") + +// === Owner-side offload sub-steps (pull & push) === +PERF_KEY_DEF(GET_SSD_OWNER_READ, "file_storage.cpp::BatchGet", "OwnerSsdRead") +PERF_KEY_DEF(GET_SSD_OWNER_ALLOC, "file_storage.cpp::BatchGet", "OwnerAllocBuffer") +PERF_KEY_DEF(GET_SSD_OWNER_LOAD, "file_storage.cpp::BatchGet", "OwnerDiskLoad") +PERF_KEY_DEF(GET_SSD_OWNER_LOAD_PLAN, "storage_backend.cpp::BatchLoad", "OwnerLoadPlan") +PERF_KEY_DEF(GET_SSD_OWNER_LOAD_URING, "storage_backend.cpp::BatchLoad", "OwnerLoadUring") +PERF_KEY_DEF(GET_SSD_OWNER_LOAD_POSIX, "storage_backend.cpp::BatchLoad", "OwnerLoadPosix") +PERF_KEY_DEF(GET_SSD_OWNER_PUSH_WRITE, "real_client.cpp::batch_get_offload_object_push", "OwnerPushWrite") +PERF_KEY_DEF(GET_SSD_OWNER_RPC_QUEUE, "real_client.cpp::batch_get_offload_object", "OwnerRpcQueue") +PERF_KEY_DEF(GET_SSD_OWNER_RPC_BATCH_GET, "real_client.cpp::batch_get_offload_object", "OwnerRpcBatchGet") +PERF_KEY_DEF(GET_SSD_OWNER_RPC_RESUME, "real_client.cpp::batch_get_offload_object", "OwnerRpcResume") +PERF_KEY_DEF(GET_SSD_OWNER_RELEASE, "file_storage.cpp::ReleaseBuffer", "OwnerReleaseBuffer") + +// === UB / URMA endpoint first connection path === +PERF_KEY_DEF(UB_HANDSHAKE_ENCODE, "transfer_metadata.cpp::TransferHandshakeUtil::encode", "Encode") +PERF_KEY_DEF(UB_HANDSHAKE_DECODE, "transfer_metadata.cpp::TransferHandshakeUtil::decode", "Decode") +PERF_KEY_DEF(UB_ENDPOINT_CONSTRUCT, "urma_endpoint.cpp::construct", "Construct") +PERF_KEY_DEF(UB_ENDPOINT_CREATE_JETTY, "urma_endpoint.cpp::construct", "CreateJetty") +PERF_KEY_DEF(UB_ENDPOINT_ACTIVE_SETUP, "urma_endpoint.cpp::setupConnectionsByActive", "ActiveSetup") +PERF_KEY_DEF(UB_ENDPOINT_ACTIVE_HANDSHAKE, "urma_endpoint.cpp::setupConnectionsByActive", "SendHandshake") +PERF_KEY_DEF(UB_ENDPOINT_PASSIVE_SETUP, "urma_endpoint.cpp::setupConnectionsByPassive", "PassiveSetup") +PERF_KEY_DEF(UB_ENDPOINT_DO_SETUP_ALL, "urma_endpoint.cpp::doSetupConnection", "DoSetupAll") +PERF_KEY_DEF(UB_ENDPOINT_IMPORT_JETTY, "urma_endpoint.cpp::doSetupConnection", "ImportJetty") +PERF_KEY_DEF(UB_ENDPOINT_BIND_JETTY, "urma_endpoint.cpp::doSetupConnection", "BindJetty") + +// ============================================================ +// === Master 服务端打点(程序名 mooncake_master) === +// ============================================================ + +// --- P0: RPC 热路径(单 key,走 execute_rpc)--- +PERF_KEY_DEF(MASTER_RPC_PUT_START, "rpc_service.cpp::PutStart", "PutStart") +PERF_KEY_DEF(MASTER_RPC_PUT_END, "rpc_service.cpp::PutEnd", "PutEnd") +PERF_KEY_DEF(MASTER_RPC_PUT_REVOKE, "rpc_service.cpp::PutRevoke", "PutRevoke") +PERF_KEY_DEF(MASTER_RPC_GET_REPLICA_LIST, "rpc_service.cpp::GetReplicaList", "GetReplicaList") +PERF_KEY_DEF(MASTER_RPC_EXIST_KEY, "rpc_service.cpp::ExistKey", "ExistKey") + +// --- P0: Batch get/put(手动插点,不走 execute_rpc)--- +PERF_KEY_DEF(MASTER_RPC_BATCH_PUT_START, "rpc_service.cpp::BatchPutStart", "BatchPutStart") +PERF_KEY_DEF(MASTER_RPC_BATCH_PUT_END, "rpc_service.cpp::BatchPutEnd", "BatchPutEnd") +PERF_KEY_DEF(MASTER_RPC_BATCH_PUT_REVOKE, "rpc_service.cpp::BatchPutRevoke", "BatchPutRevoke") +PERF_KEY_DEF(MASTER_RPC_BATCH_GET_REPLICA,"rpc_service.cpp::BatchGetReplicaList","BatchGetReplicaList") + +// --- P1: 次热路径(走 execute_rpc)--- +PERF_KEY_DEF(MASTER_RPC_UPSERT_START, "rpc_service.cpp::UpsertStart", "UpsertStart") +PERF_KEY_DEF(MASTER_RPC_UPSERT_END, "rpc_service.cpp::UpsertEnd", "UpsertEnd") +PERF_KEY_DEF(MASTER_RPC_UPSERT_REVOKE, "rpc_service.cpp::UpsertRevoke", "UpsertRevoke") +PERF_KEY_DEF(MASTER_RPC_REMOVE, "rpc_service.cpp::Remove", "Remove") +PERF_KEY_DEF(MASTER_RPC_REMOVE_BY_REGEX, "rpc_service.cpp::RemoveByRegex", "RemoveByRegex") +PERF_KEY_DEF(MASTER_RPC_MOUNT_SEGMENT, "rpc_service.cpp::MountSegment", "MountSegment") +PERF_KEY_DEF(MASTER_RPC_UNMOUNT_SEGMENT, "rpc_service.cpp::UnmountSegment", "UnmountSegment") +PERF_KEY_DEF(MASTER_RPC_COPY_START, "rpc_service.cpp::CopyStart", "CopyStart") +PERF_KEY_DEF(MASTER_RPC_COPY_END, "rpc_service.cpp::CopyEnd", "CopyEnd") +PERF_KEY_DEF(MASTER_RPC_COPY_REVOKE, "rpc_service.cpp::CopyRevoke", "CopyRevoke") +PERF_KEY_DEF(MASTER_RPC_PING, "rpc_service.cpp::Ping", "Ping") + +// --- RPC 内部子步骤 --- +PERF_KEY_DEF(MASTER_PUT_ALLOCATE_MEM, "master_service.cpp::AllocateAndInsertMetadata", "AllocateMemory") +PERF_KEY_DEF(MASTER_PUT_ALLOCATE_NOF, "master_service.cpp::AllocateAndInsertMetadata", "AllocateNoF") +PERF_KEY_DEF(MASTER_PUT_SHARD_LOCK, "master_service.cpp::PutStart", "AcquireShardLock") +PERF_KEY_DEF(MASTER_SNAPSHOT_LOCK, "master_service.cpp::SnapshotThreadFunc", "AcquireSnapshotLock") + +// --- 后台线程 --- +PERF_KEY_DEF(MASTER_BG_BATCH_EVICT, "master_service.cpp::BatchEvict", "BatchEvict") +PERF_KEY_DEF(MASTER_BG_NOF_BATCH_EVICT, "master_service.cpp::NoFBatchEvict", "NoFBatchEvict") +PERF_KEY_DEF(MASTER_BG_DISCARD_EXPIRED, "master_service.cpp::EvictionThreadFunc", "DiscardExpired") +PERF_KEY_DEF(MASTER_BG_SNAPSHOT_PERSIST, "master_service.cpp::SnapshotThreadFunc", "SnapshotPersist") +PERF_KEY_DEF(MASTER_BG_CLIENT_MONITOR, "master_service.cpp::ClientMonitorFunc", "ClientMonitorScan") +PERF_KEY_DEF(MASTER_BG_CLIENT_UNMOUNT, "master_service.cpp::ClientMonitorFunc", "ExpiredClientUnmount") + +// ============================================================ +// === vLLM MooncakeStoreConnector 路径打点(vllm 0.26.1rc0)=== +// === 覆盖 S2-S9 入口 + T1-T8 下沉 + Client 服务层 === +// ============================================================ + +// === store_py.cpp Python 绑定层(vllm 直接调用入口,S2-S9)=== +PERF_KEY_DEF(STORE_PY_SETUP, "store_py.cpp::setup", "Setup") +PERF_KEY_DEF(STORE_PY_REGISTER_BUFFER, "store_py.cpp::register_buffer", "RegisterBuffer") +PERF_KEY_DEF(STORE_PY_BATCH_PUT_MULTI, "store_py.cpp::batch_put_from_multi_buffers","BatchPutMultiBuf") +PERF_KEY_DEF(STORE_PY_BATCH_GET_INTO_MULTI, "store_py.cpp::batch_get_into_multi_buffers","BatchGetIntoMultiBuf") +PERF_KEY_DEF(STORE_PY_BATCH_IS_EXIST, "store_py.cpp::batch_is_exist", "BatchIsExist") +PERF_KEY_DEF(STORE_PY_BATCH_GET_REPLICA_DESC, "store_py.cpp::batch_get_replica_desc", "BatchGetReplicaDesc") +PERF_KEY_DEF(STORE_PY_REMOVE_ALL, "store_py.cpp::remove_all", "RemoveAll") +PERF_KEY_DEF(STORE_PY_CLOSE, "store_py.cpp::close", "Close") + +// === RealClient 核心逻辑层(下沉路径,T1-T8)=== +PERF_KEY_DEF(RC_SETUP_REAL, "real_client.cpp::setup_real", "SetupReal") +PERF_KEY_DEF(RC_SETUP_INTERNAL, "real_client.cpp::setup_internal", "SetupInternal") +PERF_KEY_DEF(RC_TEARDOWN_ALL, "real_client.cpp::tearDownAll", "TeardownAll") +PERF_KEY_DEF(RC_TEARDOWN_ALL_INTERNAL, "real_client.cpp::tearDownAll_internal", "TeardownAllInternal") +PERF_KEY_DEF(RC_REMOVE_ALL, "real_client.cpp::removeAll", "RemoveAll") +PERF_KEY_DEF(RC_REMOVE_ALL_INTERNAL, "real_client.cpp::removeAll_internal", "RemoveAllInternal") +PERF_KEY_DEF(RC_BATCH_IS_EXIST, "real_client.cpp::batchIsExist", "BatchIsExist") +PERF_KEY_DEF(RC_BATCH_IS_EXIST_INTERNAL, "real_client.cpp::batchIsExist_internal", "BatchIsExistInternal") +PERF_KEY_DEF(RC_REGISTER_BUFFER, "real_client.cpp::register_buffer", "RegisterBuffer") +PERF_KEY_DEF(RC_REGISTER_BUFFER_INTERNAL, "real_client.cpp::register_buffer_internal","RegisterBufferInternal") +PERF_KEY_DEF(RC_BATCH_PUT_MULTI, "real_client.cpp::batch_put_from_multi_buffers", "BatchPutMultiBuf") +PERF_KEY_DEF(RC_BATCH_PUT_MULTI_INTERNAL, "real_client.cpp::batch_put_from_multi_buffers_internal","BatchPutMultiBufInternal") +PERF_KEY_DEF(RC_BATCH_GET_INTO_MULTI, "real_client.cpp::batch_get_into_multi_buffers", "BatchGetIntoMultiBuf") +PERF_KEY_DEF(RC_BATCH_GET_INTO_MULTI_INTERNAL, "real_client.cpp::batch_get_into_multi_buffers_internal","BatchGetIntoMultiBufInternal") +PERF_KEY_DEF(RC_BATCH_GET_REPLICA_DESC, "real_client.cpp::batch_get_replica_desc", "BatchGetReplicaDesc") + +// === Client 服务层(vllm 路径下沉,部分已有打点)=== +PERF_KEY_DEF(CLIENT_BATCH_QUERY, "client_service.cpp::BatchQuery", "BatchQuery") diff --git a/mooncake-integration/store/store_py.cpp b/mooncake-integration/store/store_py.cpp index 0ee3a30546..82f8e7f5d2 100644 --- a/mooncake-integration/store/store_py.cpp +++ b/mooncake-integration/store/store_py.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include "memory_alloc.h" #include "ssd_register_client.h" #include "device/accelerator_registry.h" +#include "mooncake_logging.h" // MC_LOG #include // for atexit #include @@ -22,6 +24,10 @@ #include "integration_utils.h" #include "buffer_pool.h" +#define SPDIAG_PERF_DEF_FILE "mooncake_perf_points.def" +#define SPDIAG_PROGRAM_NAME "mooncake_store" +#include "spdiag/auto_perf.h" + // Forward declaration for EngramStore bindings namespace mooncake { namespace engram { @@ -470,8 +476,12 @@ class MooncakeStorePyWrapper { } pybind11::bytes get(const std::string &key) { + SpDiag::PerfPoint pt(PerfKey::GET_STORE_PY_GET, + SpDiag::PerfLevel::SUB_SYSTEM); + pt.Start(); if (!is_client_initialized()) { LOG(ERROR) << "Client is not initialized"; + pt.End(-1); return pybind11::bytes("\\0", 0); } @@ -479,13 +489,19 @@ class MooncakeStorePyWrapper { { py::gil_scoped_release release_gil; + SpDiag::PerfPoint pt_full(PerfKey::GET_BUFFER_INTERNAL, + SpDiag::PerfLevel::KEY_MODULE); + pt_full.Start(); auto buffer_handle = store_->get_buffer(key); + pt_full.End(buffer_handle ? 0 : -1); if (!buffer_handle) { + pt.End(-1); py::gil_scoped_acquire acquire_gil; return kNullString; } py::gil_scoped_acquire acquire_gil; + pt.End(0); auto runtime_accelerator = mooncake::device::GetAcceleratorRegistry() .RuntimeAccelerators(); @@ -507,18 +523,46 @@ class MooncakeStorePyWrapper { std::vector get_batch( const std::vector &keys) { + auto start = std::chrono::steady_clock::now(); + LOG(INFO) << "get_batch start num_keys[" << keys.size() << "]"; + + SpDiag::PerfPoint pt(PerfKey::GET_STORE_PY_GET_BATCH, + SpDiag::PerfLevel::SUB_SYSTEM); + pt.Start(); const auto kNullString = pybind11::bytes("\\0", 0); if (!is_client_initialized()) { LOG(ERROR) << "Client is not initialized"; + pt.End(-1); py::gil_scoped_acquire acquire_gil; + auto elapsed_us = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + LOG(INFO) << "get_batch complete num_keys[" << keys.size() + << "] rc[-1] elapsed_us[" << elapsed_us << "]"; return {kNullString}; } { py::gil_scoped_release release_gil; + SpDiag::PerfPoint pt_full(PerfKey::GET_BATCH_BUFFER_INTERNAL, + SpDiag::PerfLevel::KEY_MODULE); + pt_full.Start(); auto batch_data = store_->batch_get_buffer(keys); + pt_full.End(0); if (batch_data.empty()) { + pt.End(-1); py::gil_scoped_acquire acquire_gil; + auto elapsed_us = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + LOG(INFO) << "get_batch complete num_keys[" << keys.size() + << "] rc[-1] elapsed_us[" << elapsed_us << "]"; + if (elapsed_us > 10000) { + LOG(WARNING) << "get_batch_slow num_keys[" << keys.size() + << "] elapsed_us[" << elapsed_us << "]"; + } return {kNullString}; } @@ -526,11 +570,13 @@ class MooncakeStorePyWrapper { std::vector results; results.reserve(batch_data.size()); + size_t success_count = 0; auto runtime_accelerator = mooncake::device::GetAcceleratorRegistry() .RuntimeAccelerators(); for (const auto &data : batch_data) { + if (data) success_count++; if (!data) { results.emplace_back(kNullString); continue; @@ -549,6 +595,18 @@ class MooncakeStorePyWrapper { pybind11::bytes((char *)data->ptr(), data->size())); } } + pt.End(0); + auto elapsed_us = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + LOG(INFO) << "get_batch complete num_keys[" << keys.size() + << "] success[" << success_count << "] rc[0] elapsed_us[" + << elapsed_us << "]"; + if (elapsed_us > 10000) { + LOG(WARNING) << "get_batch_slow num_keys[" << keys.size() + << "] elapsed_us[" << elapsed_us << "]"; + } return results; } } @@ -2027,6 +2085,9 @@ PYBIND11_MODULE(store, m) { const std::string &tenant_id = "default", bool enable_client_http_server = false, int client_http_port = DEFAULT_CLIENT_HTTP_PORT) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_SETUP, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); auto real_client = self.init_real_client(); std::shared_ptr transfer_engine = nullptr; @@ -2034,12 +2095,15 @@ PYBIND11_MODULE(store, m) { transfer_engine = engine.cast>(); } - return real_client->setup_real( + auto ret = real_client->setup_real( local_hostname, metadata_server, global_segment_size, local_buffer_size, protocol, rdma_devices, master_server_addr, transfer_engine, "", enable_ssd_offload, ssd_offload_path, tenant_id, enable_client_http_server, client_http_port); + pt.End(ret == 0 ? 0 : -1); + // MC_LOG 在下沉层 setup_real 输出(Q1b) + return ret; }, py::arg("local_hostname"), py::arg("metadata_server"), py::arg("global_segment_size"), py::arg("local_buffer_size"), @@ -2052,6 +2116,9 @@ PYBIND11_MODULE(store, m) { .def( "setup", [](MooncakeStorePyWrapper &self, const py::dict &config_dict) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_SETUP, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); auto real_client = self.init_real_client(); // Convert py::dict to ConfigDict (all values as strings) @@ -2063,8 +2130,11 @@ PYBIND11_MODULE(store, m) { } auto result = real_client->setup_internal(config); - return result.has_value() ? 0 - : static_cast(result.error()); + int ret = result.has_value() ? 0 + : static_cast(result.error()); + pt.End(ret == 0 ? 0 : -1); + // MC_LOG 在下沉层 setup_real 输出(Q1b) + return ret; }, py::arg("config"), "Setup the store with a configuration dictionary.\n" @@ -2173,8 +2243,16 @@ PYBIND11_MODULE(store, m) { .def( "remove_all", [](MooncakeStorePyWrapper &self, bool force) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_REMOVE_ALL, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); py::gil_scoped_release release; - return self.store_->removeAll(force); + auto ret = self.store_->removeAll(force); + pt.End(ret == 0 ? 0 : -1); + // 下沉 removeAll 不加 MC_LOG,入口层输出汇总(Q1b) + MC_LOG(INFO) << "[remove_all] elapsed_us=" << pt.ElapsedMicros() + << " success=" << (ret == 0 ? 1 : 0); + return ret; }, py::arg("force") = false, "Remove all objects from the store. If force=True, skip lease " @@ -2198,17 +2276,36 @@ PYBIND11_MODULE(store, m) { "batch_is_exist", [](MooncakeStorePyWrapper &self, const std::vector &keys) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_BATCH_IS_EXIST, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); py::gil_scoped_release release; - return self.store_->batchIsExist(keys); + auto ret = self.store_->batchIsExist(keys); + pt.End(0); + // MC_LOG 在下沉层 batchIsExist 输出 per-key(Q1b) + return ret; }, py::arg("keys"), "Check if multiple objects exist. Returns list of results: 1 if " "exists, 0 if not exists, -1 if error") .def("close", [](MooncakeStorePyWrapper &self) { - if (!self.store_) return 0; + SpDiag::PerfPoint pt(PerfKey::STORE_PY_CLOSE, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); + if (!self.store_) { + pt.End(0); + // 无下沉 MC_LOG,入口层输出汇总(Q1b) + MC_LOG(INFO) << "[close] elapsed_us=" << pt.ElapsedMicros() + << " success=1"; + return 0; + } int rc = self.store_->tearDownAll(); self.store_.reset(); + pt.End(rc == 0 ? 0 : -1); + // 下沉 tearDownAll 不加 MC_LOG,入口层输出汇总(Q1b) + MC_LOG(INFO) << "[close] elapsed_us=" << pt.ElapsedMicros() + << " success=" << (rc == 0 ? 1 : 0); return rc; }) .def("health_check", &MooncakeStorePyWrapper::health_check, @@ -2592,10 +2689,16 @@ PYBIND11_MODULE(store, m) { "register_buffer", [](MooncakeStorePyWrapper &self, uintptr_t buffer_ptr, size_t size) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_REGISTER_BUFFER, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); // Register memory buffer for RDMA operations void *buffer = reinterpret_cast(buffer_ptr); py::gil_scoped_release release; - return self.store_->register_buffer(buffer, size); + auto ret = self.store_->register_buffer(buffer, size); + pt.End(ret == 0 ? 0 : -1); + // MC_LOG 在下沉层 register_buffer 输出汇总(Q1b) + return ret; }, py::arg("buffer_ptr"), py::arg("size"), "Register a memory buffer for direct access operations") @@ -2729,12 +2832,23 @@ PYBIND11_MODULE(store, m) { py::buffer buf, const ReplicateConfig &config = ReplicateConfig{}) { py::buffer_info info = buf.request(/*writable=*/false); + + SpDiag::PerfPoint pt(PerfKey::PUT_STORE_PY_PUT, + SpDiag::PerfLevel::SUB_SYSTEM); + pt.Start(); py::gil_scoped_release release; - return self.store_->put( + SpDiag::PerfPoint pt_full(PerfKey::PUT_INTERNAL_FULL, + SpDiag::PerfLevel::KEY_MODULE); + pt_full.Start(); + auto ret = self.store_->put( key, std::span(static_cast(info.ptr), static_cast(info.size)), config); + pt_full.End(ret == 0 ? 0 : -1); + pt.End(ret == 0 ? 0 : -1); + + return ret; }, py::arg("key"), py::arg("value"), py::arg("config") = ReplicateConfig{}) @@ -2772,21 +2886,49 @@ PYBIND11_MODULE(store, m) { const std::vector &keys, const std::vector &buffers, const ReplicateConfig &config = ReplicateConfig{}) { + auto start = std::chrono::steady_clock::now(); + + SpDiag::PerfPoint pt(PerfKey::PUT_STORE_PY_PUT_BATCH, + SpDiag::PerfLevel::SUB_SYSTEM); + pt.Start(); // Convert pybuffers to spans without copying std::vector infos; std::vector> spans; infos.reserve(buffers.size()); spans.reserve(buffers.size()); + size_t total_size = 0; for (const auto &buf : buffers) { infos.emplace_back(buf.request(/*writable=*/false)); const auto &info = infos.back(); + total_size += static_cast(info.size); spans.emplace_back(static_cast(info.ptr), static_cast(info.size)); } + LOG(INFO) << "put_batch start num_keys[" << keys.size() + << "] total_size[" << total_size << "]"; + py::gil_scoped_release release; - return self.store_->put_batch(keys, spans, config); + SpDiag::PerfPoint pt_full(PerfKey::PUT_BATCH_INTERNAL_FULL, + SpDiag::PerfLevel::KEY_MODULE); + pt_full.Start(); + auto ret = self.store_->put_batch(keys, spans, config); + pt_full.End(ret == 0 ? 0 : -1); + pt.End(ret == 0 ? 0 : -1); + + auto elapsed_us = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + LOG(INFO) << "put_batch complete num_keys[" << keys.size() + << "] rc[" << ret << "] elapsed_us[" << elapsed_us + << "]"; + if (elapsed_us > 10000) { + LOG(WARNING) << "put_batch_slow num_keys[" << keys.size() + << "] elapsed_us[" << elapsed_us << "]"; + } + return ret; }, py::arg("keys"), py::arg("values"), py::arg("config") = ReplicateConfig{}) @@ -2801,13 +2943,20 @@ PYBIND11_MODULE(store, m) { const std::vector> &all_buffer_ptrs, const std::vector> &all_sizes, const ReplicateConfig &config = ReplicateConfig{}) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_BATCH_PUT_MULTI, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); if (!self.is_client_initialized()) { LOG(ERROR) << "Client is not initialized"; + pt.End(-1); return std::vector{}; } py::gil_scoped_release release; - return self.store_->batch_put_from_multi_buffers( + auto ret = self.store_->batch_put_from_multi_buffers( keys, CastAddrs2Ptrs(all_buffer_ptrs), all_sizes, config); + pt.End(ret.empty() ? -1 : 0); + // MC_LOG 在下沉层 *_internal 输出 汇总+per-key(Q1b) + return ret; }, py::arg("keys"), py::arg("all_buffer_ptrs"), py::arg("all_sizes"), py::arg("config") = ReplicateConfig{}, @@ -2821,10 +2970,16 @@ PYBIND11_MODULE(store, m) { const std::vector> &all_buffer_ptrs, const std::vector> &all_sizes, bool prefer_alloc_in_same_node = false) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_BATCH_GET_INTO_MULTI, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); py::gil_scoped_release release; - return self.store_->batch_get_into_multi_buffers( + auto ret = self.store_->batch_get_into_multi_buffers( keys, CastAddrs2Ptrs(all_buffer_ptrs), all_sizes, prefer_alloc_in_same_node); + pt.End(ret.empty() ? -1 : 0); + // MC_LOG 在下沉层 *_internal 输出 汇总+per-key(Q1b) + return ret; }, py::arg("keys"), py::arg("all_buffer_ptrs"), py::arg("all_sizes"), py::arg("prefer_alloc_in_same_node") = false, @@ -2842,8 +2997,14 @@ PYBIND11_MODULE(store, m) { "batch_get_replica_desc", [](MooncakeStorePyWrapper &self, const std::vector &keys) { + SpDiag::PerfPoint pt(PerfKey::STORE_PY_BATCH_GET_REPLICA_DESC, + SpDiag::PerfLevel::KEY_MODULE); + pt.Start(); py::gil_scoped_release release; - return self.store_->batch_get_replica_desc(keys); + auto ret = self.store_->batch_get_replica_desc(keys); + pt.End(0); + // MC_LOG 在下沉层 batch_get_replica_desc 输出 per-key(Q1b) + return ret; }, py::arg("keys")) .def( diff --git a/mooncake-p2p-store/CMakeLists.txt b/mooncake-p2p-store/CMakeLists.txt index 5b9980de90..013e69514b 100644 --- a/mooncake-p2p-store/CMakeLists.txt +++ b/mooncake-p2p-store/CMakeLists.txt @@ -1,8 +1,11 @@ # Currently you have to manually execute makefile in the src subdirectory. add_custom_target(build_p2p_store DEPENDS transfer_engine) add_custom_command( - TARGET build_p2p_store - COMMAND bash build.sh ${CMAKE_CURRENT_BINARY_DIR} ${USE_ETCD} ${USE_REDIS} ${USE_HTTP} ${USE_ETCD_LEGACY} ${CMAKE_BINARY_DIR} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) + TARGET build_p2p_store + COMMAND + bash build.sh ${CMAKE_CURRENT_BINARY_DIR} ${USE_ETCD} ${USE_REDIS} + ${USE_HTTP} ${USE_ETCD_LEGACY} ${CMAKE_BINARY_DIR} + "${MOONCAKE_SPDIAG_ACTIVE_LAYER}" "${MOONCAKE_SPDIAG_LIBRARY_DIR}" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + VERBATIM) set_property(TARGET build_p2p_store PROPERTY EXCLUDE_FROM_ALL FALSE) diff --git a/mooncake-p2p-store/build.sh b/mooncake-p2p-store/build.sh index 774e6ba688..f631b8d4fc 100644 --- a/mooncake-p2p-store/build.sh +++ b/mooncake-p2p-store/build.sh @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [ "$#" -ne 6 ]; then - echo "Usage: $0 TARGET_PATH USE_ETCD USE_REDIS USE_HTTP USE_ETCD_LEGACY BUILD_DIR" +if [ "$#" -ne 8 ]; then + echo "Usage: $0 TARGET_PATH USE_ETCD USE_REDIS USE_HTTP USE_ETCD_LEGACY BUILD_DIR SPDIAG_LAYER SPDIAG_LIB_DIR" exit 1 fi @@ -24,6 +24,8 @@ USE_REDIS=$3 USE_HTTP=$4 USE_ETCD_LEGACY=$5 BUILD_DIR=$6 +SPDIAG_LAYER=$7 +SPDIAG_LIB_DIR=$8 cd "src/p2pstore" if [ $? -ne 0 ]; then @@ -37,6 +39,10 @@ EXT_LDFLAGS+=" -L$BUILD_DIR/mooncake-common" EXT_LDFLAGS+=" -L$BUILD_DIR/mooncake-common/src" EXT_LDFLAGS+=" -ltransfer_engine -lbase -lasio -lstdc++ -lnuma -lglog -libverbs -lmlx5 -ljsoncpp -lmooncake_common -lm" +if [ "$SPDIAG_LAYER" = "system" ]; then + EXT_LDFLAGS+=" -L$SPDIAG_LIB_DIR -lspdiag" +fi + if [ -d "/usr/local/cuda/lib64/stubs" ]; then EXT_LDFLAGS+=" -L/usr/local/cuda/lib64/stubs" fi diff --git a/mooncake-store/benchmarks/CMakeLists.txt b/mooncake-store/benchmarks/CMakeLists.txt index 63af87d0f3..003f0b8724 100644 --- a/mooncake-store/benchmarks/CMakeLists.txt +++ b/mooncake-store/benchmarks/CMakeLists.txt @@ -33,6 +33,19 @@ target_link_libraries( allocation_strategy_bench PRIVATE mooncake_store cachelib_memory_allocator gflags::gflags glog::glog pthread) +add_executable(stress_cluster_bench stress_cluster_bench.cpp) +target_link_libraries( + stress_cluster_bench PRIVATE mooncake_store transfer_engine asio_shared + gflags::gflags glog::glog pthread) + +# Benchmark for RealClient::get_into_ranges with configurable value size, +# fragments per key and keys per query. +add_executable(stress_cluster_ranges_bench stress_cluster_ranges_bench.cpp) +target_link_libraries( + stress_cluster_ranges_bench PRIVATE mooncake_store transfer_engine + asio_shared gflags::gflags glog::glog + pthread) + # Add NoF worker pool benchmark executable if(USE_NOF) add_executable(nof_worker_pool_bench nof_worker_pool_bench.cpp) diff --git a/mooncake-store/benchmarks/cluster_mooncake_diag.py b/mooncake-store/benchmarks/cluster_mooncake_diag.py new file mode 100644 index 0000000000..86ba16c0d6 --- /dev/null +++ b/mooncake-store/benchmarks/cluster_mooncake_diag.py @@ -0,0 +1,2403 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Mooncake 集群诊断工具(单文件、单 HTML、无配置文件) + +================================================================================ +【使用方法】 +================================================================================ + +1. 必改:编辑本文件的 CONFIG 区域(约第 106 行),只需填: + - k8s:K8S_NAMESPACE + K8S_LOG_PATH(自动发现该 namespace 下所有 pod) + - client 超节点:CLIENT_HOSTS 列表 + CLIENT_LOG_PATH + - master 超节点:MASTER_HOSTS 列表 + MASTER_LOG_PATH + - SSH 密码:SSH_PASSWORD(如有必要,使用 sshpass;免密则留空) + - SSH 用户/端口:SSH_USER / SSH_PORT(默认 root:22) + + 示例: + K8S_NAMESPACE = "default" + K8S_LOG_PATH = "/var/log/mooncake" + CLIENT_HOSTS = ["10.0.1.5", "10.0.1.6"] + CLIENT_LOG_PATH = "/var/log/mooncake" + MASTER_HOSTS = ["10.0.1.1"] + MASTER_LOG_PATH = "/var/log/mooncake" + SSH_USER = "root" + SSH_PASSWORD = "" # 免密则留空 + SPDIAG_BIN = "spdiag" + +2. 常用命令: + + # 默认模式:拉日志 + spdiag show + 分析 → 单 HTML + python cluster_mooncake_diag.py --since 30min -o report.html + + # 仅在所有目标上执行命令(如 spdiag start / clear),不分析 + python cluster_mooncake_diag.py --exec "spdiag start" + python cluster_mooncake_diag.py --exec "spdiag clear" + + # 只重新分析已收集的日志(不连远程) + python cluster_mooncake_diag.py --analyze-only --since 30min + + # 生成示例 HTML(用伪造数据,不连任何目标,用于查看报告样式) + python cluster_mooncake_diag.py --sample -o sample.html + +3. 时间窗口(--since / --until 在解析阶段按行时间戳真实过滤): + --since 30min # 最近 30 分钟 + --since 2h # 最近 2 小时 + --since "2026-08-05 10:00:00" # 绝对时间 + --until "2026-08-05 11:00:00" # 绝对时间(--until 仅支持绝对) + +4. 典型工作流: + a. python cluster_mooncake_diag.py --exec "spdiag start" # 启动 spdiag 共享内存 + b. 重启 Mooncake 进程(Windows 必须重启;Linux 上 spdiag 会自动激活) + c. 跑你的 benchmark / 业务负载 + d. python cluster_mooncake_diag.py --since 30min -o report.html + e. python cluster_mooncake_diag.py --exec "spdiag stop" # 可选,停止 spdiag + +5. 输出 HTML 包含的区块: + - 概览卡片(targets / spdiag ok / log files / get requests / p50/p99/max / 带宽) + - [仅 --exec 模式] Command Execution Results + - spdiag show 概览 + top 30 慢点位表 + - QPS by slot 折线图 + 每秒 QPS 表格 + - Bandwidth by slot 折线图 + 每秒带宽表格(MB/s) + - get_into 耗时拆解(按 slot 分面板,6 阶段折线,鼠标框选缩放) + - 8 类操作 Summary Statistics 表(count/avg/p50/p95/p99/p999/p9999/min/max) + - Per-pod file stats(get 多少文件,#c# chunk vs 其他,字节数) + - 最慢 100 个请求跨角色关联表(trace_id 关联 real_client/RPC/storage/master) + - Per-target 详情(spdiag 原始输出 + 日志文件列表) + +6. 可调常量(文件顶部): + CHART_MAX_POINTS = 6000 # get_into 图每 slot 最大均匀抽样点数 + CHART_SLOW_POINTS = 200 # get_into 图强制保留的最慢点数 + SLOW_TRACE_COUNT = 100 # 最慢请求表行数 + DEFAULT_CHUNK_SIZE = 4194304 # chunk 字节数,默认 4MB + +7. 前置依赖: + - Python 3.10+ + - kubectl(仅当 TARGETS 中有 k8s_pod 时) + - ssh / scp / rsync(仅当 TARGETS 中有 supernode 时) + - 目标主机上 spdiag 可执行文件已就位 + +================================================================================ +""" + +from __future__ import annotations + +import argparse +import html +import json +import math +import re +import shlex +import subprocess +import sys +from collections import Counter, defaultdict +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from pathlib import Path +from typing import Iterable + + +# =========================================================================== +# CONFIG - just set these, then run. No other configuration needed. +# =========================================================================== + +# --- SSH settings --- +SSH_USER = "root" # SSH login user for all supernode targets +SSH_PORT = 22 # SSH port +SSH_PASSWORD = "" # Leave empty to use SSH keys. + # If set, sshpass is used (install: apt-get install sshpass). + +# --- k8s: auto-discover all pods in this namespace --- +K8S_NAMESPACE = "e2b" # e.g. "default". Empty = skip k8s. +K8S_LOG_PATH = "/var/log/mooncake" + +# --- client supernodes (store/client reader nodes) --- +CLIENT_HOSTS: list[str] = [ + # "10.0.1.5", + # "10.0.1.6", +] +CLIENT_LOG_PATH = "/var/log/mooncake" + +# --- master supernodes (master service nodes) --- +MASTER_HOSTS: list[str] = [ + # "10.0.1.1", +] +MASTER_LOG_PATH = "/var/log/mooncake" + +# --- spdiag binary path on remote targets --- +SPDIAG_BIN = "spdiag" + +# =========================================================================== + + +# --------------------------------------------------------------------------- +# Defaults tunable via CLI +# --------------------------------------------------------------------------- +DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024 +DEFAULT_CHUNK_MARKER = "#c#" +DEFAULT_PATH_SEPARATOR = "/chunk/" +DEFAULT_CHUNK_STYLE = "auto" +CHART_MAX_POINTS = 6000 +CHART_SLOW_POINTS = 200 +SLOW_TRACE_COUNT = 100 +TOP_TEMPLATE_TABLE_N = 30 + +# Operations recognized in breakdown logs +OPS = ( + "get_into_breakdown", + "batch_get_into_breakdown", + "put_into_breakdown", + "batch_put_into_breakdown", + "get_breakdown", + "batch_get_breakdown", + "put_breakdown", + "batch_put_breakdown", +) +OP_LABEL = { + "get_into_breakdown": "get_into", + "batch_get_into_breakdown": "batch_get_into", + "put_into_breakdown": "put_into", + "batch_put_into_breakdown": "batch_put_into", + "get_breakdown": "get", + "batch_get_breakdown": "batch_get", + "put_breakdown": "put", + "batch_put_breakdown": "batch_put", +} + +# get_into stages for stacked area chart (from bench script) +GET_INTO_STAGES = ( + "query_us", + "select_us", + "offload_rpc_us", + "transfer_data_us", + "release_buffer_us", + "read_overhead_us", +) +GET_INTO_STAGE_LABEL = { + "query_us": "query", + "select_us": "select", + "offload_rpc_us": "offload RPC", + "transfer_data_us": "transfer", + "release_buffer_us": "release", + "read_overhead_us": "overhead", +} + +# bench-style cross-role events +BENCH_EVENTS = { + "get_into_breakdown", + "offload_rpc_client_breakdown", + "offload_rpc_server_breakdown", + "storage_read_breakdown", + "storage_release_breakdown", + "master_rpc_client_breakdown", +} +BENCH_STAGES = ( + "query_us", "select_us", "read_us", "offload_rpc_us", + "transfer_data_us", "release_buffer_us", "read_overhead_us", "total_us", +) +RPC_STAGES = ("pool_lookup_us", "rpc_call_us", "result_get_us", "result_parse_us") + +# regexes +GLOG_TS_RE = re.compile( + r"^[IWEF](\d{4})(\d{2})(\d{2}) (\d{2}:\d{2}:\d{2})\.(\d{6})" +) +ISO_TS_RE = re.compile( + r"(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d+))?" +) +BRACKET_RE = re.compile(r"([A-Za-z_][\w]*)\[([^\]]*)\]") +FIELD_RE = re.compile(r"([A-Za-z_][\w]*)=([^\s]+)") +TRACE_RE = re.compile(r"trace_id\[(\d+)\]") +BENCH_LOG_RE = re.compile( + r"^[IWEF](?P\d{8}) (?P