From 246fe62c2f5767a3a295673cf70a7ad10b1e3b7c Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Sat, 28 Feb 2026 00:39:14 +0800 Subject: [PATCH 1/7] =?UTF-8?q?doc:=20=E7=BB=86=E5=8C=96=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DESIGN.md | 720 +++++++++++ docs/README.md | 28 + docs/modules/01-access-layer.md | 122 ++ docs/modules/02-protocol-adapter.md | 157 +++ docs/modules/03-metadata-cluster.md | 1163 ++++++++++++++++++ docs/modules/04-cache-layer.md | 668 +++++++++++ docs/modules/05-scheduler-layer.md | 789 ++++++++++++ docs/modules/06-tape-layer.md | 1012 ++++++++++++++++ docs/modules/07-consistency-performance.md | 765 ++++++++++++ docs/modules/08-observability.md | 1250 ++++++++++++++++++++ docs/modules/09-admin-console.md | 1054 +++++++++++++++++ docs/modules/README.md | 34 + 12 files changed, 7762 insertions(+) create mode 100644 docs/DESIGN.md create mode 100644 docs/README.md create mode 100644 docs/modules/01-access-layer.md create mode 100644 docs/modules/02-protocol-adapter.md create mode 100644 docs/modules/03-metadata-cluster.md create mode 100644 docs/modules/04-cache-layer.md create mode 100644 docs/modules/05-scheduler-layer.md create mode 100644 docs/modules/06-tape-layer.md create mode 100644 docs/modules/07-consistency-performance.md create mode 100644 docs/modules/08-observability.md create mode 100644 docs/modules/09-admin-console.md create mode 100644 docs/modules/README.md diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..ad9d533 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,720 @@ +# ColdStore 详细设计方案 + +> 版本:1.1 +> 更新日期:2025-02-27 + +## 1. 文档概述 + +本文档描述 ColdStore 冷存储系统的详细技术设计方案,重点涵盖: + +> **模块设计文档**:各层独立设计详见 [modules/](modules/) 目录。 + +1. **协议层**:兼容 S3 冷归档协议(Glacier 语义) +2. **集群元数据**:基于 [OpenRaft](https://github.com/databendlabs/openraft) + RocksDB 持久化 +3. **数据缓存层**:基于 [async-spdk](https://github.com/madsys-dev/async-spdk) 的高性能用户态缓存 + +### 1.1 核心技术选型(已确定) + +| 组件 | 库/方案 | 说明 | +|------|---------|------| +| Raft 共识 | **openraft** | [databendlabs/openraft](https://github.com/databendlabs/openraft),异步 Raft,Tokio 驱动 | +| Raft 存储 | **openraft-rocksstore** | RocksDB 后端,RaftLogStorage + RaftStateMachine | +| SPDK 绑定 | **async-spdk** | [madsys-dev/async-spdk](https://github.com/madsys-dev/async-spdk),原生 async/await | +| 磁带 SDK | **自研抽象层** | 前期对接 Linux SCSI(st 驱动 + MTIO ioctl),后期可扩展厂商 SDK | + +--- + +## 2. 系统架构总览 + +### 2.1 物理部署模型 + +ColdStore 由五类节点组成。**Scheduler Worker 是唯一的业务中枢**, +Gateway 仅连接 Scheduler Worker,Console 仅连接 Metadata。 + +``` +┌────────────────┐ ┌────────────────┐ +│ Gateway (N台) │ │ Console (1台) │ +│ S3 HTTP 前端 │ │ 管控面 Web UI │ +│ 无状态 │ │ 无状态 │ +└──────┬─────────┘ └──────┬─────────┘ + │ gRPC (配置: Scheduler 地址) │ gRPC (配置: Metadata 地址) + ▼ ▼ + ┌─────────────────────────────┐ ┌──────────────────────────┐ + │ 同一物理节点 │ │ Metadata 节点 (3/5 台) │ + │ ┌─────────────────────────┐ │ │ Raft 共识 + RocksDB │ + │ │ Scheduler Worker │─┼───►│ 元数据 + Worker 注册中心 │ + │ │ 业务中枢:调度编排 │ │ └──────────────────────────┘ + │ ├─────────────────────────┤ │ ▲ + │ │ Cache Worker │ │ │ 心跳 + │ │ SPDK Blobstore 缓存 │ │ ┌──────────┴───────────────┐ + │ └─────────────────────────┘ │ │ Tape Worker (独立物理节点) │ + └─────────────────────────────┘ │ 磁带驱动 /dev/nst* │ + │ 带库 /dev/sg* │ + └────────────────────────────┘ +``` + +| 节点类型 | 职责 | 数量 | 连接对象 | +|----------|------|------|----------| +| **Metadata** | Raft 共识、元数据存储、Worker 注册中心 | 3/5 | 三类 Worker 心跳;Console 管控 | +| **Scheduler Worker** | **业务中枢**:调度编排、对接全部层(与 Cache 同机) | 1~N | Metadata、Cache Worker、Tape Worker、Gateway | +| **Cache Worker** | SPDK NVMe 数据缓存(与 Scheduler 同机) | 1~N | Scheduler Worker (gRPC)、Metadata (心跳) | +| **Tape Worker** | 磁带驱动管理、数据读写(独立物理节点) | 1~N | Scheduler Worker (gRPC)、Metadata (心跳) | +| **Gateway** | S3 HTTP 接入 + 协议适配(无状态) | N | **仅 Scheduler Worker** | +| **Console** | 管控面 Web UI + Admin API(无状态) | 1 | **仅 Metadata** | + +**关键设计决策**: + +- **Gateway 不直连 Metadata**:Gateway 是纯粹的 S3 协议前端,所有业务请求(包括元数据查询) + 都通过 Scheduler Worker 代理。Gateway 配置文件中只需写 Scheduler Worker 地址。 +- **Scheduler Worker 是唯一业务中枢**:持有 MetadataClient,编排 Cache Worker 和 Tape Worker, + 接受 Gateway 的全部请求。 +- **Console 仅连 Metadata**:管控面直接查询/操作元数据集群,管理 Worker 的增删。 +- **Scheduler ↔ Cache 使用 gRPC**:虽然同机部署,仍通过 gRPC 跨网络连接, + 保持架构一致性,未来可独立扩展。 + +### 2.2 逻辑分层架构 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ S3 接入层 (Axum) — Gateway │ +│ PUT / GET / HEAD / DELETE / RestoreObject / ListBuckets ... │ +│ S3 冷归档协议适配层 │ +│ StorageClass 映射 | RestoreRequest 解析 | x-amz-restore 响应 | 错误码映射 │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ gRPC (全部请求) + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 归档/取回调度器 — Scheduler Worker(业务中枢) │ +│ 接受 Gateway 请求 │ 编排 Cache 和 Tape │ 读写 Metadata │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ │ │ + ▼ gRPC ▼ gRPC (同机) ▼ gRPC (远程) +┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ +│ 元数据集群 (Raft) │ │ 数据缓存层 (SPDK) │ │ 磁带管理层 (Tape) │ +│ Metadata 节点 │ │ Cache Worker │ │ Tape Worker │ +│ RocksDB 持久化 │ │ 用户态 NVMe 缓存 │ │ 自研 SDK + SCSI │ +└───────────────────────┘ └───────────────────────┘ └───────────────────────┘ +``` + +### 2.3 交互模式 + +**Scheduler Worker 是唯一的元数据读写入口**(Console 管控除外)。 +Gateway、Cache Worker、Tape Worker 均不直接访问 Metadata。 + +``` + Console ──管控读写──▶ Metadata + ▲ +Gateway ──全部请求──▶ Scheduler Worker ──读+写──────────┘ + │ + ├──gRPC──▶ Cache Worker (纯数据存储) + └──gRPC──▶ Tape Worker (纯硬件抽象) +``` + +| 层 | 与 Metadata 的关系 | 与 Scheduler 的关系 | 说明 | +|------|------|------|------| +| Gateway | **无直接连接** | gRPC 客户端 | 所有请求代理给 Scheduler | +| Scheduler Worker | 读 + 写 | — | 唯一业务中枢 | +| Cache Worker | 仅心跳注册 | gRPC 服务端 | 纯数据存储 | +| Tape Worker | 仅心跳注册 | gRPC 服务端 | 纯硬件抽象 | +| Console | 管控读写 | 无 | Worker 增删、元数据查询 | + +--- + +## 3. S3 冷归档协议兼容设计 + +### 3.1 协议兼容目标 + +ColdStore 是**纯冷归档系统**,设计模型与 AWS S3 Glacier Deep Archive 一致: +所有对象写入即排队归档到磁带,不提供在线热存储层。外部热存储系统在需要冷归档时调用 ColdStore 的 PutObject API,迁移决策由外部系统负责。 + +在协议层面兼容 AWS S3 Glacier 冷归档语义,使现有 S3 客户端、SDK 及业务系统无需改造即可接入。 + +### 3.2 存储类别映射 + +ColdStore 仅有两种内部存储状态: + +| S3 Storage Class | ColdStore 内部状态 | 说明 | +|------------------|-------------------|------| +| 任意(`GLACIER` / `DEEP_ARCHIVE` / `STANDARD` 等) | `ColdPending` | 写入即排队归档(统一冷存储入口) | +| 归档完成 | `Cold` | 已归档到磁带 | + +### 3.3 RestoreObject API 规范 + +#### 3.3.1 请求格式 + +``` +POST /{Bucket}/{Key}?restore&versionId={VersionId} HTTP/1.1 +Host: {endpoint} +Content-Type: application/xml + + + integer + + Expedited|Standard|Bulk + + +``` + +**关键参数:** + +| 参数 | 必填 | 说明 | +|------|------|------| +| `Days` | 是 | 解冻数据保留天数,最小 1 天 | +| `GlacierJobParameters.Tier` | 否 | 取回优先级:Expedited / Standard / Bulk | +| `versionId` | 否 | 对象版本,缺省为当前版本 | + +#### 3.3.2 取回层级 (Tier) 语义 + +| Tier | 预期完成时间 | ColdStore 实现策略 | +|------|-------------|-------------------| +| **Expedited** | 1–5 分钟 | 高优先级队列,可配置预留容量 | +| **Standard** | 3–5 小时 | 默认队列,常规调度 | +| **Bulk** | 5–12 小时 | 低优先级,批量合并取回 | + +#### 3.3.3 响应规范 + +| 场景 | HTTP 状态码 | 说明 | +|------|------------|------| +| 首次解冻请求 | `202 Accepted` | 任务已接受,等待处理 | +| 已解冻且未过期 | `200 OK` | 可延长 Days,仅更新过期时间 | +| 解冻进行中 | `409 Conflict` | 错误码 `RestoreAlreadyInProgress` | +| Expedited 容量不足 | `503 Service Unavailable` | 错误码 `GlacierExpeditedRetrievalNotAvailable` | +| 对象尚未归档 | `409 Conflict` | ColdPending 状态对象不可 Restore | + +### 3.4 HEAD Object 与 x-amz-restore 响应头 + +对冷对象执行 HEAD 时,需返回 `x-amz-restore` 头以表示解冻状态: + +``` +x-amz-restore: ongoing-request="true" +``` +或 +``` +x-amz-restore: ongoing-request="false", expiry-date="Fri, 28 Feb 2025 12:00:00 GMT" +``` + +### 3.5 GET Object 冷对象访问控制 + +| 对象状态 | GET 行为 | +|----------|----------| +| ColdPending(排队中) | 返回 `403 InvalidObjectState`,对象尚未归档 | +| Cold + 未解冻 | 返回 `403 InvalidObjectState`,提示需先 Restore | +| Cold + 解冻中 | 返回 `403 InvalidObjectState` | +| Cold + 已解冻 | 从 SPDK 缓存读取并返回 | +| Cold + 解冻已过期 | 返回 `403 InvalidObjectState`,需重新 Restore | + +### 3.6 错误码映射 + +| AWS 错误码 | HTTP 状态 | ColdStore 实现 | +|------------|-----------|----------------| +| `InvalidObjectState` | 403 | 冷对象未解冻时 GET | +| `RestoreAlreadyInProgress` | 409 | 重复 Restore 请求 | +| `GlacierExpeditedRetrievalNotAvailable` | 503 | Expedited 容量不足 | +### 3.7 归档触发模型 + +ColdStore 是纯冷归档系统(类似 AWS Glacier Deep Archive),所有对象 PutObject 写入即标记为 `ColdPending`, +自动排队等待调度器聚合写入磁带。不支持 Hot/Warm 存储类别和生命周期规则。 + +外部热存储系统在需要冷归档时,直接调用 ColdStore 的 PutObject API 写入对象。 +迁移决策(何时归档)由外部系统负责,ColdStore 不做生命周期管理。 + +--- + +## 4. 集群元数据:Raft + RocksDB 设计 + +### 4.1 设计目标 + +- **强一致性**:元数据读写通过 Raft 达成共识 +- **高可用**:多数节点存活即可服务 +- **持久化**:RocksDB 作为 Raft 日志与状态机的存储引擎 +- **可扩展**:支持 3/5/7 节点集群 + +### 4.2 架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ColdStore 元数据节点 │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Raft Core (openraft) │ │ +│ │ - databendlabs/openraft │ │ +│ │ - 异步事件驱动,Tokio 运行时 │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ RaftLogStorage / RaftStateMachine │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ openraft-rocksstore (RocksStore) │ │ +│ │ - Raft 日志 + 状态机持久化 │ │ +│ │ - RocksDB 单实例,可扩展 CF 存储业务元数据 │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.3 技术选型(已确定) + +| 组件 | 选型 | 说明 | +|------|------|------| +| Raft 实现 | **openraft** | [databendlabs/openraft](https://github.com/databendlabs/openraft),异步 Raft,Tokio 驱动 | +| Raft 存储 | **openraft-rocksstore** | [crates.io/openraft-rocksstore](https://crates.io/crates/openraft-rocksstore),RocksDB 后端 | +| 存储引擎 | RocksDB | 通过 openraft-rocksstore 封装 | + +### 4.3.1 OpenRaft 集成要点 + +- **依赖**:`openraft = "0.9"`,`openraft-rocksstore = "0.9"` +- **TypeConfig**:定义 `NodeId`、`Node`、`AppData`、`AppDataResponse` 等类型 +- **RocksRequest**:扩展 `AppData`,定义 `PutObject`、`DeleteObject`、`PutRecallTask` 等命令 +- **RocksStateMachine**:实现 `apply` 逻辑,将命令写入 RocksDB 业务 CF +- **线性读**:`raft.ensure_linearizable().await` 保证读到已提交状态 + +### 4.4 RocksDB 数据模型 + +#### 4.4.1 Column Family 设计 + +| CF 名称 | Key 格式 | Value 格式 | 说明 | +|---------|----------|-----------|------| +| `objects` | `obj:{bucket}:{key}` | ObjectMetadata (JSON/Protobuf) | 对象元数据 | +| `object_versions` | `objv:{bucket}:{key}:{version}` | ObjectMetadata | 多版本对象 | +| `bundles` | `bundle:{uuid}` | ArchiveBundle | 归档包 | +| `tapes` | `tape:{tape_id}` | TapeInfo | 磁带信息 | +| `recall_tasks` | `recall:{uuid}` | RecallTask | 取回任务 | +| `archive_tasks` | `archive:{uuid}` | ArchiveTask | 归档任务 | +| `index_bundle_objects` | `idx_b:{archive_id}:{bucket}:{key}` | 空 | 归档包→对象反向索引 | +| `index_tape_bundles` | `idx_t:{tape_id}:{bundle_id}` | 空 | 磁带→归档包索引 | + +#### 4.4.2 Raft 日志存储 (raftdb) + +- **Key**: `{log_index}` (8 bytes) +- **Value**: Raft 日志条目序列化 +- **用途**: 仅存储 Raft 共识日志,与业务 KV 分离 + +### 4.5 Raft 状态机命令(AppData / RocksRequest 扩展) + +参考 openraft-rocksstore 的 `RocksRequest` 模式,扩展 ColdStore 专用命令: + +```rust +// 实现 openraft::AppData +#[derive(Clone, Debug, Serialize, Deserialize)] +enum ColdStoreRequest { + PutObject(ObjectMetadata), + DeleteObject { bucket: String, key: String }, + UpdateStorageClass { bucket: String, key: String, class: StorageClass }, + PutArchiveBundle(ArchiveBundle), + PutTape(TapeInfo), + PutRecallTask(RecallTask), + UpdateRecallTask { id: Uuid, status: RestoreStatus }, + PutArchiveTask(ArchiveTask), +} +``` + +可基于 `openraft-rocksstore` 的 `RocksStore` 进行扩展,或实现自定义 `RaftStorage` 以支持上述业务命令。 + +### 4.6 读写路径 + +| 操作类型 | 路径 | +|----------|------| +| **写** | Client → Leader → Raft Propose → 多数派 Append → Apply → RocksDB Write | +| **读** | Client → 任意节点 → 本地 RocksDB Read(需保证 ReadYourWrites 时可选走 Leader) | +| **线性读** | 通过 Raft 的 ReadIndex 保证读到已提交状态 | + +### 4.7 集群部署 + +- **推荐配置**:3 或 5 节点 +- **Leader 选举**:Raft 自动选举 +- **网络**:节点间 gRPC 或自定义 RPC +- **持久化路径**:`/var/lib/coldstore/metadata/{raftdb|kvdb}` + +### 4.8 与现有代码集成 + +- 新增 `MetadataBackend::RaftRocksDB` 枚举变体 +- `MetadataService` 通过 Raft 客户端代理写请求 +- 读请求可直接访问本地 RocksDB(Follower 只读) + +--- + +## 5. 数据缓存层:async-spdk 设计 + +### 5.1 设计目标 + +- **高性能**:用户态 I/O,绕过内核,降低延迟 +- **高吞吐**:Poll 模式、零拷贝,充分发挥 NVMe 性能 +- **解冻数据缓存**:承载从磁带取回的数据,供 GET 快速响应 +- **原生 async/await**:与 Tokio 无缝集成,无需回调桥接 + +### 5.2 技术选型:async-spdk + SPDK Blob + +采用 [madsys-dev/async-spdk](https://github.com/madsys-dev/async-spdk): + +- **异步 Rust 绑定**:原生 `async`/`await`,基于 Tokio +- **存储方式**:**SPDK Blobstore (Blob)**,非 raw bdev;Blobstore 构建于 bdev 之上 +- **优势**:Blob 提供持久化块分配、xattrs 元数据、thin provisioning,适合对象缓存 + +### 5.3 架构概览 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ColdStore 缓存服务进程 │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Cache Manager (Rust) │ │ +│ │ - cache_key → blob_id 索引 │ │ +│ │ - LRU/LFU/TTL 淘汰策略 │ │ +│ │ - 容量与并发控制 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ async-spdk (SPDK Blobstore / Blob API) │ │ +│ │ - 1 对象 = 1 Blob,xattrs 存 bucket/key/expire_at │ │ +│ │ - blob_write / blob_read 按 page 读写 │ │ +│ │ - 构建于 bdev 之上 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ SPDK Bdev (Blobstore 底层) │ │ +│ │ - Malloc0: 测试用内存 bdev │ │ +│ │ - Nvme0n1: 生产 NVMe 设备 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.4 集成方式与启动模型 + +async-spdk 采用 **SPDK 事件循环 + Tokio** 的混合模型: + +```rust +use async_spdk::{event::AppOpts, bdev::*, env}; + +fn main() { + event::AppOpts::new() + .name("coldstore_cache") + .config_file(&std::env::args().nth(1).expect("config file")) + .block_on(async_main()) + .unwrap(); +} + +async fn async_main() -> Result<()> { + // Blobstore 方式:先加载/初始化 Blobstore,再通过 Blob API 读写 + // let bs = Blobstore::load("Malloc0").await?; + // let blob_id = bs.create_blob().await?; + // bs.blob_write(blob_id, offset_pages, buf).await?; + // bs.blob_read(blob_id, offset_pages, buf).await?; + app_stop(); + Ok(()) +} +``` + +**注意**:ColdStore 主进程若已使用 Tokio(Axum),需将 async-spdk 作为**独立子模块**或**专用线程**运行,因 SPDK 需独占 reactor。可选方案: + +- **方案 A**:缓存服务作为独立二进制,通过 gRPC/HTTP 与主服务通信 +- **方案 B**:主进程在 SPDK `block_on` 内启动 Axum(需调整启动顺序) +- **方案 C**:缓存层先使用文件后端,待架构稳定后再接入 async-spdk + +### 5.5 SPDK JSON 配置示例 + +**测试用 Malloc bdev**(`cache_spdk.json`): + +```json +{ + "subsystems": [ + { + "subsystem": "bdev", + "config": [ + { + "method": "bdev_malloc_create", + "params": { + "name": "Malloc0", + "num_blocks": 32768, + "block_size": 512 + } + } + ] + } + ] +} +``` + +**生产 NVMe**:根据 [async-spdk README](https://github.com/madsys-dev/async-spdk),需配置对应 bdev 名称(如 `Nvme0n1`),并确保: + +- 以 root 运行 +- 大页内存:`echo "1024" > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages` +- SPDK 编译:`./configure --without-isal` 可避免部分 isa-l 错误 + +### 5.6 缓存数据布局(SPDK Blob 方式) + +| 层级 | 存储介质 | 用途 | +|------|----------|------| +| L1 | NVMe SSD (SPDK Blobstore on bdev) | 热解冻数据,低延迟 | +| L2 (可选) | 文件系统 | 兼容旧实现,温数据 | + +对象与 Blob 的对应: + +- **1 对象 = 1 Blob**:每个解冻对象对应一个 Blobstore Blob +- **元数据**:Blob xattrs 存储 bucket、key、size、checksum、expire_at +- **索引**:自维护 `cache_key → blob_id` 映射(Blobstore 不提供按名查找) +- **读写单位**:Page(4KB),Cluster(1MB)为分配单元 + +### 5.7 缓存策略 + +| 策略 | 说明 | +|------|------| +| **LRU** | 淘汰最久未访问对象 | +| **LFU** | 淘汰访问频率最低对象 | +| **TTL** | 按解冻过期时间淘汰 | +| **容量** | 总容量上限,超限触发淘汰 | + +### 5.8 配置项 + +```yaml +cache: + backend: "spdk" # spdk | file (兼容旧实现) + spdk: + enabled: true + config_file: "/etc/coldstore/cache_spdk.json" + bdev_name: "Malloc0" # 测试用; 生产改为 "Nvme0n1" + max_size_gb: 100 + block_size: 4096 + ttl_secs: 86400 + eviction_policy: "Lru" +``` + +### 5.9 Cargo 依赖 + +```toml +[dependencies] +async-spdk = { git = "https://github.com/madsys-dev/async-spdk" } +# 或发布到 crates.io 后: async-spdk = "0.1" +``` + +--- + +## 6. 磁带管理层:自研 SDK 抽象层 + +### 6.1 设计目标 + +- **自研 SDK 抽象层**:不依赖厂商闭源 SDK,统一磁带、驱动、库的访问接口 +- **前期对接 Linux SCSI**:基于 Linux 内核 SCSI 磁带驱动(st/ sg)实现 +- **可扩展**:后期可替换为厂商 SDK、LTFS 等实现,无需改动上层调度逻辑 + +### 6.2 架构分层 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ TapeManager (调度层) │ +│ - 磁带库抽象、驱动调度、归档包聚合、取回合并 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Tape SDK 抽象层 (自研) │ +│ - TapeDrive trait: read, write, seek, status, eject │ +│ - TapeLibrary trait: list_slots, load, unload, inventory │ +│ - 与具体实现解耦 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 实现层 (前期) │ +│ Linux SCSI: st 驱动 (/dev/nst0) + MTIO ioctl │ +│ - 顺序读写、定位、状态查询 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 6.3 前期实现:Linux SCSI 协议 + +#### 6.3.1 设备与接口 + +| 接口 | 设备节点 | 说明 | +|------|----------|------| +| **st 驱动** | `/dev/nst0` | 非自动回卷,推荐用于顺序读写(避免每次操作回卷) | +| **st 驱动** | `/dev/st0` | 自动回卷,操作后回卷到 BOT | +| **SCSI Generic** | `/dev/sg*` | 透传 SCSI 命令,用于高级控制(可选) | + +#### 6.3.2 MTIO 控制接口 + +基于 ``,主要 ioctl: + +| ioctl | 用途 | +|-------|------| +| `MTIOCTOP` | 执行磁带操作:`MTFSF`(前进文件)、`MTBSF`(后退文件)、`MTFSR`(前进记录)、`MTBSR`(后退记录)、`MTREW`(回卷)、`MTEOM`(到卷尾) | +| `MTIOCGET` | 获取驱动状态 | +| `MTIOCPOS` | 获取当前磁带位置 | + +#### 6.3.3 数据读写 + +- **块模式**:可变块或固定块(通过 ioctl 设置) +- **读写**:标准 `read()` / `write()` 系统调用 +- **顺序性**:磁带必须顺序写入,随机读需 seek 后再读 + +#### 6.3.4 依赖与工具 + +- `mt-st`:mt 命令,用于控制磁带(可选,SDK 自实现 ioctl) +- 内核:`st` 模块(`modprobe st`) +- 权限:设备节点需可读写(通常 root 或 tape 组) + +### 6.4 SDK 抽象接口定义 + +```rust +/// 磁带驱动抽象 +pub trait TapeDrive: Send + Sync { + fn drive_id(&self) -> &str; + async fn read(&self, offset: u64, length: u64) -> Result>; + async fn write(&self, data: &[u8]) -> Result<()>; + async fn seek(&self, position: u64) -> Result<()>; + async fn status(&self) -> Result; + async fn rewind(&self) -> Result<()>; + async fn eject(&self) -> Result<()>; +} + +/// 磁带库抽象(带机械臂的库,后期扩展) +pub trait TapeLibrary: Send + Sync { + async fn list_slots(&self) -> Result>; + async fn load(&self, slot_id: &str, drive_id: &str) -> Result<()>; + async fn unload(&self, drive_id: &str) -> Result<()>; + async fn inventory(&self) -> Result>; +} + +/// Linux SCSI 实现(前期) +pub struct ScsiTapeDrive { + device_path: PathBuf, // e.g. /dev/nst0 + // ... +} +impl TapeDrive for ScsiTapeDrive { ... } +``` + +### 6.5 实现路径 + +| 阶段 | 实现 | 说明 | +|------|------|------| +| **前期** | `ScsiTapeDrive` | 基于 st 驱动 + MTIO,单驱动直连 | +| **中期** | `ScsiTapeLibrary` | 带库场景,通过 sg 或厂商 MMC 协议控制机械臂 | +| **后期** | `VendorTapeDrive` | 可选对接厂商 SDK(如 IBM、HP 等) | + +### 6.6 配置项 + +```yaml +tape: + sdk: + backend: "scsi" # scsi | vendor (后期) + scsi: + device: "/dev/nst0" + block_size: 262144 # 256KB,LTO 常用 + buffer_size_mb: 64 + library_path: null # 带库场景时配置 + supported_formats: + - "LTO-9" + - "LTO-10" +``` + +### 6.7 模块结构 + +``` +src/tape/ +├── mod.rs +├── sdk/ # 自研 SDK 抽象层 +│ ├── mod.rs +│ ├── drive.rs # TapeDrive trait +│ ├── library.rs # TapeLibrary trait (可选) +│ └── types.rs # DriveStatus, SlotInfo 等 +├── scsi/ # Linux SCSI 实现 +│ ├── mod.rs +│ ├── drive.rs # ScsiTapeDrive +│ └── mtio.rs # MTIO ioctl 封装 +├── manager.rs # TapeManager +└── driver.rs # 兼容旧接口,委托给 sdk +``` + +--- + +## 7. 模块交互与数据流 + +### 7.1 Restore 完整流程 + +``` +1. Client: POST /bucket/key?restore + RestoreRequest +2. S3 Handler: 解析 Days/Tier,校验对象为 Cold +3. Metadata (Raft): 检查是否已有进行中 Restore,写入 RecallTask +4. Recall Scheduler: 按 archive_id 合并任务,调度磁带读取 +5. Tape: 顺序读取 → 数据块 +6. Cache (SPDK): 写入 SPDK bdev,更新索引 +7. Metadata (Raft): 更新 ObjectMetadata.restore_status=Completed, restore_expire_at +8. Client: HEAD 可见 x-amz-restore: expiry-date="..." +9. Client: GET 从 SPDK 缓存返回数据 +``` + +### 7.2 Archive 完整流程 + +``` +1. PutObject → 对象直接标记 ColdPending(写入即归档) +2. Archive Scheduler: 扫描 ColdPending,按策略聚合为 ArchiveBundle +3. Tape: 顺序写入磁带 +4. Metadata (Raft): 写入 ArchiveBundle, 更新 ObjectMetadata (Cold, archive_id, tape_id) +5. 清理缓存层临时数据 +``` + +--- + +## 8. 非功能性设计 + +### 8.1 高可用 + +- 元数据:Raft 3/5 节点,多数派存活即可 +- 缓存:单点故障时,解冻数据需重新从磁带取回(可接受) +- S3 接入:无状态,可多实例 + 负载均衡 + +### 8.2 可观测性 + +- 日志:tracing + structured logging +- 指标:归档/取回队列长度、缓存命中率、Raft 提交延迟 +- 追踪:关键路径分布式 trace + +### 8.3 安全 + +- S3 签名 v4 认证 +- 传输加密 TLS +- 元数据集群节点间 mTLS(可选) + +--- + +## 9. 实施路线图 + +| 阶段 | 内容 | +|------|------| +| **Phase 1** | S3 RestoreObject 协议完整实现,错误码与 x-amz-restore 头 | +| **Phase 2** | Raft + RocksDB 元数据集群,替换 Postgres/Etcd | +| **Phase 3** | SPDK 缓存层集成,替换文件系统缓存 | +| **Phase 4** | 磁带 SDK 抽象层 + Linux SCSI 实现(ScsiTapeDrive) | +| **Phase 5** | 性能调优、生产就绪 | + +--- + +## 10. Cargo 依赖汇总 + +```toml +[dependencies] +# 元数据集群 +openraft = "0.9" +openraft-rocksstore = "0.9" + +# 数据缓存层 +async-spdk = { git = "https://github.com/madsys-dev/async-spdk" } + +# 现有依赖保持不变 +tokio = { version = "1.35", features = ["full"] } +axum = "0.7" +rocksdb = "0.22" # openraft-rocksstore 会引入,可按需显式指定版本 +``` + +--- + +## 11. 参考资料 + +- [AWS S3 RestoreObject API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_RestoreObject.html) +- [S3 Glacier Retrieval Options](https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects-retrieval-options.html) +- [OpenRaft](https://github.com/databendlabs/openraft) - databendlabs/openraft +- [OpenRaft RocksStore](https://crates.io/crates/openraft-rocksstore) +- [async-spdk](https://github.com/madsys-dev/async-spdk) - madsys-dev/async-spdk +- [SPDK Documentation](https://spdk.io/doc/) +- [Linux st(4) - SCSI tape device](https://man7.org/linux/man-pages/man4/st.4.html) +- [Linux SCSI tape driver (kernel.org)](https://kernel.org/doc/Documentation/scsi/st.txt) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b8ca9fc --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +# ColdStore 设计文档 + +## 文档索引 + +| 文档 | 说明 | +|------|------| +| [DESIGN.md](./DESIGN.md) | **总架构设计** - 协议、元数据集群、缓存层、磁带层完整设计 | +| [modules/](./modules/) | **模块设计** - 按架构分层拆分的独立设计文档 | + +## 模块设计文档 + +| 模块 | 文档 | +|------|------| +| 接入层 | [01-access-layer.md](./modules/01-access-layer.md) | +| 协议适配层 | [02-protocol-adapter.md](./modules/02-protocol-adapter.md) | +| 元数据集群 | [03-metadata-cluster.md](./modules/03-metadata-cluster.md) | +| 数据缓存层 | [04-cache-layer.md](./modules/04-cache-layer.md) | +| 归档取回调度层 | [05-scheduler-layer.md](./modules/05-scheduler-layer.md) | +| 磁带管理层 | [06-tape-layer.md](./modules/06-tape-layer.md) | + +## 设计要点摘要 + +- **协议**:兼容 S3 Glacier 冷归档协议(RestoreObject、x-amz-restore、取回层级) +- **元数据**:[OpenRaft](https://github.com/databendlabs/openraft) + [openraft-rocksstore](https://crates.io/crates/openraft-rocksstore),强一致性集群 +- **缓存**:[async-spdk](https://github.com/madsys-dev/async-spdk) 用户态 NVMe 缓存,原生 async/await +- **磁带**:自研 SDK 抽象层,前期对接 Linux SCSI(st 驱动 + MTIO) + +详见 [DESIGN.md](./DESIGN.md) 与 [modules/README.md](./modules/README.md)。 diff --git a/docs/modules/01-access-layer.md b/docs/modules/01-access-layer.md new file mode 100644 index 0000000..f501d87 --- /dev/null +++ b/docs/modules/01-access-layer.md @@ -0,0 +1,122 @@ +# 接入层模块设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) + +## 1. 模块概述 + +接入层是 ColdStore 对外提供对象存储服务的入口,负责接收并分发 S3 兼容的 HTTP 请求。 + +### 1.1 职责 + +- 提供 HTTP 服务,监听 S3 端点 +- 路由解析:将请求分发到对应 Handler +- 请求/响应透传与基础校验 +- 与协议适配层、元数据、缓存、调度器协同 + +### 1.2 在架构中的位置 + +``` + ┌─────────────────────────────────────┐ + │ S3 接入层 (本模块) │ + │ PUT / GET / HEAD / RestoreObject │ + │ ListBuckets / DeleteObject │ + └─────────────────────────────────────┘ + │ + ▼ + 协议适配层 / 元数据 / 缓存 / 调度器 +``` + +--- + +## 2. 技术选型 + +| 组件 | 选型 | 说明 | +|------|------|------| +| HTTP 框架 | **Axum** | 异步、类型安全、与 Tokio 集成 | +| 运行时 | **Tokio** | 异步运行时 | +| 中间件 | **Tower** / **tower-http** | 中间件栈、CORS、trace | + +--- + +## 3. 接口清单 + +### 3.1 对象操作 + +| 方法 | 路径 | 说明 | +|------|------|------| +| PUT | `/{bucket}/{key}` | 上传对象 | +| GET | `/{bucket}/{key}` | 下载对象 | +| HEAD | `/{bucket}/{key}` | 获取对象元数据 | +| DELETE | `/{bucket}/{key}` | 删除对象 | +| POST | `/{bucket}/{key}?restore` | 解冻冷对象(RestoreObject) | + +### 3.2 桶操作 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/` | ListBuckets | +| PUT | `/{bucket}` | CreateBucket | +| GET | `/{bucket}` | ListObjects | +| DELETE | `/{bucket}` | DeleteBucket | + +### 3.3 路由规则 + +- 路径参数:`bucket`、`key`(支持多级 key) +- 查询参数:`versionId`、`restore` 等 +- 虚拟主机风格与路径风格均需支持(可配置) + +--- + +## 4. 模块结构 + +``` +src/ +├── main.rs # 启动 Axum,加载配置 +├── s3/ +│ ├── mod.rs +│ ├── server.rs # Axum Router 定义、路由注册 +│ ├── handler.rs # 请求处理入口,委托给协议适配/业务逻辑 +│ └── routes.rs # 路由常量、路径解析 +``` + +--- + +## 5. 依赖关系 + +| 依赖模块 | 通信方式 | 用途 | +|----------|----------|------| +| 协议适配层 | 本地调用(同进程) | 解析 RestoreRequest、生成 x-amz-restore 响应、错误码映射 | +| Scheduler Worker | gRPC(配置地址) | **全部**业务请求代理(PUT/GET/HEAD/DELETE/Restore/List) | + +> Gateway **不连接 Metadata**,也**不连接 Cache Worker 和 Tape Worker**。 +> 所有业务流量都通过 Scheduler Worker 中转。Gateway 是纯粹的 S3 HTTP → gRPC 协议前端。 + +--- + +## 6. 配置项 + +```yaml +gateway: + s3: + host: "0.0.0.0" + port: 9000 + path_style: true + + # 仅需配置 Scheduler Worker 地址 + scheduler_addrs: + - "10.0.2.1:22001" + - "10.0.2.2:22001" + + # 多 Scheduler Worker 时的路由策略 + routing: + strategy: "round_robin" # round_robin | hash | least_pending +``` + +--- + +## 7. 非功能性要求 + +- **无状态**:可水平扩展,多实例 + 负载均衡 +- **可观测**:请求 trace、耗时、错误率 +- **安全**:支持 S3 签名 v4、TLS diff --git a/docs/modules/02-protocol-adapter.md b/docs/modules/02-protocol-adapter.md new file mode 100644 index 0000000..6a24ad4 --- /dev/null +++ b/docs/modules/02-protocol-adapter.md @@ -0,0 +1,157 @@ +# 协议适配层模块设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) + +## 1. 模块概述 + +协议适配层负责将 S3 冷归档(Glacier)协议语义转换为 ColdStore 内部模型,并生成符合 S3 规范的响应。 + +### 1.1 职责 + +- StorageClass 映射(S3 ↔ 内部状态) +- RestoreRequest 解析(Days、Tier) +- x-amz-restore 响应头生成 +- 错误码映射(InvalidObjectState、RestoreAlreadyInProgress 等) +- PutObject 默认 ColdPending 语义 + +### 1.2 在架构中的位置 + +``` +S3 接入层 ──► 协议适配层 ──► Scheduler Worker(业务中枢) + │ + ├─ StorageClass 映射 + ├─ RestoreRequest 解析 + ├─ x-amz-restore 响应 + └─ 错误码映射 +``` + +--- + +## 2. StorageClass 映射 + +ColdStore 是纯冷归档系统,所有对象写入即为 `ColdPending`,归档后变为 `Cold`。与 AWS S3 Glacier Deep Archive 模型一致。 + +| S3 Storage Class | ColdStore 内部 | 说明 | +|------------------|----------------|------| +| `GLACIER` / `DEEP_ARCHIVE` / `STANDARD` / 任意 | `ColdPending` | 写入即排队归档(不区分热/冷,统一冷存储) | +| 归档完成 | `Cold` | 已归档到磁带 | + +--- + +## 3. RestoreObject 协议适配 + +### 3.1 请求解析 + +**输入**:`POST /{bucket}/{key}?restore` + XML Body + +```xml + + 7 + + Standard + + +``` + +**输出**(内部结构): + +```rust +pub struct RestoreRequest { + pub days: u32, // 最小 1 + pub tier: RestoreTier, // Expedited | Standard | Bulk + pub version_id: Option, +} +``` + +### 3.2 取回层级 (Tier) 映射 + +| Tier | 内部优先级 | 调度策略 | +|------|------------|----------| +| Expedited | 高 | 高优先级队列,可配置预留容量 | +| Standard | 中 | 默认队列 | +| Bulk | 低 | 批量合并取回 | + +### 3.3 响应规范 + +| 场景 | HTTP 状态 | 说明 | +|------|-----------|------| +| 首次解冻 | 202 Accepted | 任务已接受 | +| 已解冻且未过期 | 200 OK | 可延长 Days | +| 解冻进行中 | 409 Conflict | RestoreAlreadyInProgress | +| Expedited 容量不足 | 503 Service Unavailable | GlacierExpeditedRetrievalNotAvailable | +| 对象尚未归档(ColdPending) | 409 Conflict | 尚在归档队列中,不可 Restore | + +--- + +## 4. x-amz-restore 响应头 + +对冷对象执行 HEAD 时,根据解冻状态返回: + +| 状态 | 响应头 | +|------|--------| +| 解冻中 | `x-amz-restore: ongoing-request="true"` | +| 已解冻 | `x-amz-restore: ongoing-request="false", expiry-date="Fri, 28 Feb 2025 12:00:00 GMT"` | +| 未解冻 | 不返回该头(或可省略) | + +--- + +## 5. 错误码映射 + +| AWS 错误码 | HTTP 状态 | 触发条件 | +|------------|-----------|----------| +| InvalidObjectState | 403 | 冷对象未解冻时 GET | +| RestoreAlreadyInProgress | 409 | 重复 Restore 请求 | +| GlacierExpeditedRetrievalNotAvailable | 503 | Expedited 容量不足 | +| ObjectNotYetArchived | 409 | ColdPending 状态对象执行 Restore(尚未归档完成) | + +--- + +## 6. GET Object 冷对象访问控制 + +| 对象状态 | GET 行为 | +|----------|----------| +| Cold + 未解冻 | 403 InvalidObjectState | +| Cold + 解冻中 | 403 InvalidObjectState | +| Cold + 已解冻 | 从缓存读取并返回 | +| Cold + 解冻已过期 | 403 InvalidObjectState | + +--- + +## 7. PutObject 语义 + +ColdStore 作为纯冷归档系统,PutObject 写入的对象一律标记为 `ColdPending`, +由调度器扫描后自动归档到磁带。外部热存储系统在需要归档时直接调用 ColdStore 的 PutObject。 + +--- + +## 8. 模块结构 + +``` +src/ +├── s3/ +│ ├── protocol/ # 协议适配(可独立子模块) +│ │ ├── mod.rs +│ │ ├── storage_class.rs # StorageClass 映射 +│ │ ├── restore.rs # RestoreRequest 解析、响应生成 +│ │ ├── errors.rs # 错误码映射 +│ │ └── put_object.rs # PutObject → ColdPending 语义 +``` + +--- + +## 9. 依赖关系 + +| 依赖 | 用途 | +|------|------| +| Scheduler Worker (gRPC) | 全部业务请求:元数据查询、数据读写、Restore 任务提交 | + +> 协议适配层运行在 Gateway 进程内,与接入层同进程。 +> 所有对外通信都经由 Scheduler Worker,不直连 Metadata/Cache/Tape。 + +--- + +## 10. 参考资料 + +- [AWS S3 RestoreObject API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_RestoreObject.html) +- [S3 Glacier Retrieval Options](https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects-retrieval-options.html) diff --git a/docs/modules/03-metadata-cluster.md b/docs/modules/03-metadata-cluster.md new file mode 100644 index 0000000..5bb35e8 --- /dev/null +++ b/docs/modules/03-metadata-cluster.md @@ -0,0 +1,1163 @@ +# 元数据集群模块设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) + +## 1. 模块概述 + +元数据集群提供强一致性的集群元数据存储,基于 OpenRaft + RocksDB 实现,是 ColdStore 的"控制中枢"。 + +元数据分为两大类: + +| 类别 | 内容 | 说明 | +|------|------|------| +| **集群元数据** | 节点信息、集群拓扑、Raft 状态 | 集群自身管理 | +| **归档元数据** | 对象、桶、归档包、磁带、任务 | 业务数据管理 | + +### 1.1 职责 + +- 对象元数据:bucket、key、storage_class、archive_id、tape_id、restore_status 等 +- 桶管理:桶创建/删除 +- 归档包、磁带、取回任务、归档任务索引 +- 集群管理:节点注册、Raft 领导选举、成员变更 +- 强一致性读写、高可用 + +### 1.2 在架构中的位置 + +``` + Console ──管控读写──▶ Metadata + ▲ +Gateway ──全部请求──▶ Scheduler Worker ──读+写──────┘ + │ + ├──gRPC──▶ Cache Worker (纯数据) + └──gRPC──▶ Tape Worker (纯硬件) +``` + +> Gateway 不直连 Metadata。Scheduler Worker 是唯一的元数据业务读写入口。 + +--- + +## 2. 技术选型 + +| 组件 | 选型 | 说明 | +|------|------|------| +| Raft 共识 | **openraft** | [databendlabs/openraft](https://github.com/databendlabs/openraft) | +| 存储后端 | **openraft-rocksstore** | RocksDB 后端 | +| 存储引擎 | RocksDB | LSM-tree,高吞吐 | + +--- + +## 3. 核心数据结构 + +### 3.1 ObjectMetadata(对象元数据) + +系统中最核心的结构,记录每个 S3 对象的完整状态。 + +```rust +pub struct ObjectMetadata { + pub bucket: String, + pub key: String, + pub version_id: Option, + pub size: u64, + pub checksum: String, + pub content_type: Option, + pub etag: Option, + pub storage_class: StorageClass, + pub archive_id: Option, + pub tape_id: Option, + pub tape_set: Option>, + pub tape_block_offset: Option, + pub restore_status: Option, + pub restore_expire_at: Option>, + pub created_at: DateTime, + pub updated_at: DateTime, +} +``` + +| 字段 | 类型 | 含义 | 写入方 | 消费方 | +|------|------|------|--------|--------| +| `bucket` | String | S3 桶名 | 接入层 PutObject | 全部 | +| `key` | String | S3 对象键 | 接入层 PutObject | 全部 | +| `version_id` | Option\ | 对象版本 ID | 接入层 PutObject | 协议层、调度层 | +| `size` | u64 | 对象字节数 | 接入层 PutObject | 调度层(聚合计算)、协议层(Content-Length) | +| `checksum` | String | SHA256 hex | 接入层 PutObject | 调度层(写磁带/校验)、缓存层(透传) | +| `content_type` | Option\ | MIME 类型 | 接入层 PutObject | 协议层 GET 响应头、调度层传给缓存层 | +| `etag` | Option\ | S3 ETag | 接入层 PutObject | 协议层 GET 响应头、调度层传给缓存层 | +| `storage_class` | StorageClass | 存储类别 | 接入层 PutObject / 调度层归档完成 | 协议层(判断是否可 GET)、调度层(扫描 ColdPending) | +| `archive_id` | Option\ | 所属 ArchiveBundle ID | 调度层归档完成 | 调度层取回时定位 Bundle | +| `tape_id` | Option\ | 主副本所在磁带 ID | 调度层归档完成 | 调度层取回时定位磁带 | +| `tape_set` | Option\\> | 所有副本磁带列表 | 调度层归档完成 | 调度层副本切换 | +| `tape_block_offset` | Option\ | 对象在磁带上的块偏移 | 调度层归档完成 | 调度层取回时 seek 定位 | +| `restore_status` | Option\ | 解冻状态 | 调度层取回流程 | 协议层(判断 GET/HEAD 响应) | +| `restore_expire_at` | Option\\> | 解冻过期时间 | 调度层取回完成 | 协议层(x-amz-restore 头)、缓存层(TTL) | +| `created_at` | DateTime\ | 创建时间 | 接入层 PutObject | 调度层(聚合排序) | +| `updated_at` | DateTime\ | 最后更新时间 | 每次写入 | 审计 | + +### 3.2 枚举类型 + +```rust +pub enum StorageClass { + ColdPending, // 待归档(PutObject 写入即标记,等待调度器扫描写入磁带) + Cold, // 已归档到磁带 +} + +pub enum RestoreStatus { + Pending, // 已入队,等待调度 + WaitingForMedia, // 磁带离线,等待人工上线 + InProgress, // 正在从磁带读取 + Completed, // 已写入缓存,可 GET + Expired, // 解冻过期 + Failed, // 取回失败 +} + +pub enum RestoreTier { + Expedited, // 加急:1-5 分钟 + Standard, // 标准:3-5 小时 + Bulk, // 批量:5-12 小时 +} +``` + +### 3.3 ArchiveBundle(归档包) + +与 05-scheduler-layer §8.3 定义一致。 + +```rust +pub struct ArchiveBundle { + pub id: Uuid, + pub tape_id: String, + pub tape_set: Vec, + pub entries: Vec, + pub total_size: u64, + pub filemark_start: u32, + pub filemark_end: u32, + pub checksum: Option, + pub status: ArchiveBundleStatus, + pub created_at: DateTime, + pub completed_at: Option>, +} +``` + +| 字段 | 类型 | 含义 | 写入方 | +|------|------|------|--------| +| `id` | Uuid | 归档包唯一标识 | 调度层 | +| `tape_id` | String | 主副本磁带 ID | 调度层 | +| `tape_set` | Vec\ | 所有副本磁带 | 调度层 | +| `entries` | Vec\ | 包内对象列表与偏移 | 调度层 | +| `total_size` | u64 | 总字节数 | 调度层 | +| `filemark_start` | u32 | 起始 FileMark | 调度层(从 WriteResult 获取) | +| `filemark_end` | u32 | 结束 FileMark | 调度层 | +| `checksum` | Option\ | 整包 SHA256 | 调度层(可选) | +| `status` | ArchiveBundleStatus | 状态 | 调度层 | +| `created_at` | DateTime\ | 创建时间 | 调度层 | +| `completed_at` | Option\\> | 完成时间 | 调度层 | + +```rust +pub struct BundleEntry { + pub bucket: String, + pub key: String, + pub version_id: Option, + pub size: u64, + pub offset_in_bundle: u64, + pub tape_block_offset: u64, + pub checksum: String, +} + +pub enum ArchiveBundleStatus { + Pending, Writing, Completed, Failed, +} +``` + +### 3.4 TapeInfo(磁带信息) + +与 06-tape-layer §4.6 定义一致。由调度层写入元数据。 + +```rust +pub struct TapeInfo { + pub id: String, + pub barcode: Option, + pub format: String, + pub status: TapeStatus, + pub location: Option, + pub capacity_bytes: u64, + pub used_bytes: u64, + pub remaining_bytes: u64, + pub archive_bundles: Vec, + pub last_verified_at: Option>, + pub error_count: u32, + pub registered_at: DateTime, +} +``` + +| 字段 | 类型 | 含义 | 写入方 | +|------|------|------|--------| +| `id` | String | 磁带唯一标识 | 调度层(注册时) | +| `barcode` | Option\ | 条码 | 调度层 | +| `format` | String | "LTO-9"、"LTO-10" | 调度层 | +| `status` | TapeStatus | Online/Offline/Error/Retired | 调度层 | +| `location` | Option\ | 位置描述 | 调度层 | +| `capacity_bytes` | u64 | 总容量 | 调度层 | +| `used_bytes` | u64 | 已使用 | 调度层(每次写入后更新) | +| `remaining_bytes` | u64 | 剩余 | 调度层 | +| `archive_bundles` | Vec\ | 已写入的归档包列表 | 调度层 | +| `last_verified_at` | Option\\> | 最近校验时间 | 调度层 | +| `error_count` | u32 | 累计错误次数 | 调度层 | +| `registered_at` | DateTime\ | 注册时间 | 调度层 | + +```rust +pub enum TapeStatus { + Online, Offline, Error, Retired, Unknown, +} +``` + +### 3.5 RecallTask(取回任务) + +与 05-scheduler-layer §8.7 定义一致。 + +```rust +pub struct RecallTask { + pub id: Uuid, + pub bucket: String, + pub key: String, + pub version_id: Option, + pub archive_id: Uuid, + pub tape_id: String, + pub tape_set: Vec, + pub tape_block_offset: u64, + pub object_size: u64, + pub checksum: String, + pub tier: RestoreTier, + pub days: u32, + pub expire_at: DateTime, + pub status: RestoreStatus, + pub drive_id: Option, + pub retry_count: u32, + pub created_at: DateTime, + pub started_at: Option>, + pub completed_at: Option>, + pub error: Option, +} +``` + +### 3.6 ArchiveTask(归档任务) + +与 05-scheduler-layer §8.6 定义一致。 + +```rust +pub struct ArchiveTask { + pub id: Uuid, + pub bundle_id: Uuid, + pub tape_id: String, + pub drive_id: Option, + pub object_count: u32, + pub total_size: u64, + pub bytes_written: u64, + pub status: ArchiveTaskStatus, + pub retry_count: u32, + pub created_at: DateTime, + pub started_at: Option>, + pub completed_at: Option>, + pub error: Option, +} + +pub enum ArchiveTaskStatus { + Pending, InProgress, Completed, Failed, +} +``` + +### 3.7 BucketInfo(桶信息) + +```rust +pub struct BucketInfo { + pub name: String, + pub created_at: DateTime, + pub owner: Option, + pub versioning_enabled: bool, + pub object_count: u64, + pub total_size: u64, +} +``` + +| 字段 | 类型 | 含义 | 写入方 | +|------|------|------|--------| +| `name` | String | 桶名 | 接入层 CreateBucket | +| `created_at` | DateTime\ | 创建时间 | 接入层 | +| `owner` | Option\ | 拥有者 | 接入层 | +| `versioning_enabled` | bool | 是否启用多版本 | 接入层 | +| `object_count` | u64 | 对象数 | 写入时增减 | +| `total_size` | u64 | 总大小 | 写入时增减 | + +### 3.8 ListObjectsResult + +```rust +pub struct ListObjectsResult { + pub objects: Vec, + pub next_marker: Option, + pub is_truncated: bool, +} +``` + +| 字段 | 类型 | 含义 | +|------|------|------| +| `objects` | Vec\ | 当前页对象列表 | +| `next_marker` | Option\ | 分页游标(用于下次请求的 marker) | +| `is_truncated` | bool | 是否还有更多结果 | + +### 3.10 集群元数据与部署模型 + +#### 3.10.1 物理部署模型 + +ColdStore 由五类节点组成。**Scheduler Worker 是唯一业务中枢**:Gateway 仅连 Scheduler, +Console 仅连 Metadata,三类 Worker 向 Metadata 注册并上报心跳。 + +``` +┌────────────────┐ ┌────────────────┐ +│ Gateway (N台) │ │ Console (1台) │ +│ S3 HTTP 前端 │ │ 管控面 Web UI │ +│ 无状态 │ │ 无状态 │ +└──────┬─────────┘ └──────┬─────────┘ + │ 配置: scheduler_addrs │ 配置: metadata_addrs + ▼ ▼ + ┌─────────────────────────────┐ ┌──────────────────────────┐ + │ 同一物理节点 │ │ Metadata 节点 (3/5 台) │ + │ ┌─────────────────────────┐ │ │ Raft 共识 + RocksDB │ + │ │ Scheduler Worker │─┼──►│ 元数据 + Worker 注册中心 │ + │ │ 业务中枢:调度编排 │ │ └─────────────────────────┘ + │ ├─────────────────────────┤ │ ▲ + │ │ Cache Worker │ │ │ 心跳 + │ │ SPDK Blobstore 缓存 │ │ ┌──────────┴──────────────┐ + │ └─────────────────────────┘ │ │ Tape Worker (独立物理节点) │ + └─────────────────────────────┘ │ 磁带驱动 /dev/nst* │ + │ 带库 /dev/sg* │ + └────────────────────────────┘ +``` + +| 节点类型 | 职责 | 数量 | 连接对象 | +|----------|------|------|----------| +| **Metadata** | Raft 共识、元数据存储、Worker 注册中心 | 3/5 | Scheduler (读写)、三类 Worker (心跳)、Console (管控) | +| **Scheduler Worker** | **业务中枢**:调度编排、对接全部层(与 Cache 同机) | 1~N | Metadata, Cache Worker, Tape Worker, Gateway | +| **Cache Worker** | SPDK NVMe 数据缓存(与 Scheduler 同机) | 1~N | Scheduler Worker (gRPC)、Metadata (心跳) | +| **Tape Worker** | 磁带驱动管理、数据读写(独立物理节点) | 1~N | Scheduler Worker (gRPC)、Metadata (心跳) | +| **Gateway** | S3 HTTP 接入 + 协议适配(无状态) | N | **仅 Scheduler Worker** | +| **Console** | 管控面 Web UI + Admin API(无状态) | 1 | **仅 Metadata** | + +**关键设计决策**: + +- **Gateway 不直连 Metadata**:Gateway 是纯 S3 协议前端,所有业务请求都发往 Scheduler Worker。 + Gateway 配置文件中只需写 Scheduler Worker 地址。 +- **Scheduler Worker 是唯一业务中枢**:持有 MetadataClient,编排 Cache Worker 和 Tape Worker, + 接受 Gateway 的全部请求,是唯一的元数据读写业务入口。 +- **Scheduler ↔ Cache 使用 gRPC**:虽然同机部署,仍通过 gRPC 通信,保持架构一致性, + 未来可独立扩展。 +- **Console 仅连 Metadata**:管控面直接读写元数据集群,管理 Worker 增删。 +- **Tape Worker 独立部署**:磁带机为独立物理节点,通过 gRPC 接收 Scheduler 的读写指令。 + +#### 3.10.2 节点间通信拓扑 + +``` +Gateway Console + │ 配置: scheduler_addrs │ 配置: metadata_addrs + │ │ + └──(gRPC)──► Scheduler Worker └──(gRPC)──► Metadata + │ ▲ + ├──(gRPC)──► Metadata ───────────┘ (元数据读写) + ├──(gRPC)──► Cache Worker (同机,gRPC) + └──(gRPC)──► Tape Worker (远程,gRPC) + │ + 三类 Worker ──(gRPC)──► Metadata (心跳注册) +``` + +| 通信路径 | 协议 | 场景 | +|----------|------|------| +| Gateway → Scheduler Worker | gRPC | **全部** S3 请求(PUT/GET/HEAD/DELETE/Restore) | +| Scheduler → Metadata | gRPC | 元数据读写(PutObject、扫描 ColdPending、状态更新等) | +| Scheduler → Cache Worker | gRPC(同机) | 缓存写入/读取(PutObject 暂存、GET 解冻数据) | +| Scheduler → Tape Worker | gRPC(远程) | 磁带写入/读取指令下发 | +| Console → Metadata | gRPC | 集群管理、Worker 增删、元数据查询 | +| 三类 Worker → Metadata | gRPC | 心跳上报(资源状态) | +| Metadata ↔ Metadata | Raft RPC | 共识协议 | + +> Gateway **不连接** Metadata,也**不连接** Cache Worker 和 Tape Worker。 +> 所有业务流量都经过 Scheduler Worker 中转。 + +#### 3.10.3 集群全局信息 + +```rust +pub struct ClusterInfo { + pub cluster_id: String, + pub metadata_nodes: Vec, + pub scheduler_workers: Vec, + pub cache_workers: Vec, + pub tape_workers: Vec, + pub leader_id: Option, + pub term: u64, + pub committed_index: u64, +} +``` + +| 字段 | 类型 | 含义 | +|------|------|------| +| `cluster_id` | String | 集群唯一标识 | +| `metadata_nodes` | Vec\ | Raft 元数据节点列表 | +| `scheduler_workers` | Vec\ | 调度 Worker 列表 | +| `cache_workers` | Vec\ | 缓存 Worker 列表 | +| `tape_workers` | Vec\ | 磁带 Worker 列表 | +| `leader_id` | Option\ | 当前 Raft Leader 节点 ID | +| `term` | u64 | 当前 Raft 任期 | +| `committed_index` | u64 | 已提交日志索引 | + +> Gateway / Console 不出现在 `ClusterInfo` 中。 +> Gateway 通过配置直连 Scheduler Worker;Console 通过配置直连 Metadata。 + +#### 3.10.4 公共类型 + +```rust +pub enum NodeStatus { + Online, // 正常服务 + Offline, // 失联(心跳超时) + Draining, // 排空中(不再分配新任务,等待现有任务完成) + Maintenance, // 维护模式(管理员手动设置) +} + +pub enum RaftRole { + Leader, + Follower, + Learner, +} + +pub enum WorkerType { + Scheduler, + Cache, + Tape, +} +``` + +#### 3.10.5 MetadataNodeInfo(元数据节点) + +```rust +pub struct MetadataNodeInfo { + pub node_id: u64, + pub addr: String, // Raft RPC + gRPC 服务地址 + pub raft_role: RaftRole, + pub last_heartbeat: Option>, + pub status: NodeStatus, +} +``` + +| 字段 | 类型 | 含义 | +|------|------|------| +| `node_id` | u64 | Raft 节点 ID | +| `addr` | String | 服务地址(`host:port`),供 Console/Worker 连接 | +| `raft_role` | RaftRole | 当前 Raft 角色 | +| `last_heartbeat` | Option\\> | Raft 内部心跳时间 | +| `status` | NodeStatus | 节点状态 | + +#### 3.10.6 SchedulerWorkerInfo(调度 Worker) + +调度 Worker 是 ColdStore 的**业务中枢**,接受 Gateway 全部 S3 请求,编排 Cache Worker 和 Tape Worker。 +物理上与 Cache Worker **同机部署**,通过 gRPC 通信(保持架构一致性)。 + +```rust +pub struct SchedulerWorkerInfo { + pub node_id: u64, + pub addr: String, // gRPC 服务地址 + pub status: NodeStatus, + pub last_heartbeat: Option>, + + // ── 调度状态 ── + pub is_active: bool, // 主备模式下是否为 active scheduler + pub pending_archive_tasks: u64, // 待归档任务数 + pub pending_recall_tasks: u64, // 待取回任务数 + pub active_jobs: u64, // 正在执行的作业数 + + // ── 关联的 Cache Worker ── + pub paired_cache_worker_id: u64, // 同机部署的 Cache Worker ID +} +``` + +| 字段 | 类型 | 含义 | 心跳更新 | +|------|------|------|:---:| +| `node_id` | u64 | 全局唯一 ID | 否 | +| `addr` | String | gRPC 地址 | 否 | +| `status` | NodeStatus | 节点状态 | 是 | +| `is_active` | bool | 是否为活跃调度器(多 Scheduler 时仅一个 active) | 是 | +| `pending_archive_tasks` | u64 | 待归档任务数 | 是 | +| `pending_recall_tasks` | u64 | 待取回任务数 | 是 | +| `active_jobs` | u64 | 正在执行作业数 | 是 | +| `paired_cache_worker_id` | u64 | 同机 Cache Worker 的 ID | 否(注册时固定) | + +#### 3.10.7 CacheWorkerInfo(缓存 Worker) + +缓存 Worker 运行 SPDK Blobstore,提供高性能 NVMe 数据缓存。 +物理上与 Scheduler Worker **同机部署**。 + +```rust +pub struct CacheWorkerInfo { + pub node_id: u64, + pub addr: String, // gRPC 服务地址(供远程 GET 读取解冻数据) + pub status: NodeStatus, + pub last_heartbeat: Option>, + + // ── 缓存资源 ── + pub bdev_name: String, // SPDK bdev 名称(如 "NVMe0n1") + pub total_capacity: u64, // 总容量 (bytes) + pub used_capacity: u64, // 已用容量 (bytes) + pub blob_count: u64, // 当前缓存对象数 + pub io_unit_size: u32, // SPDK io_unit 大小 (bytes) +} +``` + +| 字段 | 类型 | 含义 | 心跳更新 | +|------|------|------|:---:| +| `node_id` | u64 | 全局唯一 ID | 否 | +| `addr` | String | gRPC 地址(Gateway GET 时连接此地址) | 否 | +| `status` | NodeStatus | 节点状态 | 是 | +| `bdev_name` | String | SPDK bdev 名称 | 否(注册时固定) | +| `total_capacity` | u64 | NVMe 总容量 | 否 | +| `used_capacity` | u64 | 已用容量 | 是 | +| `blob_count` | u64 | 缓存对象数 | 是 | +| `io_unit_size` | u32 | SPDK io_unit 大小 | 否 | + +#### 3.10.8 TapeWorkerInfo(磁带 Worker) + +磁带 Worker 独立部署于挂载磁带驱动和带库的物理节点上, +通过 gRPC 接收调度器的读写指令。 + +```rust +pub struct TapeWorkerInfo { + pub node_id: u64, + pub addr: String, // gRPC 服务地址 + pub status: NodeStatus, + pub last_heartbeat: Option>, + + // ── 磁带驱动 ── + pub drives: Vec, + + // ── 带库(可选,无带库时为 None) ── + pub library: Option, +} + +/// 磁带驱动端点 +pub struct DriveEndpoint { + pub drive_id: String, // 驱动唯一标识 + pub device_path: String, // 设备路径 /dev/nst0 + pub drive_type: String, // 驱动类型 "LTO-9" / "LTO-10" + pub status: DriveStatus, + pub current_tape: Option, // 当前装载的磁带 ID +} + +pub enum DriveStatus { + Idle, // 空闲可分配 + InUse, // 正在执行读写任务 + Loading, // 正在装载磁带 + Unloading, // 正在卸载磁带 + Error, // 故障 + Offline, // 离线维护 +} + +/// 带库端点 +pub struct LibraryEndpoint { + pub device_path: String, // /dev/sg5 + pub slot_count: u32, // 存储槽位数 + pub import_export_count: u32, // 进出槽位数(邮箱) + pub drive_count: u32, // 带库中驱动数量 +} +``` + +| 字段 | 类型 | 含义 | 心跳更新 | +|------|------|------|:---:| +| `node_id` | u64 | 全局唯一 ID | 否 | +| `addr` | String | gRPC 地址(调度器连接此地址下发指令) | 否 | +| `status` | NodeStatus | 节点状态 | 是 | +| `drives[].status` | DriveStatus | 每个驱动的当前状态 | 是 | +| `drives[].current_tape` | Option\ | 驱动中当前磁带 ID | 是 | +| `drives[].drive_type` | String | 驱动型号 | 否 | +| `library.slot_count` | u32 | 槽位总数 | 否 | + +#### 3.10.9 节点注册、心跳与管理 + +``` + Console (添加/下线操作) + │ + ▼ + ┌──────────────┐ RegisterWorker ┌──────────────────────┐ + │ Scheduler │ ─────────────────► │ │ + │ Worker │ Heartbeat (5s) │ Metadata Cluster │ + ├──────────────┤ ─────────────────► │ (Raft + RocksDB) │ + │ Cache Worker │ Heartbeat (5s) │ │ + ├──────────────┤ ─────────────────► │ cf_scheduler_workers │ + │ Tape Worker │ Heartbeat (5s) │ cf_cache_workers │ + └──────────────┘ │ cf_tape_workers │ + └──────────────────────┘ +``` + +**注册流程**: + +1. 管理员通过 Console 添加 Worker(指定类型、地址等基本信息) +2. Console 调用 Metadata 的 `RegisterXxxWorker` 写入集群信息 +3. Worker 进程启动后连接 Metadata,确认自身已注册,开始心跳上报 + +**心跳机制**: + +- **频率**:每 5s 一次 +- **内容**:各 Worker 上报自身实时状态(队列深度、缓存容量、驱动状态等) +- **存储**:`last_heartbeat` 由 Metadata Leader 本地内存更新,**不写 Raft 日志** +- **状态变更走 Raft**:仅当状态发生实质变更时(Online ↔ Offline、驱动状态切换) + 才通过 Raft Propose 持久化 + +**失联检测**: + +- Metadata Leader 每 15s 扫描,连续 3 次未收到心跳(>15s)的 Worker 标记为 `Offline` +- Offline 的 Tape Worker 上的驱动不再被调度器分配任务 + +**优雅下线**: + +1. 管理员通过 Console 发起 Drain(排空) +2. Worker 状态变为 `Draining`,不再接受新任务 +3. 等待正在执行的任务完成 +4. 管理员确认后通过 Console 执行 Deregister + +#### 3.10.10 配置与服务发现 + +各节点的配置各不相同,遵循"最小知识"原则: + +```yaml +# Gateway 配置 — 仅需 Scheduler Worker 地址 +gateway: + scheduler_addrs: + - "10.0.2.1:22001" + - "10.0.2.2:22001" + +# Console 配置 — 仅需 Metadata 地址 +console: + metadata_addrs: + - "10.0.1.1:21001" + - "10.0.1.2:21001" + - "10.0.1.3:21001" + +# Scheduler Worker 配置 — 需要 Metadata 地址(Cache/Tape 通过 Metadata 发现) +scheduler_worker: + metadata_addrs: + - "10.0.1.1:21001" + - "10.0.1.2:21001" + - "10.0.1.3:21001" + +# Cache Worker / Tape Worker 配置 — 仅需 Metadata 地址(用于心跳注册) +cache_worker: + metadata_addrs: + - "10.0.1.1:21001" + - "10.0.1.2:21001" + - "10.0.1.3:21001" +``` + +**Scheduler Worker 的服务发现**:Scheduler 启动时通过 Metadata 查询在线的 Cache Worker 和 Tape Worker: + +```rust +async fn discover_workers(metadata: &MetadataClient) -> WorkerTopology { + let caches = metadata.list_online_cache_workers().await?; + let tapes = metadata.list_online_tape_workers().await?; + WorkerTopology { caches, tapes } +} +``` + +| 场景 | 发现路径 | +|------|----------| +| Gateway 全部请求 | 配置的 Scheduler 地址 → gRPC 发送请求 | +| Scheduler 操作缓存 | 查 Metadata → 获取 Cache Worker 地址 → gRPC 读写缓存 | +| Scheduler 操作磁带 | 查 Metadata → 获取 Tape Worker 地址 → gRPC 下发磁带指令 | +| Console 管理集群 | 配置的 Metadata 地址 → gRPC 管控操作 | + +--- + +## 4. Column Family 设计 + +| CF 名称 | Key 格式 | Value 类型 | 说明 | +|---------|----------|-----------|------| +| `cf_objects` | `obj:{bucket}:{key}` | ObjectMetadata | 对象元数据(当前版本) | +| `cf_object_versions` | `objv:{bucket}:{key}:{version_id}` | ObjectMetadata | 多版本对象 | +| `cf_buckets` | `bkt:{bucket}` | BucketInfo | 桶信息 | +| `cf_bundles` | `bundle:{uuid}` | ArchiveBundle | 归档包(含 entries) | +| `cf_tapes` | `tape:{tape_id}` | TapeInfo | 磁带信息 | +| `cf_recall_tasks` | `recall:{uuid}` | RecallTask | 取回任务 | +| `cf_archive_tasks` | `archive:{uuid}` | ArchiveTask | 归档任务 | +| `cf_idx_bundle_objects` | `ibo:{archive_id}:{bucket}:{key}` | 空 | 归档包→对象 反查索引 | +| `cf_idx_tape_bundles` | `itb:{tape_id}:{bundle_id}` | 空 | 磁带→归档包 反查索引 | +| `cf_idx_pending` | `pend:{created_at}:{bucket}:{key}` | 空 | ColdPending 对象扫描索引(按时间排序) | +| `cf_idx_recall_by_tape` | `rbt:{tape_id}:{recall_id}` | 空 | 按磁带查取回任务(合并用) | +| `cf_scheduler_workers` | `sw:{node_id}` | SchedulerWorkerInfo | 调度 Worker 注册信息 | +| `cf_cache_workers` | `cw:{node_id}` | CacheWorkerInfo | 缓存 Worker 注册信息 | +| `cf_tape_workers` | `tw:{node_id}` | TapeWorkerInfo | 磁带 Worker 注册信息 | + +--- + +## 5. MetadataService trait:面向各层的 API + +### 5.1 设计原则 + +MetadataService 拆分为多个子 trait,按消费方组织: + +``` + ┌──────────────────────────────┐ + │ MetadataService │ + │ impl ObjectApi │ + │ impl BucketApi │ + │ impl ArchiveApi │ + │ impl RecallApi │ + │ impl TapeApi │ + │ impl ClusterApi │ + └──────────────────────────────┘ + │ │ + ┌─────────────┘ └───────────────┐ + ▼ ▼ + 接入层/协议层 调度层 + ObjectApi (读+写) ArchiveApi (读+写) + BucketApi (读+写) RecallApi (读+写) + RecallApi (只写:创建) TapeApi (读+写) + ObjectApi (读+写) +``` + +### 5.2 ObjectApi(对象元数据) + +```rust +#[async_trait] +pub trait ObjectApi: Send + Sync { + // ── 接入层使用 ── + async fn put_object(&self, meta: ObjectMetadata) -> Result<()>; + async fn get_object(&self, bucket: &str, key: &str) -> Result>; + async fn get_object_version( + &self, bucket: &str, key: &str, version_id: &str, + ) -> Result>; + async fn delete_object(&self, bucket: &str, key: &str) -> Result<()>; + async fn head_object(&self, bucket: &str, key: &str) -> Result>; + async fn list_objects( + &self, bucket: &str, prefix: Option<&str>, marker: Option<&str>, max_keys: u32, + ) -> Result; + + // ── 调度层使用 ── + async fn update_storage_class( + &self, bucket: &str, key: &str, class: StorageClass, + ) -> Result<()>; + async fn update_archive_location( + &self, + bucket: &str, + key: &str, + archive_id: Uuid, + tape_id: &str, + tape_set: Vec, + tape_block_offset: u64, + ) -> Result<()>; + async fn update_restore_status( + &self, + bucket: &str, + key: &str, + status: RestoreStatus, + expire_at: Option>, + ) -> Result<()>; + async fn scan_cold_pending( + &self, limit: u32, + ) -> Result>; +} +``` + +| 方法 | 消费方 | 说明 | +|------|--------|------| +| `put_object` | Scheduler(代理 Gateway PutObject) | 创建/覆盖对象元数据 | +| `get_object` | Scheduler(代理 Gateway GET/HEAD) | 查询当前版本 | +| `get_object_version` | Scheduler | 查询指定版本 | +| `delete_object` | Scheduler(代理 Gateway DELETE) | 删除对象 | +| `head_object` | Scheduler(代理 Gateway HEAD) | 返回 storage_class、restore_status | +| `list_objects` | Scheduler(代理 Gateway ListObjects) | ListObjects | +| `update_storage_class` | 调度层 | 归档完成后 ColdPending → Cold | +| `update_archive_location` | 调度层 | 归档完成后写入 archive_id、tape_id、tape_block_offset | +| `update_restore_status` | 调度层 | 取回状态流转 + expire_at | +| `scan_cold_pending` | 调度层 | 扫描待归档对象(使用 `cf_idx_pending` 索引) | + +### 5.3 BucketApi(桶管理) + +```rust +#[async_trait] +pub trait BucketApi: Send + Sync { + async fn create_bucket(&self, info: BucketInfo) -> Result<()>; + async fn get_bucket(&self, name: &str) -> Result>; + async fn delete_bucket(&self, name: &str) -> Result<()>; + async fn list_buckets(&self) -> Result>; +} +``` + +| 方法 | 消费方 | 说明 | +|------|--------|------| +| `create_bucket` | 接入层 | CreateBucket | +| `delete_bucket` | 接入层 | DeleteBucket(需检查桶为空) | +| `list_buckets` | 接入层 | ListBuckets | + +### 5.4 ArchiveApi(归档元数据) + +```rust +#[async_trait] +pub trait ArchiveApi: Send + Sync { + async fn put_archive_bundle(&self, bundle: ArchiveBundle) -> Result<()>; + async fn get_archive_bundle(&self, id: Uuid) -> Result>; + async fn update_archive_bundle_status( + &self, id: Uuid, status: ArchiveBundleStatus, + ) -> Result<()>; + async fn list_bundles_by_tape(&self, tape_id: &str) -> Result>; + + async fn put_archive_task(&self, task: ArchiveTask) -> Result<()>; + async fn get_archive_task(&self, id: Uuid) -> Result>; + async fn update_archive_task(&self, task: ArchiveTask) -> Result<()>; + async fn list_pending_archive_tasks(&self) -> Result>; +} +``` + +| 方法 | 消费方 | 说明 | +|------|--------|------| +| `put_archive_bundle` | 调度层 | 归档完成后写入 Bundle(含 entries) | +| `get_archive_bundle` | 调度层 | 取回时获取 Bundle(含 entries、filemark_start) | +| `list_bundles_by_tape` | 调度层 | 查询磁带上所有 Bundle(使用 `cf_idx_tape_bundles`) | +| `put_archive_task` | 调度层 | 创建归档任务 | +| `update_archive_task` | 调度层 | 更新任务状态/进度 | + +### 5.5 RecallApi(取回元数据) + +```rust +#[async_trait] +pub trait RecallApi: Send + Sync { + async fn put_recall_task(&self, task: RecallTask) -> Result<()>; + async fn get_recall_task(&self, id: Uuid) -> Result>; + async fn update_recall_task(&self, task: RecallTask) -> Result<()>; + async fn list_pending_recall_tasks(&self) -> Result>; + async fn list_recall_tasks_by_tape(&self, tape_id: &str) -> Result>; + async fn find_active_recall( + &self, bucket: &str, key: &str, + ) -> Result>; +} +``` + +| 方法 | 消费方 | 说明 | +|------|--------|------| +| `put_recall_task` | Scheduler(代理 Gateway RestoreObject) | 创建取回任务 | +| `get_recall_task` | Scheduler | 查询任务详情 | +| `update_recall_task` | Scheduler | 更新状态 Pending → InProgress → Completed | +| `list_pending_recall_tasks` | Scheduler | 扫描待调度任务 | +| `list_recall_tasks_by_tape` | Scheduler | 合并同磁带任务(使用 `cf_idx_recall_by_tape`) | +| `find_active_recall` | Scheduler(代理 RestoreObject 时检测重复) | RestoreAlreadyInProgress | + +### 5.6 TapeApi(磁带元数据) + +```rust +#[async_trait] +pub trait TapeApi: Send + Sync { + async fn put_tape(&self, info: TapeInfo) -> Result<()>; + async fn get_tape(&self, tape_id: &str) -> Result>; + async fn update_tape(&self, info: TapeInfo) -> Result<()>; + async fn list_tapes(&self) -> Result>; + async fn list_tapes_by_status(&self, status: TapeStatus) -> Result>; +} +``` + +| 方法 | 消费方 | 说明 | +|------|--------|------| +| `put_tape` | 调度层 | 注册新磁带 | +| `get_tape` | 调度层 | 查询磁带信息 | +| `update_tape` | 调度层 | 更新 used_bytes、status、error_count 等 | +| `list_tapes_by_status` | 调度层 | 筛选可用磁带(Online + 有剩余空间) | + +### 5.7 ClusterApi(集群管理) + +```rust +#[async_trait] +pub trait ClusterApi: Send + Sync { + // ── Raft 状态 ── + async fn cluster_info(&self) -> Result; + async fn is_leader(&self) -> bool; + async fn ensure_linearizable(&self) -> Result<()>; + + // ── Scheduler Worker ── + async fn register_scheduler_worker(&self, info: SchedulerWorkerInfo) -> Result<()>; + async fn deregister_scheduler_worker(&self, node_id: u64) -> Result<()>; + async fn list_online_scheduler_workers(&self) -> Result>; + async fn get_scheduler_worker(&self, node_id: u64) -> Result>; + + // ── Cache Worker ── + async fn register_cache_worker(&self, info: CacheWorkerInfo) -> Result<()>; + async fn deregister_cache_worker(&self, node_id: u64) -> Result<()>; + async fn list_online_cache_workers(&self) -> Result>; + async fn get_cache_worker(&self, node_id: u64) -> Result>; + + // ── Tape Worker ── + async fn register_tape_worker(&self, info: TapeWorkerInfo) -> Result<()>; + async fn deregister_tape_worker(&self, node_id: u64) -> Result<()>; + async fn list_online_tape_workers(&self) -> Result>; + async fn get_tape_worker(&self, node_id: u64) -> Result>; + + // ── 通用 Worker 操作 ── + async fn update_worker_status(&self, worker_type: WorkerType, node_id: u64, status: NodeStatus) -> Result<()>; + async fn drain_worker(&self, worker_type: WorkerType, node_id: u64) -> Result<()>; +} +``` + +| 方法 | 消费方 | 说明 | +|------|--------|------| +| `cluster_info` | Console/Scheduler | 获取集群全局信息 | +| `register_xxx_worker` | Console(管理员添加) | 注册 Worker 到集群 | +| `deregister_xxx_worker` | Console(管理员下线) | 从集群移除 Worker | +| `list_online_xxx_workers` | Scheduler(服务发现) | 获取在线 Cache/Tape Worker 列表 | +| `update_worker_status` | Metadata Leader | 心跳超时时更新状态 | +| `drain_worker` | Console | 排空 Worker(不再分配新任务) | + +### 5.8 MetadataService(聚合类型) + +```rust +pub struct MetadataService { + raft: Arc>, + store: Arc, +} + +impl ObjectApi for MetadataService { /* ... */ } +impl BucketApi for MetadataService { /* ... */ } +impl ArchiveApi for MetadataService { /* ... */ } +impl RecallApi for MetadataService { /* ... */ } +impl TapeApi for MetadataService { /* ... */ } +impl ClusterApi for MetadataService { /* ... */ } +``` + +为方便外部引用,提供类型别名: + +```rust +pub type MetadataClient = MetadataService; +``` + +调度层和接入层注入时使用 `Arc`(即 `Arc`): + +```rust +// 接入层 +pub struct S3Handler { + metadata: Arc, // ObjectApi + BucketApi + RecallApi + cache: Arc, +} + +// 调度层 +pub struct ArchiveScheduler { + metadata: Arc, // ObjectApi + ArchiveApi + TapeApi + tape_manager: Arc, +} + +pub struct RecallScheduler { + metadata: Arc, // ObjectApi + RecallApi + ArchiveApi + TapeApi + tape_manager: Arc, + cache: Arc, +} +``` + +--- + +## 6. Raft 状态机命令 + +```rust +pub enum ColdStoreRequest { + // ── 对象 ── + PutObject(ObjectMetadata), + DeleteObject { bucket: String, key: String }, + UpdateStorageClass { bucket: String, key: String, class: StorageClass }, + UpdateArchiveLocation { + bucket: String, key: String, + archive_id: Uuid, tape_id: String, tape_set: Vec, tape_block_offset: u64, + }, + UpdateRestoreStatus { + bucket: String, key: String, + status: RestoreStatus, expire_at: Option>, + }, + + // ── 桶 ── + CreateBucket(BucketInfo), + DeleteBucket { name: String }, + + // ── 归档 ── + PutArchiveBundle(ArchiveBundle), + UpdateArchiveBundleStatus { id: Uuid, status: ArchiveBundleStatus }, + PutArchiveTask(ArchiveTask), + UpdateArchiveTask(ArchiveTask), + + // ── 取回 ── + PutRecallTask(RecallTask), + UpdateRecallTask(RecallTask), + + // ── 磁带 ── + PutTape(TapeInfo), + UpdateTape(TapeInfo), + + // ── Worker 注册(三类) ── + RegisterSchedulerWorker(SchedulerWorkerInfo), + DeregisterSchedulerWorker { node_id: u64 }, + RegisterCacheWorker(CacheWorkerInfo), + DeregisterCacheWorker { node_id: u64 }, + RegisterTapeWorker(TapeWorkerInfo), + DeregisterTapeWorker { node_id: u64 }, + + // ── Worker 状态变更 ── + UpdateWorkerStatus { worker_type: WorkerType, node_id: u64, status: NodeStatus }, +} +``` + +| 命令 | 写入方 | 说明 | +|------|--------|------| +| `PutObject` | Scheduler Worker(代理 Gateway 请求) | 创建对象(storage_class = ColdPending) | +| `DeleteObject` | Scheduler Worker(代理 Gateway 请求) | 删除对象 | +| `UpdateStorageClass` | Scheduler Worker | ColdPending → Cold | +| `UpdateArchiveLocation` | Scheduler Worker | 归档完成后写入磁带位置 | +| `UpdateRestoreStatus` | Scheduler Worker | 取回状态流转(含 expire_at) | +| `PutArchiveBundle` | Scheduler Worker | 创建归档包 | +| `PutRecallTask` | Scheduler Worker(代理 Gateway RestoreObject) | 创建取回任务 | +| `UpdateRecallTask` | Scheduler Worker | 更新取回任务状态 | +| `PutTape` / `UpdateTape` | Scheduler Worker | 磁带信息管理 | +| `RegisterXxxWorker` | Console(管理员添加) | 三类 Worker 注册 | +| `DeregisterXxxWorker` | Console(管理员下线) | 三类 Worker 注销 | +| `UpdateWorkerStatus` | Metadata Leader | 仅在状态变更时写入(Online ↔ Offline 等) | + +> **心跳优化**:三类 Worker 每 5s 上报心跳,但 `last_heartbeat` 仅在 Leader 本地内存更新, +> 不写 Raft 日志。仅当**状态发生实质变更**(节点上下线、驱动状态切换)时才通过 +> `UpdateWorkerStatus` 走 Raft Propose,避免高频心跳占满日志。 + +--- + +## 7. 读写路径 + +| 操作 | 路径 | 一致性 | +|------|------|--------| +| 写 | Client → Leader → Raft Propose → 多数派 Append → Apply → RocksDB | 强一致 | +| 读(默认) | 任意节点 → 本地 RocksDB | 最终一致 | +| 线性读 | `raft.ensure_linearizable().await` → 本地 RocksDB | 强一致 | + +**写入方使用建议**: + +| 操作 | 一致性要求 | 建议 | +|------|-----------|------| +| PutObject / DeleteObject | 强一致 | 写入走 Raft | +| RestoreObject 检查重复 | 强一致 | `find_active_recall` 使用线性读 | +| GET 查 restore_status | 可容忍短暂延迟 | 默认读即可 | +| scan_cold_pending | 可容忍短暂延迟 | 默认读 | +| 归档完成更新 | 强一致 | 写入走 Raft | + +--- + +## 8. 架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ MetadataService │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ObjectApi | BucketApi | ArchiveApi | RecallApi | TapeApi | ClusterApi │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────┴──────────┐ │ +│ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ 写路径 │ │ 读路径 │ │ +│ │ Raft │ │ 本地 │ │ +│ │ Propose │ │ RocksDB │ │ +│ └────┬─────┘ └──────────┘ │ +│ ▼ │ +│ ┌───────────────────────┐ │ +│ │ Raft Core (openraft) │ │ +│ │ 多数派复制 → Apply │ │ +│ └───────────┬───────────┘ │ +│ ▼ │ +│ ┌───────────────────────┐ │ +│ │ RocksDB │ │ +│ │ cf_objects │ │ +│ │ cf_bundles │ │ +│ │ cf_tapes │ │ +│ │ cf_recall_tasks │ │ +│ │ cf_archive_tasks │ │ +│ │ cf_buckets │ │ +│ │ cf_idx_* │ │ +│ └───────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 9. 模块结构 + +``` +src/metadata/ +├── mod.rs # pub use,MetadataService 导出 +├── service.rs # MetadataService 结构体,聚合所有 trait 实现 +├── traits/ # ─── trait 定义 ─── +│ ├── mod.rs +│ ├── object.rs # ObjectApi +│ ├── bucket.rs # BucketApi +│ ├── archive.rs # ArchiveApi +│ ├── recall.rs # RecallApi +│ ├── tape.rs # TapeApi +│ └── cluster.rs # ClusterApi +├── models/ # ─── 数据结构 ─── +│ ├── mod.rs +│ ├── object.rs # ObjectMetadata, StorageClass, RestoreStatus +│ ├── bundle.rs # ArchiveBundle, BundleEntry, ArchiveBundleStatus +│ ├── tape.rs # TapeInfo, TapeStatus +│ ├── task.rs # RecallTask, ArchiveTask, RestoreTier +│ ├── bucket.rs # BucketInfo +│ ├── cluster.rs # ClusterInfo, MetadataNodeInfo, RaftRole, NodeStatus, WorkerType +│ └── worker.rs # SchedulerWorkerInfo, CacheWorkerInfo, TapeWorkerInfo, DriveEndpoint, LibraryEndpoint +├── raft/ # ─── Raft 实现 ─── +│ ├── mod.rs +│ ├── client.rs # Raft 客户端(Propose + 线性读) +│ ├── store.rs # RocksStore 扩展(CF 初始化) +│ ├── state_machine.rs # ColdStoreRequest Apply 逻辑 +│ └── network.rs # 节点间 RPC +└── index.rs # 二级索引维护逻辑 +``` + +--- + +## 10. 配置项 + +```yaml +metadata: + backend: "RaftRocksDB" + raft: + node_id: 1 + cluster: "1:127.0.0.1:21001,2:127.0.0.1:21002,3:127.0.0.1:21003" + data_path: "/var/lib/coldstore/metadata" + snapshot_interval: 10000 # 每 10000 条日志做一次 snapshot + heartbeat_interval_ms: 200 + election_timeout_ms: 1000 + rocksdb: + max_open_files: 1024 + write_buffer_size_mb: 64 + max_background_jobs: 4 +``` + +--- + +## 11. 依赖关系(方案 B) + +### 11.1 元数据写入入口 + +| 写入方 | 使用的 trait | 操作 | +|--------|-------------|------| +| **接入层** | ObjectApi, BucketApi | PutObject, DeleteObject, CreateBucket | +| **协议层** | RecallApi | PutRecallTask(RestoreObject 时创建) | +| **调度层** | ObjectApi, ArchiveApi, RecallApi, TapeApi | 归档/取回全流程状态流转 | + +### 11.2 元数据读取方 + +| 读取方 | 使用的 trait | 用途 | +|--------|-------------|------| +| 接入层/协议层 | ObjectApi, BucketApi, RecallApi | 查询对象状态、桶列表、检测重复 Restore | +| 调度层 | ObjectApi, ArchiveApi, RecallApi, TapeApi | 扫描 ColdPending、查磁带位置、合并任务 | + +### 11.3 不直接依赖的层 + +| 层 | 关系 | +|------|------| +| 缓存层 | **不持有 MetadataService**,由调度层编排 | +| 磁带层 | **不持有 MetadataService**,由调度层编排 | + +--- + +## 12. 参考资料 + +- [OpenRaft](https://github.com/databendlabs/openraft) +- [openraft-rocksstore](https://crates.io/crates/openraft-rocksstore) +- [RocksDB Column Families](https://github.com/facebook/rocksdb/wiki/Column-Families) diff --git a/docs/modules/04-cache-layer.md b/docs/modules/04-cache-layer.md new file mode 100644 index 0000000..a7298bf --- /dev/null +++ b/docs/modules/04-cache-layer.md @@ -0,0 +1,668 @@ +# 数据缓存层模块设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) + +## 1. 模块概述 + +数据缓存层承载从磁带取回的解冻数据,供 GET 请求快速响应,避免重复触发磁带读取。 + +> **部署模型**:缓存层运行在 **Cache Worker** 节点上,与 **Scheduler Worker** 同机部署。 +> Scheduler 通过 gRPC 与 Cache Worker 通信(虽然同机,仍使用 gRPC 保持架构一致性)。 +> Gateway 不直连 Cache Worker,所有数据读写都由 Scheduler Worker 代理。 + +### 1.1 职责 + +- 存储解冻后的对象数据 +- 响应 GET 请求(冷对象已解冻时) +- 缓存淘汰:LRU、LFU、TTL、容量限制 +- 与 SPDK 集成,实现高性能用户态 I/O + +### 1.2 在架构中的位置 + +``` +取回调度器 ──► 磁带读取 ──► 数据缓存层 ──► 协议层/接入层 ──► GET 响应 + │ + └─ async-spdk (SPDK Blobstore on bdev) + +注意:缓存层不直接持有元数据 client。 +元数据更新由调度层统一负责(方案 B)。 +``` + +--- + +## 2. 技术选型 + +| 组件 | 选型 | 说明 | +|------|------|------| +| SPDK 绑定 | **async-spdk** | [madsys-dev/async-spdk](https://github.com/madsys-dev/async-spdk) | +| SPDK 存储方式 | **Blobstore (Blob)** | 基于 Blob 的持久化块分配器,非 raw bdev | +| 底层设备 | NVMe bdev / Malloc (测试) | Blobstore 构建于 bdev 之上 | + +--- + +## 3. 数据布局:SPDK Blob 方式 + +### 3.1 Blobstore 层级结构 + +SPDK Blobstore 定义以下抽象([SPDK Blobstore 文档](https://spdk.io/doc/blob.html)): + +| 层级 | 典型大小 | 说明 | +|------|----------|------| +| **Logical Block** | 512B / 4KB | 磁盘暴露的基本单位 | +| **Page** | 4KB | 固定页,读写最小单位 | +| **Cluster** | 1MB (256 pages) | 分配单元,Blob 由 cluster 组成 | +| **Blob** | 可变 | 有序 cluster 列表,持久化,由 blob_id 标识 | +| **Blobstore** | 整设备 | 拥有底层 bdev,含元数据区 + blob 集合 | + +### 3.2 缓存层与 Blob 的对应关系 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Blobstore 布局(整 bdev) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ [ Super Block ] [ Metadata Region ] [ Blob 数据区 ] │ +│ │ │ │ │ +│ Cluster 0 专用 Blob 元数据链表 Cluster 1..N │ +│ 签名/版本/配置 per-blob 元数据 各 Blob 的 cluster │ +└─────────────────────────────────────────────────────────────────────────┘ + +每个缓存对象 = 1 个 Blob + - blob_id: Blobstore 分配的唯一 ID + - 数据: 按 page 读写,支持 thin provisioning(首次写时分配 cluster) + - xattrs: bucket, key, size, checksum, expire_at, cached_at +``` + +### 3.3 对象与 Blob 映射 + +| 缓存对象 | Blob 表示 | +|----------|-----------| +| 1 个 S3 对象 (bucket, key) | 1 个 Blob | +| 对象数据 | Blob 的 cluster 序列,按 page 读写 | +| 元数据 | Blob xattrs(见 3.3.1) | + +**cache_key → blob_id 索引**: + +- Blobstore 不提供按名称查找,需自维护 `cache_key → blob_id` 映射 +- 存储方式:内存 HashMap + 可选持久化(如写入 Super Blob 或独立索引 Blob) +- `cache_key = format!("{}:{}", bucket, key)` 或含 version_id + +### 3.3.1 Blob xattrs 字段定义 + +xattrs 为 Blob 的扩展属性,键值对形式,用于存储对象级元数据。字段含义与用途如下: + +| xattr key | 类型 | 必填 | 含义 | 用途 | +|-----------|------|------|------|------| +| `bucket` | string | 是 | S3 桶名 | 与 key 共同唯一标识对象;GET 时校验、淘汰时按桶过滤 | +| `key` | string | 是 | S3 对象键 | 与 bucket 共同唯一标识对象;重建 cache_key | +| `size` | u64 (string 序列化) | 是 | 对象字节大小 | 读取时分配缓冲;校验读取长度;淘汰时计算容量 | +| `expire_at` | i64 Unix 时间戳 (string) | 是 | 解冻过期时间 | 与元数据 restore_expire_at 一致;TTL 淘汰依据 | +| `cached_at` | i64 Unix 时间戳 (string) | 是 | 写入缓存时间 | LRU 淘汰依据;可观测性(缓存时长) | +| `checksum` | string (hex) | 否 | 对象校验和 SHA256 | 读后校验数据完整性;可选,调度层传入时写入 | +| `version_id` | string | 否 | S3 对象版本 ID | 多版本支持;cache_key 含 version 时需存 | +| `content_type` | string | 否 | 对象 Content-Type | 协议层 GET 响应头;可选透传 | +| `etag` | string | 否 | 对象 ETag | 协议层 GET 响应头;可选透传 | + +**xattrs 用途汇总**: + +- **查找与校验**:bucket + key 用于重建 cache_key,与索引一致时确认 Blob 归属 +- **淘汰**:expire_at 驱动 TTL 淘汰,cached_at 驱动 LRU 淘汰,size 参与容量计算 +- **完整性**:checksum 用于读后校验,发现损坏可触发重新 Restore +- **协议透传**:content_type、etag 供 GET 响应头使用,避免再查元数据 + +### 3.3.2 cache_key → blob_id 索引条目 + +内存索引(及可选持久化)中每条目的字段: + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `cache_key` | string | `{bucket}:{key}` 或含 version | 主键,唯一标识缓存对象 | +| `blob_id` | u64 | Blobstore 分配的 Blob ID | 定位 Blob,执行 open/read/write/delete | +| `size` | u64 | 对象大小 | 淘汰时容量统计;读时分配缓冲 | +| `expire_at` | i64 | 过期时间戳 | 快速判断是否过期,无需开 Blob 读 xattr | +| `cached_at` | i64 | 缓存时间戳 | LRU 排序;避免开 Blob 读 xattr | + +**索引与 xattrs 的关系**:索引用于快速查找(cache_key → blob_id)和淘汰决策;xattrs 为 Blob 内持久化元数据,用于校验、重建索引、协议透传。两者需保持一致,写入时同步更新。 + +### 3.3.3 其他相关字段 + +| 来源 | 字段 | 含义 | 用途 | +|------|------|------|------| +| Blobstore | `blob_id` | Blob 唯一标识 | 所有 Blob 操作的句柄 | +| Blobstore | `blob_size` (num_clusters × cluster_size) | Blob 逻辑大小 | resize 时设置;与对象 size 对齐到 cluster | +| 调度层传入 | `data` | 对象原始字节 | 写入 Blob 的 payload | +| 调度层传入 | `checksum` | 可选 SHA256 | 写入 xattr,读后校验 | +| 调度层传入 | `expire_at` | 解冻保留截止时间 | 写入 xattr 与索引;来自 RestoreRequest.Days | + +### 3.4 Blob 创建与读写流程 + +``` +创建/写入: + 1. 创建 Blob: spdk_bs_create_blob() → blob_id + 2. 打开 Blob: spdk_bs_open_blob(blob_id) → blob handle + 3. 设置 xattrs: spdk_blob_set_xattr(blob, "bucket", ...) 等 + 4. 调整大小: spdk_blob_resize(blob, num_clusters) + 5. 同步元数据: spdk_blob_sync_md(blob) + 6. 写入数据: spdk_blob_io_write(blob, channel, offset, len, buf, cb) + 7. 关闭 Blob: spdk_blob_close(blob) + +读取: + 1. 查索引: cache_key → blob_id + 2. 打开 Blob: spdk_bs_open_blob(blob_id) → blob handle + 3. 读取: spdk_blob_io_read(blob, channel, offset, len, buf, cb) + 4. 关闭 Blob: spdk_blob_close(blob) + +注意:set_xattr / resize 需在 open 之后的 blob handle 上调用。 +读写单位为 io_unit(通常 4KB,可通过 spdk_bs_get_io_unit_size() 获取)。 +``` + +### 3.5 Blob 数据格式 + +- **读写单位**:io_unit(通常 4KB),`spdk_blob_io_read`/`spdk_blob_io_write` 的 offset 和 length 以 io_unit 为单位 +- **对象数据**:连续写入 Blob,不足一 page 的尾部需填充或按 page 写入 +- **Thin Provisioning**:可创建 thin blob,首次写入时分配 cluster,节省空间 + +### 3.6 关键参数 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| cluster_size | 1MB | Blobstore 创建时指定,Blob 按 cluster 分配 | +| page_size | 4KB | 读写粒度 | +| max_object_size | 5GB | 单对象上限,超限不缓存或分块 | +| blobstore_type | "coldstore_cache" | 用于识别 Blobstore 归属 | + +### 3.7 与 async-spdk 的集成 + +async-spdk 提供 `hello_blob` 示例,使用 Blobstore API。缓存层需: + +- 初始化 Blobstore:`spdk_bs_init()` / `spdk_bs_load()` 于指定 bdev +- 使用 Blob API:`create_blob`、`open_blob`、`resize`、`write`、`read`、`close`、`delete_blob` +- 通过 xattrs 存储 bucket、key、expire_at 等 +- 自维护 `cache_key → blob_id` 索引(内存 + 可选持久化) + +--- + +## 4. 核心数据结构 + +### 4.1 CacheManager + +缓存层的顶层入口,同时实现 `CacheReadApi` + `CacheWriteApi`。 + +```rust +pub struct CacheManager { + backend: Box, + index: CacheIndex, + config: CacheConfig, + stats: CacheStats, +} +``` + +| 字段 | 类型 | 含义 | 说明 | +|------|------|------|------| +| `backend` | `Box` | 存储后端实例 | 多态:SpdkBlobCacheBackend 或 FileCacheBackend | +| `index` | `CacheIndex` | cache_key → blob_id 的内存索引 | 所有查找/淘汰决策的入口 | +| `config` | `CacheConfig` | 缓存配置 | 容量上限、TTL、淘汰策略等 | +| `stats` | `CacheStats` | 运行时统计 | 命中率、容量使用、淘汰次数等 | + +### 4.2 CacheBackend trait + +```rust +#[async_trait] +pub trait CacheBackend: Send + Sync { + async fn write_blob( + &self, blob_id: u64, data: &[u8], xattrs: &BlobXattrs, + ) -> Result; + async fn read_blob(&self, blob_id: u64) -> Result>; + async fn delete_blob(&self, blob_id: u64) -> Result<()>; + async fn create_blob(&self, xattrs: &BlobXattrs, size: u64) -> Result; + async fn read_xattrs(&self, blob_id: u64) -> Result; + async fn list_blobs(&self) -> Result>; +} +``` + +| 方法 | 含义 | +|------|------| +| `write_blob` | 向已创建的 Blob 写入对象数据 | +| `read_blob` | 从 Blob 读取对象全部数据 | +| `delete_blob` | 删除 Blob(淘汰时调用) | +| `create_blob` | 创建新 Blob、设置 xattrs、resize,返回 blob_id | +| `read_xattrs` | 读取 Blob 的 xattrs(启动时重建索引用) | +| `list_blobs` | 列出 Blobstore 中所有 Blob ID(启动时重建索引用) | + +### 4.3 BlobXattrs + +对应 Blob 上持久化的 xattrs 键值对,Rust 侧的结构化表示。 + +```rust +pub struct BlobXattrs { + pub bucket: String, + pub key: String, + pub size: u64, + pub expire_at: i64, + pub cached_at: i64, + pub checksum: Option, + pub version_id: Option, + pub content_type: Option, + pub etag: Option, +} +``` + +| 字段 | 类型 | 必填 | 含义 | 写入时机 | +|------|------|------|------|----------| +| `bucket` | String | 是 | S3 桶名 | 调度层 put_restored 传入 | +| `key` | String | 是 | S3 对象键 | 调度层 put_restored 传入 | +| `size` | u64 | 是 | 对象原始字节数(非 Blob 占用的 cluster 数) | 调度层传入 data.len() | +| `expire_at` | i64 | 是 | 解冻过期 Unix 时间戳(秒) | 调度层从 RestoreRequest.Days 计算后传入 | +| `cached_at` | i64 | 是 | 写入缓存的 Unix 时间戳(秒) | CacheManager 写入时取 Utc::now() | +| `checksum` | Option\ | 否 | SHA256 hex 字符串 | 调度层传入(若磁带读取时有校验) | +| `version_id` | Option\ | 否 | S3 对象版本 ID | 调度层传入(多版本场景) | +| `content_type` | Option\ | 否 | MIME 类型 | 调度层传入(协议层 GET 响应透传) | +| `etag` | Option\ | 否 | S3 ETag | 调度层传入(协议层 GET 响应透传) | + +### 4.4 CacheEntry(内存索引条目) + +```rust +pub struct CacheEntry { + pub cache_key: String, + pub blob_id: u64, + pub size: u64, + pub expire_at: i64, + pub cached_at: i64, + pub last_accessed_at: i64, + pub access_count: u64, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `cache_key` | String | `{bucket}:{key}` 或 `{bucket}:{key}:{version_id}` | 索引主键,唯一标识缓存对象 | +| `blob_id` | u64 | Blobstore 分配的 Blob 唯一 ID | 所有 Blob 操作的句柄 | +| `size` | u64 | 对象原始字节数 | 淘汰时计算总容量;读时预分配缓冲 | +| `expire_at` | i64 | 解冻过期 Unix 时间戳 | TTL 淘汰判断,过期即可删除 | +| `cached_at` | i64 | 写入缓存 Unix 时间戳 | LRU 兜底排序(若无访问记录则用此值) | +| `last_accessed_at` | i64 | 最近一次 GET 读取时间 | LRU 淘汰排序,每次 get 时更新 | +| `access_count` | u64 | 累计 GET 次数 | LFU 淘汰排序(可选策略) | + +### 4.5 CacheIndex + +```rust +pub struct CacheIndex { + entries: HashMap, + total_size: u64, +} +``` + +| 字段 | 类型 | 含义 | 说明 | +|------|------|------|------| +| `entries` | HashMap\ | cache_key → CacheEntry | 全量内存索引,O(1) 查找 | +| `total_size` | u64 | 当前缓存总占用字节 | 每次 put/delete 更新,淘汰时与 max_size 比较 | + +### 4.6 CachedObject(读取返回) + +```rust +pub struct CachedObject { + pub data: Vec, + pub size: u64, + pub expire_at: DateTime, + pub content_type: Option, + pub etag: Option, + pub checksum: Option, +} +``` + +| 字段 | 类型 | 含义 | 消费方 | +|------|------|------|--------| +| `data` | Vec\ | 对象完整数据 | 协议层写入 HTTP 响应体 | +| `size` | u64 | 数据字节数 | 协议层 Content-Length | +| `expire_at` | DateTime\ | 解冻过期时间 | 协议层生成 `x-amz-restore` 头 | +| `content_type` | Option\ | MIME 类型 | 协议层 Content-Type 响应头 | +| `etag` | Option\ | ETag | 协议层 ETag 响应头 | +| `checksum` | Option\ | SHA256 hex | 协议层可选校验数据完整性 | + +### 4.7 CacheConfig + +```rust +pub struct CacheConfig { + pub backend: CacheBackendType, + pub max_size_bytes: u64, + pub default_ttl_secs: u64, + pub eviction_policy: EvictionPolicy, + pub eviction_batch_size: usize, + pub eviction_low_watermark: f64, + pub bdev_name: String, + pub cluster_size_mb: u32, +} +``` + +| 字段 | 类型 | 含义 | 说明 | +|------|------|------|------| +| `backend` | CacheBackendType | 后端类型 | `Spdk` / `File`(测试) | +| `max_size_bytes` | u64 | 缓存总容量上限 | 超过此值触发淘汰 | +| `default_ttl_secs` | u64 | 默认 TTL | 未指定 expire_at 时的兜底值 | +| `eviction_policy` | EvictionPolicy | 淘汰策略 | `Lru` / `Lfu` / `TtlFirst` | +| `eviction_batch_size` | usize | 单次淘汰个数 | 批量删除,减少淘汰频率 | +| `eviction_low_watermark` | f64 | 淘汰水位线 | 0.8 表示淘汰到 80% 容量后停止 | +| `bdev_name` | String | SPDK bdev 名称 | 如 `"Malloc0"` 或 NVMe bdev | +| `cluster_size_mb` | u32 | Blobstore cluster 大小 | 如 1MB | + +### 4.8 CacheStats + +```rust +pub struct CacheStats { + pub total_size: u64, + pub object_count: u64, + pub hit_count: AtomicU64, + pub miss_count: AtomicU64, + pub put_count: AtomicU64, + pub evict_count: AtomicU64, + pub evict_bytes: AtomicU64, +} +``` + +| 字段 | 类型 | 含义 | +|------|------|------| +| `total_size` | u64 | 当前缓存数据总字节 | +| `object_count` | u64 | 当前缓存对象数 | +| `hit_count` | AtomicU64 | 累计缓存命中次数 | +| `miss_count` | AtomicU64 | 累计缓存未命中次数 | +| `put_count` | AtomicU64 | 累计写入次数 | +| `evict_count` | AtomicU64 | 累计淘汰对象次数 | +| `evict_bytes` | AtomicU64 | 累计淘汰字节数 | + +--- + +## 5. 缓存策略 + +| 策略 | 说明 | +|------|------| +| LRU | 淘汰最久未访问 | +| LFU | 淘汰访问频率最低 | +| TTL | 按 expire_at 淘汰(独立后台扫描) | +| 容量 | 总容量上限,超限触发淘汰 | + +```rust +pub enum EvictionPolicy { + Lru, // 按 last_accessed_at 排序淘汰 + Lfu, // 按 access_count 排序淘汰 + TtlFirst, // 优先淘汰已过期对象,再按 LRU +} +``` + +**淘汰逻辑**: + +1. **TTL 扫描**(独立后台任务):定期扫描 `expire_at < now` 的 CacheEntry,直接删除对应 Blob +2. **容量淘汰**:当 `total_size > max_size_bytes` 时,按 `EvictionPolicy` 选择淘汰对象,批量删除 `eviction_batch_size` 个,直到 `total_size ≤ max_size_bytes × eviction_low_watermark` + +--- + +## 6. 与调度层的对接 + +### 5.1 接口定义 + +```rust +/// 缓存层提供给调度层的接口 +#[async_trait] +pub trait CacheWriteApi: Send + Sync { + async fn put_restored(&self, item: RestoredItem) -> Result<()>; + async fn put_restored_batch(&self, items: Vec) -> Result<()>; + async fn delete(&self, bucket: &str, key: &str, version_id: Option<&str>) -> Result<()>; +} + +pub struct RestoredItem { + pub bucket: String, + pub key: String, + pub version_id: Option, + pub data: Vec, + pub checksum: Option, // SHA256 hex + pub expire_at: DateTime, + pub content_type: Option, // 调度层从元数据获取后传入 + pub etag: Option, // 调度层从元数据获取后传入 +} +``` + +| 字段 | 类型 | 含义 | 来源 | +|------|------|------|------| +| `bucket` | String | S3 桶名 | 调度层传入 | +| `key` | String | S3 对象键 | 调度层传入 | +| `version_id` | Option\ | 对象版本 | 调度层从元数据获取 | +| `data` | Vec\ | 对象原始数据 | 磁带读取 | +| `checksum` | Option\ | SHA256 hex 字符串 | 调度层校验后传入 | +| `expire_at` | DateTime\ | 解冻过期时间 | 调度层从 RestoreRequest.Days 计算 | +| `content_type` | Option\ | MIME 类型 | 调度层从元数据获取,用于 GET 响应透传 | +| `etag` | Option\ | S3 ETag | 调度层从元数据获取,用于 GET 响应透传 | + +> `delete` 方法用于接入层 DeleteObject 时清理缓存(由接入层调用)。 + +### 5.2 调用时机与流程 + +缓存层自身**不**触发元数据更新。写入完成后返回 `Result<()>` 给调度层,由调度层统一负责后续元数据变更。 + +``` +取回调度器执行 TapeReadJob (调度层职责) + │ + ├─ 1. 从磁带顺序读取对象 A, B, C + │ + ├─ 2. 每读完一个对象: + │ cache.put_restored(bucket, key, data, checksum, expire_at).await + │ 若失败: 重试或标记 RecallTask 失败 + │ + └─ 3. 全部写入缓存后: (调度层负责更新元数据) + metadata.update_restore_status(Completed, restore_expire_at) +``` + +> **关键原则**:缓存层是纯数据存储,不持有 MetadataClient。元数据写入入口收敛在调度层。 + +### 5.3 数据流与校验 + +| 步骤 | 负责方 | 说明 | +|------|--------|------| +| 磁带读取 | 调度器 | 带 checksum 校验 | +| 写入缓存 | 缓存层 | 存储 data + checksum,纯数据操作 | +| 更新元数据 | **调度器** | restore_status=Completed, restore_expire_at | +| GET 时校验 | 协议层/缓存层 | 可选读后校验 checksum | + +### 5.4 失败与重试 + +- 缓存写入失败:调度器重试 2 次,仍失败则 RecallTask 标记 Failed,**不更新元数据** +- 缓存层空间不足:先触发淘汰,再写入;若仍不足则返回错误,调度器不更新元数据 +- 缓存写入成功但元数据更新失败:缓存有数据但 GET 仍返回未解冻状态,调度器需重试元数据更新 + +--- + +## 7. 与元数据层的关系(方案 B:缓存层不直接依赖元数据) + +### 6.1 设计原则 + +**缓存层不持有 MetadataClient,不直接读写元数据。** + +元数据的写入入口收敛为两个: +- **接入层**:`PutObject`、`DeleteObject` +- **调度层**:归档/取回状态流转(`UpdateStorageClass`、`UpdateRestoreStatus` 等) + +缓存层是纯粹的数据存储层,只暴露 `CacheReadApi` 和 `CacheWriteApi`。 + +### 6.2 缓存层对元数据的间接关系 + +| 场景 | 依赖 | 说明 | +|------|------|------| +| GET 前 | 无直接依赖 | 协议层先查元数据,若 Completed 再调缓存 | +| 淘汰时 | 无 | 基于自身 xattrs 中的 expire_at 判断,无需查元数据 | +| 写入时 | 无 | 调度层传入 expire_at,缓存层直接写入 xattr | +| 启动时 | 无 | 从 Blobstore xattrs 重建内存索引,不依赖元数据 | + +### 6.3 一致性保证(由调度层负责) + +| 顺序 | 操作 | 负责方 | +|------|------|--------| +| 1 | 磁带读取完成 | 调度器 | +| 2 | `cache.put_restored(...)` 成功 | 调度器 → 缓存层 | +| 3 | `metadata.update_restore_status(Completed)` | **调度器** → 元数据层 | +| 4 | GET:查元数据 → Completed → 从缓存读 | 协议层 | + +若 2 成功、3 失败:缓存有数据但元数据未更新,GET 仍返回未解冻。调度器需重试元数据更新(补偿机制)。 + +--- + +## 8. 与协议层/接入层的对接 + +### 7.1 接口定义 + +```rust +/// 缓存层提供给协议层/接入层的接口 +#[async_trait] +pub trait CacheReadApi: Send + Sync { + async fn get( + &self, + bucket: &str, + key: &str, + version_id: Option<&str>, + ) -> Result>; + + async fn contains( + &self, + bucket: &str, + key: &str, + version_id: Option<&str>, + ) -> Result; +} +``` + +> `CachedObject` 定义见 §4.6,包含 data、size、expire_at、content_type、etag、checksum。 + +### 7.2 协议层调用流程 + +``` +GET /{bucket}/{key} + │ + ├─ 协议层/Handler: metadata.get_object(bucket, key) + │ + ├─ if storage_class != Cold: + │ 从热存储读取,返回 + │ + ├─ if storage_class == Cold: + │ if restore_status != Completed: + │ 返回 403 InvalidObjectState + │ if restore_expire_at < now: + │ 返回 403 InvalidObjectState(已过期) + │ + └─ cache.get(bucket, key) + if Some(obj): 返回 obj.data,附带 x-amz-restore 头 + if None: 返回 404 或 503(缓存丢失,需重新 Restore) +``` + +### 7.3 缓存未命中处理 + +| 情况 | 协议层行为 | +|------|------------| +| 元数据 Completed 但缓存无数据 | 返回 503,提示重新 Restore;或触发异步取回 | +| 缓存已淘汰(TTL 内) | 同上,元数据 restore_expire_at 未过期但缓存已删 | +| 缓存损坏 | 校验失败时返回 500,建议重新 Restore | + +### 7.4 响应头 + +GET 命中缓存时,协议层需附加: + +``` +x-amz-restore: ongoing-request="false", expiry-date="Fri, 28 Feb 2025 12:00:00 GMT" +``` + +--- + +## 9. 对接汇总图(方案 B) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 协议层 / 接入层 │ +│ GET: metadata.get_object → if Completed → cache.get → 返回 data │ +│ HEAD: metadata.get_object → 生成 x-amz-restore 头 │ +│ PUT/DELETE: metadata.put_object / delete_object │ +└─────────────────────────────────────────────────────────────────────────┘ + │ 读 │ 读 + ▼ ▼ +┌───────────────────┐ ┌───────────────────────────────┐ +│ 元数据层 │ │ 缓存层(纯数据存储) │ +│ restore_status │ │ CacheReadApi: get/contains │ +│ restore_expire_at│ │ CacheWriteApi: put_restored │ +└───────────────────┘ │ ⚡ 不持有 MetadataClient │ + ▲ └───────────────────────────────┘ + │ 写(唯一写入协调者) ▲ + │ │ 写 + │ ┌───────────────────────────┘ + │ │ +┌───────┴──────────────┴───────────────────────────────┐ +│ 调度层(归档/取回) │ +│ 1. 磁带读取 → 2. cache.put_restored → 3. 更新元数据 │ +│ 元数据写入的唯一协调者(除接入层 PUT/DELETE 外) │ +└──────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────┐ +│ 磁带层(纯硬件) │ +│ 不持有 │ +│ MetadataClient │ +└──────────────────┘ +``` + +--- + +## 10. 集成方式 + +- **方案 A**:缓存服务独立二进制,通过 gRPC/HTTP 与主服务通信 +- **方案 B**:主进程在 SPDK block_on 内启动 Axum(需调整启动顺序) +- **方案 C**:缓存层先使用文件后端,待架构稳定后再接入 async-spdk + +--- + +## 11. 模块结构 + +``` +src/cache/ +├── mod.rs +├── manager.rs # CacheManager,实现 CacheReadApi + CacheWriteApi +├── index.rs # cache_key → blob_id 索引(内存 + 可选持久化) +├── spdk/ +│ ├── mod.rs +│ ├── blob_backend.rs # SpdkBlobCacheBackend,Blobstore/Blob API 封装 +│ └── config.rs +└── file/ # 文件后端(兼容/测试) + └── backend.rs +``` + +--- + +## 12. 配置项 + +```yaml +cache: + backend: "spdk" + spdk: + enabled: true + config_file: "/etc/coldstore/cache_spdk.json" + bdev_name: "Malloc0" + blobstore_type: "coldstore_cache" + cluster_size_mb: 1 + max_size_gb: 100 + ttl_secs: 86400 + eviction_policy: "Lru" +``` + +--- + +## 13. 依赖关系 + +| 对接方 | 方向 | 接口 | 说明 | +|--------|------|------|------| +| 调度层 | 调度 → 缓存 | `CacheWriteApi`: `put_restored` / `put_restored_batch` | 取回后写数据 | +| 接入层 | 接入 → 缓存 | `CacheWriteApi`: `delete` | DeleteObject 时清理缓存 | +| 协议层/接入层 | 协议 → 缓存 | `CacheReadApi`: `get` / `contains` | GET 读数据 | +| 元数据层 | **无依赖** | — | 缓存层不持有 MetadataClient | + +> **方案 B 原则**:缓存层是纯数据存储,不感知元数据。元数据写入由调度层统一协调。 + +--- + +## 14. 参考资料 + +- [async-spdk](https://github.com/madsys-dev/async-spdk)(含 hello_blob 示例) +- [SPDK Blobstore Programmer's Guide](https://spdk.io/doc/blob.html) +- [SPDK blob.h API](https://spdk.io/doc/blob_8h.html) diff --git a/docs/modules/05-scheduler-layer.md b/docs/modules/05-scheduler-layer.md new file mode 100644 index 0000000..d063911 --- /dev/null +++ b/docs/modules/05-scheduler-layer.md @@ -0,0 +1,789 @@ +# 归档取回调度层模块设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) + +## 1. 模块概述 + +归档取回调度层负责冷数据的归档与取回调度,是 ColdStore 的核心业务逻辑层。设计需充分考虑**磁带的顺序读写特性**,通过聚合与合并策略最大化吞吐、减少换带与 seek。 + +> **部署模型**:调度层运行在 **Scheduler Worker** 节点上,是 ColdStore 的**唯一业务中枢**。 +> Gateway 的全部 S3 请求都发往 Scheduler Worker 处理。 +> Scheduler 通过 gRPC 对接 Cache Worker(同机)和 Tape Worker(远程), +> 通过 gRPC 读写 Metadata 集群。Cache/Tape Worker 不直接接受 Gateway 请求。 +> +> **元数据写入协调原则(方案 B)**:Scheduler Worker 是元数据读写的**唯一业务入口**。 +> Gateway 不直连 Metadata。缓存层和磁带层不持有 MetadataClient, +> 所有元数据变更由 Scheduler 统一负责。Console 仅用于管控操作(Worker 增删等)。 + +### 1.1 职责 + +- **归档调度**:扫描 ColdPending 对象,聚合为 ArchiveBundle,调度磁带顺序写入 +- **取回调度**:管理 Restore 队列,按 archive/tape 合并请求,调度磁带顺序读取 +- 合并同磁带/同归档包的请求,减少换带与定位 +- 控制并发、优先级、超时,保证性能与数据质量 + +### 1.2 磁带特性约束 + +| 特性 | 约束 | 调度影响 | +|------|------|----------| +| 顺序写入 | 必须顺序写,不可随机写 | 归档聚合需保证一次写入流 | +| 顺序读取 | 随机读需 seek,成本高 | 取回合并需按物理位置排序 | +| 换带成本 | 机械臂加载/卸载约 30s–2min | 同磁带任务优先合并 | +| 块大小 | 256KB–1MB 常用,LTO 支持 64KB–8MB | 归档块对齐,缓冲匹配 | +| 吞吐 | LTO-9 约 400 MB/s,LTO-10 更高 | 流式写入,避免小 IO | + +--- + +## 2. 归档调度器:写入聚合逻辑 + +### 2.1 聚合目标 + +- **适配顺序写入**:一次 ArchiveBundle 对应磁带上一段连续写入 +- **最大化吞吐**:聚合足够多对象,避免小 IO、填满驱动流水线 +- **块对齐**:与磁带 block_size(如 256KB)对齐,减少驱动内部缓冲碎片 + +### 2.2 ArchiveBundle 构建策略 + +#### 2.2.1 聚合维度 + +| 维度 | 策略 | 说明 | +|------|------|------| +| **大小** | `min_archive_size_mb` ~ `max_archive_size_mb` | 单 Bundle 总大小范围,避免过小(浪费换带)或过大(单任务过长) | +| **对象数** | `batch_size` | 单 Bundle 最大对象数,控制元数据与索引开销 | +| **时间窗口** | `aggregation_window_secs` | 同一窗口内 ColdPending 对象可聚合,避免长时间等待 | +| **桶/前缀** | 可选同 bucket 聚合 | 便于后续按桶管理,非强制 | + +#### 2.2.2 聚合算法(伪代码) + +``` +1. 扫描 ColdPending 对象,按 created_at 或 size 排序 +2. 初始化当前 Bundle = [] +3. for each object: + if 加入后 total_size <= max_archive_size_mb AND count <= batch_size: + Bundle.append(object) + else: + 提交当前 Bundle 为 ArchiveBundle + 重置 Bundle,将 object 加入 +4. 若剩余 Bundle 且 total_size >= min_archive_size_mb: 提交 +5. 若剩余 Bundle 且 total_size < min_archive_size_mb: 等待下一轮或超时强制提交 +``` + +#### 2.2.3 磁带上的物理布局 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 磁带物理布局 (一个 ArchiveBundle) │ +├─────────────────────────────────────────────────────────────────┤ +│ [FileMark] [Obj1 Header][Obj1 Data] [FileMark] [Obj2 Header]... │ +│ │ │ │ │ │ +│ │ │ │ └─ 对象边界,便于定位 │ +│ │ │ └─ 块对齐(256KB)连续写入 │ +│ │ └─ 元数据:bucket, key, size, checksum, offset │ +│ └─ 归档包边界,MTFSF/MTBSF 可跳过 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +- **FileMark**:MTIO 文件标记,用于 seek 时按“文件”定位 +- **对象块**:固定或可变块,与 `block_size` 对齐 +- **索引**:每个对象在 Bundle 内的逻辑偏移写入 Bundle 头部或独立索引区,供取回定位 + +### 2.3 顺序写入流水线 + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ 对象读取 │───▶│ 块对齐 │───▶│ 双缓冲 │───▶│ 磁带写入 │ +│ (热存储) │ │ 填充 │ │ 流水线 │ │ (顺序) │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ +``` + +- **块对齐**:不足 block_size 的对象尾部填充(padding),或使用可变块(需驱动支持) +- **双缓冲**:预读下一批数据,写入当前批,保持驱动持续流式写入 +- **背压**:若热存储读取慢于磁带写入,可降低并发或增大缓冲 + +### 2.4 归档流程(细化) + +1. **调度层**:从元数据扫描 ColdPending,按策略聚合为 ArchiveBundle +2. **调度层**:选定目标磁带(当前写入头或新磁带) +3. **调度层 → 磁带层**:申请磁带驱动,获取独占 +4. **磁带层**:顺序写入:FileMark → [Obj1 Header + Data] → FileMark → [Obj2...] +5. **调度层**:写入完成后计算并存储 Bundle 校验和(可选) +6. **调度层 → 元数据层**:更新 ObjectMetadata(Cold, archive_id, tape_id, tape_block_offset)、ArchiveBundle、TapeInfo +7. **调度层 → 磁带层**:释放驱动;释放在线存储空间 + +> 步骤 6 由调度层统一负责,磁带层不直接写元数据。 + +### 2.5 归档关键参数 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| block_size | 262144 (256KB) | 与 LTO 常用块对齐 | +| min_archive_size_mb | 100 | 单 Bundle 最小,避免过小 | +| max_archive_size_mb | 10240 (10GB) | 单 Bundle 最大,控制单任务时长 | +| batch_size | 500–2000 | 单 Bundle 对象数 | +| write_buffer_mb | 64–128 | 写入缓冲,匹配驱动流水线 | +| target_throughput_mbps | 300 | 目标吞吐,LTO-9 可达 400 | + +--- + +## 3. 取回调度器:拉取聚合逻辑 + +### 3.1 聚合目标 + +- **减少换带**:同磁带上的多个请求合并为一次加载、一次顺序读取 +- **减少 seek**:同 archive 内按物理偏移排序,顺序读取 +- **合并同对象请求**:同一对象被多次 Restore 时只读一次,多路分发 + +### 3.2 取回合并策略 + +#### 3.2.1 合并层次 + +``` +Level 1: 同 archive_id 的多个对象 + → 一次磁带读取,按 tape_block_offset 排序,顺序读 + +Level 2: 同 tape_id 的多个 archive + → 一次换带,按 archive 在磁带上的物理顺序排序 + +Level 3: 同对象的多个 RecallTask(重复请求) + → 合并为一个读取,结果分发给多个请求方 +``` + +#### 3.2.2 合并算法(伪代码) + +``` +1. 从队列取出待调度 RecallTask 集合 +2. 按 tape_id 分组 +3. for each tape_id: + tasks = 该磁带上的所有任务 + 按 (archive_id, tape_block_offset) 排序,保证顺序读 + 若存在同 (bucket, key) 的多个 task,去重,保留一个读取 + 生成 TapeReadJob: { tape_id, [(archive_id, [objects])] } +4. 按优先级(Expedited > Standard > Bulk)排序 TapeReadJob +5. 分配可用驱动,执行 TapeReadJob +``` + +#### 3.2.3 磁带读取顺序 + +``` +磁带物理顺序: Archive1 → Archive2 → Archive3 → ... + +取回请求: Obj_A∈Archive2, Obj_B∈Archive1, Obj_C∈Archive2 + +优化后读取顺序: + 1. Seek 到 Archive1 起始 + 2. 读取 Obj_B + 3. Seek 到 Archive2 起始(或顺序经过则无需 seek) + 4. 顺序读取 Obj_A, Obj_C +``` + +- 同一 Archive 内对象按 `tape_block_offset` 排序,尽量顺序读 +- 跨 Archive 时,按磁带物理顺序排列 Archive,减少回退(MTBSF 成本高于 MTFSF) + +### 3.3 取回流程(细化) + +1. **协议层 → 调度层**:接收 Restore 请求;协议层查询 ObjectMetadata 获取 `archive_id`、`tape_id`、`tape_block_offset`、`object_size`、`checksum` 等,构造 RecallTask 后提交给调度层入队 +2. **调度层**:从队列取任务,按 tape_id + archive_id 合并 +3. **调度层 → 磁带层**:检查磁带状态,ONLINE → 继续;OFFLINE → 通知并等待 +4. **调度层 → 磁带层**:申请驱动,加载磁带(若未加载) +5. **磁带层**:按物理顺序执行读取:Seek → Read → 返回数据 +6. **调度层 → 缓存层**:`cache.put_restored(bucket, key, data, checksum, expire_at)` +7. **调度层 → 元数据层**:更新 restore_status=Completed,restore_expire_at +8. **调度层 → 磁带层**:释放驱动 + +> 步骤 6-7 体现方案 B 的核心原则:调度层先写缓存,再更新元数据。缓存层和磁带层均不持有 MetadataClient。 + +### 3.4 取回关键参数 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| queue_size | 10000 | 队列容量 | +| max_concurrent_restores | 驱动数 | 每驱动一个取回流水线 | +| restore_timeout_secs | 3600 | 单任务超时 | +| min_restore_interval_secs | 300 | 最小取回间隔(5 分钟 SLA) | +| read_buffer_mb | 64 | 读取缓冲 | +| merge_window_secs | 60 | 合并窗口,窗口内同磁带任务可合并 | + +--- + +## 4. 调度机制 + +### 4.1 驱动分配与竞争 + +| 资源 | 策略 | +|------|------| +| 磁带驱动 | 归档与取回共享驱动池,可配置比例(如 2:1 或独立池) | +| 优先级 | 取回 Expedited > 归档 > 取回 Standard > 取回 Bulk | +| 抢占 | 一般不抢占,当前任务完成后按优先级分配 | +| 预留 | Expedited 可预留 1 个驱动,保证高优先级 | + +### 4.2 队列模型 + +``` + ┌─────────────────────────────────┐ + │ Recall 优先级队列 │ + │ Expedited | Standard | Bulk │ + └─────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ ▼ + ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ + │ 合并器 │ │ 合并器 │ │ 合并器 │ + │ (按 tape 聚合) │ │ (按 tape 聚合) │ │ (按 tape 聚合) │ + └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ + │ │ │ + └───────────────────┼───────────────────┘ + ▼ + ┌─────────────────────────────────┐ + │ 驱动调度器 │ + │ Drive 1 | Drive 2 | Drive 3 │ + └─────────────────────────────────┘ +``` + +### 4.3 归档调度周期 + +- **扫描周期**:`scan_interval_secs`(如 3600s) +- **触发条件**:定时 + 可选事件触发(ColdPending 积累达阈值) +- **并发**:多驱动时可并行执行多个 ArchiveTask,每个 Task 独占一驱动 + +--- + +## 5. 性能设计 + +### 5.1 吞吐目标 + +| 场景 | 目标 | 实现要点 | +|------|------|----------| +| 归档写入 | ≥ 300 MB/s | 块对齐、双缓冲、流式写入 | +| 取回 | ≥ 1 任务/5 分钟 | 合并减少换带,多驱动并行 | + +### 5.2 性能优化 + +| 手段 | 说明 | +|------|------| +| 块对齐 | 256KB 块,减少驱动内部碎片 | +| 大缓冲 | 64–128MB 写入/读取缓冲 | +| 预读 | 归档时预读下一批对象 | +| 合并 | 取回时同磁带、同 archive 合并 | +| 并行 | 多驱动时归档与取回可并行 | + +### 5.3 背压与限流 + +- 归档:若热存储读取慢,可暂停新 Bundle 提交 +- 取回:队列满时拒绝新 Restore,返回 503 +- Expedited:可配置最大并发,超限返回 GlacierExpeditedRetrievalNotAvailable + +--- + +## 6. 质量与可靠性 + +### 6.1 数据完整性 + +| 机制 | 说明 | +|------|------| +| 对象校验和 | 归档时写入 CRC32/SHA256,取回时校验 | +| Bundle 校验和 | 可选,整 Bundle 校验 | +| 写入后验证 | 可选,归档完成后读回校验(成本高) | + +### 6.2 失败与重试 + +| 场景 | 策略 | +|------|------| +| 归档写入失败 | 重试 3 次,仍失败则标记 Bundle Failed,对象回退 ColdPending | +| 取回读取失败 | 重试 2 次,仍失败则 RecallTask Failed,通知用户 | +| 驱动故障 | 切换备用驱动,重新加载磁带 | +| 磁带不可读 | 若有副本,切换副本;否则通知人工 | + +### 6.3 一致性(调度层统一协调) + +调度层作为元数据写入的唯一协调者,需保证以下写入顺序: + +**归档**: + +| 顺序 | 操作 | 失败处理 | +|------|------|----------| +| 1 | 磁带层顺序写入完成 | 重试/换带 | +| 2 | **调度层 → 元数据层**:更新 ObjectMetadata、ArchiveBundle、TapeInfo | 重试/补偿任务 | + +**取回**: + +| 顺序 | 操作 | 失败处理 | +|------|------|----------| +| 1 | 磁带层读取完成 | 重试/切换副本 | +| 2 | **调度层 → 缓存层**:put_restored | 重试/标记 Failed | +| 3 | **调度层 → 元数据层**:restore_status=Completed | 重试;若持续失败,缓存有数据但 GET 不可用,需补偿 | + +> 缓存层和磁带层均为被编排方,不主动写元数据。 + +--- + +## 7. 离线磁带处理 + +- 磁带状态:ONLINE / OFFLINE / UNKNOWN +- 取回命中 OFFLINE 磁带: + 1. 生成通知事件(tape_id、槽位、archive_id、请求方) + 2. 任务进入等待队列,不占用驱动 + 3. 人工加载磁带并确认 ONLINE 后,调度器自动重试 +- 合并:多个请求命中同一离线磁带时,合并为一次通知,加载后批量处理 + +--- + +## 8. 核心数据结构 + +### 8.1 ArchiveScheduler + +```rust +pub struct ArchiveScheduler { + metadata: Arc, + tape_manager: Arc, + config: ArchiveConfig, + running: AtomicBool, +} +``` + +| 字段 | 类型 | 含义 | 说明 | +|------|------|------|------| +| `metadata` | Arc\ | 元数据客户端 | 扫描 ColdPending、写入 ArchiveBundle、更新 ObjectMetadata | +| `tape_manager` | Arc\ | 磁带管理器 | 申请驱动、选磁带、顺序写入 | +| `config` | ArchiveConfig | 归档配置 | 聚合参数、块大小、缓冲等 | +| `running` | AtomicBool | 运行状态标记 | 优雅停止扫描循环 | + +### 8.2 RecallScheduler + +```rust +pub struct RecallScheduler { + metadata: Arc, + tape_manager: Arc, + cache: Arc, + queue: RecallQueue, + config: RecallConfig, +} +``` + +| 字段 | 类型 | 含义 | 说明 | +|------|------|------|------| +| `metadata` | Arc\ | 元数据客户端 | 读取 RecallTask、更新 restore_status | +| `tape_manager` | Arc\ | 磁带管理器 | 申请驱动、加载磁带、定位读取 | +| `cache` | Arc\ | 缓存写接口 | put_restored 写入解冻数据 | +| `queue` | RecallQueue | 取回优先级队列 | 三级优先级 + 合并窗口 | +| `config` | RecallConfig | 取回配置 | 队列大小、超时、合并窗口等 | + +### 8.3 ArchiveBundle(归档包) + +一批对象在磁带上的连续写入单元,是归档调度的基本粒度。 + +```rust +pub struct ArchiveBundle { + pub id: Uuid, + pub tape_id: String, + pub tape_set: Vec, + pub entries: Vec, + pub total_size: u64, + pub filemark_start: u32, + pub filemark_end: u32, + pub checksum: Option, + pub status: ArchiveBundleStatus, + pub created_at: DateTime, + pub completed_at: Option>, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `id` | Uuid | 归档包唯一标识 | 全局唯一,元数据 CF `bundles` 的 key | +| `tape_id` | String | 主副本所在磁带 ID | 取回时定位磁带 | +| `tape_set` | Vec\ | 所有副本磁带 ID 列表 | 双副本场景:`["TAPE001", "TAPE002"]` | +| `entries` | Vec\ | 包内对象列表(含偏移信息) | 取回时按 offset 定位单个对象 | +| `total_size` | u64 | 包内所有对象总字节 | 选磁带时判断剩余空间 | +| `filemark_start` | u32 | Bundle 起始 FileMark 编号 | 磁带 seek 时 `MTFSF(filemark_start)` 快速定位 | +| `filemark_end` | u32 | Bundle 结束 FileMark 编号 | 标记 Bundle 边界 | +| `checksum` | Option\ | 整个 Bundle 的 SHA256(可选) | 写入后可选验证 | +| `status` | ArchiveBundleStatus | 状态 | `Pending` → `Writing` → `Completed` / `Failed` | +| `created_at` | DateTime\ | 创建时间 | 聚合窗口计算 | +| `completed_at` | Option\\> | 完成时间 | 审计与可观测性 | + +### 8.4 BundleEntry(Bundle 内单个对象条目) + +描述一个对象在 Bundle 内的物理位置,用于取回时精确定位。 + +```rust +pub struct BundleEntry { + pub bucket: String, + pub key: String, + pub version_id: Option, + pub size: u64, + pub offset_in_bundle: u64, + pub tape_block_offset: u64, + pub checksum: String, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `bucket` | String | S3 桶名 | 与 key 唯一标识对象 | +| `key` | String | S3 对象键 | 取回后写缓存、更新元数据时需要 | +| `version_id` | Option\ | 对象版本 ID | 多版本场景 | +| `size` | u64 | 对象原始字节数 | 读取时分配缓冲 | +| `offset_in_bundle` | u64 | 对象在 Bundle 内的字节偏移 | 取回时计算磁带读取偏移 | +| `tape_block_offset` | u64 | 对象在磁带上的块偏移(相对 FileMark) | `MTFSR(tape_block_offset)` 精确定位 | +| `checksum` | String | SHA256 hex | 取回时校验数据完整性 | + +### 8.5 ObjectHeader(磁带上对象头) + +写入磁带时,每个对象前写入 Header,采用**定长前缀 + 变长 bucket/key** 的混合格式。解析时先读定长部分获取 `bucket_len`/`key_len`,再按长度读取变长字符串。 + +> ObjectHeader 与 BundleEntry 存在冗余:BundleEntry 在元数据中记录同样信息。ObjectHeader 的作用是**磁带端自描述**——用于边界检测(magic)、取回时校验(checksum)、以及元数据丢失后的数据恢复。 + +```rust +pub struct ObjectHeader { + pub magic: [u8; 4], + pub version: u8, + pub bucket_len: u16, + pub bucket: String, + pub key_len: u16, + pub key: String, + pub size: u64, + pub checksum: [u8; 32], + pub flags: u8, + pub reserved: [u8; 16], +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `magic` | [u8; 4] | 魔数 `0x43 0x53 0x4F 0x48`("CSOH") | 标识 Header 起始,校验格式正确 | +| `version` | u8 | Header 格式版本号 | 向前兼容,当前 `1` | +| `bucket_len` | u16 | bucket 字符串字节长度 | 解析定位 | +| `bucket` | String | S3 桶名 | 取回时还原对象归属 | +| `key_len` | u16 | key 字符串字节长度 | 解析定位 | +| `key` | String | S3 对象键 | 取回时还原对象标识 | +| `size` | u64 | 对象数据字节数 | 取回时读取确切长度 | +| `checksum` | [u8; 32] | SHA256 原始字节 | 取回时逐字节校验 | +| `flags` | u8 | 标志位(bit 0: 压缩, bit 1: 加密) | 预留,当前全 0 | +| `reserved` | [u8; 16] | 保留字段 | 未来扩展,写入时全 0 | + +### 8.6 ArchiveTask(归档任务) + +```rust +pub struct ArchiveTask { + pub id: Uuid, + pub bundle_id: Uuid, + pub tape_id: String, + pub drive_id: Option, + pub object_count: u32, + pub total_size: u64, + pub bytes_written: u64, + pub status: ArchiveTaskStatus, + pub retry_count: u32, + pub created_at: DateTime, + pub started_at: Option>, + pub completed_at: Option>, + pub error: Option, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `id` | Uuid | 任务唯一标识 | 元数据 CF `archive_tasks` 的 key | +| `bundle_id` | Uuid | 关联的 ArchiveBundle ID | 追踪任务与 Bundle 的关系 | +| `tape_id` | String | 目标磁带 ID | 记录写入的磁带 | +| `drive_id` | Option\ | 分配的驱动 ID | 执行中才有值 | +| `object_count` | u32 | 本次归档对象数 | 进度监控 | +| `total_size` | u64 | 总字节数 | 进度百分比 = bytes_written / total_size | +| `bytes_written` | u64 | 已写入字节数 | 进度追踪、断点续传参考 | +| `status` | ArchiveTaskStatus | 任务状态 | `Pending` → `InProgress` → `Completed` / `Failed` | +| `retry_count` | u32 | 已重试次数 | 超过 max_retries 则标记 Failed | +| `created_at` | DateTime\ | 任务创建时间 | 审计 | +| `started_at` | Option\\> | 开始执行时间 | 获取驱动后设置 | +| `completed_at` | Option\\> | 完成时间 | 计算耗时 | +| `error` | Option\ | 错误信息 | Failed 时记录原因 | + +### 8.7 RecallTask(取回任务) + +```rust +pub struct RecallTask { + pub id: Uuid, + pub bucket: String, + pub key: String, + pub version_id: Option, + pub archive_id: Uuid, + pub tape_id: String, + pub tape_set: Vec, + pub tape_block_offset: u64, + pub object_size: u64, + pub checksum: String, + pub tier: RestoreTier, + pub days: u32, + pub expire_at: DateTime, + pub status: RestoreStatus, + pub drive_id: Option, + pub retry_count: u32, + pub created_at: DateTime, + pub started_at: Option>, + pub completed_at: Option>, + pub error: Option, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `id` | Uuid | 任务唯一标识 | 元数据 CF `recall_tasks` 的 key | +| `bucket` | String | S3 桶名 | 取回后写缓存、更新元数据 | +| `key` | String | S3 对象键 | 与 bucket 唯一标识对象 | +| `version_id` | Option\ | 对象版本 | 多版本场景 | +| `archive_id` | Uuid | 所属 ArchiveBundle ID | 定位 BundleEntry 获取磁带偏移 | +| `tape_id` | String | 主副本磁带 ID | 默认从此磁带读取 | +| `tape_set` | Vec\ | 所有副本磁带列表 | 主磁带不可读时切换副本 | +| `tape_block_offset` | u64 | 对象在磁带上的块偏移 | seek 定位 | +| `object_size` | u64 | 对象字节数 | 读取时分配缓冲 | +| `checksum` | String | SHA256 hex | 读取后校验完整性,写缓存时传入 | +| `tier` | RestoreTier | 取回优先级 | `Expedited` / `Standard` / `Bulk`,影响队列排序 | +| `days` | u32 | 解冻保留天数 | 计算 expire_at = now + days | +| `expire_at` | DateTime\ | 解冻过期时间 | 写入缓存 xattr 和元数据 restore_expire_at | +| `status` | RestoreStatus | 任务状态 | `Pending` → `InProgress` → `Completed` / `Failed` | +| `drive_id` | Option\ | 分配的驱动 ID | 执行中才有值 | +| `retry_count` | u32 | 已重试次数 | 超限则标记 Failed | +| `created_at` | DateTime\ | 任务创建时间 | 排队顺序参考 | +| `started_at` | Option\\> | 开始执行时间 | 计算 SLA | +| `completed_at` | Option\\> | 完成时间 | 计算耗时 | +| `error` | Option\ | 错误信息 | 失败原因 | + +### 8.8 RestoreTier(取回优先级) + +```rust +pub enum RestoreTier { + Expedited, + Standard, + Bulk, +} +``` + +| 值 | 含义 | 对应 SLA | 队列优先级 | +|------|------|----------|-----------| +| `Expedited` | 加急 | 1–5 分钟 | 最高,可预留驱动 | +| `Standard` | 标准 | 3–5 小时 | 中 | +| `Bulk` | 批量 | 5–12 小时 | 最低 | + +### 8.9 TapeReadJob(磁带读取作业) + +取回合并后的执行单元,一个 TapeReadJob = 一次换带 + 一次顺序读取。 + +```rust +pub struct TapeReadJob { + pub job_id: Uuid, + pub tape_id: String, + pub drive_id: Option, + pub segments: Vec, + pub total_objects: u32, + pub total_bytes: u64, + pub priority: RestoreTier, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `job_id` | Uuid | 作业唯一标识 | 日志追踪 | +| `tape_id` | String | 目标磁带 ID | 加载到驱动 | +| `drive_id` | Option\ | 分配的驱动 | 执行时填入 | +| `segments` | Vec\ | 按物理顺序排列的读取段列表 | 顺序执行,避免回退 seek | +| `total_objects` | u32 | 本次作业对象总数 | 监控 | +| `total_bytes` | u64 | 总字节数 | 监控 | +| `priority` | RestoreTier | 作业优先级(取组内最高) | 驱动分配排序 | + +### 8.10 ReadSegment(读取段) + +```rust +pub struct ReadSegment { + pub archive_id: Uuid, + pub filemark: u32, + pub objects: Vec, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `archive_id` | Uuid | 所属 ArchiveBundle ID | 日志追踪 | +| `filemark` | u32 | 该 Bundle 的起始 FileMark(来自 `ArchiveBundle.filemark_start`) | `MTFSF(filemark)` 定位 | +| `objects` | Vec\ | 段内对象列表,按 tape_block_offset 升序 | 顺序读取 | + +### 8.11 ReadObject(待读取对象) + +```rust +pub struct ReadObject { + pub recall_task_id: Uuid, + pub bucket: String, + pub key: String, + pub tape_block_offset: u64, + pub size: u64, + pub checksum: String, + pub expire_at: DateTime, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `recall_task_id` | Uuid | 关联的 RecallTask ID | 完成后更新任务状态 | +| `bucket` | String | S3 桶名 | 写缓存时传入 | +| `key` | String | S3 对象键 | 写缓存时传入 | +| `tape_block_offset` | u64 | 磁带上块偏移 | `MTFSR(offset)` 精确定位 | +| `size` | u64 | 对象字节数 | 分配读取缓冲 | +| `checksum` | String | SHA256 hex | 读取后校验 | +| `expire_at` | DateTime\ | 解冻过期时间 | 写入缓存 xattr | + +### 8.12 TapeWriteJob(磁带写入作业) + +```rust +pub struct TapeWriteJob { + pub job_id: Uuid, + pub bundle: ArchiveBundle, + pub tape_id: String, + pub drive_id: Option, + pub bytes_written: u64, + pub objects_written: u32, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `job_id` | Uuid | 作业唯一标识 | 日志追踪 | +| `bundle` | ArchiveBundle | 待写入的归档包 | 含完整对象列表和元数据 | +| `tape_id` | String | 目标磁带 | 写入到此磁带 | +| `drive_id` | Option\ | 分配的驱动 | 执行时填入 | +| `bytes_written` | u64 | 已写字节 | 进度追踪 | +| `objects_written` | u32 | 已写对象数 | 进度追踪 | + +### 8.13 RecallQueue(取回优先级队列) + +```rust +pub struct RecallQueue { + expedited: VecDeque, + standard: VecDeque, + bulk: VecDeque, + pending_by_tape: HashMap>, + capacity: usize, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `expedited` | VecDeque\ | 加急队列 | 最高优先级,优先出队 | +| `standard` | VecDeque\ | 标准队列 | 中等优先级 | +| `bulk` | VecDeque\ | 批量队列 | 最低优先级 | +| `pending_by_tape` | HashMap\\> | tape_id → 待处理 task_id 列表 | 合并时快速按磁带分组 | +| `capacity` | usize | 队列总容量上限 | 超限拒绝新任务 | + +**pending_by_tape 维护规则**: +- **入队**:将 task_id 加入 `pending_by_tape[tape_id]` +- **出队/合并**:合并时从 `pending_by_tape` 按 tape 分组取出任务,结合优先级队列做排序 +- **完成/取消**:任务完成或取消后,从 `pending_by_tape` 中移除对应 task_id + +### 8.14 枚举汇总 + +```rust +pub enum ArchiveBundleStatus { + Pending, // 聚合完成,等待写入 + Writing, // 正在写入磁带 + Completed, // 写入成功 + Failed, // 写入失败 +} + +pub enum ArchiveTaskStatus { + Pending, // 等待驱动 + InProgress, // 执行中 + Completed, // 完成 + Failed, // 失败 +} + +pub enum RestoreStatus { + Pending, // 已入队,等待调度 + WaitingForMedia, // 磁带离线,等待人工上线 + InProgress, // 正在从磁带读取 + Completed, // 已写入缓存+元数据 + Expired, // 解冻过期(由定时任务或 GET 时检查 restore_expire_at 触发) + Failed, // 失败 +} +``` + +--- + +## 9. 模块结构 + +``` +src/scheduler/ +├── mod.rs +├── archive/ +│ ├── mod.rs +│ ├── aggregator.rs # 归档聚合逻辑 +│ ├── writer.rs # 顺序写入流水线 +│ └── task.rs # ArchiveTask 执行 +├── recall/ +│ ├── mod.rs +│ ├── merger.rs # 取回合并逻辑 +│ ├── reader.rs # 顺序读取流水线 +│ └── task.rs # RecallTask 执行 +├── queue.rs # 优先级队列 +├── drive_allocator.rs # 驱动分配 +└── types.rs # TapeReadJob, ArchiveBundle 等 +``` + +--- + +## 10. 配置项 + +```yaml +scheduler: + archive: + scan_interval_secs: 3600 + batch_size: 1000 + min_archive_size_mb: 100 + max_archive_size_mb: 10240 + aggregation_window_secs: 3600 + block_size: 262144 + write_buffer_mb: 64 + target_throughput_mbps: 300 + recall: + queue_size: 10000 + max_concurrent_restores: 10 + restore_timeout_secs: 3600 + min_restore_interval_secs: 300 + merge_window_secs: 60 + read_buffer_mb: 64 + expedited_reserved_drives: 1 + drive: + total_drives: 3 + archive_drives: 2 + recall_drives: 1 +``` + +--- + +## 11. 依赖关系 + +**调度层主动依赖**: + +| 依赖 | 方向 | 协议 | 说明 | +|------|------|------|------| +| 元数据层 | Scheduler → Metadata | gRPC | **唯一的元数据业务读写入口** | +| 缓存层 | Scheduler → Cache Worker | gRPC(同机) | 数据暂存/读取 | +| 磁带层 | Scheduler → Tape Worker | gRPC(远程) | 磁带读写指令下发 | + +**被依赖**: + +| 被依赖方 | 协议 | 说明 | +|----------|------|------| +| Gateway | gRPC | **全部** S3 请求(PUT/GET/HEAD/DELETE/Restore/List) | + +> **架构要点**:Scheduler Worker 是唯一的业务中枢,同时持有 MetadataClient、 +> CacheClient(gRPC)、TapeClient(gRPC) 三个依赖。 +> Gateway 所有请求都发往 Scheduler,Gateway 不直连 Metadata/Cache/Tape。 +> 缓存层和磁带层均为纯功能组件,不感知元数据。 + +--- + +## 12. 实施要点 + +- 归档与取回可独立扩缩容 +- 每个磁带驱动对应一条顺序 I/O 流水线,避免多任务共享同一驱动导致 seek +- 增加驱动可线性提升并发归档与取回能力 +- 元数据需记录 `tape_block_offset`,供取回时按物理顺序排序 +- **调度层同时注入 MetadataClient + CacheWriteApi + TapeManager**,是系统中唯一的编排者 diff --git a/docs/modules/06-tape-layer.md b/docs/modules/06-tape-layer.md new file mode 100644 index 0000000..82440a3 --- /dev/null +++ b/docs/modules/06-tape-layer.md @@ -0,0 +1,1012 @@ +# 磁带管理层模块设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) + +## 1. 模块概述 + +磁带管理层提供磁带设备与磁带库的完整管理能力,通过自研 SDK 抽象层实现与底层硬件的解耦,前期对接 Linux SCSI 协议。 + +### 1.1 完整职责 + +| 功能域 | 说明 | +|--------|------| +| 磁带驱动管理 | 驱动发现、状态监控、独占分配、释放 | +| 磁带介质管理 | 介质注册、容量跟踪、格式化、可读性校验、生命周期 | +| 磁带库管理 | 槽位管理、机械臂加载/卸载、盘点、条码扫描 | +| 数据读写 | 顺序写入、顺序/定位读取、FileMark 管理 | +| 离线磁带协同 | 状态跟踪、通知触发、人工确认流程 | +| 可靠性 | 双副本、校验、错误处理、坏带替换 | +| 可观测性 | 驱动利用率、读写吞吐、错误率、换带次数 | + +### 1.2 在架构中的位置 + +``` +归档/取回调度器 + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 磁带管理层 │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ TapeManager (上层 API,面向调度器) │ │ +│ │ - 驱动分配、磁带选择、介质跟踪、通知 │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────────┼────────────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌───────────────┐ ┌────────────────────┐ │ +│ │ TapeDrive│ │ TapeMedia │ │ TapeLibrary │ │ +│ │ trait │ │ trait │ │ trait │ │ +│ │ (驱动) │ │ (介质) │ │ (带库/机械臂) │ │ +│ └──────────┘ └───────────────┘ └────────────────────┘ │ +│ │ │ │ │ +│ ┌────┴────────────────┴───────────────────────┴────┐ │ +│ │ 具体实现层 │ │ +│ │ ScsiTapeDrive | ScsiTapeMedia | ScsiTapeLibrary │ │ +│ │ (前期 Linux SCSI) │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────┴─────────────────────────────────────────────┐ │ +│ │ 底层封装 │ │ +│ │ mtio.rs (MTIO ioctl) | sg.rs (SCSI Generic) │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 代码架构分层 + +### 2.1 分层原则 + +| 层级 | 职责 | 稳定性 | +|------|------|--------| +| **TapeManager** | 面向调度器的业务 API,编排驱动/介质/库 | 最稳定,不随硬件变化 | +| **SDK 抽象层** (trait) | 定义 TapeDrive / TapeMedia / TapeLibrary 接口 | 稳定,接口不频繁变更 | +| **实现层** | 具体硬件对接(SCSI、厂商 SDK) | 可替换,不影响上层 | +| **底层封装** | ioctl / SCSI 命令封装 | 最底层,平台相关 | + +### 2.2 扩展策略 + +- 新硬件后端:实现 TapeDrive / TapeMedia / TapeLibrary trait 即可 +- 上层调度器:仅依赖 TapeManager API,不感知底层实现 +- 配置切换:通过 `tape.sdk.backend` 配置选择实现 + +--- + +## 3. SDK 抽象层:trait 定义 + +### 3.1 TapeDrive trait(磁带驱动) + +驱动是数据读写的执行单元,一次绑定一盘磁带。 + +```rust +/// 磁带驱动抽象 — 数据 I/O 的执行单元 +#[async_trait] +pub trait TapeDrive: Send + Sync { + // ── 标识 ── + fn drive_id(&self) -> &str; + fn device_path(&self) -> &str; + + // ── 数据读写 ── + async fn write_blocks(&self, data: &[u8]) -> Result; + async fn read_blocks(&self, buf: &mut [u8]) -> Result; + + // ── 定位 ── + async fn seek_to_filemark(&self, count: i32) -> Result<()>; + async fn seek_to_record(&self, count: i32) -> Result<()>; + async fn seek_to_end_of_data(&self) -> Result<()>; + async fn rewind(&self) -> Result<()>; + + // ── FileMark ── + async fn write_filemark(&self, count: u32) -> Result<()>; + + // ── 状态 ── + async fn status(&self) -> Result; + async fn position(&self) -> Result; + async fn is_ready(&self) -> Result; + + // ── 控制 ── + async fn eject(&self) -> Result<()>; + async fn set_block_size(&self, size: u32) -> Result<()>; + async fn set_compression(&self, enabled: bool) -> Result<()>; +} +``` + +### 3.2 TapeMedia trait(磁带介质) + +介质管理覆盖单盘磁带的生命周期。 + +```rust +/// 磁带介质抽象 — 单盘磁带的元数据与生命周期 +#[async_trait] +pub trait TapeMedia: Send + Sync { + // ── 标识 ── + fn media_id(&self) -> &str; + fn barcode(&self) -> Option<&str>; + fn format(&self) -> &str; // "LTO-9", "LTO-10" + + // ── 容量 ── + async fn capacity(&self) -> Result; + async fn remaining(&self) -> Result; + async fn is_full(&self) -> Result; + + // ── 状态 ── + fn status(&self) -> MediaStatus; + fn set_status(&mut self, status: MediaStatus); + fn location(&self) -> MediaLocation; + fn set_location(&mut self, location: MediaLocation); + + // ── 生命周期 ── + async fn verify_readable(&self, drive: &dyn TapeDrive) -> Result; + async fn erase(&self, drive: &dyn TapeDrive) -> Result<()>; + + // ── 元数据 ── + fn label(&self) -> Option<&str>; + async fn write_label(&self, drive: &dyn TapeDrive, label: &str) -> Result<()>; + fn archive_bundles(&self) -> &[Uuid]; + fn add_archive_bundle(&mut self, bundle_id: Uuid); +} +``` + +### 3.3 TapeLibrary trait(磁带库) + +带库管理覆盖机械臂、槽位、盘点等自动化操作。 + +```rust +/// 磁带库抽象 — 含机械臂的自动化磁带库 +#[async_trait] +pub trait TapeLibrary: Send + Sync { + fn library_id(&self) -> &str; + + // ── 槽位管理 ── + async fn list_slots(&self) -> Result>; + async fn list_drives(&self) -> Result>; + + // ── 机械臂操作 ── + // drive_element 为 SCSI Data Transfer Element Address(u32) + // TapeManager 负责将字符串 drive_id 映射为 SCSI element address + async fn load(&self, slot_id: u32, drive_element: u32) -> Result<()>; + async fn unload(&self, drive_element: u32, slot_id: u32) -> Result<()>; + async fn transfer(&self, from_slot: u32, to_slot: u32) -> Result<()>; + + // ── 盘点 ── + async fn inventory(&self) -> Result; + + // ── 导入/导出 ── + async fn list_import_export_slots(&self) -> Result>; + + // ── 状态 ── + async fn status(&self) -> Result; +} +``` + +### 3.4 类型定义与字段说明 + +#### 3.4.1 DriveStatus(驱动状态) + +```rust +pub struct DriveStatus { + pub drive_id: String, + pub is_ready: bool, + pub has_media: bool, + pub media_id: Option, + pub block_size: u32, + pub compression: bool, + pub write_protected: bool, + pub error: Option, +} +``` + +| 字段 | 类型 | 含义 | 来源 | +|------|------|------|------| +| `drive_id` | String | 驱动唯一标识 | 配置文件定义 | +| `is_ready` | bool | 驱动是否就绪可执行 I/O | MTIOCGET 解析 | +| `has_media` | bool | 是否已装载磁带 | MTIOCGET `GMT_DR_OPEN` 取反 | +| `media_id` | Option\ | 当前装载的磁带 ID | MediaRegistry 关联 | +| `block_size` | u32 | 当前块大小(字节) | MTIOCGET `mt_dsreg` 低 24 位 | +| `compression` | bool | 硬件压缩是否开启 | MTIOCGET 驱动标志 | +| `write_protected` | bool | 磁带是否写保护 | MTIOCGET `GMT_WR_PROT` | +| `error` | Option\ | 当前错误(无错误为 None) | 异常检测后设置 | + +#### 3.4.2 TapePosition(磁带位置) + +```rust +pub struct TapePosition { + pub filemark_number: u32, + pub block_number: u64, + pub at_bot: bool, + pub at_eod: bool, + pub at_filemark: bool, +} +``` + +| 字段 | 类型 | 含义 | 来源 | +|------|------|------|------| +| `filemark_number` | u32 | 当前所在 FileMark 编号(0-based) | MTIOCPOS 或软件计数 | +| `block_number` | u64 | 当前块编号 | MTIOCPOS `mt_blkno` | +| `at_bot` | bool | 是否在磁带起始(Beginning of Tape) | MTIOCGET `GMT_BOT` | +| `at_eod` | bool | 是否在数据末尾(End of Data) | MTIOCGET `GMT_EOD` | +| `at_filemark` | bool | 是否刚经过 FileMark | MTIOCGET `GMT_EOF` | + +#### 3.4.3 MediaCapacity(介质容量) + +```rust +pub struct MediaCapacity { + pub total_bytes: u64, + pub used_bytes: u64, + pub remaining_bytes: u64, +} +``` + +| 字段 | 类型 | 含义 | 来源 | +|------|------|------|------| +| `total_bytes` | u64 | 磁带总容量(字节) | 介质规格(LTO-9: 18TB 原始) | +| `used_bytes` | u64 | 已使用字节 | MediaRegistry 累计写入跟踪 | +| `remaining_bytes` | u64 | 剩余可用字节 | `total_bytes - used_bytes` | + +#### 3.4.4 MediaStatus(介质状态) + +```rust +pub enum MediaStatus { + Online, + Offline, + InDrive(String), + ImportExport, + Error(String), + Retired, +} +``` + +| 值 | 含义 | 触发条件 | +|------|------|----------| +| `Online` | 在库中,空闲可用 | 盘点确认在槽位中 | +| `Offline` | 离线存放,不在库中 | 盘点未发现或人工标记 | +| `InDrive(drive_id)` | 已加载到指定驱动中 | `load()` 成功后设置 | +| `ImportExport` | 在导入/导出槽位 | 盘点发现在 I/E 槽位 | +| `Error(msg)` | 异常状态 | 读写错误、校验失败 | +| `Retired` | 已退役 | 不可读块过多或超龄 | + +#### 3.4.5 MediaLocation(介质位置) + +```rust +pub enum MediaLocation { + Slot(u32), + Drive(String), + Offsite(String), + Unknown, +} +``` + +| 值 | 含义 | 用途 | +|------|------|------| +| `Slot(slot_id)` | 在库中第 N 号槽位 | `load(slot_id, drive_id)` 时定位 | +| `Drive(drive_id)` | 在某驱动中 | 已加载,可直接读写 | +| `Offsite(location)` | 离线存放位置描述 | 人工找回时参考,如 "机房B-柜3-层2" | +| `Unknown` | 未知 | 初始状态或盘点失败 | + +#### 3.4.6 DriveError(驱动错误) + +```rust +pub enum DriveError { + MediaNotLoaded, + WriteProtected, + HardwareError(String), + MediaError(String), + CleaningRequired, +} +``` + +| 值 | 含义 | 处理策略 | +|------|------|----------| +| `MediaNotLoaded` | 未装载磁带 | 需先 load | +| `WriteProtected` | 磁带写保护开关已开 | 无法写入,需更换磁带或关闭物理开关 | +| `HardwareError(msg)` | 驱动硬件故障 | 标记驱动不可用,切换备用 | +| `MediaError(msg)` | 介质错误(坏块等) | 重试 → 切换副本 → 标记坏带 | +| `CleaningRequired` | 驱动需要清洁 | 发送通知,插入清洁带 | + +#### 3.4.7 SlotInfo(槽位信息) + +```rust +pub struct SlotInfo { + pub slot_id: u32, + pub element_type: SlotType, + pub is_full: bool, + pub media_id: Option, + pub barcode: Option, +} +``` + +| 字段 | 类型 | 含义 | 来源 | +|------|------|------|------| +| `slot_id` | u32 | SCSI Element Address(槽位编号) | READ ELEMENT STATUS 响应 | +| `element_type` | SlotType | 槽位类型 | SCSI Element Type Code | +| `is_full` | bool | 是否有磁带 | Element Status `Full` 位 | +| `media_id` | Option\ | 磁带 ID(若有) | MediaRegistry 关联 | +| `barcode` | Option\ | 条码读取结果 | Volume Tag(若带库支持) | + +#### 3.4.8 SlotType(槽位类型) + +```rust +pub enum SlotType { + Storage, + DataTransfer, + ImportExport, + MediumTransport, +} +``` + +| 值 | SCSI Element Type | 含义 | +|------|-------------------|------| +| `Storage` | 0x02 | 存储槽位,存放磁带 | +| `DataTransfer` | 0x04 | 驱动位,磁带装载到此处可读写 | +| `ImportExport` | 0x03 | 导入/导出槽位("邮箱"),人工放入/取出磁带 | +| `MediumTransport` | 0x01 | 机械臂本身 | + +#### 3.4.9 VerifyResult(校验结果) + +```rust +pub struct VerifyResult { + pub readable: bool, + pub blocks_verified: u64, + pub blocks_failed: u64, + pub errors: Vec, + pub duration_secs: u64, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `readable` | bool | 整体是否可读 | 快速判断磁带健康 | +| `blocks_verified` | u64 | 成功校验的块数 | 校验覆盖度 | +| `blocks_failed` | u64 | 校验失败的块数 | 超过阈值触发退役 | +| `errors` | Vec\ | 具体错误列表 | 定位坏块位置 | +| `duration_secs` | u64 | 校验耗时(秒) | 可观测性 | + +#### 3.4.10 WriteResult(写入结果) + +```rust +pub struct WriteResult { + pub bytes_written: u64, + pub blocks_written: u64, + pub filemark_position: u32, + pub tape_block_offset: u64, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `bytes_written` | u64 | 实际写入字节数 | 更新 MediaCapacity.used_bytes | +| `blocks_written` | u64 | 实际写入块数 | 日志/监控 | +| `filemark_position` | u32 | 写入后的 FileMark 编号 | 记录到 BundleEntry 用于取回定位 | +| `tape_block_offset` | u64 | 写入起始的磁带块偏移 | 记录到 BundleEntry.tape_block_offset | + +#### 3.4.11 MediaRegistration(介质注册信息) + +```rust +pub struct MediaRegistration { + pub media_id: String, + pub barcode: Option, + pub format: String, + pub capacity_bytes: u64, + pub label: Option, + pub initial_location: MediaLocation, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `media_id` | String | 磁带唯一标识 | 全局唯一,如 "TAPE001" | +| `barcode` | Option\ | 条码(物理粘贴) | 带库自动识别 | +| `format` | String | 介质格式 | "LTO-9"、"LTO-10" | +| `capacity_bytes` | u64 | 标称容量 | LTO-9: 18TB = 18,000,000,000,000 | +| `label` | Option\ | 逻辑标签 | 写入磁带头部,人工可识别 | +| `initial_location` | MediaLocation | 初始位置 | Slot(N) 或 Offsite("...") | + +#### 3.4.12 LibraryInventory / DriveSlotInfo / LibraryStatus + +```rust +pub struct LibraryInventory { + pub slots: Vec, + pub drives: Vec, + pub import_export: Vec, + pub total_media: u32, + pub empty_slots: u32, +} +``` + +| 字段 | 类型 | 含义 | +|------|------|------| +| `slots` | Vec\ | 所有存储槽位信息 | +| `drives` | Vec\ | 所有驱动位信息 | +| `import_export` | Vec\ | 导入/导出槽位信息 | +| `total_media` | u32 | 库中磁带总数 | +| `empty_slots` | u32 | 空槽位数 | + +```rust +pub struct DriveSlotInfo { + pub drive_id: u32, + pub device_path: String, + pub is_loaded: bool, + pub media_id: Option, +} +``` + +| 字段 | 类型 | 含义 | +|------|------|------| +| `drive_id` | u32 | SCSI Data Transfer Element Address | +| `device_path` | String | 设备节点路径(如 `/dev/nst0`) | +| `is_loaded` | bool | 是否已装载磁带 | +| `media_id` | Option\ | 已装载磁带的 ID | + +```rust +pub struct LibraryStatus { + pub library_id: String, + pub is_online: bool, + pub total_slots: u32, + pub total_drives: u32, + pub error: Option, +} +``` + +| 字段 | 类型 | 含义 | +|------|------|------| +| `library_id` | String | 带库唯一标识 | +| `is_online` | bool | 带库是否在线可操作 | +| `total_slots` | u32 | 存储槽位总数 | +| `total_drives` | u32 | 驱动总数 | +| `error` | Option\ | 当前错误信息 | + +#### 3.4.13 OfflineRequest / OfflineRequestStatus + +```rust +pub struct OfflineRequest { + pub request_id: Uuid, + pub media_id: String, + pub barcode: Option, + pub last_known_location: String, + pub reason: String, + pub requested_at: DateTime, + pub status: OfflineRequestStatus, + pub notified_at: Option>, + pub confirmed_at: Option>, + pub timeout_at: DateTime, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `request_id` | Uuid | 请求唯一标识 | 跟踪去重 | +| `media_id` | String | 需要上线的磁带 ID | 通知内容 | +| `barcode` | Option\ | 磁带条码 | 人工查找用 | +| `last_known_location` | String | 最后已知位置 | 如 "Offsite:机房B-柜3" | +| `reason` | String | 请求原因 | 如 "RecallTask xxx 需要读取" | +| `requested_at` | DateTime\ | 请求创建时间 | 审计 | +| `status` | OfflineRequestStatus | 请求状态 | 流程跟踪 | +| `notified_at` | Option\\> | 通知发送时间 | 审计 | +| `confirmed_at` | Option\\> | 确认上线时间 | 计算等待时长 | +| `timeout_at` | DateTime\ | 超时截止时间 | 超时后自动标记 RecallTask Failed | + +```rust +pub enum OfflineRequestStatus { + Pending, // 已创建,未发通知 + Notified, // 已发送通知 + Confirmed, // 人工确认磁带已上线 + Cancelled, // 取消(任务被用户取消等) + TimedOut, // 超时未确认 +} +``` + +--- + +## 4. TapeManager:面向调度器的业务 API + +### 4.1 职责 + +TapeManager 编排 TapeDrive / TapeMedia / TapeLibrary,提供面向调度器的高层 API。 + +### 4.2 TapeManager 结构体 + +```rust +pub struct TapeManager { + drives: Vec>, + media_registry: MediaRegistry, + library: Option>, + drive_allocator: DriveAllocator, + notification: NotificationSender, +} +``` + +| 字段 | 类型 | 含义 | 说明 | +|------|------|------|------| +| `drives` | Vec\\> | 所有已注册驱动实例 | 启动时根据配置创建 | +| `media_registry` | MediaRegistry | 介质注册表 | 磁带 ID → 容量/状态/位置/归档包映射 | +| `library` | Option\\> | 带库实例(可选) | 前期无带库时为 None | +| `drive_allocator` | DriveAllocator | 驱动分配器 | 独占分配 + 优先级队列 | +| `notification` | NotificationSender | 通知发送器 | 离线请求、错误告警 | + +### 4.3 核心接口 + +```rust + +impl TapeManager { + // ── 驱动分配 ── + // priority: 影响排队顺序和预留驱动分配 + pub async fn acquire_drive(&self, priority: DrivePriority) -> Result; + + // ── 磁带选择与加载 ── + pub async fn select_writable_tape(&self, min_remaining: u64) -> Result; + pub async fn load_tape(&self, media_id: &str, drive: &DriveGuard) -> Result<()>; + pub async fn unload_tape(&self, drive: &DriveGuard) -> Result<()>; + + // ── 数据写入(供归档调度器) ── + pub async fn sequential_write( + &self, + drive: &DriveGuard, + data: &[u8], + ) -> Result; + pub async fn write_filemark(&self, drive: &DriveGuard) -> Result<()>; + + // ── 数据读取(供取回调度器) ── + pub async fn seek_and_read( + &self, + drive: &DriveGuard, + filemark: u32, + offset: u64, + length: u64, + ) -> Result>; + + // ── 介质管理 ── + pub async fn register_media(&self, info: MediaRegistration) -> Result<()>; + pub async fn retire_media(&self, media_id: &str) -> Result<()>; + pub async fn get_media_info(&self, media_id: &str) -> Result; + pub async fn update_media_status(&self, media_id: &str, status: MediaStatus) -> Result<()>; + + // ── 离线磁带 ── + pub async fn check_media_online(&self, media_id: &str) -> Result; + pub async fn request_offline_media( + &self, + media_id: &str, + reason: &str, + ) -> Result; + pub async fn confirm_media_online(&self, media_id: &str) -> Result<()>; + + // ── 库盘点 ── + pub async fn run_inventory(&self) -> Result; + + // ── 健康与校验 ── + pub async fn verify_media(&self, media_id: &str) -> Result; + pub async fn get_drive_status(&self, drive_id: &str) -> Result; + pub async fn list_all_drives(&self) -> Result>; +} +``` + +### 4.4 DriveGuard(驱动独占令牌) + +```rust +pub struct DriveGuard { + pub drive_id: String, + drive: Arc>, + release_tx: oneshot::Sender, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `drive_id` | String | 被独占的驱动 ID | 日志追踪 | +| `drive` | Arc\\> | 驱动实例引用 | 调用 read/write/seek 等操作 | +| `release_tx` | Option\\> | 释放通道 | Drop 时发送 drive_id 归还到 DriveAllocator | + +> **RAII 设计**:不提供显式 `release_drive` 方法。DriveGuard 在 Drop 时自动通过 `release_tx` 归还驱动(`take()` 语义保证只发送一次),防止忘记释放。调用者只需让 DriveGuard 离开作用域即可。 + +### 4.5 DriveAllocator(驱动分配器) + +```rust +pub struct DriveAllocator { + available: Mutex>, + waiters: Mutex>, + reserved_for_expedited: u32, + total_drives: u32, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `available` | Mutex\\> | 空闲驱动 ID 队列 | 分配时 pop | +| `waiters` | Mutex\\> | 等待分配的请求(按优先级排序) | 无可用驱动时入堆 | +| `reserved_for_expedited` | u32 | 为 Expedited 预留的驱动数 | 非 Expedited 请求不可占用预留驱动 | +| `total_drives` | u32 | 驱动总数 | 计算利用率 | +| `drive_map` | HashMap\\>\> | drive_id → 驱动实例映射 | acquire 时从此获取实例构造 DriveGuard | + +```rust +pub enum DrivePriority { + Expedited, // 最高,可使用预留驱动 + Archive, // 归档写入 + Standard, // 标准取回 + Bulk, // 批量取回,最低 +} +``` + +### 4.6 TapeInfo(磁带信息 DTO) + +`TapeManager.get_media_info()` 返回的只读视图,由 `MediaRecord` 投影而来。 + +```rust +pub struct TapeInfo { + pub id: String, + pub barcode: Option, + pub format: String, + pub status: MediaStatus, + pub location: MediaLocation, + pub capacity: MediaCapacity, + pub archive_bundles: Vec, + pub last_verified_at: Option>, + pub error_count: u32, +} +``` + +> `TapeInfo` 为 `MediaRecord` 的只读子集,不含可变操作接口。元数据层的 `tapes` CF 也存储此结构(由调度层写入)。 + +### 4.7 MediaRegistry(介质注册表) + +```rust +pub struct MediaRegistry { + media: HashMap, +} + +pub struct MediaRecord { + pub media_id: String, + pub barcode: Option, + pub format: String, + pub capacity: MediaCapacity, + pub status: MediaStatus, + pub location: MediaLocation, + pub label: Option, + pub archive_bundles: Vec, + pub registered_at: DateTime, + pub last_verified_at: Option>, + pub error_count: u32, +} +``` + +| 字段 | 类型 | 含义 | 用途 | +|------|------|------|------| +| `media_id` | String | 磁带唯一标识 | 主键 | +| `barcode` | Option\ | 条码 | 带库自动识别 | +| `format` | String | 介质格式 | "LTO-9" 等 | +| `capacity` | MediaCapacity | 容量信息 | total/used/remaining | +| `status` | MediaStatus | 当前状态 | Online/Offline/InDrive 等 | +| `location` | MediaLocation | 当前位置 | Slot/Drive/Offsite | +| `label` | Option\ | 逻辑标签 | 人工可识别的名称 | +| `archive_bundles` | Vec\ | 已写入的归档包 ID 列表 | 磁带内容追踪 | +| `registered_at` | DateTime\ | 注册时间 | 介质寿命追踪 | +| `last_verified_at` | Option\\> | 最近校验时间 | 定期校验调度 | +| `error_count` | u32 | 累计错误次数 | 超阈值触发退役 | + +**TapeMedia trait 与 MediaRecord 的关系**: +- `TapeMedia` trait 抽象物理介质操作(需要驱动参与的操作:verify_readable、erase、write_label) +- `MediaRecord` 为内存中的逻辑管理记录,不依赖物理驱动 +- `ScsiTapeMedia` 实现 `TapeMedia` trait,同时关联一个 `MediaRecord` +- `MediaRegistry` 仅管理 `MediaRecord`;需要物理操作时,TapeManager 先从 Registry 找到 record,再通过对应的 `TapeMedia` 实例执行 + +--- + +## 5. 前期实现:Linux SCSI + +### 5.1 ScsiTapeDrive + +| 接口 | 实现 | +|------|------| +| 设备节点 | `/dev/nst0`(非自动回卷,推荐) | +| 数据读写 | 标准 `read()` / `write()` 系统调用 | +| 定位 | MTIO ioctl:MTFSF、MTBSF、MTFSR、MTBSR、MTREW、MTEOM | +| 状态 | MTIOCGET | +| 位置 | MTIOCPOS | +| FileMark | MTIOCTOP + MTWEOF | +| 块模式 | MTSETBLK 设置块大小(256KB 推荐) | +| 压缩 | MTCOMPRESSION(MTIOCTOP);MTSETDRVBUFFER + MT_ST_DEF_COMPRESSION 设默认(需 root) | +| 弹出 | MTOFFL | + +### 5.2 MTIO ioctl 封装(mtio.rs) + +```rust +pub struct MtioHandle { + fd: RawFd, +} + +impl MtioHandle { + pub fn open(device: &str) -> Result; + + // MTIOCTOP 操作 + pub fn op(&self, op: MtOp, count: i32) -> Result<()>; + + // MTIOCGET + pub fn get_status(&self) -> Result; + + // MTIOCPOS + pub fn get_position(&self) -> Result; +} + +pub enum MtOp { + Reset, Fsf, Bsf, Fsr, Bsr, Rew, Offl, Nop, Reten, Eom, Erase, + Weof, // 写 FileMark + SetBlk(u32), // 设置块大小 + SetCompression(bool), +} +``` + +### 5.3 ScsiTapeLibrary(中期实现) + +基于 SCSI Medium Changer 协议(SMC),通过 `/dev/sg*` 下发 SCSI 命令。 + +| SCSI 命令 | 用途 | +|-----------|------| +| INITIALIZE ELEMENT STATUS | 盘点 | +| READ ELEMENT STATUS | 读取槽位/驱动状态 | +| MOVE MEDIUM | 机械臂搬运(load/unload/transfer) | +| REQUEST VOLUME ELEMENT ADDRESS | 条码读取 | + +也可通过 `mtx` 命令行工具封装(开发期)。 + +### 5.4 sg.rs(SCSI Generic 封装,可选) + +```rust +pub struct SgHandle { + fd: RawFd, +} + +impl SgHandle { + pub fn open(device: &str) -> Result; + pub fn send_command(&self, cdb: &[u8], data: &mut [u8]) -> Result; +} +``` + +--- + +## 6. 离线磁带管理 + +### 6.1 离线流程 + +``` +取回调度器请求加载 tape_id + │ + ▼ +TapeManager.check_media_online(tape_id) + │ + ├─ Online: load_tape → 继续 + │ + └─ Offline: + 1. TapeManager.request_offline_media(tape_id, reason) + 2. 通知服务: 发送工单/告警(tape_id, barcode, 位置, 请求方) + 3. 任务进入 wait_for_media 状态 + 4. 人工: 将磁带插入带库 → 扫码确认 + 5. TapeManager.confirm_media_online(tape_id) + 6. 自动继续: load_tape → 读取 +``` + +### 6.2 离线请求结构 + +详见 §3.4.13 `OfflineRequest` / `OfflineRequestStatus` 定义。 + +--- + +## 7. 可靠性 + +### 7.1 双副本写入 + +| 策略 | 说明 | +|------|------| +| 双磁带副本 | 归档时写入两盘不同磁带,记录 tape_set | +| 跨库分布 | 副本分布在不同带库或离线库,防止单点灾难 | +| 调度器协同 | 调度器负责决定双写,TapeManager 负责执行 | + +### 7.2 校验机制 + +| 机制 | 触发 | 说明 | +|------|------|------| +| 写入后 CRC | 每次写入 | 驱动层记录 CRC,写后对比 | +| 定期读校验 | 定时任务 | `TapeManager.verify_media` 读取并校验 | +| Bundle 校验 | 归档完成后 | 可选读回校验整个 Bundle | + +### 7.3 错误处理 + +| 错误类型 | 处理 | +|----------|------| +| 驱动硬件错误 | 标记驱动不可用,切换备用驱动 | +| 介质读取错误 | 重试 → 切换副本 → 通知人工 | +| 介质写入错误 | 重试 → 换磁带继续写 → 标记坏带 | +| 清洁需求 | DriveError::CleaningRequired → 通知 | + +### 7.4 介质退役 + +``` +定期校验 → 发现不可读块增多 → 标记 Retired → 数据迁移到新磁带 → 淘汰 +``` + +--- + +## 8. 可观测性 + +| 指标 | 说明 | +|------|------| +| drive_utilization | 驱动利用率 | +| write_throughput_mbps | 写入吞吐 | +| read_throughput_mbps | 读取吞吐 | +| tape_swap_count | 换带次数 | +| tape_swap_duration_ms | 换带耗时 | +| media_error_count | 介质错误数 | +| drive_error_count | 驱动错误数 | +| offline_request_count | 离线请求数 | +| offline_wait_duration_s | 离线等待时长 | +| verify_pass_rate | 校验通过率 | + +--- + +## 9. 模块结构 + +``` +src/tape/ +├── mod.rs # 模块入口,pub use +│ +├── manager.rs # TapeManager — 面向调度器的业务 API +├── drive_allocator.rs # DriveAllocator — 驱动独占分配与排队 +├── media_registry.rs # MediaRegistry — 介质注册、容量、状态、位置 +├── offline.rs # 离线磁带请求与确认流程 +│ +├── sdk/ # ─── SDK 抽象层 (trait) ─── +│ ├── mod.rs +│ ├── drive.rs # TapeDrive trait +│ ├── media.rs # TapeMedia trait +│ ├── library.rs # TapeLibrary trait +│ └── types.rs # DriveStatus, SlotInfo, MediaStatus, ... +│ +├── scsi/ # ─── Linux SCSI 实现 ─── +│ ├── mod.rs +│ ├── drive.rs # ScsiTapeDrive (impl TapeDrive) +│ ├── media.rs # ScsiTapeMedia (impl TapeMedia) +│ ├── library.rs # ScsiTapeLibrary (impl TapeLibrary, 中期) +│ ├── mtio.rs # MTIO ioctl 封装 (MtioHandle) +│ └── sg.rs # SCSI Generic 封装 (SgHandle, 可选) +│ +└── vendor/ # ─── 厂商 SDK 实现 (后期) ─── + └── mod.rs # VendorTapeDrive, VendorTapeLibrary +``` + +--- + +## 10. 配置项 + +```yaml +tape: + sdk: + backend: "scsi" # scsi | vendor + + drives: + - id: "drive0" + device: "/dev/nst0" + block_size: 262144 # 256KB + compression: true + - id: "drive1" + device: "/dev/nst1" + block_size: 262144 + compression: true + + library: + enabled: false # 前期单驱动可关闭 + device: "/dev/sg2" # SCSI Medium Changer 设备 + auto_inventory: true # 启动时自动盘点 + + media: + supported_formats: + - "LTO-9" + - "LTO-10" + replication_factor: 2 # 双副本 + verify_interval_days: 90 # 每 90 天校验 + + offline: + notification_enabled: true + notification_timeout_secs: 86400 # 24h 超时 +``` + +--- + +## 11. 部署模型与依赖关系 + +磁带层运行在 **Tape Worker** 节点上(独立物理节点,挂载磁带驱动和带库), +通过 gRPC 接收 Scheduler Worker 的远程调用指令。 + +磁带层是**纯硬件抽象层**,不持有 MetadataClient。所有元数据更新由调度层统一负责。 + +| 对接方 | 方向 | 协议 | 接口 | 说明 | +|--------|------|------|------|------| +| Scheduler Worker | gRPC(远程) | 调度 → TapeManager | acquire_drive, sequential_write, seek_and_read | 调度层编排 | +| 元数据层 | **无直接依赖** | — | — | TapeInfo 等由 Scheduler Worker 负责更新 | +| Metadata 集群 | gRPC(心跳) | Tape Worker → Metadata | 上报驱动状态、在线状态 | +| 通知服务 | 本地 | TapeManager → 通知 | 离线磁带请求、错误告警 | + +--- + +## 12. 开发测试环境:mhVTL + +开发和 CI 环境中无需真实磁带硬件,使用 [mhVTL (Linux Virtual Tape Library)](https://github.com/markh794/mhvtl) 模拟完整的磁带驱动和带库。 + +### 12.1 mhVTL 概述 + +mhVTL 通过内核模块(伪 HBA)+ 用户态守护进程(`vtltape`、`vtllibrary`)模拟 SCSI 磁带设备,暴露标准的 `/dev/nst*`、`/dev/sg*` 设备节点。对 ColdStore 的 SCSI 实现层来说,虚拟磁带与真实磁带**接口完全一致**。 + +| mhVTL 组件 | 作用 | ColdStore 对接点 | +|-----------|------|-----------------| +| `mhvtl.ko` 内核模块 | 伪 SCSI HBA | 暴露 `/dev/nst*`、`/dev/sg*` | +| `vtltape` 守护进程 | SSC target,磁带读写 | `ScsiTapeDrive` 的 mtio ioctl | +| `vtllibrary` 守护进程 | SMC target,带库管理 | `ScsiTapeLibrary` 的 SCSI Medium Changer | +| `mktape` 工具 | 创建虚拟磁带 | 测试前准备介质 | + +### 12.2 开发环境配置示例 + +```bash +# 安装 mhVTL(以 Ubuntu/Debian 为例) +sudo apt-get install mhvtl-utils lsscsi sg3-utils + +# 启动虚拟磁带库 +sudo systemctl start mhvtl + +# 查看模拟设备 +lsscsi -g +# [3:0:0:0] mediumx SPECTRA PYTHON ... /dev/sch0 /dev/sg5 +# [3:0:1:0] tape QUANTUM ULTRIUM-HH7 ... /dev/st0 /dev/sg6 +# [3:0:2:0] tape QUANTUM ULTRIUM-HH7 ... /dev/st1 /dev/sg7 +``` + +ColdStore 配置(开发模式): + +```yaml +tape: + sdk: + backend: "scsi" + drives: + - id: "drive0" + device: "/dev/nst0" # mhVTL 虚拟驱动 + block_size: 262144 + - id: "drive1" + device: "/dev/nst1" # mhVTL 虚拟驱动 + block_size: 262144 + library: + enabled: true + device: "/dev/sg5" # mhVTL 虚拟带库 +``` + +### 12.3 测试用途 + +| 用途 | 说明 | +|------|------| +| 单元测试 | 验证 mtio ioctl 封装、FileMark 定位、块读写 | +| 集成测试 | 全链路:S3 PUT → 归档调度 → 磁带写入 → 取回 → 缓存 → GET | +| 故障注入 | 停止 vtltape 进程模拟驱动故障,测试错误处理和重试 | +| CI/CD | GitHub Actions / GitLab CI 中安装 mhVTL,自动化端到端测试 | +| 调度验证 | 模拟多驱动、多磁带,验证合并、聚合、优先级等调度算法 | + +--- + +## 13. 实现路径 + +| 阶段 | 实现 | 范围 | 测试环境 | +|------|------|------|----------| +| **Phase 1** | ScsiTapeDrive + MtioHandle | 单驱动读写、定位、FileMark、状态 | mhVTL 单驱动 | +| **Phase 2** | TapeManager + DriveAllocator | 多驱动分配、独占、MediaRegistry | mhVTL 多驱动 | +| **Phase 3** | ScsiTapeLibrary + SgHandle | 带库盘点、load/unload、条码 | mhVTL 带库 | +| **Phase 4** | 离线流程 + 通知 | request_offline_media、confirm | mhVTL + 手动移除 | +| **Phase 5** | 可靠性 | 双副本、verify_media、退役 | mhVTL 多磁带 | +| **Phase 6** | VendorTapeDrive(可选) | 厂商 SDK 对接 | 真实硬件 | + +--- + +## 14. 参考资料 + +- [Linux st(4) - SCSI tape device](https://man7.org/linux/man-pages/man4/st.4.html) +- [Linux SCSI tape driver (kernel.org)](https://kernel.org/doc/Documentation/scsi/st.txt) +- [mtx - control SCSI media changer devices](https://manpages.ubuntu.com/manpages/jammy/man1/mtx.1.html) +- [SCSI T-10 SSC (Streaming Commands)](https://www.t10.org/) +- [SCSI T-10 SMC (Medium Changer Commands)](https://www.t10.org/) +- [mhVTL - Linux Virtual Tape Library](https://sites.google.com/site/linuxvtl2/) — 开发测试用虚拟磁带库 +- [mhVTL GitHub](https://github.com/markh794/mhvtl) — mhVTL 源码 diff --git a/docs/modules/07-consistency-performance.md b/docs/modules/07-consistency-performance.md new file mode 100644 index 0000000..da01480 --- /dev/null +++ b/docs/modules/07-consistency-performance.md @@ -0,0 +1,765 @@ +# 跨层数据一致性与性能设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) | [03-元数据集群](./03-metadata-cluster.md) | [04-缓存层](./04-cache-layer.md) | [05-调度层](./05-scheduler-layer.md) | [06-磁带层](./06-tape-layer.md) + +## 1. 设计背景 + +ColdStore 跨越四个核心层(元数据 → 缓存 → 调度 → 磁带),每次归档或取回操作涉及多步写入,且目标介质(磁带)具有高延迟、不可随机写的特性。在无分布式事务支持的情况下,需要通过 **Saga 模式 + 幂等操作 + 补偿机制** 保证最终一致性,同时通过 **流水线、聚合、并发控制** 保证吞吐性能。 + +### 1.1 核心约束 + +| 约束 | 来源 | 影响 | +|------|------|------| +| 磁带不可回滚 | 磁带追加写入,无法撤销 | 归档写入必须"先磁带后元数据",不可反序 | +| Raft 写入串行 | 所有写入经 Leader 排序 | 元数据写入是全局瓶颈,需批量化 | +| SPDK 用户态 | 缓存层运行在独立线程 | 跨线程通信需 channel/Arc,不能直接共享锁 | +| 磁带换带延迟 | 30s–2min 机械操作 | 调度层必须合并同磁带请求,减少换带 | + +### 1.2 一致性层级 + +| 层 | 一致性保证 | 说明 | +|------|-----------|------| +| 元数据集群 (03) | **强一致(线性一致性)** | Raft 共识,写入多数派确认 | +| 调度层 (05) | **Saga 最终一致性** | 多步操作通过补偿保证最终状态正确 | +| 缓存层 (04) | **弱一致(最终一致)** | 缓存数据是元数据的"投影",可丢失可重建 | +| 磁带层 (06) | **持久化一致性** | 一旦写入成功即持久,双副本保障 | + +--- + +## 2. 数据一致性模型 + +### 2.1 元数据强一致(Raft 共识) + +``` + 写入路径 +Client ──▶ Leader ──▶ Raft Propose + │ + ┌─────────┼─────────┐ + ▼ ▼ ▼ + Follower1 Follower2 Leader + │ │ │ + └────┬────┘ │ + 多数派 Ack │ + └──────▶ Commit ──▶ Apply ──▶ RocksDB +``` + +**关键保证**: + +| 保证 | 实现 | 适用场景 | +|------|------|----------| +| 写入原子性 | 单个 `ColdStoreRequest` 为一条 Raft 日志 | 所有写操作 | +| 写入顺序 | Raft log index 全局有序 | 同一对象的状态流转不乱序 | +| 线性读 | `ensure_linearizable()` 确认 Leader 地位 | `find_active_recall` 检测重复 | +| 最终一致读 | 任意节点本地 RocksDB | GET 查询 restore_status | + +**写入合并优化**: + +对于归档完成这类需要同时更新多个 CF 的操作,状态机 apply 内使用 RocksDB `WriteBatch` 保证原子性: + +```rust +fn apply_archive_complete( + &self, + bundle: &ArchiveBundle, + object_updates: &[(String, String, Uuid, String, Vec, u64)], +) -> Result<()> { + let mut batch = WriteBatch::default(); + + // 1. 写入 ArchiveBundle + batch.put_cf(cf_bundles, bundle_key, serialize(bundle)); + + // 2. 批量更新 ObjectMetadata + for (bucket, key, archive_id, tape_id, tape_set, offset) in object_updates { + let obj_key = format!("obj:{}:{}", bucket, key); + let mut meta: ObjectMetadata = self.get_cf(cf_objects, &obj_key)?; + meta.storage_class = StorageClass::Cold; + meta.archive_id = Some(*archive_id); + meta.tape_id = Some(tape_id.clone()); + meta.tape_set = Some(tape_set.clone()); + meta.tape_block_offset = Some(*offset); + meta.updated_at = Utc::now(); + batch.put_cf(cf_objects, &obj_key, serialize(&meta)); + + // 3. 删除 ColdPending 索引 + let pend_key = format!("pend:{}:{}:{}", meta.created_at.timestamp(), bucket, key); + batch.delete_cf(cf_idx_pending, &pend_key); + + // 4. 写入反查索引 + let ibo_key = format!("ibo:{}:{}:{}", archive_id, bucket, key); + batch.put_cf(cf_idx_bundle_objects, &ibo_key, b""); + } + + // 5. 写入磁带→Bundle 索引 + let itb_key = format!("itb:{}:{}", bundle.tape_id, bundle.id); + batch.put_cf(cf_idx_tape_bundles, &itb_key, b""); + + self.db.write(batch)?; + Ok(()) +} +``` + +### 2.2 归档流程一致性(Saga 模式) + +归档是不可逆操作(磁带数据已写入),采用**前向恢复**策略: + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 归档 Saga 流程 │ +│ │ +│ Step 1 Step 2 Step 3 Step 4 │ +│ 扫描聚合 ──▶ 申请驱动/磁带 ──▶ 磁带顺序写入 ──▶ 更新元数据 │ +│ (调度层) (磁带层) (磁带层) (元数据层) │ +│ │ +│ 失败处理: │ +│ Step 1 失败: 无副作用,直接重试 │ +│ Step 2 失败: 释放资源,重入队列 │ +│ Step 3 失败: ┌─ 重试(同磁带/换磁带) │ +│ └─ 超限 → Bundle 标记 Failed,对象回退 ColdPending │ +│ Step 4 失败: ┌─ 磁带已写入(不可撤销) │ +│ ├─ 重试元数据更新(指数退避,最多 10 次) │ +│ └─ 超限 → 写入补偿日志,后台修复任务定期扫描重试 │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +**关键不变量**: + +| 不变量 | 含义 | 违反后果 | 保护机制 | +|--------|------|----------|----------| +| 磁带写入 → 元数据更新 | 先确认磁带持久化,再标记 Cold | 若反序:元数据为 Cold 但磁带无数据,取回时丢数据 | 强制顺序 + Step 4 重试 | +| archive_id 唯一 | 每个 Bundle 全局唯一 | 若重复:覆盖旧 Bundle 信息 | UUID v4 + 幂等写入 | +| ColdPending → Cold 原子 | 状态流转不可跳过 | 若跳过:对象状态异常 | WriteBatch 原子操作 | + +**补偿日志**: + +Step 4 持续失败时,将未完成的元数据更新写入本地补偿日志: + +```rust +pub struct CompensationEntry { + pub id: Uuid, + pub created_at: DateTime, + pub saga_type: SagaType, + pub payload: CompensationPayload, + pub retry_count: u32, + pub last_retry_at: Option>, + pub status: CompensationStatus, +} + +pub enum SagaType { + ArchiveComplete, + RecallComplete, +} + +pub enum CompensationPayload { + ArchiveComplete { + bundle: ArchiveBundle, + object_keys: Vec<(String, String)>, + }, + RecallComplete { + recall_task_id: Uuid, + bucket: String, + key: String, + restore_expire_at: DateTime, + }, +} + +pub enum CompensationStatus { + Pending, + Retrying, + Resolved, + Abandoned, +} +``` + +**补偿任务调度器**: + +```rust +pub struct CompensationRunner { + metadata: Arc, + log_store: Arc, + config: CompensationConfig, +} + +pub struct CompensationConfig { + pub scan_interval_secs: u64, // 扫描间隔:60s + pub max_retries: u32, // 最大重试次数:100 + pub retry_backoff_base_ms: u64, // 退避基数:1000ms + pub retry_backoff_max_ms: u64, // 退避上限:300_000ms(5 分钟) + pub abandon_after_hours: u64, // 超时放弃:72 小时 +} +``` + +### 2.3 取回流程一致性(Saga 模式) + +取回流程涉及三个目标(磁带 → 缓存 → 元数据),采用**前向恢复 + 容错降级**: + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 取回 Saga 流程 │ +│ │ +│ Step 1 Step 2 Step 3 Step 4 │ +│ 申请驱动 ──▶ 磁带读取 ──▶ 写入缓存 ──▶ 更新元数据 │ +│ 定位磁带 (磁带层) (缓存层) (元数据层) │ +│ (磁带层) │ +│ │ +│ 失败处理: │ +│ Step 1 失败: 释放资源,重入队列 / WaitingForMedia(离线) │ +│ Step 2 失败: ┌─ 重试(同磁带 2 次) │ +│ ├─ 切换副本磁带(tape_set 中选下一盘) │ +│ └─ 超限 → RecallTask Failed │ +│ Step 3 失败: ┌─ 触发缓存淘汰后重试(2 次) │ +│ └─ 超限 → RecallTask Failed,不更新元数据 │ +│ Step 4 失败: ┌─ 缓存已有数据(不可撤销,等待 TTL 过期自动清理) │ +│ ├─ 重试元数据更新(指数退避,最多 10 次) │ +│ └─ 超限 → 写入补偿日志;此时缓存有数据但 GET 不可用 │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +**取回状态机**: + +``` + ┌─────────────────────────────────┐ + │ │ + ▼ │ + RestoreObject ──▶ Pending ──▶ WaitingForMedia ──────────┘ + │ (离线磁带上线后) + ▼ + InProgress ──▶ Completed + │ │ + ▼ └──▶ Expired (TTL 过期) + Failed +``` + +| 状态流转 | 触发条件 | 写入方 | 幂等性 | +|----------|----------|--------|--------| +| → Pending | RestoreObject API | 协议层 | `find_active_recall` 线性读防重复 | +| Pending → WaitingForMedia | 磁带 Offline | 调度层 | 幂等:重复设置无副作用 | +| WaitingForMedia → Pending | 磁带 Online 确认 | 调度层 | 幂等 | +| Pending → InProgress | 分配驱动、开始读取 | 调度层 | 幂等 | +| InProgress → Completed | 缓存写入 + 元数据更新均成功 | 调度层 | 幂等:重复更新同值 | +| InProgress → Failed | 超过最大重试 | 调度层 | 幂等 | +| Completed → Expired | expire_at < now | 后台扫描 | 幂等 | + +### 2.4 缓存-元数据一致性 + +缓存层不持有 MetadataClient(方案 B),因此缓存数据与元数据之间存在天然的弱一致窗口: + +| 场景 | 缓存状态 | 元数据状态 | 用户可见行为 | 自愈方式 | +|------|----------|-----------|-------------|----------| +| 正常取回完成 | 有数据 | Completed | GET 正常返回 | — | +| Step 3 成功,Step 4 待重试 | 有数据 | InProgress/Pending | GET 返回 403(未解冻) | 补偿任务重试元数据更新 | +| 缓存淘汰(TTL/容量) | 无数据 | Completed | GET 返回 503 | 用户重新 RestoreObject | +| 缓存损坏 | 数据校验失败 | Completed | GET 返回 500 | 删除坏 Blob,用户重新 Restore | +| 元数据 Expired 但缓存未淘汰 | 有数据 | Expired | GET 返回 403 | 缓存 TTL 扫描最终删除 | + +**设计原则**:元数据是**唯一真相源**(Source of Truth),缓存仅为性能优化层。任何缓存丢失都可通过重新 RestoreObject 恢复。 + +### 2.5 幂等性设计 + +所有跨层操作必须幂等,以支持安全重试: + +| 操作 | 幂等 key | 幂等实现 | +|------|----------|----------| +| PutObject | `(bucket, key)` | 覆盖写入,同 key 重复 PUT 覆盖旧值 | +| PutArchiveBundle | `bundle.id` (Uuid) | 同 id 重复写入覆盖,CF `bundles` 按 id 去重 | +| UpdateStorageClass | `(bucket, key, class)` | 同 key 重复设置同值无副作用 | +| UpdateArchiveLocation | `(bucket, key)` | 同 key 重复设置同值无副作用 | +| UpdateRestoreStatus | `(bucket, key, status)` | 状态机检查:只允许合法流转 | +| PutRecallTask | `recall.id` (Uuid) | 同 id 重复写入覆盖 | +| put_restored (缓存) | `(bucket, key)` | 同 key 覆盖旧 Blob | + +**状态机防护**: + +```rust +fn validate_restore_transition(current: &RestoreStatus, target: &RestoreStatus) -> bool { + matches!( + (current, target), + (RestoreStatus::Pending, RestoreStatus::InProgress) + | (RestoreStatus::Pending, RestoreStatus::WaitingForMedia) + | (RestoreStatus::Pending, RestoreStatus::Failed) + | (RestoreStatus::WaitingForMedia, RestoreStatus::Pending) + | (RestoreStatus::InProgress, RestoreStatus::Completed) + | (RestoreStatus::InProgress, RestoreStatus::Failed) + | (RestoreStatus::Completed, RestoreStatus::Expired) + ) +} +``` + +--- + +## 3. 并发控制模型 + +### 3.1 全局并发架构 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Tokio Runtime │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ S3 Handler │ │ Archive │ │ Recall │ │ Compensation │ │ +│ │ (并发请求) │ │ Scheduler │ │ Scheduler │ │ Runner │ │ +│ │ │ │ (单循环) │ │ (单循环) │ │ (单循环) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Arc │ │ +│ │ 内部:Raft client(自动转发到 Leader) │ │ +│ │ 并发安全:Raft 日志序列化所有写入 │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────┐ ┌───────────────────────────────────────┐ │ +│ │ Arc │ │ Arc │ │ +│ │ 内部:RwLock│ │ 内部:Mutex │ │ +│ │ + SPDK thread pool │ │ + per-drive sequential executor │ │ +│ └──────────────────────────┘ └───────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 锁策略与粒度 + +| 组件 | 锁类型 | 粒度 | 持锁时间 | 争用场景 | +|------|--------|------|----------|----------| +| MetadataClient | **无锁**(Raft 串行化) | 全局 | N/A | 所有写入经 Raft Leader 排序,无需应用层锁 | +| CacheIndex | `RwLock` | 全局索引 | 微秒级(内存 HashMap 操作) | GET 热路径读锁 + put_restored 写锁 | +| CacheBackend (SPDK) | **无锁**(SPDK reactor 线程模型) | per-thread | N/A | SPDK 内部保证线程安全 | +| DriveAllocator.available | `Mutex` | 驱动池 | 微秒级 | 归档/取回同时申请驱动 | +| DriveAllocator.waiters | `Mutex` | 等待队列 | 微秒级 | 驱动归还时唤醒等待者 | +| CacheStats | `AtomicU64` | per-counter | 纳秒级 | 高频统计更新 | +| RecallQueue | `Mutex` | per-queue | 微秒级 | 入队/出队/合并 | + +### 3.3 关键并发路径分析 + +#### 3.3.1 GET 热路径(最频繁) + +``` +GET /{bucket}/{key} + │ + ├─ 1. metadata.get_object() ← Follower 本地读,无锁,< 100μs + │ + ├─ 2. 检查 storage_class/restore_status ← 纯内存判断 + │ + └─ 3. cache.get(bucket, key) + │ + ├─ 3a. index.read() ← RwLock 读锁,< 1μs + │ + └─ 3b. backend.read_blob() ← SPDK 异步 I/O,10-100μs (NVMe) +``` + +**优化**:GET 路径全程无写锁,读锁开销可忽略。元数据使用 Follower 本地读(最终一致),接受短暂延迟。 + +#### 3.3.2 put_restored 写路径 + +``` +put_restored(item) + │ + ├─ 1. backend.create_blob(xattrs, size) ← SPDK 创建 Blob,异步 + │ + ├─ 2. backend.write_blob(blob_id, data) ← SPDK 写入数据,异步 + │ + └─ 3. index.write() ← RwLock 写锁,< 1μs +``` + +**优化**:写锁仅在最后一步更新内存索引时持有,持锁时间极短。SPDK I/O 不持有 CacheIndex 锁。 + +#### 3.3.3 驱动分配竞争 + +``` +acquire_drive(priority) + │ + ├─ 1. lock(available) ← Mutex,< 1μs + │ ├─ 有空闲驱动:pop + return DriveGuard + │ └─ 无空闲: + │ lock(waiters) ← Mutex,< 1μs + │ push(DriveWaiter { priority, oneshot }) + │ drop(available) + │ await oneshot.recv() ← 等待驱动释放,可能数分钟 + │ + └─ DriveGuard Drop: + lock(available) + if waiters.peek().priority ≤ current: + pop waiter, send drive via oneshot + else: + push drive back to available +``` + +**优化**:Mutex 仅保护内存数据结构操作(微秒级),实际等待通过 `oneshot channel` 异步化,不阻塞 Tokio 线程。 + +### 3.4 死锁预防 + +| 规则 | 说明 | +|------|------| +| 锁排序 | 若需同时持有多锁,按固定顺序获取:`CacheIndex` → `DriveAllocator` | +| 无嵌套锁 | MetadataClient 无应用层锁,SPDK 内部锁不暴露 | +| Mutex + async | 所有 `Mutex` 在 `await` 前释放,使用 `tokio::sync::Mutex` 仅在必须跨 await 时 | +| 超时保护 | `acquire_drive` 设置超时(`drive_acquire_timeout_secs`),避免无限等待 | + +--- + +## 4. 性能设计 + +### 4.1 端到端性能目标 + +| 场景 | 目标 | 瓶颈 | 关键优化 | +|------|------|------|----------| +| **PUT Object** | < 10ms(小对象)| 元数据 Raft 写入 | 异步 Raft,批量合并 | +| **GET Object(热)** | < 1ms | 缓存 SPDK 读取 | Follower 读 + SPDK 用户态 I/O | +| **GET Object(已解冻)** | < 5ms | 缓存 SPDK 读取 | 同上 | +| **归档吞吐** | ≥ 300 MB/s/drive | 磁带写入速度 | 块对齐 + 双缓冲 + 流水线 | +| **取回延迟(Expedited)** | < 5 分钟 | 换带 + seek + 读取 | 预留驱动 + 优先队列 | +| **取回延迟(Standard)** | < 5 小时 | 队列等待 + 换带 | 合并同磁带任务 | +| **元数据写入 QPS** | ≥ 10,000/s | Raft 共识延迟 | WriteBatch + 批量 Propose | + +### 4.2 元数据层性能(03) + +#### 4.2.1 写入批量化 + +高频写入场景(如归档完成批量更新 ObjectMetadata)使用复合命令减少 Raft 往返: + +```rust +pub enum ColdStoreRequest { + // 单对象操作(低频) + PutObject(ObjectMetadata), + + // 批量操作(高频,一条 Raft 日志完成多 CF 更新) + BatchArchiveComplete { + bundle: ArchiveBundle, + tape_info_update: TapeInfo, + object_updates: Vec, + }, + BatchRecallComplete { + task_updates: Vec, + object_updates: Vec, + }, +} + +pub struct ObjectArchiveUpdate { + pub bucket: String, + pub key: String, + pub archive_id: Uuid, + pub tape_id: String, + pub tape_set: Vec, + pub tape_block_offset: u64, +} + +pub struct ObjectRestoreUpdate { + pub bucket: String, + pub key: String, + pub status: RestoreStatus, + pub expire_at: Option>, +} + +pub struct RecallTaskUpdate { + pub id: Uuid, + pub status: RestoreStatus, + pub completed_at: Option>, + pub error: Option, +} +``` + +**收益**:一个 ArchiveBundle(500 个对象)只需 1 条 Raft 日志,而非 500+1 条。 + +#### 4.2.2 读路径优化 + +| 优化 | 实现 | 收益 | +|------|------|------| +| Follower 读 | GET/HEAD 直接读本地 RocksDB | 读请求水平扩展,不经 Leader | +| 前缀扫描 | `cf_idx_pending` 按时间排序 key | `scan_cold_pending` 无需全表扫描 | +| Column Family 隔离 | 12 个独立 CF | 读写互不干扰,compaction 独立 | +| Block Cache | RocksDB block_cache 配置 | 热对象元数据缓存在内存 | +| Bloom Filter | 每个 CF 配置 Bloom Filter | 减少不存在 key 的磁盘 I/O | + +#### 4.2.3 RocksDB 调优参数 + +```yaml +rocksdb: + block_cache_size_mb: 256 # 热数据缓存 + bloom_filter_bits: 10 # 每 key 10 bit Bloom Filter + write_buffer_size_mb: 64 # MemTable 大小 + max_write_buffer_number: 3 # MemTable 数量(写入平滑) + level0_file_num_compaction_trigger: 4 + max_background_compactions: 4 + max_background_flushes: 2 + compression: "lz4" # L1+ 压缩 + bottommost_compression: "zstd" # 最底层高压缩比 +``` + +### 4.3 缓存层性能(04) + +#### 4.3.1 SPDK 用户态 I/O 优势 + +``` + 传统内核路径 SPDK 用户态路径 +App ──▶ syscall ──▶ VFS ──▶ Block Layer App ──▶ SPDK Blobstore ──▶ NVMe Driver + │ │ │ │ + 上下文切换 锁竞争 中断处理 轮询(polling) + ~2μs ~1μs ~5μs < 1μs +``` + +| 指标 | 内核路径 | SPDK 路径 | 提升 | +|------|---------|-----------|------| +| 4KB 随机读延迟 | 10-20μs | 2-5μs | 3-5x | +| 4KB 随机写延迟 | 15-30μs | 3-8μs | 3-5x | +| IOPS (4KB 随机读) | 200-400K | 800K-1.5M | 3-4x | +| CPU 占用/IOPS | 高(中断+上下文切换) | 低(轮询) | 显著降低 | + +#### 4.3.2 缓存读写流水线 + +``` +写入流水线(put_restored 批量): + + Object A ──▶ [create_blob] ──▶ [write_data] ──▶ [update_index] + Object B ──▶ [create_blob] ──▶ [write_data] ──▶ [update_index] + Object C ──▶ [create_blob] ──▶ [write_data] ──▶ [update_index] + ↑ ↑ + SPDK 异步并行 写锁(顺序,极短) +``` + +`put_restored_batch` 内部对多个对象的 SPDK I/O 并行提交,仅在最后更新索引时短暂串行。 + +#### 4.3.3 淘汰对性能的影响 + +| 淘汰策略 | 读路径影响 | 写路径影响 | 适用场景 | +|----------|-----------|-----------|----------| +| LRU | 无(读时更新 `last_access` 用 AtomicI64) | 无 | 通用 | +| LFU | 无(读时原子递增计数器) | 无 | 热点集中 | +| TTL 扫描 | 无(独立后台任务) | 无 | 解冻过期清理 | +| 容量淘汰 | **微弱**(RwLock 写锁删除索引条目) | **微弱** | 容量接近上限时 | + +**关键**:淘汰在独立后台 task 中执行,批量删除 `eviction_batch_size` 个 Blob 后统一更新索引,减少写锁次数。 + +### 4.4 调度层性能(05) + +#### 4.4.1 归档写入流水线 + +``` +┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ +│ Stage 1 │ │ Stage 2 │ │ Stage 3 │ │ Stage 4 │ +│ 对象读取 │─▶│ 序列化 │─▶│ 双缓冲 │─▶│ 磁带写入 │ +│ │ │ + Header │ │ 流水线 │ │ │ +└───────────┘ └───────────┘ └───────────┘ └───────────┘ + │ │ │ │ + 热存储/缓存 ObjectHeader Buffer A/B 驱动写入 + async read 构建 交替写入 顺序追加 +``` + +**双缓冲机制**: + +```rust +pub struct ArchiveWritePipeline { + buffer_a: DmaBuf, // 64-128MB + buffer_b: DmaBuf, // 64-128MB + active: AtomicBool, // true=A, false=B + block_size: usize, // 256KB +} +``` + +| 阶段 | Buffer A | Buffer B | +|------|----------|----------| +| T1 | 填充对象数据 | 磁带写入上一批 | +| T2 | 磁带写入 | 填充对象数据 | +| T3 | 填充对象数据 | 磁带写入 | + +**收益**:磁带驱动持续流式写入,无等待间隙,吞吐接近驱动理论上限。 + +#### 4.4.2 取回合并收益量化 + +| 场景 | 无合并 | 有合并 | 收益 | +|------|--------|--------|------| +| 10 个对象,同 Bundle | 10 次换带 + 10 次 seek | 1 次换带 + 1 次顺序读 | **10x** | +| 20 个对象,分 3 盘磁带 | 20 次换带 | 3 次换带 + 3 次顺序读 | **6-7x** | +| 100 个对象,同磁带不同 Bundle | 100 次换带 + 100 次 seek | 1 次换带 + N 次 FSF seek | **~50x** | + +合并窗口 `merge_window_secs`(默认 60s)是**延迟与吞吐的权衡**: +- 窗口越大:合并率越高,吞吐越好;但首个请求延迟增加 +- 窗口越小:响应快,但合并率低,换带频繁 + +#### 4.4.3 多驱动并行 + +``` +Drive Pool: [Drive 0] [Drive 1] [Drive 2] [Drive 3] + │ │ │ │ + ▼ ▼ ▼ ▼ + Archive Archive Recall Recall + Bundle A Bundle B Job X Job Y + (Tape001) (Tape002) (Tape003) (Tape004) +``` + +| 配置 | archive_drives | recall_drives | expedited_reserved | +|------|---------------|---------------|-------------------| +| 4 驱动均衡 | 2 | 2 | 1 (从 recall 预留) | +| 写入优先 | 3 | 1 | 0 | +| 取回优先 | 1 | 3 | 1 | +| 动态调整 | 共享池,按优先级竞争 | — | 1 | + +### 4.5 磁带层性能(06) + +#### 4.5.1 顺序 I/O 最大化 + +| 优化 | 实现 | 说明 | +|------|------|------| +| 块对齐写入 | 数据填充到 `block_size`(256KB) | 减少驱动内部碎片和缓冲区刷新 | +| 大块写入 | 每次 `write()` 写入完整块 | 避免小 I/O 触发驱动微停顿 | +| FileMark 定位 | `MTFSF` 跳过整个 Bundle | 比逐块 seek 快 100x | +| 块内定位 | `MTFSR` 跳过块 | 在 Bundle 内精确定位对象 | +| 硬件压缩 | LTO 硬件压缩(可配) | 有效容量提升 2-3x,不占 CPU | + +#### 4.5.2 换带优化 + +``` +换带流程: + unload(current_tape) ~15s ─┐ + robot move to slot ~10s ├─ 总计 30s-120s + load(new_tape) ~15s │ + locate/rewind ~5s ─┘ +``` + +**减少换带的策略**(按优先级): + +1. **同磁带合并**:调度层将同 tape_id 的任务合并为一个 TapeReadJob +2. **磁带亲和**:归档调度器优先使用当前已加载磁带的剩余空间 +3. **预加载**:若下一个 Job 的 tape_id 已知,提前 unload/load +4. **驱动保持**:读取完成后保持磁带在驱动中一段时间(`tape_hold_secs`),等待同磁带请求 + +--- + +## 5. 故障场景矩阵 + +### 5.1 归档流程故障 + +| 序号 | 故障点 | 已完成步骤 | 数据状态 | 恢复策略 | 恢复后状态 | +|------|--------|-----------|----------|----------|-----------| +| A1 | 扫描 ColdPending 失败 | 无 | 无副作用 | 下轮扫描自动重试 | 正常 | +| A2 | 驱动分配失败 | 聚合完成 | Bundle 内存态 | 重入队列等待驱动 | 正常 | +| A3 | 磁带写入部分失败 | 磁带有部分数据 | 磁带脏数据(不可回滚) | 写 FileMark 标记废弃,换新位置重写整个 Bundle | 正常 | +| A4 | 磁带写入全部失败 | 无有效写入 | 驱动错误 | 切换驱动/磁带,重试 | 正常 | +| A5 | 元数据更新失败 | 磁带已写入 | 磁带有数据但元数据未更新 | 补偿任务重试元数据写入 | 正常 | +| A6 | 元数据更新超时 | 磁带已写入 | 不确定元数据状态 | 查询确认后决定重试或跳过 | 正常 | +| A7 | 调度器崩溃 | 不确定 | 磁带可能有部分数据 | 重启后扫描补偿日志 + 重扫 ColdPending | 正常 | + +### 5.2 取回流程故障 + +| 序号 | 故障点 | 已完成步骤 | 数据状态 | 恢复策略 | 恢复后状态 | +|------|--------|-----------|----------|----------|-----------| +| R1 | 驱动分配超时 | 无 | RecallTask Pending | 重入队列,超时后 Failed | 用户可重试 | +| R2 | 磁带加载失败 | 驱动已分配 | 磁带物理故障 | 切换副本磁带(tape_set) | 正常 | +| R3 | 磁带 Offline | 驱动已分配 | 磁带不在库中 | WaitingForMedia + 人工通知 | 人工上线后自动继续 | +| R4 | 磁带读取校验失败 | 部分读取 | 数据损坏 | 重试 → 切换副本 → Failed | 取决于副本 | +| R5 | 缓存写入失败 | 磁带读取完成 | 数据在内存中 | 触发淘汰后重试(2 次) | 正常或 Failed | +| R6 | 缓存空间不足 | 磁带读取完成 | 数据在内存中 | 强制淘汰 → 重试 → Failed | 取决于淘汰效果 | +| R7 | 元数据更新失败 | 缓存已写入 | 缓存有数据,元数据未更新 | 补偿任务重试 | GET 暂不可用,补偿后正常 | +| R8 | 调度器崩溃 | 不确定 | 可能有缓存数据 | 重启后扫描 Pending 任务重试 | 正常 | + +### 5.3 元数据集群故障 + +| 序号 | 故障点 | 影响 | 恢复策略 | +|------|--------|------|----------| +| M1 | Follower 宕机 | 读能力降低 | 自动剔除,恢复后追赶日志 | +| M2 | Leader 宕机 | 写入暂停(秒级) | Raft 自动选举新 Leader | +| M3 | 少数派分区 | 无影响(多数派仍可读写) | 分区恢复后自动追赶 | +| M4 | 多数派分区 | 写入不可用 | 等待恢复 / 人工干预 | +| M5 | RocksDB 损坏 | 单节点数据丢失 | 从其他节点 Snapshot 恢复 | +| M6 | 全集群重启 | 服务暂停 | Raft 日志回放恢复状态 | + +### 5.4 缓存层故障 + +| 序号 | 故障点 | 影响 | 恢复策略 | +|------|--------|------|----------| +| C1 | SPDK 进程崩溃 | 缓存不可用 | 重启 SPDK,从 Blobstore xattrs 重建索引 | +| C2 | NVMe 设备故障 | 缓存全部丢失 | 更换设备,所有已解冻对象需重新 Restore | +| C3 | Blob 损坏 | 单个对象不可读 | 删除坏 Blob,返回 500,用户重新 Restore | +| C4 | 索引与 Blob 不一致 | 部分对象查找异常 | 启动时全量扫描 Blobstore 重建索引 | + +--- + +## 6. 可观测性与告警 + +> 完整的可观测性设计详见 [08-observability.md](./08-observability.md),包括 OpenTelemetry 集成、Span 拓扑、Metrics 指标体系、结构化日志、仪表板和告警规则。本节仅列出与一致性/性能直接相关的核心告警。 + +### 6.1 一致性相关核心告警 + +| 指标 | 条件 | 含义 | +|------|------|------| +| `compensation_pending` | > 0 持续 > 5min | Saga 补偿任务积压,数据一致性风险 | +| `raft_leader_changes` | > 3/hour | Leader 频繁切换,写入可能中断 | +| `raft_propose_latency_ms` P99 | > 100ms | 元数据写入延迟,影响 Saga 完成速度 | +| `recall_tasks_failed` | increase > 0 | 取回失败,可能需人工介入 | +| `archive_bundles_failed` | increase > 0 | 归档失败,对象需回退 ColdPending | + +### 6.2 性能相关核心告警 + +| 指标 | 条件 | 含义 | +|------|------|------| +| `cache_hit_rate` | < 0.7 持续 > 1h | 缓存命中率过低 | +| `archive_throughput_mbps` | < 200 持续 > 30min | 归档吞吐不达标 | +| `recall_e2e_duration{tier="Expedited"}` P99 | > 300s | Expedited SLA 超标 | +| `drive_utilization` avg | > 0.95 持续 > 1h | 驱动饱和 | +| `recall_queue_depth` | > 5000 | 取回队列积压 | + +--- + +## 7. 配置参数汇总 + +### 7.1 一致性相关 + +```yaml +consistency: + raft: + heartbeat_interval_ms: 200 + election_timeout_ms: 1000 + snapshot_interval: 10000 + + compensation: + scan_interval_secs: 60 + max_retries: 100 + retry_backoff_base_ms: 1000 + retry_backoff_max_ms: 300000 + abandon_after_hours: 72 + + restore_state_machine: + enable_transition_validation: true + enable_idempotent_writes: true +``` + +### 7.2 性能相关 + +```yaml +performance: + metadata: + rocksdb_block_cache_mb: 256 + bloom_filter_bits: 10 + enable_batch_propose: true + + cache: + spdk_reactor_mask: "0x3" # CPU 核绑定 + io_unit_size: 4096 + eviction_batch_size: 64 + eviction_low_watermark: 0.8 + + scheduler: + archive: + write_buffer_mb: 128 + block_size: 262144 + min_archive_size_mb: 100 + max_archive_size_mb: 10240 + aggregation_window_secs: 300 + pipeline_depth: 2 # 双缓冲 + + recall: + read_buffer_mb: 64 + merge_window_secs: 60 + max_concurrent_restores: 10 + + tape: + tape_hold_secs: 300 # 读取后保持磁带在驱动中 + drive_acquire_timeout_secs: 600 + expedited_reserved_drives: 1 +``` + +--- + +## 8. 参考资料 + +- [Saga Pattern](https://microservices.io/patterns/data/saga.html) — 分布式事务补偿模式 +- [SPDK Performance](https://spdk.io/doc/performance_reports.html) — SPDK 性能报告 +- [Raft Consensus](https://raft.github.io/) — Raft 共识协议 +- [RocksDB Tuning Guide](https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide) — RocksDB 调优 diff --git a/docs/modules/08-observability.md b/docs/modules/08-observability.md new file mode 100644 index 0000000..f6ed25f --- /dev/null +++ b/docs/modules/08-observability.md @@ -0,0 +1,1250 @@ +# 可观测性与链路追踪设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) | [07-一致性与性能](./07-consistency-performance.md) + +## 1. 设计目标 + +ColdStore 的可观测性基于 **OpenTelemetry** 统一标准,覆盖三大支柱: + +| 支柱 | 用途 | 技术 | +|------|------|------| +| **Traces** | 跨层链路追踪,定位延迟瓶颈 | `tracing` + `tracing-opentelemetry` | +| **Metrics** | 实时指标监控,容量规划 | `opentelemetry` Meter API | +| **Logs** | 结构化日志,故障排查 | `tracing` + JSON formatter | + +### 1.1 核心挑战 + +| 挑战 | 原因 | 解决方案 | +|------|------|----------| +| 同步请求 → 异步调度 | RestoreObject 请求后,取回在后台异步执行 | Span Link 关联请求 trace 与调度 trace | +| SPDK 用户态线程 | SPDK reactor 线程不在 Tokio 运行时内 | 跨线程 Context 显式传播 | +| 磁带操作超长耗时 | 单次取回可能 30 分钟+ | 长 Span 分段记录,Event 标记关键节点 | +| 高吞吐 GET 路径 | GET 缓存命中需 < 1ms,不能有过大开销 | 采样策略,热路径最小化 instrumentation | + +--- + +## 2. 技术选型 + +### 2.1 Crate 依赖 + +```toml +[dependencies] +# ─── OpenTelemetry 核心 ─── +opentelemetry = "0.24" +opentelemetry_sdk = { version = "0.24", features = ["rt-tokio"] } +opentelemetry-otlp = { version = "0.17", features = ["tonic", "metrics", "logs"] } +opentelemetry-semantic-conventions = "0.16" + +# ─── tracing 生态 ─── +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-opentelemetry = "0.25" + +# ─── Metrics 导出 ─── +opentelemetry-prometheus = "0.17" # Prometheus pull 模式(可选) +prometheus = "0.13" # Prometheus client(可选) +``` + +### 2.2 导出架构 + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ColdStore 进程 │ +│ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ 接入层 │ │ 元数据 │ │ 缓存层 │ │ 调度层 │ │ 磁带层 │ │ +│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ +│ │ │ │ │ │ │ +│ └────────────┴────────────┴────────────┴────────────┘ │ +│ │ │ +│ ┌──────────────┼──────────────┐ │ +│ ▼ ▼ ▼ │ +│ TracerProvider MeterProvider LoggerProvider │ +│ │ │ │ │ +│ └──────────────┼──────────────┘ │ +│ ▼ │ +│ OTLP gRPC Exporter │ +└──────────────────────────────┬─────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────┐ + │ OpenTelemetry Collector │ + │ (otel-collector) │ + └─────────┬───────────────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ Jaeger / │ │Prometheus│ │ Loki / │ + │ Tempo │ │ │ │ ES │ + │ (Traces) │ │ (Metrics)│ │ (Logs) │ + └──────────┘ └──────────┘ └──────────┘ + │ │ │ + └────────────┼────────────┘ + ▼ + ┌──────────┐ + │ Grafana │ + │ Dashboard│ + └──────────┘ +``` + +### 2.3 初始化 + +```rust +use opentelemetry::KeyValue; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{runtime, trace, Resource}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +pub struct TelemetryConfig { + pub service_name: String, + pub otlp_endpoint: String, + pub node_id: u64, + pub environment: String, + pub trace_sample_rate: f64, + pub log_level: String, + pub metrics_export_interval_secs: u64, +} + +pub fn init_telemetry(config: &TelemetryConfig) -> Result { + let resource = Resource::new(vec![ + KeyValue::new("service.name", config.service_name.clone()), + KeyValue::new("service.instance.id", format!("node-{}", config.node_id)), + KeyValue::new("deployment.environment", config.environment.clone()), + KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), + ]); + + // ── Traces ── + let tracer = opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint(&config.otlp_endpoint), + ) + .with_trace_config( + trace::Config::default() + .with_resource(resource.clone()) + .with_sampler(trace::Sampler::TraceIdRatioBased(config.trace_sample_rate)), + ) + .install_batch(runtime::Tokio)?; + + // ── Metrics ── + let meter_provider = opentelemetry_otlp::new_pipeline() + .metrics(runtime::Tokio) + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint(&config.otlp_endpoint), + ) + .with_resource(resource.clone()) + .with_period(Duration::from_secs(config.metrics_export_interval_secs)) + .build()?; + + opentelemetry::global::set_meter_provider(meter_provider); + + // ── 组装 tracing subscriber ── + let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer); + + let fmt_layer = tracing_subscriber::fmt::layer() + .json() + .with_target(true) + .with_thread_ids(true) + .with_span_list(true); + + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(&config.log_level)); + + tracing_subscriber::registry() + .with(filter) + .with(otel_layer) + .with(fmt_layer) + .init(); + + Ok(TelemetryGuard { /* shutdown handles */ }) +} + +pub struct TelemetryGuard { + // Drop 时调用 opentelemetry::global::shutdown_tracer_provider() +} + +impl Drop for TelemetryGuard { + fn drop(&mut self) { + opentelemetry::global::shutdown_tracer_provider(); + } +} +``` + +--- + +## 3. 分布式追踪设计 + +### 3.1 Trace 拓扑总览 + +ColdStore 的操作分为**同步请求链路**和**异步调度链路**两类,通过 **Span Link** 关联: + +``` +═══ 同步请求 Trace ═══ + +[PUT Object] Trace A + └─ [parse_request] + └─ [metadata.put_object] + └─ [raft_propose] + +[GET Object] Trace B + └─ [parse_request] + └─ [metadata.get_object] + └─ [check_restore_status] + └─ [cache.get] + └─ [spdk_blob_read] + +[RestoreObject] Trace C + └─ [parse_restore_request] + └─ [metadata.find_active_recall] ← linearizable read + └─ [metadata.put_recall_task] + └─ [response: 202 Accepted] + + +═══ 异步调度 Trace(Link → Trace C)═══ + +[recall_scheduler.process] Trace D ──Link──▶ Trace C + └─ [dequeue_task] + └─ [merge_by_tape] + └─ [execute_tape_read_job] + └─ [tape.acquire_drive] + └─ [tape.load] + └─ [tape.seek] + └─ [tape.read_objects] + └─ [read_object: obj1] + └─ [read_object: obj2] + └─ [cache.put_restored_batch] + └─ [spdk_create_blob] + └─ [spdk_write_blob] + └─ [metadata.update_restore_status] + └─ [tape.unload] +``` + +### 3.2 Span 命名规范 + +采用 `{layer}.{operation}` 的扁平命名,便于在 Jaeger/Tempo 中按层过滤: + +| 层 | Span 名称模式 | 示例 | +|------|--------------|------| +| 接入层 | `s3.{method}` | `s3.put_object`, `s3.get_object`, `s3.restore_object` | +| 协议层 | `protocol.{operation}` | `protocol.parse_restore`, `protocol.build_response` | +| 元数据 | `metadata.{api}` | `metadata.put_object`, `metadata.scan_cold_pending` | +| Raft | `raft.{operation}` | `raft.propose`, `raft.apply`, `raft.ensure_linearizable` | +| 缓存 | `cache.{operation}` | `cache.get`, `cache.put_restored`, `cache.evict_batch` | +| SPDK | `spdk.{operation}` | `spdk.create_blob`, `spdk.write_blob`, `spdk.read_blob` | +| 调度 | `scheduler.{operation}` | `scheduler.archive_scan`, `scheduler.recall_process` | +| 磁带 | `tape.{operation}` | `tape.acquire_drive`, `tape.load`, `tape.write`, `tape.read` | + +### 3.3 Span Attributes(标准化字段) + +#### 3.3.1 通用 Attributes + +| Attribute | 类型 | 说明 | 示例 | +|-----------|------|------|------| +| `coldstore.bucket` | string | S3 桶名 | `"my-bucket"` | +| `coldstore.key` | string | S3 对象键 | `"photos/2025/img.jpg"` | +| `coldstore.version_id` | string | 对象版本 | `"v1"` | +| `coldstore.object_size` | i64 | 对象字节数 | `104857600` | +| `coldstore.storage_class` | string | 存储类别 | `"Cold"` | +| `coldstore.node_id` | i64 | 当前节点 ID | `1` | + +#### 3.3.2 元数据层 Attributes + +| Attribute | 类型 | 说明 | +|-----------|------|------| +| `raft.is_leader` | bool | 当前节点是否 Leader | +| `raft.term` | i64 | 当前 Raft 任期 | +| `raft.log_index` | i64 | 写入的日志索引 | +| `raft.commit_latency_ms` | f64 | Raft 提交延迟 | +| `metadata.cf` | string | 操作的 Column Family | +| `metadata.batch_size` | i64 | WriteBatch 中的操作数 | + +#### 3.3.3 缓存层 Attributes + +| Attribute | 类型 | 说明 | +|-----------|------|------| +| `cache.hit` | bool | 是否命中缓存 | +| `cache.blob_id` | i64 | SPDK Blob ID | +| `cache.blob_size` | i64 | Blob 占用空间 | +| `cache.io_unit_count` | i64 | 读写的 io_unit 数 | +| `cache.usage_ratio` | f64 | 当前容量使用率 | +| `cache.eviction_reason` | string | 淘汰原因(`"ttl"` / `"capacity"` / `"lru"`) | + +#### 3.3.4 调度层 Attributes + +| Attribute | 类型 | 说明 | +|-----------|------|------| +| `scheduler.task_id` | string | 任务 ID (UUID) | +| `scheduler.task_type` | string | `"archive"` / `"recall"` | +| `scheduler.bundle_id` | string | ArchiveBundle ID | +| `scheduler.tier` | string | `"Expedited"` / `"Standard"` / `"Bulk"` | +| `scheduler.merge_count` | i64 | 合并的任务数 | +| `scheduler.queue_depth` | i64 | 当前队列深度 | +| `scheduler.retry_count` | i64 | 重试次数 | + +#### 3.3.5 磁带层 Attributes + +| Attribute | 类型 | 说明 | +|-----------|------|------| +| `tape.tape_id` | string | 磁带 ID | +| `tape.drive_id` | string | 驱动 ID | +| `tape.operation` | string | `"write"` / `"read"` / `"seek"` / `"load"` / `"unload"` | +| `tape.bytes_transferred` | i64 | 传输字节数 | +| `tape.blocks_transferred` | i64 | 传输块数 | +| `tape.filemark` | i64 | FileMark 位置 | +| `tape.seek_blocks` | i64 | seek 跳过的块数 | +| `tape.swap_duration_ms` | f64 | 换带耗时 | + +### 3.4 跨异步边界 Context 传播 + +#### 3.4.1 问题:RestoreObject → 异步取回 + +`RestoreObject` 是同步 HTTP 请求(返回 202),取回在后台异步执行。两个 Trace 需通过 **Span Link** 关联: + +```rust +use tracing::Span; +use opentelemetry::trace::TraceContextExt; + +// ─── 协议层:RestoreObject Handler ─── +#[tracing::instrument(name = "s3.restore_object", skip(state, body))] +async fn restore_object( + state: AppState, + bucket: &str, + key: &str, + body: RestoreRequest, +) -> Result { + let recall_task = RecallTask::new(bucket, key, body.tier, body.days); + + // 捕获当前 span context,存入 RecallTask 供调度层关联 + let current_cx = Span::current().context(); + let span_context = current_cx.span().span_context().clone(); + let trace_context = serialize_span_context(&span_context); + + recall_task.trace_context = Some(trace_context); + state.metadata.put_recall_task(recall_task).await?; + + Ok(Response::new(StatusCode::ACCEPTED)) +} + +// ─── 调度层:处理 RecallTask ─── +#[tracing::instrument(name = "scheduler.recall_process", skip(self))] +async fn process_recall_task(&self, task: RecallTask) { + let span = Span::current(); + + // 建立 Link 到原始 RestoreObject 请求 + if let Some(ref ctx) = task.trace_context { + if let Some(remote_cx) = deserialize_span_context(ctx) { + span.add_link(remote_cx, vec![ + KeyValue::new("link.type", "restore_request"), + KeyValue::new("link.recall_task_id", task.id.to_string()), + ]); + } + } + + // 后续执行... + self.execute_tape_read_job(task).await; +} +``` + +#### 3.4.2 问题:Tokio → SPDK reactor 线程 + +SPDK reactor 运行在独立线程,不在 Tokio 运行时内。需显式传递 Context: + +```rust +use opentelemetry::Context; + +// ─── CacheManager:在 Tokio 侧捕获 Context ─── +#[tracing::instrument(name = "cache.put_restored", skip(self, data))] +async fn put_restored(&self, item: RestoredItem) -> Result<()> { + let parent_cx = Context::current(); + + // 通过 channel 发送到 SPDK reactor 线程 + let (tx, rx) = oneshot::channel(); + self.spdk_sender.send(SpdkCommand::WriteBlob { + xattrs: item.to_xattrs(), + data: item.data, + parent_cx, // 传递上下文 + reply: tx, + })?; + + rx.await? +} + +// ─── SPDK reactor 线程侧 ─── +fn handle_spdk_command(cmd: SpdkCommand) { + match cmd { + SpdkCommand::WriteBlob { xattrs, data, parent_cx, reply } => { + let _guard = parent_cx.attach(); + let span = tracing::info_span!("spdk.write_blob", + cache.blob_size = data.len() as i64, + ); + let _enter = span.enter(); + + // SPDK 同步操作... + let result = spdk_blob_write_sync(&xattrs, &data); + let _ = reply.send(result); + } + } +} +``` + +#### 3.4.3 问题:归档扫描 → 批量磁带写入 + +归档调度器定时扫描产生多个 ArchiveTask,每个 Task 是独立 Trace,但共享同一 scan 的上下文: + +```rust +#[tracing::instrument(name = "scheduler.archive_scan")] +async fn scan_and_archive(&self) { + let objects = self.metadata.scan_cold_pending(self.config.batch_size).await?; + let bundles = self.aggregate(objects); + + for bundle in bundles { + let task_span = tracing::info_span!("scheduler.archive_task", + scheduler.bundle_id = %bundle.id, + scheduler.task_type = "archive", + coldstore.object_count = bundle.entries.len() as i64, + ); + + // 每个 ArchiveTask 独立 spawn,继承 scan span 为 Link + let metadata = self.metadata.clone(); + let tape_mgr = self.tape_manager.clone(); + let scan_cx = Span::current().context(); + + tokio::spawn(async move { + task_span.add_link( + scan_cx.span().span_context().clone(), + vec![KeyValue::new("link.type", "archive_scan")], + ); + execute_archive(metadata, tape_mgr, bundle) + .instrument(task_span) + .await; + }); + } +} +``` + +### 3.5 采样策略 + +| 路径 | 采样率 | 理由 | +|------|--------|------| +| `s3.get_object`(缓存命中) | 1% | 高频热路径,全量采集开销过大 | +| `s3.put_object` | 10% | 中频操作 | +| `s3.restore_object` | 100% | 低频且关键操作,需全量 | +| `scheduler.archive_*` | 100% | 低频,每个 Bundle 必须可追踪 | +| `scheduler.recall_*` | 100% | 低频且面向用户 SLA | +| `tape.*` | 100% | 低频,磁带操作需完整记录 | +| `raft.propose` | 10% | 中频,Leader 节点可能较多 | +| `cache.evict_batch` | 100% | 低频后台任务 | + +**实现**:基于 Span 名称的自定义 Sampler: + +```rust +pub struct ColdStoreSampler { + default_rate: f64, + overrides: HashMap, +} + +impl trace::ShouldSample for ColdStoreSampler { + fn should_sample( + &self, + parent_context: Option<&Context>, + _trace_id: TraceId, + name: &str, + _span_kind: &SpanKind, + _attributes: &[KeyValue], + _links: &[Link], + ) -> SamplingResult { + // 父 span 已采样则继续采样(保持链路完整) + if let Some(cx) = parent_context { + if cx.span().span_context().is_sampled() { + return SamplingResult { + decision: SamplingDecision::RecordAndSample, + ..Default::default() + }; + } + } + + let rate = self.overrides.get(name).copied().unwrap_or(self.default_rate); + if rand::random::() < rate { + SamplingResult { decision: SamplingDecision::RecordAndSample, ..Default::default() } + } else { + SamplingResult { decision: SamplingDecision::Drop, ..Default::default() } + } + } +} +``` + +### 3.6 RecallTask 中的 trace_context 字段 + +为支持 Span Link,RecallTask 和 ArchiveTask 增加 trace context 字段: + +```rust +pub struct RecallTask { + // ... 已有字段 ... + pub trace_context: Option, // W3C TraceContext 序列化 +} + +pub struct ArchiveTask { + // ... 已有字段 ... + pub trace_context: Option, +} +``` + +序列化格式采用 W3C Trace Context(`traceparent` header 格式): + +``` +00-{trace_id_hex}-{span_id_hex}-{trace_flags_hex} +``` + +--- + +## 4. Metrics 指标体系 + +### 4.1 指标注册 + +```rust +use opentelemetry::global; +use opentelemetry::metrics::{Counter, Histogram, Meter, UpDownCounter}; + +pub struct ColdStoreMetrics { + meter: Meter, + + // ── 接入层 ── + pub s3_requests_total: Counter, + pub s3_request_duration: Histogram, + pub s3_request_body_size: Histogram, + pub s3_response_body_size: Histogram, + pub s3_errors_total: Counter, + + // ── 元数据层 ── + pub raft_propose_total: Counter, + pub raft_propose_duration: Histogram, + pub raft_apply_duration: Histogram, + pub raft_leader_changes: Counter, + pub raft_log_entries: Counter, + pub metadata_read_duration: Histogram, + pub metadata_write_batch_size: Histogram, + + // ── 缓存层 ── + pub cache_hits: Counter, + pub cache_misses: Counter, + pub cache_puts: Counter, + pub cache_deletes: Counter, + pub cache_evictions: Counter, + pub cache_eviction_bytes: Counter, + pub cache_get_duration: Histogram, + pub cache_put_duration: Histogram, + pub cache_usage_bytes: UpDownCounter, + pub cache_object_count: UpDownCounter, + pub spdk_io_duration: Histogram, + pub spdk_io_bytes: Histogram, + + // ── 调度层 ── + pub archive_bundles_created: Counter, + pub archive_bundles_completed: Counter, + pub archive_bundles_failed: Counter, + pub archive_objects_total: Counter, + pub archive_bytes_total: Counter, + pub archive_throughput_bytes: Counter, + pub archive_task_duration: Histogram, + + pub recall_tasks_created: Counter, + pub recall_tasks_completed: Counter, + pub recall_tasks_failed: Counter, + pub recall_queue_depth: UpDownCounter, + pub recall_wait_duration: Histogram, + pub recall_read_duration: Histogram, + pub recall_e2e_duration: Histogram, + + pub compensation_pending: UpDownCounter, + pub compensation_retries: Counter, + + // ── 磁带层 ── + pub tape_write_bytes: Counter, + pub tape_read_bytes: Counter, + pub tape_write_duration: Histogram, + pub tape_read_duration: Histogram, + pub tape_seek_duration: Histogram, + pub tape_swap_total: Counter, + pub tape_swap_duration: Histogram, + pub tape_errors: Counter, + pub drive_utilization: Histogram, + pub drive_queue_wait: Histogram, + pub offline_requests: Counter, + pub offline_wait_duration: Histogram, + pub verify_total: Counter, + pub verify_failures: Counter, +} +``` + +### 4.2 指标详细定义 + +#### 4.2.1 接入层指标 + +| 指标名 | 类型 | 单位 | Labels | 说明 | +|--------|------|------|--------|------| +| `coldstore.s3.requests.total` | Counter | 1 | `method`, `status_code`, `bucket` | S3 请求总数 | +| `coldstore.s3.request.duration` | Histogram | ms | `method`, `bucket` | 请求延迟分布 | +| `coldstore.s3.request.body.size` | Histogram | bytes | `method` | 请求体大小分布 | +| `coldstore.s3.response.body.size` | Histogram | bytes | `method` | 响应体大小分布 | +| `coldstore.s3.errors.total` | Counter | 1 | `method`, `error_code` | 错误总数(按 S3 错误码) | + +**Histogram buckets(延迟)**:`[0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 5000]` ms + +**Label 值**: + +| Label | 取值 | +|-------|------| +| `method` | `PUT`, `GET`, `HEAD`, `DELETE`, `RESTORE`, `LIST` | +| `status_code` | `200`, `202`, `403`, `404`, `409`, `500`, `503` | +| `error_code` | `InvalidObjectState`, `RestoreAlreadyInProgress`, ... | + +#### 4.2.2 元数据层指标 + +| 指标名 | 类型 | 单位 | Labels | 说明 | +|--------|------|------|--------|------| +| `coldstore.raft.propose.total` | Counter | 1 | `command_type` | Raft Propose 次数 | +| `coldstore.raft.propose.duration` | Histogram | ms | `command_type` | Propose 到 Commit 延迟 | +| `coldstore.raft.apply.duration` | Histogram | ms | `command_type` | Apply 到 RocksDB 延迟 | +| `coldstore.raft.leader.changes` | Counter | 1 | — | Leader 切换次数 | +| `coldstore.raft.log.entries` | Counter | 1 | — | 累计日志条目数 | +| `coldstore.metadata.read.duration` | Histogram | ms | `cf`, `operation` | 读操作延迟(按 CF) | +| `coldstore.metadata.write_batch.size` | Histogram | 1 | `command_type` | WriteBatch 内操作数 | + +**Label `command_type`**:`PutObject`, `DeleteObject`, `BatchArchiveComplete`, `BatchRecallComplete`, `PutRecallTask`, `UpdateTape`, ... + +#### 4.2.3 缓存层指标 + +| 指标名 | 类型 | 单位 | Labels | 说明 | +|--------|------|------|--------|------| +| `coldstore.cache.hits` | Counter | 1 | — | 缓存命中次数 | +| `coldstore.cache.misses` | Counter | 1 | — | 缓存未命中次数 | +| `coldstore.cache.puts` | Counter | 1 | — | 写入次数 | +| `coldstore.cache.deletes` | Counter | 1 | — | 删除次数(接入层 DELETE / 淘汰) | +| `coldstore.cache.evictions` | Counter | 1 | `reason` | 淘汰次数 | +| `coldstore.cache.eviction.bytes` | Counter | bytes | `reason` | 淘汰字节数 | +| `coldstore.cache.get.duration` | Histogram | ms | `hit` | GET 延迟(区分命中/未命中) | +| `coldstore.cache.put.duration` | Histogram | ms | — | PUT 延迟 | +| `coldstore.cache.usage.bytes` | UpDownCounter | bytes | — | 当前使用量 | +| `coldstore.cache.object.count` | UpDownCounter | 1 | — | 当前对象数 | +| `coldstore.spdk.io.duration` | Histogram | μs | `operation` | SPDK I/O 延迟 | +| `coldstore.spdk.io.bytes` | Histogram | bytes | `operation` | SPDK I/O 大小 | + +**Label `reason`**:`ttl`, `capacity_lru`, `capacity_lfu`, `manual` + +**Label `operation`(SPDK)**:`create_blob`, `write_blob`, `read_blob`, `delete_blob` + +#### 4.2.4 调度层指标 + +| 指标名 | 类型 | 单位 | Labels | 说明 | +|--------|------|------|--------|------| +| `coldstore.archive.bundles.created` | Counter | 1 | — | 创建的 Bundle 数 | +| `coldstore.archive.bundles.completed` | Counter | 1 | — | 完成的 Bundle 数 | +| `coldstore.archive.bundles.failed` | Counter | 1 | — | 失败的 Bundle 数 | +| `coldstore.archive.objects.total` | Counter | 1 | — | 已归档对象总数 | +| `coldstore.archive.bytes.total` | Counter | bytes | — | 已归档字节总数 | +| `coldstore.archive.throughput.bytes` | Counter | bytes | — | 写入磁带字节数(计算吞吐) | +| `coldstore.archive.task.duration` | Histogram | s | — | 单个 ArchiveTask 耗时 | +| `coldstore.recall.tasks.created` | Counter | 1 | `tier` | 创建的取回任务数 | +| `coldstore.recall.tasks.completed` | Counter | 1 | `tier` | 完成的取回任务数 | +| `coldstore.recall.tasks.failed` | Counter | 1 | `tier` | 失败的取回任务数 | +| `coldstore.recall.queue.depth` | UpDownCounter | 1 | `tier` | 当前队列深度 | +| `coldstore.recall.wait.duration` | Histogram | s | `tier` | 入队到开始执行的等待时间 | +| `coldstore.recall.read.duration` | Histogram | s | — | 磁带读取耗时 | +| `coldstore.recall.e2e.duration` | Histogram | s | `tier` | 端到端耗时(RestoreObject → Completed) | +| `coldstore.compensation.pending` | UpDownCounter | 1 | `saga_type` | 待补偿条目数 | +| `coldstore.compensation.retries` | Counter | 1 | `saga_type` | 补偿重试次数 | + +#### 4.2.5 磁带层指标 + +| 指标名 | 类型 | 单位 | Labels | 说明 | +|--------|------|------|--------|------| +| `coldstore.tape.write.bytes` | Counter | bytes | `tape_id` | 写入字节数 | +| `coldstore.tape.read.bytes` | Counter | bytes | `tape_id` | 读取字节数 | +| `coldstore.tape.write.duration` | Histogram | s | — | 写入耗时 | +| `coldstore.tape.read.duration` | Histogram | s | — | 读取耗时 | +| `coldstore.tape.seek.duration` | Histogram | ms | `seek_type` | seek 耗时 | +| `coldstore.tape.swap.total` | Counter | 1 | — | 换带总次数 | +| `coldstore.tape.swap.duration` | Histogram | s | — | 换带耗时分布 | +| `coldstore.tape.errors` | Counter | 1 | `error_type`, `tape_id` | 错误次数 | +| `coldstore.drive.utilization` | Histogram | ratio | `drive_id` | 驱动利用率 | +| `coldstore.drive.queue.wait` | Histogram | s | `priority` | 驱动等待时间 | +| `coldstore.tape.offline.requests` | Counter | 1 | — | 离线请求次数 | +| `coldstore.tape.offline.wait.duration` | Histogram | s | — | 离线等待时间 | +| `coldstore.tape.verify.total` | Counter | 1 | `result` | 校验次数 | +| `coldstore.tape.verify.failures` | Counter | 1 | — | 校验失败次数 | + +**Label `seek_type`**:`fsf`(FileMark 前进), `fsr`(块前进), `rewind` + +**Label `error_type`**:`hardware`, `media`, `write_protected`, `cleaning_required` + +**Label `priority`**:`Expedited`, `Archive`, `Standard`, `Bulk` + +### 4.3 Instrumentation 代码示例 + +#### 4.3.1 接入层 + +```rust +use tracing::instrument; + +#[instrument( + name = "s3.get_object", + skip(state), + fields( + coldstore.bucket = %bucket, + coldstore.key = %key, + coldstore.storage_class, + cache.hit, + ) +)] +async fn handle_get_object( + state: AppState, + bucket: &str, + key: &str, +) -> Result { + let start = Instant::now(); + + let meta = state.metadata.get_object(bucket, key).await?; + let meta = meta.ok_or(Error::NoSuchKey)?; + + Span::current().record("coldstore.storage_class", &meta.storage_class.as_str()); + + if meta.storage_class == StorageClass::Cold { + match meta.restore_status { + Some(RestoreStatus::Completed) if meta.restore_expire_at > Some(Utc::now()) => { + // 从缓存读取 + let cached = state.cache.get(bucket, key, None).await?; + match cached { + Some(obj) => { + Span::current().record("cache.hit", true); + state.metrics.cache_hits.add(1, &[]); + state.metrics.s3_request_duration.record( + start.elapsed().as_secs_f64() * 1000.0, + &[KeyValue::new("method", "GET"), KeyValue::new("bucket", bucket)], + ); + Ok(build_response(obj, &meta)) + } + None => { + Span::current().record("cache.hit", false); + state.metrics.cache_misses.add(1, &[]); + Err(Error::ServiceUnavailable("cache miss, please restore again")) + } + } + } + _ => Err(Error::InvalidObjectState), + } + } else { + // 热对象直接返回... + todo!() + } +} +``` + +#### 4.3.2 调度层 + +```rust +#[instrument( + name = "scheduler.recall_process", + skip(self), + fields( + scheduler.task_type = "recall", + scheduler.merge_count, + ) +)] +async fn process_recall_batch(&self) { + let tasks = self.metadata.list_pending_recall_tasks().await.unwrap_or_default(); + if tasks.is_empty() { return; } + + let jobs = self.merge_by_tape(tasks); + Span::current().record("scheduler.merge_count", jobs.len() as i64); + self.metrics.recall_queue_depth.add(-(jobs.len() as i64), &[]); + + for job in jobs { + let span = tracing::info_span!("scheduler.execute_tape_read_job", + tape.tape_id = %job.tape_id, + scheduler.task_id = %job.job_id, + coldstore.object_count = job.total_objects as i64, + ); + + self.execute_tape_read_job(job).instrument(span).await; + } +} + +#[instrument( + name = "scheduler.execute_tape_read_job", + skip(self, job), + fields( + tape.drive_id, + tape.bytes_transferred, + ) +)] +async fn execute_tape_read_job(&self, job: TapeReadJob) { + let acquire_start = Instant::now(); + + // 申请驱动 + let guard = self.tape_manager.acquire_drive( + job.priority.into() + ).instrument(tracing::info_span!("tape.acquire_drive")) + .await?; + + self.metrics.drive_queue_wait.record( + acquire_start.elapsed().as_secs_f64(), + &[KeyValue::new("priority", job.priority.as_str())], + ); + + Span::current().record("tape.drive_id", guard.drive_id()); + + // 加载磁带 + self.tape_manager.load_tape(&guard, &job.tape_id) + .instrument(tracing::info_span!("tape.load", tape.tape_id = %job.tape_id)) + .await?; + + let mut total_bytes = 0u64; + + for segment in &job.segments { + // seek 到 FileMark + guard.drive().seek_filemark(segment.filemark) + .instrument(tracing::info_span!("tape.seek", + tape.filemark = segment.filemark as i64, + )) + .await?; + + for obj in &segment.objects { + let read_span = tracing::info_span!("tape.read_object", + coldstore.bucket = %obj.bucket, + coldstore.key = %obj.key, + coldstore.object_size = obj.size as i64, + ); + let _enter = read_span.enter(); + + let data = guard.drive().read(obj.size).await?; + total_bytes += data.len() as u64; + + // 校验 + verify_checksum(&data, &obj.checksum)?; + + // 写入缓存 + self.cache.put_restored(RestoredItem { + bucket: obj.bucket.clone(), + key: obj.key.clone(), + data, + expire_at: obj.expire_at, + // ... + }).instrument(tracing::info_span!("cache.put_restored")) + .await?; + + // 更新元数据 + self.metadata.update_restore_status( + &obj.bucket, &obj.key, + RestoreStatus::Completed, + Some(obj.expire_at), + ).instrument(tracing::info_span!("metadata.update_restore_status")) + .await?; + + self.metrics.recall_tasks_completed.add(1, &[ + KeyValue::new("tier", job.priority.as_str()), + ]); + } + } + + Span::current().record("tape.bytes_transferred", total_bytes as i64); + self.metrics.tape_read_bytes.add(total_bytes, &[ + KeyValue::new("tape_id", job.tape_id.clone()), + ]); +} +``` + +#### 4.3.3 元数据层 + +```rust +#[instrument( + name = "metadata.put_object", + skip(self, meta), + fields( + coldstore.bucket = %meta.bucket, + coldstore.key = %meta.key, + raft.log_index, + raft.commit_latency_ms, + ) +)] +async fn put_object(&self, meta: ObjectMetadata) -> Result<()> { + let start = Instant::now(); + + let request = ColdStoreRequest::PutObject(meta); + let response = self.raft.client_write(request).await?; + + let latency = start.elapsed().as_secs_f64() * 1000.0; + Span::current().record("raft.log_index", response.log_id.index as i64); + Span::current().record("raft.commit_latency_ms", latency); + + self.metrics.raft_propose_total.add(1, &[ + KeyValue::new("command_type", "PutObject"), + ]); + self.metrics.raft_propose_duration.record(latency, &[ + KeyValue::new("command_type", "PutObject"), + ]); + + Ok(()) +} +``` + +--- + +## 5. 结构化日志设计 + +### 5.1 日志与 Trace 关联 + +`tracing` + `tracing-subscriber` JSON formatter 自动在日志中注入 `trace_id` 和 `span_id`: + +```json +{ + "timestamp": "2026-02-27T10:15:30.123Z", + "level": "INFO", + "target": "coldstore::scheduler::recall", + "message": "recall task completed", + "span": { + "name": "scheduler.execute_tape_read_job", + "scheduler.task_id": "a1b2c3d4-...", + "tape.tape_id": "TAPE001" + }, + "spans": [ + { "name": "scheduler.recall_process" }, + { "name": "scheduler.execute_tape_read_job" } + ], + "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", + "span_id": "00f067aa0ba902b7", + "fields": { + "recall_task_id": "a1b2c3d4-...", + "bucket": "my-bucket", + "key": "data/file.bin", + "duration_ms": 45230, + "objects_read": 15, + "bytes_read": 1073741824 + } +} +``` + +### 5.2 日志级别规范 + +| 级别 | 使用场景 | 示例 | +|------|----------|------| +| **ERROR** | 不可恢复错误、需人工干预 | 补偿任务放弃、磁带硬件故障、Raft 多数派丢失 | +| **WARN** | 可恢复异常、性能退化 | 重试发生、缓存未命中、驱动分配超时、离线磁带 | +| **INFO** | 关键业务事件 | 归档完成、取回完成、缓存淘汰批次、Leader 切换 | +| **DEBUG** | 操作详情 | 单对象读写、Raft 日志详情、Blob 操作详情 | +| **TRACE** | 最细粒度 | SPDK I/O 细节、RocksDB 读写字节、块对齐填充 | + +### 5.3 关键事件(Span Event) + +在长 Span 内部使用 `tracing::event!` 记录里程碑: + +```rust +#[instrument(name = "scheduler.execute_tape_read_job")] +async fn execute_tape_read_job(&self, job: TapeReadJob) { + tracing::info!(tape_id = %job.tape_id, "acquiring drive"); + let guard = self.tape_manager.acquire_drive(priority).await?; + + tracing::info!(drive_id = %guard.drive_id(), "drive acquired, loading tape"); + self.tape_manager.load_tape(&guard, &job.tape_id).await?; + + tracing::info!("tape loaded, starting sequential read"); + + for (i, obj) in objects.iter().enumerate() { + tracing::debug!( + object_index = i, + bucket = %obj.bucket, + key = %obj.key, + size = obj.size, + "reading object from tape" + ); + // ... + } + + tracing::info!( + objects_read = objects.len(), + bytes_read = total_bytes, + duration_ms = start.elapsed().as_millis() as u64, + "tape read job completed" + ); +} +``` + +--- + +## 6. 仪表板设计 + +### 6.1 Dashboard 布局 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ColdStore Overview Dashboard │ +├─────────────┬─────────────┬─────────────┬───────────────────────┤ +│ S3 QPS │ Error Rate │ P99 Latency │ Active Restores │ +│ [Counter] │ [Gauge %] │ [Gauge ms] │ [Gauge] │ +├─────────────┴─────────────┴─────────────┴───────────────────────┤ +│ │ +│ ┌─ S3 Request Rate (by method) ──────────────────────────────┐ │ +│ │ PUT ████████ 120/s │ │ +│ │ GET ████████████████████████ 850/s │ │ +│ │ RESTORE ██ 5/s │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Request Latency (P50/P95/P99) ───────────────────────────┐ │ +│ │ [Time series graph: GET, PUT, RESTORE] │ │ +│ └────────────────────────────────────────────────────────────┘ │ +├───────────────────────────────────────────────────────────────────┤ +│ Metadata Cluster │ +├─────────────┬─────────────┬───────────────────────────────────────┤ +│ Raft QPS │ Leader Node │ Propose P99 │ +│ [Counter] │ [Stat] │ [Gauge ms] │ +├─────────────┴─────────────┴───────────────────────────────────────┤ +│ │ +│ ┌─ Raft Propose Latency ────────────────────────────────────┐ │ +│ │ [Histogram heatmap by command_type] │ │ +│ └───────────────────────────────────────────────────────────┘ │ +├───────────────────────────────────────────────────────────────────┤ +│ Cache Layer │ +├─────────────┬─────────────┬─────────────┬─────────────────────────┤ +│ Hit Rate │ Usage │ Objects │ Eviction Rate │ +│ [Gauge %] │ [Gauge GB] │ [Gauge] │ [Counter /min] │ +├─────────────┴─────────────┴─────────────┴─────────────────────────┤ +│ │ +│ ┌─ Cache Hit/Miss Rate ─────┐ ┌─ SPDK I/O Latency ──────────┐│ +│ │ [Stacked area chart] │ │ [Histogram: read/write] ││ +│ └───────────────────────────┘ └──────────────────────────────┘│ +├───────────────────────────────────────────────────────────────────┤ +│ Scheduler Layer │ +├─────────────┬─────────────┬─────────────┬─────────────────────────┤ +│ Archive │ Recall │ Queue Depth │ Compensation │ +│ Throughput │ Completed │ [Gauge] │ Pending │ +│ [MB/s] │ [/min] │ │ [Gauge] │ +├─────────────┴─────────────┴─────────────┴─────────────────────────┤ +│ │ +│ ┌─ Recall E2E Latency (by tier) ─────────────────────────────┐ │ +│ │ Expedited ██ P50=2min P99=4.5min │ │ +│ │ Standard █████████████ P50=2hr P99=4.8hr │ │ +│ │ Bulk ██████████████████ P50=6hr P99=11hr │ │ +│ └────────────────────────────────────────────────────────────┘ │ +├───────────────────────────────────────────────────────────────────┤ +│ Tape Layer │ +├─────────────┬─────────────┬─────────────┬─────────────────────────┤ +│ Drive │ Swap Rate │ Throughput │ Errors │ +│ Utilization │ [/hour] │ [MB/s] │ [Counter] │ +│ [Gauge %] │ │ │ │ +├─────────────┴─────────────┴─────────────┴─────────────────────────┤ +│ │ +│ ┌─ Drive Utilization (per drive) ─┐ ┌─ Tape Errors ─────────┐│ +│ │ [Multi-line gauge chart] │ │ [by error_type] ││ +│ └─────────────────────────────────┘ └────────────────────────┘│ +└───────────────────────────────────────────────────────────────────┘ +``` + +### 6.2 核心 Dashboard 查询 + +#### 6.2.1 S3 请求速率 + +```promql +rate(coldstore_s3_requests_total[5m]) +``` + +#### 6.2.2 S3 GET P99 延迟 + +```promql +histogram_quantile(0.99, rate(coldstore_s3_request_duration_bucket{method="GET"}[5m])) +``` + +#### 6.2.3 缓存命中率 + +```promql +rate(coldstore_cache_hits[5m]) +/ (rate(coldstore_cache_hits[5m]) + rate(coldstore_cache_misses[5m])) +``` + +#### 6.2.4 归档吞吐(MB/s) + +```promql +rate(coldstore_archive_throughput_bytes[5m]) / 1048576 +``` + +#### 6.2.5 取回 Expedited P99 端到端延迟 + +```promql +histogram_quantile(0.99, rate(coldstore_recall_e2e_duration_bucket{tier="Expedited"}[30m])) +``` + +#### 6.2.6 驱动利用率 + +```promql +avg(coldstore_drive_utilization) by (drive_id) +``` + +#### 6.2.7 补偿任务积压 + +```promql +coldstore_compensation_pending +``` + +--- + +## 7. 告警规则 + +### 7.1 严重告警(P1 — 立即响应) + +| 规则 | 条件 | 含义 | +|------|------|------| +| Raft 无 Leader | `absent(raft_leader)` 持续 > 30s | 元数据写入不可用 | +| 多数派丢失 | `raft_peers_connected < quorum` 持续 > 60s | 集群不可用 | +| 全部驱动故障 | `sum(drive_status == "available") == 0` 持续 > 5min | 归档/取回全部阻塞 | +| 缓存设备故障 | `spdk_bdev_status != "online"` | 缓存完全不可用 | +| 补偿任务放弃 | `increase(compensation_abandoned[1h]) > 0` | 数据一致性风险 | + +### 7.2 高优先级告警(P2 — 1 小时内响应) + +| 规则 | 条件 | 含义 | +|------|------|------| +| S3 错误率飙升 | `rate(s3_errors_total[5m]) / rate(s3_requests_total[5m]) > 0.05` | 5% 以上请求失败 | +| Raft Propose 延迟 | `histogram_quantile(0.99, raft_propose_duration) > 100` ms | 元数据写入变慢 | +| Expedited SLA 超标 | `histogram_quantile(0.99, recall_e2e_duration{tier="Expedited"}) > 300` s | 加急取回超 5 分钟 | +| 缓存使用率过高 | `cache_usage_bytes / cache_max_bytes > 0.95` | 即将触发频繁淘汰 | +| 补偿任务积压 | `compensation_pending > 10` | 元数据更新失败积压 | +| 离线磁带等待 | `offline_wait_duration > 3600` s | 磁带超 1 小时未上线 | + +### 7.3 中优先级告警(P3 — 24 小时内关注) + +| 规则 | 条件 | 含义 | +|------|------|------| +| 缓存命中率低 | `cache_hit_rate < 0.7` 持续 > 1h | 可能需要扩容缓存 | +| 归档吞吐下降 | `archive_throughput_mbps < 200` 持续 > 30min | 写入性能不达标 | +| 换带频繁 | `rate(tape_swap_total[1h]) > 50` | 合并策略可能需优化 | +| 磁带错误率上升 | `rate(tape_errors[1h]) > 5` | 介质质量退化 | +| 取回队列积压 | `recall_queue_depth > 5000` | 取回能力不足 | +| 驱动利用率饱和 | `avg(drive_utilization) > 0.95` 持续 > 1h | 需增加驱动 | +| Raft Leader 频繁切换 | `increase(raft_leader_changes[1h]) > 3` | 网络或节点不稳定 | + +--- + +## 8. 模块结构 + +``` +src/telemetry/ +├── mod.rs # pub use,init_telemetry 导出 +├── config.rs # TelemetryConfig +├── init.rs # TracerProvider + MeterProvider + LoggerProvider 初始化 +├── metrics.rs # ColdStoreMetrics 结构体 + 所有指标注册 +├── sampler.rs # ColdStoreSampler 自定义采样器 +├── context.rs # trace_context 序列化/反序列化(W3C TraceContext) +├── middleware.rs # Axum 中间件(自动记录 S3 请求 span + metrics) +└── shutdown.rs # TelemetryGuard,优雅关闭 +``` + +--- + +## 9. 配置项 + +```yaml +telemetry: + service_name: "coldstore" + environment: "production" + otlp_endpoint: "http://otel-collector:4317" + log_level: "info" + + traces: + enabled: true + sample_rate: 0.1 # 默认 10% + sample_overrides: + "s3.get_object": 0.01 # GET 热路径 1% + "s3.restore_object": 1.0 # Restore 100% + "scheduler.archive_task": 1.0 + "scheduler.recall_process": 1.0 + "tape.*": 1.0 + export_batch_size: 512 + export_timeout_ms: 30000 + + metrics: + enabled: true + export_interval_secs: 15 + histogram_buckets: + latency_ms: [0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 5000] + latency_s: [0.1, 0.5, 1, 5, 10, 30, 60, 300, 600, 1800, 3600] + size_bytes: [1024, 65536, 1048576, 10485760, 104857600, 1073741824] + prometheus: + enabled: true + listen: "0.0.0.0:9090" + + logs: + format: "json" + include_trace_id: true + include_span_list: true +``` + +--- + +## 10. 与其他层的集成要点 + +### 10.1 各层 instrumentation 清单 + +| 层 | 集成方式 | 需要的 Crate 特性 | +|------|----------|------------------| +| 接入层 (01) | Axum middleware + `#[instrument]` | `tower-http` tracing layer | +| 协议层 (02) | `#[instrument]` on parse/build 函数 | — | +| 元数据 (03) | `#[instrument]` on trait impl + Raft hooks | `openraft` metrics callback | +| 缓存 (04) | `#[instrument]` + SPDK 线程 Context 传播 | 显式 `Context::current().attach()` | +| 调度 (05) | `#[instrument]` + Span Link + Event | `tracing::Span::add_link` | +| 磁带 (06) | `#[instrument]` on trait impl | — | + +### 10.2 Axum 中间件 + +```rust +use axum::middleware; +use tower_http::trace::TraceLayer; + +let app = Router::new() + .route("/*path", any(s3_handler)) + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &Request| { + let method = request.method().as_str(); + let path = request.uri().path(); + tracing::info_span!( + "s3.request", + http.method = %method, + http.url = %path, + http.status_code = tracing::field::Empty, + coldstore.bucket = tracing::field::Empty, + coldstore.key = tracing::field::Empty, + ) + }) + .on_response(|response: &Response, latency: Duration, span: &Span| { + span.record("http.status_code", response.status().as_u16()); + }), + ); +``` + +### 10.3 性能开销评估 + +| 组件 | 开销 | 说明 | +|------|------|------| +| `#[instrument]` | ~100ns/span(非采样时) | Span 判断是否采样后短路 | +| 采样 Span 创建 | ~1-2μs | 分配 + attributes 填充 | +| Metrics Counter/Gauge | ~10ns | 原子操作 | +| Metrics Histogram | ~50ns | bucket 查找 + 原子操作 | +| OTLP 批量导出 | 后台异步 | 不阻塞业务路径 | +| JSON 日志格式化 | ~1-5μs/行 | 仅在 log level 匹配时 | + +**对 GET 热路径的总开销**(1% 采样):~99% 请求仅 ~200ns overhead,1% 被采样的请求 ~5μs。对于 < 1ms 的目标延迟,影响可忽略。 + +--- + +## 11. 参考资料 + +- [OpenTelemetry Rust](https://opentelemetry.io/docs/languages/rust/) — 官方文档 +- [opentelemetry crate](https://docs.rs/opentelemetry/latest/opentelemetry/) — API 文档 +- [tracing-opentelemetry](https://docs.rs/tracing-opentelemetry) — tracing 桥接层 +- [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/) — 标准化属性命名 +- [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) — Collector 配置 +- [Grafana + Tempo + Prometheus](https://grafana.com/docs/) — 可视化后端 diff --git a/docs/modules/09-admin-console.md b/docs/modules/09-admin-console.md new file mode 100644 index 0000000..2c3bbc2 --- /dev/null +++ b/docs/modules/09-admin-console.md @@ -0,0 +1,1054 @@ +# 管控面(Admin Console)设计 + +> 所属架构:ColdStore 冷存储系统 +> 参考:[总架构设计](../DESIGN.md) | [03-元数据集群](./03-metadata-cluster.md) | [06-磁带管理层](./06-tape-layer.md) | [08-可观测性](./08-observability.md) + +## 1. 设计目标 + +管控面为运维人员和管理员提供 Web UI 和 REST API,覆盖 ColdStore 冷归档存储系统的全面运维管理。 + +### 1.1 核心功能域 + +| 功能域 | 说明 | 对应业务层 | +|--------|------|-----------| +| 集群管理 | 三类 Worker 添加/下线/排空、Metadata 状态、Raft 监控 | 元数据层 (03) | +| 桶与对象管理 | 桶 CRUD、对象元数据查询、存储类别统计 | 元数据层 (03) | +| 归档任务管理 | ArchiveTask 列表、进度、失败重试 | 调度层 (05) | +| 取回任务管理 | RecallTask 列表、状态追踪、优先级调整 | 调度层 (05) | +| 磁带介质管理 | 磁带注册、容量、状态、离线/上线、退役 | 磁带层 (06) | +| 驱动管理 | 驱动状态、利用率、分配情况 | 磁带层 (06) | +| 缓存监控 | 容量、命中率、淘汰统计 | 缓存层 (04) | +| 系统监控 | 仪表板、指标、告警、审计日志 | 可观测性 (08) | + +### 1.2 在架构中的位置 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 浏览器 (Admin Console) │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ Vue 3 / React + TypeScript + Ant Design / Element Plus │ │ +│ └──────────────────────────┬────────────────────────────────┘ │ +└─────────────────────────────┼───────────────────────────────────┘ + │ HTTP/JSON + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ColdStore Admin API (Axum :8080) │ +│ 独立于 S3 API (:9000),共享同一进程内的服务层 │ +│ ┌──────────┬───────────┬──────────┬──────────┬───────────┐ │ +│ │ /cluster │ /buckets │ /objects │ /tapes │ /tasks │ │ +│ │ /drives │ /cache │ /metrics │ /audit │ /config │ │ +│ └──────────┴───────────┴──────────┴──────────┴───────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ MetadataService │ TapeManager │ CacheManager │ Metrics │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 技术选型 + +### 2.1 后端 + +| 组件 | 选型 | 说明 | +|------|------|------| +| HTTP 框架 | **Axum** | 与 S3 接入层共用框架,独立 Router 和端口 | +| 序列化 | **serde + serde_json** | JSON 请求/响应 | +| 认证 | **JWT** (jsonwebtoken crate) | Bearer Token 认证 | +| API 文档 | **utoipa + Swagger UI** | OpenAPI 3.0 自动生成 | +| WebSocket | **axum::extract::ws** | 实时推送任务状态、告警事件 | + +### 2.2 前端 + +| 组件 | 选型 | 说明 | +|------|------|------| +| 框架 | **Vue 3 + TypeScript** | 组合式 API、类型安全 | +| UI 库 | **Ant Design Vue** | 企业级管控面组件(Table、Form、Chart) | +| 图表 | **ECharts** | 仪表板图表 | +| HTTP | **Axios** | API 调用 | +| 状态管理 | **Pinia** | 全局状态 | +| 路由 | **Vue Router** | SPA 路由 | +| 构建 | **Vite** | 快速构建 | + +--- + +## 3. 后端 Admin API 设计 + +### 3.1 API 总览 + +所有 API 以 `/api/v1/admin` 为前缀,返回统一 JSON 格式。 + +```rust +pub struct ApiResponse { + pub code: u32, // 0 = 成功,非 0 = 错误码 + pub message: String, + pub data: Option, +} + +pub struct PageRequest { + pub page: u32, // 从 1 开始 + pub page_size: u32, // 默认 20,最大 100 + pub sort_by: Option, + pub sort_order: Option, // "asc" / "desc" +} + +pub struct PageResponse { + pub items: Vec, + pub total: u64, + pub page: u32, + pub page_size: u32, +} +``` + +### 3.2 集群管理 API + +#### 3.2.1 集群概览 + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/cluster/info` | 集群概览(全部节点信息、Raft 状态) | read | +| GET | `/cluster/health` | 健康检查(Raft 状态、各类 Worker 在线率) | read | +| GET | `/cluster/raft/status` | Raft 详细状态(log index、commit index、snapshot) | read | + +#### 3.2.2 Scheduler Worker 管理 + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/cluster/scheduler-workers` | 调度 Worker 列表 | read | +| GET | `/cluster/scheduler-workers/{id}` | 调度 Worker 详情(队列深度、活跃作业等) | read | +| POST | `/cluster/scheduler-workers` | **添加**调度 Worker(注册到 Metadata) | admin | +| DELETE | `/cluster/scheduler-workers/{id}` | **下线**调度 Worker(从 Metadata 移除) | admin | +| POST | `/cluster/scheduler-workers/{id}/drain` | 排空(不再分配新任务,等待完成) | admin | + +#### 3.2.3 Cache Worker 管理 + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/cluster/cache-workers` | 缓存 Worker 列表 | read | +| GET | `/cluster/cache-workers/{id}` | 缓存 Worker 详情(容量、对象数等) | read | +| POST | `/cluster/cache-workers` | **添加**缓存 Worker | admin | +| DELETE | `/cluster/cache-workers/{id}` | **下线**缓存 Worker | admin | +| POST | `/cluster/cache-workers/{id}/drain` | 排空 | admin | + +#### 3.2.4 Tape Worker 管理 + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/cluster/tape-workers` | 磁带 Worker 列表 | read | +| GET | `/cluster/tape-workers/{id}` | 磁带 Worker 详情(驱动列表、带库状态) | read | +| POST | `/cluster/tape-workers` | **添加**磁带 Worker | admin | +| DELETE | `/cluster/tape-workers/{id}` | **下线**磁带 Worker | admin | +| POST | `/cluster/tape-workers/{id}/drain` | 排空 | admin | + +> Gateway / Console 为无状态节点,不注册到集群,不出现在管控面中。 +> 三类 Worker 的添加和下线都通过 Console 操作,实际写入 Metadata 集群。 + +**响应示例**: + +```json +// GET /cluster/info +{ + "code": 0, + "message": "ok", + "data": { + "cluster_id": "coldstore-prod-01", + "metadata_nodes": [ + { "node_id": 1, "addr": "10.0.1.1:21001", "raft_role": "Leader", "status": "Online" }, + { "node_id": 2, "addr": "10.0.1.2:21001", "raft_role": "Follower", "status": "Online" }, + { "node_id": 3, "addr": "10.0.1.3:21001", "raft_role": "Follower", "status": "Online" } + ], + "scheduler_workers": [ + { + "node_id": 10, "addr": "10.0.2.1:22001", "status": "Online", + "is_active": true, "pending_archive": 42, "pending_recall": 5, "active_jobs": 3, + "paired_cache_worker_id": 20 + } + ], + "cache_workers": [ + { + "node_id": 20, "addr": "10.0.2.1:22002", "status": "Online", + "bdev_name": "NVMe0n1", "total_capacity": "2TB", "used_capacity": "800GB", "blob_count": 15000 + } + ], + "tape_workers": [ + { + "node_id": 30, "addr": "10.0.3.1:23001", "status": "Online", + "drives": [ + { "drive_id": "drive0", "device": "/dev/nst0", "status": "InUse", "current_tape": "TAPE001" }, + { "drive_id": "drive1", "device": "/dev/nst1", "status": "Idle", "current_tape": null } + ], + "library": { "device": "/dev/sg5", "slots": 24, "drives": 2 } + } + ], + "leader_id": 1, + "term": 42, + "committed_index": 1583920 + } +} +``` + +### 3.3 桶管理 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/buckets` | 桶列表(含对象数、容量) | read | +| GET | `/buckets/{name}` | 桶详情 | read | +| POST | `/buckets` | 创建桶 | admin | +| DELETE | `/buckets/{name}` | 删除桶 | admin | +| GET | `/buckets/{name}/stats` | 桶统计(各 StorageClass 对象数/大小) | read | + +**桶统计响应**: + +```json +// GET /buckets/my-bucket/stats +{ + "code": 0, + "data": { + "name": "my-bucket", + "total_objects": 125000, + "total_size_bytes": 5368709120, + "by_storage_class": { + "ColdPending": { "count": 500, "size_bytes": 214748364 }, + "Cold": { "count": 114500, "size_bytes": 4080218932 } + }, + "restore_in_progress": 12, + "restored_available": 45 + } +} +``` + +### 3.4 对象元数据查询 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/objects?bucket=&prefix=&storage_class=&page=&page_size=` | 对象列表(支持筛选) | read | +| GET | `/objects/{bucket}/{key}` | 对象详细元数据 | read | +| GET | `/objects/{bucket}/{key}/versions` | 对象版本列表 | read | +| GET | `/objects/{bucket}/{key}/archive` | 对象归档信息(Bundle、磁带位置) | read | +| GET | `/objects/{bucket}/{key}/restore` | 对象取回状态与历史 | read | + +**筛选参数**: + +| 参数 | 类型 | 说明 | +|------|------|------| +| `bucket` | string | 必选,桶名 | +| `prefix` | string | 可选,key 前缀过滤 | +| `storage_class` | string | 可选,`ColdPending`/`Cold` | +| `restore_status` | string | 可选,`Pending`/`InProgress`/`Completed`/`Failed` | +| `page` | u32 | 页码 | +| `page_size` | u32 | 每页大小 | + +**对象详情响应**: + +```json +// GET /objects/my-bucket/data/file.bin +{ + "code": 0, + "data": { + "bucket": "my-bucket", + "key": "data/file.bin", + "version_id": null, + "size": 104857600, + "checksum": "a1b2c3d4...", + "content_type": "application/octet-stream", + "etag": "\"9e107d9d372bb6826bd81d3542a419d6\"", + "storage_class": "Cold", + "archive_info": { + "archive_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "tape_id": "TAPE001", + "tape_set": ["TAPE001", "TAPE002"], + "tape_block_offset": 524288, + "bundle_status": "Completed", + "archived_at": "2026-02-20T08:30:00Z" + }, + "restore_info": { + "status": "Completed", + "expire_at": "2026-03-06T08:30:00Z", + "tier": "Standard", + "recall_task_id": "c1d2e3f4-..." + }, + "created_at": "2026-02-15T10:00:00Z", + "updated_at": "2026-02-20T08:30:00Z" + } +} +``` + +### 3.5 归档任务 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/tasks/archive?status=&page=&page_size=` | 归档任务列表 | read | +| GET | `/tasks/archive/{id}` | 归档任务详情(含 Bundle 对象列表) | read | +| POST | `/tasks/archive/{id}/retry` | 重试失败的归档任务 | admin | +| POST | `/tasks/archive/{id}/cancel` | 取消 Pending 的归档任务 | admin | +| GET | `/tasks/archive/stats` | 归档统计(完成数、失败数、吞吐、趋势) | read | + +**归档任务列表响应**: + +```json +{ + "code": 0, + "data": { + "items": [ + { + "id": "a1b2c3d4-...", + "bundle_id": "f47ac10b-...", + "tape_id": "TAPE001", + "drive_id": "drive-0", + "object_count": 500, + "total_size": 5368709120, + "bytes_written": 3221225472, + "progress_percent": 60.0, + "status": "InProgress", + "retry_count": 0, + "created_at": "2026-02-27T09:00:00Z", + "started_at": "2026-02-27T09:01:30Z" + } + ], + "total": 42, + "page": 1, + "page_size": 20 + } +} +``` + +### 3.6 取回任务 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/tasks/recall?status=&tier=&tape_id=&page=&page_size=` | 取回任务列表 | read | +| GET | `/tasks/recall/{id}` | 取回任务详情 | read | +| POST | `/tasks/recall/{id}/retry` | 重试失败的取回任务 | admin | +| POST | `/tasks/recall/{id}/cancel` | 取消 Pending 的取回任务 | admin | +| PUT | `/tasks/recall/{id}/priority` | 调整任务优先级 | admin | +| GET | `/tasks/recall/stats` | 取回统计(SLA 达成率、队列深度、趋势) | read | +| GET | `/tasks/recall/queue` | 当前队列状态(三级队列深度、合并情况) | read | + +**取回队列状态响应**: + +```json +// GET /tasks/recall/queue +{ + "code": 0, + "data": { + "expedited": { "depth": 2, "oldest_wait_secs": 45 }, + "standard": { "depth": 35, "oldest_wait_secs": 1800 }, + "bulk": { "depth": 150, "oldest_wait_secs": 7200 }, + "waiting_for_media": 3, + "active_jobs": [ + { "job_id": "...", "tape_id": "TAPE003", "drive_id": "drive-1", "objects": 8, "progress_percent": 37.5 } + ] + } +} +``` + +### 3.7 磁带介质管理 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/tapes?status=&format=&page=&page_size=` | 磁带列表 | read | +| GET | `/tapes/{tape_id}` | 磁带详情(含 Bundle 列表、容量、错误) | read | +| POST | `/tapes` | 注册新磁带 | admin | +| PUT | `/tapes/{tape_id}/status` | 更新磁带状态(Online/Offline/Retired) | admin | +| GET | `/tapes/{tape_id}/bundles` | 磁带上的 Bundle 列表 | read | +| POST | `/tapes/{tape_id}/verify` | 触发磁带校验 | admin | +| GET | `/tapes/{tape_id}/verify/history` | 校验历史 | read | +| POST | `/tapes/{tape_id}/retire` | 磁带退役(触发数据迁移) | admin | +| GET | `/tapes/offline-requests` | 当前离线请求列表 | read | +| POST | `/tapes/offline-requests/{id}/confirm` | 确认磁带已上线 | admin | +| GET | `/tapes/stats` | 磁带总体统计(容量、使用率、健康) | read | + +**磁带详情响应**: + +```json +// GET /tapes/TAPE001 +{ + "code": 0, + "data": { + "id": "TAPE001", + "barcode": "TAPE001L9", + "format": "LTO-9", + "status": "Online", + "location": "Library-1 / Slot-5", + "capacity": { + "total_bytes": 18000000000000, + "used_bytes": 12600000000000, + "remaining_bytes": 5400000000000, + "usage_percent": 70.0 + }, + "bundle_count": 245, + "last_write_at": "2026-02-27T08:15:00Z", + "last_read_at": "2026-02-27T09:30:00Z", + "last_verified_at": "2026-02-25T02:00:00Z", + "error_count": 0, + "registered_at": "2025-06-01T00:00:00Z" + } +} +``` + +### 3.8 驱动管理 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/drives` | 驱动列表(状态、当前磁带、利用率) | read | +| GET | `/drives/{drive_id}` | 驱动详情 | read | +| POST | `/drives/{drive_id}/eject` | 弹出磁带(紧急操作) | admin | +| POST | `/drives/{drive_id}/clean` | 触发清洁 | admin | +| GET | `/drives/stats` | 驱动总体统计 | read | + +**驱动列表响应**: + +```json +// GET /drives +{ + "code": 0, + "data": [ + { + "drive_id": "drive-0", + "device": "/dev/nst0", + "status": "InUse", + "loaded_tape": "TAPE001", + "current_task": { "type": "archive", "id": "a1b2c3d4-..." }, + "utilization_1h": 0.85, + "total_bytes_written": 1073741824000, + "total_bytes_read": 536870912000, + "error_count": 0 + }, + { + "drive_id": "drive-1", + "device": "/dev/nst1", + "status": "Available", + "loaded_tape": null, + "current_task": null, + "utilization_1h": 0.12, + "total_bytes_written": 536870912000, + "total_bytes_read": 268435456000, + "error_count": 1 + } + ] +} +``` + +### 3.9 缓存监控 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/cache/stats` | 缓存统计(容量、命中率、淘汰) | read | +| GET | `/cache/objects?bucket=&prefix=&page=&page_size=` | 缓存中的对象列表 | read | +| DELETE | `/cache/objects/{bucket}/{key}` | 手动清除缓存对象 | admin | +| POST | `/cache/evict` | 手动触发淘汰 | admin | +| GET | `/cache/config` | 缓存配置 | read | + +**缓存统计响应**: + +```json +// GET /cache/stats +{ + "code": 0, + "data": { + "capacity": { + "max_bytes": 1099511627776, + "used_bytes": 824633720832, + "usage_percent": 75.0, + "object_count": 8432 + }, + "performance": { + "hit_count": 1250000, + "miss_count": 125000, + "hit_rate": 0.909, + "put_count": 45000, + "evict_count": 12000, + "avg_get_latency_us": 85, + "avg_put_latency_us": 320 + }, + "eviction": { + "policy": "Lru", + "ttl_evictions": 8000, + "capacity_evictions": 4000, + "last_eviction_at": "2026-02-27T10:05:00Z" + }, + "spdk": { + "bdev": "NVMe0n1", + "blobstore_clusters": 1048576, + "used_clusters": 786432, + "io_unit_size": 4096 + } + } +} +``` + +### 3.10 系统监控 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/metrics/dashboard` | 仪表板聚合数据 | read | +| GET | `/metrics/prometheus` | Prometheus 格式指标(拉取端点) | read | +| GET | `/alerts/active` | 当前活跃告警 | read | +| GET | `/alerts/history?page=&page_size=` | 告警历史 | read | +| POST | `/alerts/{id}/acknowledge` | 确认告警 | admin | +| GET | `/compensation/pending` | 补偿任务列表 | read | +| POST | `/compensation/{id}/retry` | 手动触发补偿重试 | admin | + +### 3.11 审计日志 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/audit?action=&user=&start=&end=&page=` | 审计日志查询 | read | + +**审计日志结构**: + +```rust +pub struct AuditEntry { + pub id: Uuid, + pub timestamp: DateTime, + pub user: String, + pub action: String, + pub resource_type: String, + pub resource_id: String, + pub detail: serde_json::Value, + pub source_ip: String, + pub result: AuditResult, +} + +pub enum AuditResult { Success, Failed(String) } +``` + +### 3.12 配置管理 API + +| 方法 | 路径 | 说明 | 权限 | +|------|------|------|------| +| GET | `/config` | 获取当前运行配置(脱敏) | read | +| GET | `/config/scheduler` | 调度器配置 | read | +| PUT | `/config/scheduler` | 热更新调度器参数(部分字段) | admin | +| GET | `/config/cache` | 缓存配置 | read | +| PUT | `/config/cache` | 热更新缓存参数(淘汰水位等) | admin | + +**可热更新参数**: + +| 参数 | 范围 | 说明 | +|------|------|------| +| `scheduler.recall.merge_window_secs` | 10–300 | 取回合并窗口 | +| `scheduler.recall.max_concurrent_restores` | 1–100 | 最大并发取回 | +| `scheduler.archive.aggregation_window_secs` | 60–7200 | 归档聚合窗口 | +| `cache.eviction_low_watermark` | 0.5–0.95 | 淘汰低水位 | +| `cache.max_size_bytes` | — | 缓存容量上限 | + +### 3.13 WebSocket 实时推送 + +``` +WS /api/v1/admin/ws +``` + +**推送事件类型**: + +```rust +pub enum WsEvent { + TaskStatusChanged { + task_type: String, + task_id: Uuid, + old_status: String, + new_status: String, + }, + AlertFired { + alert_id: String, + severity: String, + message: String, + }, + AlertResolved { + alert_id: String, + }, + TapeOfflineRequest { + tape_id: String, + barcode: Option, + reason: String, + }, + DriveStatusChanged { + drive_id: String, + old_status: String, + new_status: String, + }, + ArchiveProgress { + task_id: Uuid, + bytes_written: u64, + total_size: u64, + progress_percent: f64, + }, + RecallProgress { + task_id: Uuid, + objects_read: u32, + total_objects: u32, + progress_percent: f64, + }, + ClusterLeaderChanged { + old_leader: Option, + new_leader: u64, + term: u64, + }, +} +``` + +客户端连接后可通过 JSON 消息订阅特定事件类别: + +```json +{ "subscribe": ["task", "alert", "tape", "drive", "cluster"] } +``` + +--- + +## 4. 认证与鉴权 + +### 4.1 认证方式 + +| 方式 | 适用场景 | +|------|----------| +| **JWT Bearer Token** | Web UI 登录后使用 | +| **API Key** | 自动化脚本 / 外部系统调用 | + +``` +Authorization: Bearer +``` + +或 + +``` +X-API-Key: +``` + +### 4.2 用户与角色 + +```rust +pub struct AdminUser { + pub id: Uuid, + pub username: String, + pub password_hash: String, + pub role: AdminRole, + pub created_at: DateTime, + pub last_login_at: Option>, +} + +pub enum AdminRole { + SuperAdmin, // 全部权限 + Admin, // 管理操作 + 读 + ReadOnly, // 只读 +} +``` + +### 4.3 权限矩阵 + +| 操作类型 | SuperAdmin | Admin | ReadOnly | +|----------|-----------|-------|----------| +| 查看集群/状态 | ✓ | ✓ | ✓ | +| 查看对象/磁带/任务 | ✓ | ✓ | ✓ | +| 查看监控/审计 | ✓ | ✓ | ✓ | +| 重试/取消任务 | ✓ | ✓ | ✗ | +| 创建/删除桶 | ✓ | ✓ | ✗ | +| 磁带上下线/退役 | ✓ | ✓ | ✗ | +| 热更新配置 | ✓ | ✓ | ✗ | +| 节点增删 | ✓ | ✗ | ✗ | +| 用户管理 | ✓ | ✗ | ✗ | + +### 4.4 登录 API + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/auth/login` | 登录,返回 JWT | +| POST | `/auth/refresh` | 刷新 Token | +| POST | `/auth/logout` | 登出(可选 Token 黑名单) | +| GET | `/auth/me` | 当前用户信息 | + +--- + +## 5. 前端页面设计 + +### 5.1 页面导航结构 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ ColdStore Admin [用户名] [退出] │ +├────────────┬─────────────────────────────────────────────────┤ +│ │ │ +│ 📊 仪表板 │ ┌─────────────────────────────────────┐ │ +│ │ │ 主内容区 │ │ +│ 🖥 集群 │ │ │ │ +│ ├ 概览 │ │ │ │ +│ └ 节点 │ │ │ │ +│ │ │ │ │ +│ 📦 存储 │ │ │ │ +│ ├ 桶管理 │ │ │ │ +│ └ 对象查询 │ │ │ │ +│ │ │ │ │ +│ 📼 磁带 │ │ │ │ +│ ├ 介质管理 │ └─────────────────────────────────────┘ │ +│ ├ 驱动 │ │ +│ └ 离线请求 │ │ +│ │ │ +│ 📋 任务 │ │ +│ ├ 归档任务 │ │ +│ └ 取回任务 │ │ +│ │ │ +│ 💾 缓存 │ │ +│ │ │ +│ 📈 监控 │ │ +│ ├ 指标 │ │ +│ └ 告警 │ │ +│ │ │ +│ ⚙ 设置 │ │ +│ ├ 配置 │ │ +│ ├ 用户 │ │ +│ └ 审计日志 │ │ +│ │ │ +└────────────┴─────────────────────────────────────────────────┘ +``` + +### 5.2 仪表板页面 + +**路由**:`/dashboard` + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ColdStore Dashboard │ +├──────────────┬──────────────┬──────────────┬────────────────────────────┤ +│ S3 QPS │ 错误率 │ 集群状态 │ 活跃告警 │ +│ ████ 975/s │ 0.3% │ ✅ 3/3 节点 │ ⚠ 2 条 │ +├──────────────┴──────────────┴──────────────┴────────────────────────────┤ +│ │ +│ ┌─ 请求延迟趋势(P50/P95/P99)───────────────────────────────────────┐│ +│ │ [折线图: 24h, 按 method 分组] ││ +│ └──────────────────────────────────────────────────────────────────┘│ +│ │ +│ ┌─ 存储概览 ─────────────────────┐ ┌─ 磁带与驱动 ──────────────────┐│ +│ │ 总对象: 2.5M │ │ 磁带总数: 120 ││ +│ │ 总容量: 2.1 PB │ │ 在线: 95 / 离线: 25 ││ +│ │ Restored: 100 GB │ │ 驱动: 4 (3 in-use, 1 idle) ││ +│ │ ColdPending: 5 GB │ │ 归档吞吐: 320 MB/s ││ +│ │ Cold: 2.0 PB │ │ ││ +│ │ [饼图] │ │ [驱动状态色块图] ││ +│ └────────────────────────────────┘ └────────────────────────────────┘│ +│ │ +│ ┌─ 缓存 ──────────────────────┐ ┌─ 任务队列 ──────────────────────┐│ +│ │ 容量: 750GB / 1TB (75%) │ │ 归档 Pending: 3 ││ +│ │ 命中率: 90.9% │ │ 取回 Expedited: 2 ││ +│ │ [使用量仪表盘] │ │ 取回 Standard: 35 ││ +│ │ │ │ 取回 Bulk: 150 ││ +│ │ [命中率趋势折线] │ │ WaitingForMedia: 3 ││ +│ └──────────────────────────────┘ └────────────────────────────────┘│ +│ │ +│ ┌─ 最近事件 ─────────────────────────────────────────────────────────┐│ +│ │ 10:15 ArchiveTask a1b2... completed (500 objects, 5.2GB) ││ +│ │ 10:12 RecallTask c3d4... started on drive-1 (TAPE003) ││ +│ │ 10:10 ⚠ Tape TAPE042 offline request created ││ +│ │ 10:05 Cache eviction batch: 64 objects, 2.1GB freed ││ +│ │ 09:58 Raft leader changed: node-2 → node-1 (term 42) ││ +│ └────────────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 5.3 集群概览页面 + +**路由**:`/cluster` + +| 区域 | 内容 | +|------|------| +| 集群状态卡片 | cluster_id、term、committed_index、Leader 节点 | +| 节点列表表格 | node_id、地址、角色(Tag 颜色)、心跳时间、操作(移除) | +| Raft 状态 | log index、commit index、apply index、snapshot index | +| 节点健康时间线 | 最近 24h 心跳/角色变化的时间轴 | + +### 5.4 桶管理页面 + +**路由**:`/buckets` + +| 区域 | 内容 | +|------|------| +| 桶列表表格 | 桶名、对象数、总大小、版本控制、创建时间 | +| 桶详情(点击展开) | 各 StorageClass(ColdPending/Cold)统计饼图 | +| 操作 | 创建桶(弹窗表单)、删除桶(二次确认) | + +### 5.5 对象查询页面 + +**路由**:`/objects` + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 桶: [my-bucket ▼] 前缀: [data/____] 类别: [全部 ▼] 状态: [全部 ▼] │ +│ [🔍 查询] │ +├─────────────────────────────────────────────────────────────────────┤ +│ Key │ Size │ StorageClass │ Restore │ 操作 │ +│─────────────────────────────────────────────────────────────────────│ +│ data/file1.bin │ 100 MB │ Cold 🏔 │ Completed │ [详情] │ +│ data/file2.bin │ 50 MB │ Cold 🏔 │ — │ [详情] │ +│ data/file3.bin │ 200 MB │ ColdPending ⏳│ — │ [详情] │ +│ data/file4.bin │ 1 GB │ Cold 🏔 │ InProgress│ [详情] │ +├─────────────────────────────────────────────────────────────────────┤ +│ 共 4 条 [< 1 >] │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**对象详情弹窗**: + +| Tab | 内容 | +|-----|------| +| 基本信息 | bucket、key、size、checksum、content_type、etag、版本 | +| 归档信息 | archive_id、tape_id、tape_set、tape_block_offset、Bundle 状态 | +| 取回信息 | restore_status、expire_at、tier、recall_task_id、历史 | +| 时间线 | 创建时间、归档时间、取回时间、状态流转时间线 | + +### 5.6 磁带介质管理页面 + +**路由**:`/tapes` + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 状态: [全部 ▼] 格式: [全部 ▼] [🔍 筛选] [+ 注册磁带] │ +├─────────────────────────────────────────────────────────────────────┤ +│ 磁带 ID │ 条码 │ 格式 │ 状态 │ 容量使用 │ Bundle │ 操作│ +│─────────────────────────────────────────────────────────────────────│ +│ TAPE001 │ TAPE001L9 │ LTO-9 │ ✅ Online│ ████░░ 70% │ 245 │ ... │ +│ TAPE002 │ TAPE002L9 │ LTO-9 │ ✅ Online│ ██████ 95% │ 380 │ ... │ +│ TAPE042 │ TAPE042L9 │ LTO-9 │ ❌ Offline│ ███░░░ 45% │ 120 │ ... │ +│ TAPE099 │ TAPE099L9 │ LTO-9 │ ⚠ Error │ █████░ 82% │ 290 │ ... │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**操作菜单**:详情、校验、上线/下线、退役 + +**磁带详情页(点击进入)**: + +| 区域 | 内容 | +|------|------| +| 基本信息 | ID、条码、格式、状态、位置 | +| 容量可视化 | 已用/剩余条形图、Bundle 数量 | +| Bundle 列表 | 该磁带上所有 Bundle(ID、对象数、大小、状态、时间) | +| 校验历史 | 最近校验结果、失败块数、时间 | +| 错误记录 | 错误类型、时间、处理状态 | + +### 5.7 驱动管理页面 + +**路由**:`/drives` + +每个驱动显示为卡片: + +``` +┌─ Drive 0 (/dev/nst0) ─────────────────┐ ┌─ Drive 1 (/dev/nst1) ──────────┐ +│ 状态: 🟢 InUse │ │ 状态: 🟡 Available │ +│ 当前磁带: TAPE001 │ │ 当前磁带: — │ +│ 当前任务: Archive a1b2... (60%) │ │ 等待队列: 2 个任务 │ +│ 利用率 (1h): ████████░░ 85% │ │ 利用率 (1h): █░░░░░░░░░ 12% │ +│ 累计写入: 1.0 TB │ │ 累计读取: 250 GB │ +│ [弹出磁带] [清洁] │ │ [弹出磁带] [清洁] │ +└────────────────────────────────────────┘ └─────────────────────────────────┘ +``` + +### 5.8 取回任务页面 + +**路由**:`/tasks/recall` + +| 区域 | 内容 | +|------|------| +| 队列概览 | 三级队列深度柱状图、WaitingForMedia 数量 | +| 任务列表表格 | ID、bucket/key、tier(标签颜色)、状态、磁带、等待时间、操作 | +| 筛选 | 状态、优先级、磁带 ID | +| 操作 | 重试、取消、调整优先级 | + +**状态标签颜色**: + +| 状态 | 颜色 | +|------|------| +| Pending | 蓝色 | +| WaitingForMedia | 橙色 | +| InProgress | 绿色动画 | +| Completed | 灰色 | +| Failed | 红色 | + +### 5.9 离线请求页面 + +**路由**:`/tapes/offline` + +| 区域 | 内容 | +|------|------| +| 待处理请求列表 | tape_id、条码、原因、请求时间、等待时长、关联任务数 | +| 操作 | 确认已上线(触发 `confirm_media_online`) | +| 历史记录 | 已处理的离线请求、处理耗时 | + +### 5.10 告警页面 + +**路由**:`/alerts` + +| Tab | 内容 | +|-----|------| +| 活跃告警 | 严重级别(P1 红/P2 橙/P3 黄)、告警名、触发时间、持续时间、确认按钮 | +| 历史告警 | 时间范围筛选、告警名、触发→恢复时间、处理人 | + +### 5.11 审计日志页面 + +**路由**:`/audit` + +可搜索、可按时间范围/用户/操作类型筛选的表格: + +| 时间 | 用户 | 操作 | 资源 | 详情 | 结果 | +|------|------|------|------|------|------| +| 10:15:30 | admin | retry_recall_task | recall/c3d4... | {"old_status":"Failed"} | ✅ | +| 10:12:00 | admin | confirm_online | tape/TAPE042 | {} | ✅ | +| 09:50:00 | system | evict_cache | cache/batch | {"count":64} | ✅ | + +--- + +## 6. 后端模块结构 + +``` +src/admin/ +├── mod.rs # pub use,Router 构建 +├── router.rs # Axum Router 定义(/api/v1/admin/*) +├── auth/ +│ ├── mod.rs +│ ├── jwt.rs # JWT 签发/验证 +│ ├── middleware.rs # 认证中间件 +│ └── models.rs # AdminUser, AdminRole +├── handlers/ +│ ├── mod.rs +│ ├── cluster.rs # 集群管理 handler +│ ├── bucket.rs # 桶管理 handler +│ ├── object.rs # 对象查询 handler +│ ├── archive_task.rs # 归档任务 handler +│ ├── recall_task.rs # 取回任务 handler +│ ├── tape.rs # 磁带管理 handler +│ ├── drive.rs # 驱动管理 handler +│ ├── cache.rs # 缓存监控 handler +│ ├── metrics.rs # 监控指标 handler +│ ├── alert.rs # 告警 handler +│ ├── config.rs # 配置管理 handler +│ └── audit.rs # 审计日志 handler +├── websocket.rs # WebSocket 推送 +├── dto/ # 请求/响应 DTO +│ ├── mod.rs +│ ├── common.rs # ApiResponse, PageRequest, PageResponse +│ ├── cluster.rs +│ ├── bucket.rs +│ ├── object.rs +│ ├── task.rs +│ ├── tape.rs +│ ├── drive.rs +│ ├── cache.rs +│ └── alert.rs +└── audit.rs # 审计日志写入逻辑 +``` + +### 6.1 前端项目结构 + +``` +console/ +├── package.json +├── vite.config.ts +├── tsconfig.json +├── src/ +│ ├── main.ts +│ ├── App.vue +│ ├── router/ +│ │ └── index.ts # 路由定义 +│ ├── api/ # API 封装 +│ │ ├── client.ts # Axios 实例(含 JWT 拦截器) +│ │ ├── cluster.ts +│ │ ├── bucket.ts +│ │ ├── object.ts +│ │ ├── task.ts +│ │ ├── tape.ts +│ │ ├── drive.ts +│ │ ├── cache.ts +│ │ ├── metrics.ts +│ │ ├── alert.ts +│ │ └── auth.ts +│ ├── stores/ # Pinia 状态 +│ │ ├── auth.ts +│ │ ├── websocket.ts # WS 连接 + 事件分发 +│ │ └── notification.ts +│ ├── views/ # 页面组件 +│ │ ├── Dashboard.vue +│ │ ├── cluster/ +│ │ │ ├── Overview.vue +│ │ │ └── NodeDetail.vue +│ │ ├── storage/ +│ │ │ ├── BucketList.vue +│ │ │ ├── BucketDetail.vue +│ │ │ └── ObjectQuery.vue +│ │ ├── tape/ +│ │ │ ├── TapeList.vue +│ │ │ ├── TapeDetail.vue +│ │ │ ├── DriveList.vue +│ │ │ └── OfflineRequests.vue +│ │ ├── task/ +│ │ │ ├── ArchiveTaskList.vue +│ │ │ └── RecallTaskList.vue +│ │ ├── cache/ +│ │ │ └── CacheOverview.vue +│ │ ├── monitor/ +│ │ │ ├── Metrics.vue +│ │ │ └── Alerts.vue +│ │ └── settings/ +│ │ ├── Config.vue +│ │ ├── UserManage.vue +│ │ └── AuditLog.vue +│ ├── components/ # 通用组件 +│ │ ├── StatusTag.vue # 状态标签(颜色映射) +│ │ ├── CapacityBar.vue # 容量条 +│ │ ├── DriveCard.vue # 驱动卡片 +│ │ ├── EventTimeline.vue # 事件时间线 +│ │ └── ConfirmDialog.vue # 二次确认弹窗 +│ └── utils/ +│ ├── format.ts # 字节/时间格式化 +│ └── constants.ts # 枚举映射 +``` + +--- + +## 7. 配置项 + +```yaml +admin: + enabled: true + listen: "0.0.0.0:8080" + cors_origins: ["http://localhost:5173"] + + # Console 仅需配置 Metadata 地址 + metadata_addrs: + - "10.0.1.1:21001" + - "10.0.1.2:21001" + - "10.0.1.3:21001" + + auth: + jwt_secret: "${ADMIN_JWT_SECRET}" + jwt_expire_hours: 24 + api_keys: + - name: "monitoring" + key: "${MONITORING_API_KEY}" + role: "ReadOnly" + + initial_user: + username: "admin" + password: "${ADMIN_INITIAL_PASSWORD}" + role: "SuperAdmin" + + websocket: + heartbeat_interval_secs: 30 + max_connections: 100 + + audit: + enabled: true + retention_days: 90 + + static_files: + enabled: true + path: "./console/dist" # 前端构建产物,生产环境嵌入 +``` + +--- + +## 8. 参考资料 + +- [Axum](https://docs.rs/axum/latest/axum/) — HTTP 框架 +- [utoipa](https://docs.rs/utoipa/latest/utoipa/) — OpenAPI 文档生成 +- [jsonwebtoken](https://docs.rs/jsonwebtoken) — JWT 实现 +- [Vue 3](https://vuejs.org/) — 前端框架 +- [Ant Design Vue](https://antdv.com/) — UI 组件库 +- [ECharts](https://echarts.apache.org/) — 图表库 diff --git a/docs/modules/README.md b/docs/modules/README.md new file mode 100644 index 0000000..dc7f828 --- /dev/null +++ b/docs/modules/README.md @@ -0,0 +1,34 @@ +# ColdStore 模块设计文档 + +按架构分层拆分的独立模块设计文档,与 [总架构设计](../DESIGN.md) 配套使用。 + +## 文档索引 + +| 序号 | 模块 | 文档 | 说明 | +|------|------|------|------| +| 01 | 接入层 | [01-access-layer.md](./01-access-layer.md) | S3 HTTP 服务、路由、Axum | +| 02 | 协议适配层 | [02-protocol-adapter.md](./02-protocol-adapter.md) | StorageClass 映射、RestoreRequest、x-amz-restore、错误码 | +| 03 | 元数据集群 | [03-metadata-cluster.md](./03-metadata-cluster.md) | OpenRaft + RocksDB、强一致性 | +| 04 | 数据缓存层 | [04-cache-layer.md](./04-cache-layer.md) | async-spdk、解冻数据缓存 | +| 05 | 归档取回调度层 | [05-scheduler-layer.md](./05-scheduler-layer.md) | Archive Scheduler、Recall Scheduler | +| 06 | 磁带管理层 | [06-tape-layer.md](./06-tape-layer.md) | 自研 SDK、Linux SCSI | +| 07 | 跨层一致性与性能 | [07-consistency-performance.md](./07-consistency-performance.md) | Saga 模式、并发控制、故障矩阵、性能优化 | +| 08 | 可观测性与链路追踪 | [08-observability.md](./08-observability.md) | OpenTelemetry、Traces、Metrics、Logs、告警 | +| 09 | 管控面 (Admin Console) | [09-admin-console.md](./09-admin-console.md) | Web UI、Admin API、集群/磁带/任务管理 | + +## 架构层次关系 + +``` +接入层 (01) + │ + ▼ +协议适配层 (02) + │ + ├──────────────────┬──────────────────┬──────────────────┐ + ▼ ▼ ▼ ▼ +元数据集群 (03) 数据缓存层 (04) 归档取回调度层 (05) + │ │ + │ ▼ + │ 磁带管理层 (06) + └──────────────────┘ +``` From 95338fb685d2d562ce6383f81a8dd963eead7c28 Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Sat, 28 Feb 2026 17:55:41 +0800 Subject: [PATCH 2/7] =?UTF-8?q?doc:=20Console=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E9=80=89=E5=9E=8B=E4=BB=8E=20Vue=203=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=20React=2018?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 框架: Vue 3 → React 18 + TypeScript - UI 库: Ant Design Vue → Ant Design 5 - 状态管理: Pinia → Zustand - 路由: Vue Router → React Router 6 - 文件后缀: .vue → .tsx - 新增 hooks/ 目录 (usePolling, useWebSocket) Made-with: Cursor --- docs/modules/09-admin-console.md | 86 +++++++++++++++++--------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/docs/modules/09-admin-console.md b/docs/modules/09-admin-console.md index 2c3bbc2..a52c24d 100644 --- a/docs/modules/09-admin-console.md +++ b/docs/modules/09-admin-console.md @@ -26,7 +26,7 @@ ┌─────────────────────────────────────────────────────────────────┐ │ 浏览器 (Admin Console) │ │ ┌───────────────────────────────────────────────────────────┐ │ -│ │ Vue 3 / React + TypeScript + Ant Design / Element Plus │ │ +│ │ React 18 + TypeScript + Ant Design 5 │ │ │ └──────────────────────────┬────────────────────────────────┘ │ └─────────────────────────────┼───────────────────────────────────┘ │ HTTP/JSON @@ -64,13 +64,13 @@ | 组件 | 选型 | 说明 | |------|------|------| -| 框架 | **Vue 3 + TypeScript** | 组合式 API、类型安全 | -| UI 库 | **Ant Design Vue** | 企业级管控面组件(Table、Form、Chart) | -| 图表 | **ECharts** | 仪表板图表 | +| 框架 | **React 18 + TypeScript** | Hooks 函数组件、类型安全 | +| UI 库 | **Ant Design 5** | 企业级管控面组件(Table、Form、Chart) | +| 图表 | **ECharts** (`echarts-for-react`) | 仪表板图表 | | HTTP | **Axios** | API 调用 | -| 状态管理 | **Pinia** | 全局状态 | -| 路由 | **Vue Router** | SPA 路由 | -| 构建 | **Vite** | 快速构建 | +| 状态管理 | **Zustand** | 轻量全局状态 | +| 路由 | **React Router 6** | SPA 路由 | +| 构建 | **Vite** (`@vitejs/plugin-react`) | 快速构建 | --- @@ -943,10 +943,9 @@ console/ ├── vite.config.ts ├── tsconfig.json ├── src/ -│ ├── main.ts -│ ├── App.vue -│ ├── router/ -│ │ └── index.ts # 路由定义 +│ ├── main.tsx +│ ├── App.tsx +│ ├── routes.tsx # React Router 路由定义 │ ├── api/ # API 封装 │ │ ├── client.ts # Axios 实例(含 JWT 拦截器) │ │ ├── cluster.ts @@ -959,42 +958,45 @@ console/ │ │ ├── metrics.ts │ │ ├── alert.ts │ │ └── auth.ts -│ ├── stores/ # Pinia 状态 -│ │ ├── auth.ts -│ │ ├── websocket.ts # WS 连接 + 事件分发 -│ │ └── notification.ts -│ ├── views/ # 页面组件 -│ │ ├── Dashboard.vue +│ ├── stores/ # Zustand 状态 +│ │ ├── useAuthStore.ts +│ │ ├── useWebSocketStore.ts # WS 连接 + 事件分发 +│ │ └── useNotificationStore.ts +│ ├── pages/ # 页面组件 +│ │ ├── Dashboard.tsx │ │ ├── cluster/ -│ │ │ ├── Overview.vue -│ │ │ └── NodeDetail.vue +│ │ │ ├── Overview.tsx +│ │ │ └── NodeDetail.tsx │ │ ├── storage/ -│ │ │ ├── BucketList.vue -│ │ │ ├── BucketDetail.vue -│ │ │ └── ObjectQuery.vue +│ │ │ ├── BucketList.tsx +│ │ │ ├── BucketDetail.tsx +│ │ │ └── ObjectQuery.tsx │ │ ├── tape/ -│ │ │ ├── TapeList.vue -│ │ │ ├── TapeDetail.vue -│ │ │ ├── DriveList.vue -│ │ │ └── OfflineRequests.vue +│ │ │ ├── TapeList.tsx +│ │ │ ├── TapeDetail.tsx +│ │ │ ├── DriveList.tsx +│ │ │ └── OfflineRequests.tsx │ │ ├── task/ -│ │ │ ├── ArchiveTaskList.vue -│ │ │ └── RecallTaskList.vue +│ │ │ ├── ArchiveTaskList.tsx +│ │ │ └── RecallTaskList.tsx │ │ ├── cache/ -│ │ │ └── CacheOverview.vue +│ │ │ └── CacheOverview.tsx │ │ ├── monitor/ -│ │ │ ├── Metrics.vue -│ │ │ └── Alerts.vue +│ │ │ ├── Metrics.tsx +│ │ │ └── Alerts.tsx │ │ └── settings/ -│ │ ├── Config.vue -│ │ ├── UserManage.vue -│ │ └── AuditLog.vue +│ │ ├── Config.tsx +│ │ ├── UserManage.tsx +│ │ └── AuditLog.tsx │ ├── components/ # 通用组件 -│ │ ├── StatusTag.vue # 状态标签(颜色映射) -│ │ ├── CapacityBar.vue # 容量条 -│ │ ├── DriveCard.vue # 驱动卡片 -│ │ ├── EventTimeline.vue # 事件时间线 -│ │ └── ConfirmDialog.vue # 二次确认弹窗 +│ │ ├── StatusTag.tsx # 状态标签(颜色映射) +│ │ ├── CapacityBar.tsx # 容量条 +│ │ ├── DriveCard.tsx # 驱动卡片 +│ │ ├── EventTimeline.tsx # 事件时间线 +│ │ └── ConfirmDialog.tsx # 二次确认弹窗 +│ ├── hooks/ # 自定义 Hooks +│ │ ├── usePolling.ts # 轮询数据刷新 +│ │ └── useWebSocket.ts # WS 连接 Hook │ └── utils/ │ ├── format.ts # 字节/时间格式化 │ └── constants.ts # 枚举映射 @@ -1049,6 +1051,8 @@ admin: - [Axum](https://docs.rs/axum/latest/axum/) — HTTP 框架 - [utoipa](https://docs.rs/utoipa/latest/utoipa/) — OpenAPI 文档生成 - [jsonwebtoken](https://docs.rs/jsonwebtoken) — JWT 实现 -- [Vue 3](https://vuejs.org/) — 前端框架 -- [Ant Design Vue](https://antdv.com/) — UI 组件库 +- [React 18](https://react.dev/) — 前端框架 +- [Ant Design 5](https://ant.design/) — UI 组件库 +- [Zustand](https://zustand-demo.pmnd.rs/) — 状态管理 +- [React Router 6](https://reactrouter.com/) — 路由 - [ECharts](https://echarts.apache.org/) — 图表库 From e42aa2013725d28b8bd2d6e85a120409ba8eb7ac Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Sat, 28 Feb 2026 18:05:25 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix(doc):=20=E8=A1=A5=E5=85=85=20WaitingFor?= =?UTF-8?q?Media=20=E2=86=92=20Failed=20=E7=8A=B6=E6=80=81=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validate_restore_transition 缺少该转换,导致 OfflineRequest 超时后无法将 RecallTask 标记为 Failed,任务会永久卡住。 Made-with: Cursor --- docs/modules/07-consistency-performance.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules/07-consistency-performance.md b/docs/modules/07-consistency-performance.md index da01480..300744d 100644 --- a/docs/modules/07-consistency-performance.md +++ b/docs/modules/07-consistency-performance.md @@ -277,6 +277,7 @@ fn validate_restore_transition(current: &RestoreStatus, target: &RestoreStatus) | (RestoreStatus::Pending, RestoreStatus::WaitingForMedia) | (RestoreStatus::Pending, RestoreStatus::Failed) | (RestoreStatus::WaitingForMedia, RestoreStatus::Pending) + | (RestoreStatus::WaitingForMedia, RestoreStatus::Failed) | (RestoreStatus::InProgress, RestoreStatus::Completed) | (RestoreStatus::InProgress, RestoreStatus::Failed) | (RestoreStatus::Completed, RestoreStatus::Expired) From 707bbb5d21d9df24e11b6dd008ab8f7712a22961 Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Mon, 2 Mar 2026 22:17:40 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(doc):=20=E4=BF=AE=E5=A4=8D=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3=E5=A4=9A=E9=A1=B9=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E4=B8=8E=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补充 PutObject 数据暂存流程:Cache Worker 作为归档前的临时暂存区, 新增 StagingWriteApi/StagingReadApi 接口定义(04, 05, DESIGN.md) - 纠正 Admin Console 架构:Console 仅连 Metadata,运行时数据通过 Worker 心跳上报(09) - 移除 observability 中残留的"热对象"代码分支(08) - 统一归档扫描间隔 scan_interval_secs 为 60s(05) - 补充 DeleteObject 对已归档磁带数据的处理策略与 Tape Compaction(05) - 新增多 Scheduler Worker 扩展占位章节(05) - 完善 S3 Multipart Upload 协议适配设计(02) - 精确定义 ArchiveBundle 中 FileMark 的使用方式(05) - 修复 04-cache-layer 章节编号错误(05→6, 6→7 等) - 改进心跳 last_heartbeat Leader 切换处理:新增宽限期机制(03) Made-with: Cursor --- docs/DESIGN.md | 22 ++- docs/modules/02-protocol-adapter.md | 90 ++++++++++- docs/modules/03-metadata-cluster.md | 43 +++++ docs/modules/04-cache-layer.md | 183 ++++++++++++++++++---- docs/modules/05-scheduler-layer.md | 235 +++++++++++++++++++++++----- docs/modules/08-observability.md | 48 +++--- docs/modules/09-admin-console.md | 27 +++- 7 files changed, 550 insertions(+), 98 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index ad9d533..2002973 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -643,14 +643,24 @@ src/tape/ 9. Client: GET 从 SPDK 缓存返回数据 ``` -### 7.2 Archive 完整流程 +### 7.2 PutObject 完整流程 ``` -1. PutObject → 对象直接标记 ColdPending(写入即归档) -2. Archive Scheduler: 扫描 ColdPending,按策略聚合为 ArchiveBundle -3. Tape: 顺序写入磁带 -4. Metadata (Raft): 写入 ArchiveBundle, 更新 ObjectMetadata (Cold, archive_id, tape_id) -5. 清理缓存层临时数据 +1. Gateway: 接收 S3 PutObject 请求 +2. Scheduler Worker: 将数据暂存到 Cache Worker (gRPC: PutStaging) +3. Scheduler Worker: 写入 Metadata (Raft: PutObject, storage_class=ColdPending, staging_id) +4. Gateway: 返回 200 OK +``` + +### 7.3 Archive 完整流程 + +``` +1. Archive Scheduler: 扫描 ColdPending 对象,获取 staging_id +2. Archive Scheduler: 从 Cache Worker 暂存区读取数据 (gRPC: GetStaging) +3. Archive Scheduler: 按策略聚合为 ArchiveBundle +4. Tape: 顺序写入磁带 +5. Metadata (Raft): 写入 ArchiveBundle, 更新 ObjectMetadata (Cold, archive_id, tape_id, 清空 staging_id) +6. Cache Worker: 删除暂存数据 (gRPC: DeleteStaging) ``` --- diff --git a/docs/modules/02-protocol-adapter.md b/docs/modules/02-protocol-adapter.md index 6a24ad4..f4fef47 100644 --- a/docs/modules/02-protocol-adapter.md +++ b/docs/modules/02-protocol-adapter.md @@ -125,7 +125,91 @@ ColdStore 作为纯冷归档系统,PutObject 写入的对象一律标记为 `C --- -## 8. 模块结构 +## 8. S3 Multipart Upload 支持 + +### 8.1 设计目标 + +支持 S3 Multipart Upload API,允许大对象分块上传。对于冷归档系统, +Multipart Upload 主要用于外部热存储系统向 ColdStore 迁移大文件(GB/TB 级别)。 + +### 8.2 API 兼容 + +| API | 说明 | ColdStore 实现 | +|-----|------|----------------| +| `CreateMultipartUpload` | 初始化分块上传,返回 upload_id | Scheduler 创建 MultipartUpload 记录到 Metadata | +| `UploadPart` | 上传单个分块 | Scheduler 将分块暂存到 Cache Worker(StagingWriteApi) | +| `CompleteMultipartUpload` | 完成上传,合并分块 | Scheduler 合并分块元数据,标记对象为 ColdPending | +| `AbortMultipartUpload` | 取消上传,清理分块 | Scheduler 删除所有暂存分块 | +| `ListParts` | 列出已上传的分块 | Scheduler 从 Metadata 查询 | +| `ListMultipartUploads` | 列出进行中的分块上传 | Scheduler 从 Metadata 查询 | + +### 8.3 分块暂存策略 + +每个 Part 独立暂存在 Cache Worker 中,通过 `staging_id` 标识。 +`CompleteMultipartUpload` 时不做物理合并(避免大量 I/O), +而是将所有 Part 的 `staging_id` 列表记录在 ObjectMetadata 中, +归档调度器在写入磁带时顺序读取各 Part 并连续写入。 + +``` +分块上传流程: + +1. CreateMultipartUpload → upload_id +2. UploadPart(upload_id, part_number, data) × N + └─ 每个 Part 独立暂存到 Cache Worker +3. CompleteMultipartUpload(upload_id, parts[]) + └─ 验证所有 Part 已上传 + └─ 创建 ObjectMetadata (ColdPending, staging_ids=[...]) + └─ 对象整体 checksum 由 S3 ETag 规则计算 + +归档时: + 调度器按 part_number 顺序从 Cache Worker 读取各 Part + 连续写入磁带,磁带上视为单个对象 +``` + +### 8.4 元数据扩展 + +```rust +pub struct MultipartUploadInfo { + pub upload_id: String, + pub bucket: String, + pub key: String, + pub parts: Vec, + pub created_at: DateTime, + pub status: MultipartUploadStatus, +} + +pub struct PartInfo { + pub part_number: u32, + pub staging_id: String, + pub size: u64, + pub etag: String, + pub uploaded_at: DateTime, +} + +pub enum MultipartUploadStatus { + InProgress, + Completed, + Aborted, +} +``` + +### 8.5 约束 + +| 约束 | 值 | 说明 | +|------|------|------| +| 最大分块数 | 10,000 | 与 S3 一致 | +| 最小分块大小 | 5 MB | 除最后一块外,与 S3 一致 | +| 最大分块大小 | 5 GB | 与 S3 一致 | +| 最大对象大小 | 5 TB | 10,000 × 5GB 理论上限 | +| 上传超时 | 7 天 | 超时后自动清理暂存分块 | + +### 8.6 清理机制 + +后台定时任务扫描超时未完成的 Multipart Upload,自动执行 Abort 并清理暂存分块。 + +--- + +## 9. 模块结构 ``` src/ @@ -140,7 +224,7 @@ src/ --- -## 9. 依赖关系 +## 10. 依赖关系 | 依赖 | 用途 | |------|------| @@ -151,7 +235,7 @@ src/ --- -## 10. 参考资料 +## 11. 参考资料 - [AWS S3 RestoreObject API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_RestoreObject.html) - [S3 Glacier Retrieval Options](https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects-retrieval-options.html) diff --git a/docs/modules/03-metadata-cluster.md b/docs/modules/03-metadata-cluster.md index 5bb35e8..088783a 100644 --- a/docs/modules/03-metadata-cluster.md +++ b/docs/modules/03-metadata-cluster.md @@ -609,6 +609,49 @@ pub struct LibraryEndpoint { - Metadata Leader 每 15s 扫描,连续 3 次未收到心跳(>15s)的 Worker 标记为 `Offline` - Offline 的 Tape Worker 上的驱动不再被调度器分配任务 +**Leader 切换时的 last_heartbeat 处理**: + +`last_heartbeat` 仅在 Leader 本地内存维护,不写入 Raft 日志。Leader 切换后,新 Leader +的 `last_heartbeat` 表为空,需要特殊处理以避免误判所有 Worker 为 Offline: + +1. **宽限期(Grace Period)**:新 Leader 就任后,设置一个等于 2 倍心跳间隔(即 10s)的宽限期。 + 宽限期内不执行失联检测 +2. **首次心跳恢复**:宽限期内收到的首次心跳即恢复该 Worker 的 `last_heartbeat` 记录 +3. **宽限期后处理**:宽限期结束后,仍未收到心跳的 Worker 标记为 `Offline` + (这些 Worker 确实已失联,不是 Leader 切换造成的误判) + +```rust +pub struct HeartbeatManager { + last_heartbeat: HashMap<(WorkerType, u64), Instant>, + leader_since: Option, + grace_period: Duration, // 默认 10s(2 × heartbeat_interval) +} + +impl HeartbeatManager { + fn on_become_leader(&mut self) { + self.last_heartbeat.clear(); + self.leader_since = Some(Instant::now()); + } + + fn is_in_grace_period(&self) -> bool { + self.leader_since + .map(|since| since.elapsed() < self.grace_period) + .unwrap_or(false) + } + + fn check_offline(&self) -> Vec<(WorkerType, u64)> { + if self.is_in_grace_period() { + return vec![]; + } + // 正常失联检测逻辑... + } +} +``` + +**持久化策略**:`last_heartbeat` 本身不需要持久化到 Raft(高频写入会占满日志)。 +Leader 切换后通过宽限期机制即可恢复状态,代价仅为一个短暂的窗口期(10s)内 +不进行失联检测,对系统可用性影响极小。 + **优雅下线**: 1. 管理员通过 Console 发起 Drain(排空) diff --git a/docs/modules/04-cache-layer.md b/docs/modules/04-cache-layer.md index a7298bf..7d706cd 100644 --- a/docs/modules/04-cache-layer.md +++ b/docs/modules/04-cache-layer.md @@ -5,7 +5,10 @@ ## 1. 模块概述 -数据缓存层承载从磁带取回的解冻数据,供 GET 请求快速响应,避免重复触发磁带读取。 +数据缓存层承担两大核心职责: + +1. **PutObject 数据暂存**:接收 PutObject 写入的原始数据,作为归档到磁带前的临时暂存区 +2. **解冻数据缓存**:承载从磁带取回的解冻数据,供 GET 请求快速响应,避免重复触发磁带读取 > **部署模型**:缓存层运行在 **Cache Worker** 节点上,与 **Scheduler Worker** 同机部署。 > Scheduler 通过 gRPC 与 Cache Worker 通信(虽然同机,仍使用 gRPC 保持架构一致性)。 @@ -13,7 +16,8 @@ ### 1.1 职责 -- 存储解冻后的对象数据 +- **PutObject 暂存**:接收并暂存 PutObject 的数据,等待调度器归档到磁带 +- **解冻数据缓存**:存储从磁带取回的解冻对象数据 - 响应 GET 请求(冷对象已解冻时) - 缓存淘汰:LRU、LFU、TTL、容量限制 - 与 SPDK 集成,实现高性能用户态 I/O @@ -21,9 +25,14 @@ ### 1.2 在架构中的位置 ``` -取回调度器 ──► 磁带读取 ──► 数据缓存层 ──► 协议层/接入层 ──► GET 响应 - │ - └─ async-spdk (SPDK Blobstore on bdev) +PutObject 写入流程: + 接入层 ──► Scheduler ──► Cache Worker (暂存) ──► 等待归档调度 + │ +归档流程: ▼ + 归档调度器 ──► Cache Worker (读取暂存数据) ──► 磁带写入 ──► 删除暂存 + +取回流程: + 取回调度器 ──► 磁带读取 ──► Cache Worker (缓存) ──► GET 响应 注意:缓存层不直接持有元数据 client。 元数据更新由调度层统一负责(方案 B)。 @@ -371,7 +380,123 @@ pub struct CacheStats { --- -## 5. 缓存策略 +## 5. PutObject 数据暂存 + +ColdStore 是纯冷归档系统,PutObject 写入的数据不会直接发送到磁带,而是先暂存在 Cache Worker 中, +等待调度器扫描 `ColdPending` 对象后聚合写入磁带。暂存数据与解冻缓存使用相同的存储后端,但逻辑上独立管理。 + +### 5.1 暂存流程 + +``` +PutObject 完整数据流: + +1. Gateway 接收 S3 PutObject 请求 +2. Gateway → Scheduler Worker (gRPC: PutObject) +3. Scheduler → Cache Worker (gRPC: PutStaging) + └─ Cache Worker 将数据写入暂存区,返回 staging_id +4. Scheduler → Metadata (Raft: PutObject) + └─ 写入 ObjectMetadata (storage_class=ColdPending, staging_id) +5. Scheduler → Gateway: 返回 200 OK + +归档时数据流: + +1. 归档调度器扫描 ColdPending 对象 +2. Scheduler → Cache Worker (gRPC: GetStaging) + └─ 按 staging_id 读取暂存数据 +3. Scheduler → Tape Worker (gRPC: Write) + └─ 数据写入磁带 +4. Scheduler → Metadata (Raft: 更新 ObjectMetadata → Cold) +5. Scheduler → Cache Worker (gRPC: DeleteStaging) + └─ 归档完成后删除暂存数据 +``` + +### 5.2 暂存接口定义 + +```rust +/// 缓存层提供给调度层的暂存写入接口 +#[async_trait] +pub trait StagingWriteApi: Send + Sync { + /// 写入暂存数据,返回 staging_id + async fn put_staging( + &self, + bucket: &str, + key: &str, + version_id: Option<&str>, + data: &[u8], + checksum: &str, + ) -> Result; + + /// 删除暂存数据(归档完成后调用) + async fn delete_staging(&self, staging_id: &str) -> Result<()>; + + /// 批量删除暂存数据 + async fn delete_staging_batch(&self, staging_ids: &[String]) -> Result<()>; +} + +/// 缓存层提供给调度层的暂存读取接口 +#[async_trait] +pub trait StagingReadApi: Send + Sync { + /// 按 staging_id 读取暂存数据 + async fn get_staging(&self, staging_id: &str) -> Result>; +} + +pub struct StagedObject { + pub staging_id: String, + pub bucket: String, + pub key: String, + pub version_id: Option, + pub data: Vec, + pub size: u64, + pub checksum: String, + pub staged_at: DateTime, +} +``` + +### 5.3 暂存数据布局 + +暂存数据与解冻缓存共用存储后端(SPDK Blobstore / HDD),但通过 xattr 中的 `data_type` 字段区分: + +| xattr key | 值 | 说明 | +|-----------|------|------| +| `data_type` | `"staging"` | 暂存数据(PutObject 写入,待归档) | +| `data_type` | `"restored"` | 解冻数据(磁带取回,供 GET 读取) | +| `staging_id` | UUID 字符串 | 暂存数据唯一标识(仅 staging 类型) | + +### 5.4 暂存数据生命周期 + +| 阶段 | 事件 | 说明 | +|------|------|------| +| 写入 | PutObject 请求到达 | Scheduler 调用 `put_staging`,数据写入 Cache Worker | +| 等待 | 对象处于 ColdPending | 暂存数据留存,等待归档调度器扫描 | +| 读取 | 归档调度器聚合 Bundle | Scheduler 调用 `get_staging` 读取数据,写入磁带 | +| 删除 | 归档完成 | Scheduler 调用 `delete_staging` 清理暂存数据 | +| 超时清理 | 对象被删除但暂存残留 | 后台定时扫描清理孤儿暂存数据 | + +### 5.5 容量管理 + +暂存数据与解冻缓存共享容量,通过以下策略管理: + +- **容量预留**:可配置 `staging_reserved_percent`(如 30%),保证暂存空间 +- **背压**:当暂存区使用率超过阈值时,PutObject 返回 `503 SlowDown`,由外部系统降速 +- **优先淘汰**:容量不足时,优先淘汰已过期的解冻缓存数据,暂存数据不主动淘汰(除非对象已删除) + +### 5.6 ObjectMetadata 中的 staging_id + +`ObjectMetadata` 增加 `staging_id` 字段,记录对象在 Cache Worker 中的暂存标识: + +```rust +pub struct ObjectMetadata { + // ... 已有字段 ... + pub staging_id: Option, // Cache Worker 中暂存数据的 ID + pub staging_worker_id: Option, // 暂存数据所在的 Cache Worker +} +``` + +归档完成后,调度层将 `staging_id` 和 `staging_worker_id` 清空。 + +--- + +## 6. 缓存策略 | 策略 | 说明 | |------|------| @@ -395,9 +520,9 @@ pub enum EvictionPolicy { --- -## 6. 与调度层的对接 +## 7. 与调度层的对接 -### 5.1 接口定义 +### 7.1 接口定义 ```rust /// 缓存层提供给调度层的接口 @@ -433,7 +558,7 @@ pub struct RestoredItem { > `delete` 方法用于接入层 DeleteObject 时清理缓存(由接入层调用)。 -### 5.2 调用时机与流程 +### 7.2 调用时机与流程 缓存层自身**不**触发元数据更新。写入完成后返回 `Result<()>` 给调度层,由调度层统一负责后续元数据变更。 @@ -452,7 +577,7 @@ pub struct RestoredItem { > **关键原则**:缓存层是纯数据存储,不持有 MetadataClient。元数据写入入口收敛在调度层。 -### 5.3 数据流与校验 +### 7.3 数据流与校验 | 步骤 | 负责方 | 说明 | |------|--------|------| @@ -461,7 +586,7 @@ pub struct RestoredItem { | 更新元数据 | **调度器** | restore_status=Completed, restore_expire_at | | GET 时校验 | 协议层/缓存层 | 可选读后校验 checksum | -### 5.4 失败与重试 +### 7.4 失败与重试 - 缓存写入失败:调度器重试 2 次,仍失败则 RecallTask 标记 Failed,**不更新元数据** - 缓存层空间不足:先触发淘汰,再写入;若仍不足则返回错误,调度器不更新元数据 @@ -469,9 +594,9 @@ pub struct RestoredItem { --- -## 7. 与元数据层的关系(方案 B:缓存层不直接依赖元数据) +## 8. 与元数据层的关系(方案 B:缓存层不直接依赖元数据) -### 6.1 设计原则 +### 8.1 设计原则 **缓存层不持有 MetadataClient,不直接读写元数据。** @@ -481,7 +606,7 @@ pub struct RestoredItem { 缓存层是纯粹的数据存储层,只暴露 `CacheReadApi` 和 `CacheWriteApi`。 -### 6.2 缓存层对元数据的间接关系 +### 8.2 缓存层对元数据的间接关系 | 场景 | 依赖 | 说明 | |------|------|------| @@ -490,7 +615,7 @@ pub struct RestoredItem { | 写入时 | 无 | 调度层传入 expire_at,缓存层直接写入 xattr | | 启动时 | 无 | 从 Blobstore xattrs 重建内存索引,不依赖元数据 | -### 6.3 一致性保证(由调度层负责) +### 8.3 一致性保证(由调度层负责) | 顺序 | 操作 | 负责方 | |------|------|--------| @@ -503,9 +628,9 @@ pub struct RestoredItem { --- -## 8. 与协议层/接入层的对接 +## 9. 与协议层/接入层的对接 -### 7.1 接口定义 +### 9.1 接口定义 ```rust /// 缓存层提供给协议层/接入层的接口 @@ -529,7 +654,7 @@ pub trait CacheReadApi: Send + Sync { > `CachedObject` 定义见 §4.6,包含 data、size、expire_at、content_type、etag、checksum。 -### 7.2 协议层调用流程 +### 9.2 协议层调用流程 ``` GET /{bucket}/{key} @@ -550,7 +675,7 @@ GET /{bucket}/{key} if None: 返回 404 或 503(缓存丢失,需重新 Restore) ``` -### 7.3 缓存未命中处理 +### 9.3 缓存未命中处理 | 情况 | 协议层行为 | |------|------------| @@ -558,7 +683,7 @@ GET /{bucket}/{key} | 缓存已淘汰(TTL 内) | 同上,元数据 restore_expire_at 未过期但缓存已删 | | 缓存损坏 | 校验失败时返回 500,建议重新 Restore | -### 7.4 响应头 +### 9.4 响应头 GET 命中缓存时,协议层需附加: @@ -568,7 +693,7 @@ x-amz-restore: ongoing-request="false", expiry-date="Fri, 28 Feb 2025 12:00:00 G --- -## 9. 对接汇总图(方案 B) +## 10. 对接汇总图(方案 B) ``` ┌─────────────────────────────────────────────────────────────────────────┐ @@ -605,7 +730,7 @@ x-amz-restore: ongoing-request="false", expiry-date="Fri, 28 Feb 2025 12:00:00 G --- -## 10. 集成方式 +## 11. 集成方式 - **方案 A**:缓存服务独立二进制,通过 gRPC/HTTP 与主服务通信 - **方案 B**:主进程在 SPDK block_on 内启动 Axum(需调整启动顺序) @@ -613,7 +738,7 @@ x-amz-restore: ongoing-request="false", expiry-date="Fri, 28 Feb 2025 12:00:00 G --- -## 11. 模块结构 +## 12. 模块结构 ``` src/cache/ @@ -630,7 +755,7 @@ src/cache/ --- -## 12. 配置项 +## 13. 配置项 ```yaml cache: @@ -648,20 +773,22 @@ cache: --- -## 13. 依赖关系 +## 14. 依赖关系 | 对接方 | 方向 | 接口 | 说明 | |--------|------|------|------| -| 调度层 | 调度 → 缓存 | `CacheWriteApi`: `put_restored` / `put_restored_batch` | 取回后写数据 | +| 调度层 | 调度 → 缓存 | `StagingWriteApi`: `put_staging` / `delete_staging` | PutObject 数据暂存与归档后清理 | +| 调度层 | 调度 → 缓存 | `StagingReadApi`: `get_staging` | 归档时读取暂存数据 | +| 调度层 | 调度 → 缓存 | `CacheWriteApi`: `put_restored` / `put_restored_batch` | 取回后写解冻数据 | | 接入层 | 接入 → 缓存 | `CacheWriteApi`: `delete` | DeleteObject 时清理缓存 | -| 协议层/接入层 | 协议 → 缓存 | `CacheReadApi`: `get` / `contains` | GET 读数据 | +| 协议层/接入层 | 协议 → 缓存 | `CacheReadApi`: `get` / `contains` | GET 读解冻数据 | | 元数据层 | **无依赖** | — | 缓存层不持有 MetadataClient | > **方案 B 原则**:缓存层是纯数据存储,不感知元数据。元数据写入由调度层统一协调。 --- -## 14. 参考资料 +## 15. 参考资料 - [async-spdk](https://github.com/madsys-dev/async-spdk)(含 hello_blob 示例) - [SPDK Blobstore Programmer's Guide](https://spdk.io/doc/blob.html) diff --git a/docs/modules/05-scheduler-layer.md b/docs/modules/05-scheduler-layer.md index d063911..f36d702 100644 --- a/docs/modules/05-scheduler-layer.md +++ b/docs/modules/05-scheduler-layer.md @@ -91,27 +91,32 @@ ### 2.3 顺序写入流水线 ``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ 对象读取 │───▶│ 块对齐 │───▶│ 双缓冲 │───▶│ 磁带写入 │ -│ (热存储) │ │ 填充 │ │ 流水线 │ │ (顺序) │ -└──────────┘ └──────────┘ └──────────┘ └──────────┘ +┌──────────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ 对象读取 │───▶│ 块对齐 │───▶│ 双缓冲 │───▶│ 磁带写入 │ +│ (Cache Worker 暂存)│ │ 填充 │ │ 流水线 │ │ (顺序) │ +└──────────────────┘ └──────────┘ └──────────┘ └──────────┘ ``` +- **数据来源**:归档时对象数据从 Cache Worker 暂存区读取(`StagingReadApi.get_staging`),不从外部热存储读取 - **块对齐**:不足 block_size 的对象尾部填充(padding),或使用可变块(需驱动支持) - **双缓冲**:预读下一批数据,写入当前批,保持驱动持续流式写入 -- **背压**:若热存储读取慢于磁带写入,可降低并发或增大缓冲 +- **背压**:若 Cache Worker 读取慢于磁带写入,可降低并发或增大缓冲 ### 2.4 归档流程(细化) -1. **调度层**:从元数据扫描 ColdPending,按策略聚合为 ArchiveBundle -2. **调度层**:选定目标磁带(当前写入头或新磁带) -3. **调度层 → 磁带层**:申请磁带驱动,获取独占 -4. **磁带层**:顺序写入:FileMark → [Obj1 Header + Data] → FileMark → [Obj2...] -5. **调度层**:写入完成后计算并存储 Bundle 校验和(可选) -6. **调度层 → 元数据层**:更新 ObjectMetadata(Cold, archive_id, tape_id, tape_block_offset)、ArchiveBundle、TapeInfo -7. **调度层 → 磁带层**:释放驱动;释放在线存储空间 +1. **调度层**:从元数据扫描 ColdPending 对象,获取 `staging_id` 和 `staging_worker_id` +2. **调度层**:按策略聚合为 ArchiveBundle +3. **调度层**:选定目标磁带(当前写入头或新磁带) +4. **调度层 → Cache Worker**:按 `staging_id` 从 Cache Worker 暂存区读取对象数据(`StagingReadApi.get_staging`) +5. **调度层 → 磁带层**:申请磁带驱动,获取独占 +6. **磁带层**:顺序写入:FileMark → [Obj1 Header + Data] → [Obj2...] → FileMark +7. **调度层**:写入完成后计算并存储 Bundle 校验和(可选) +8. **调度层 → 元数据层**:更新 ObjectMetadata(Cold, archive_id, tape_id, tape_block_offset,清空 staging_id)、ArchiveBundle、TapeInfo +9. **调度层 → Cache Worker**:删除暂存数据(`StagingWriteApi.delete_staging_batch`) +10. **调度层 → 磁带层**:释放驱动 -> 步骤 6 由调度层统一负责,磁带层不直接写元数据。 +> 步骤 4 的数据来源是 Cache Worker 暂存区,而非外部热存储。 +> 步骤 8-9 由调度层统一负责,磁带层和缓存层不直接写元数据。 ### 2.5 归档关键参数 @@ -242,7 +247,7 @@ Level 3: 同对象的多个 RecallTask(重复请求) ### 4.3 归档调度周期 -- **扫描周期**:`scan_interval_secs`(如 3600s) +- **扫描周期**:`scan_interval_secs`(默认 60s) - **触发条件**:定时 + 可选事件触发(ColdPending 积累达阈值) - **并发**:多驱动时可并行执行多个 ArchiveTask,每个 Task 独占一驱动 @@ -328,9 +333,88 @@ Level 3: 同对象的多个 RecallTask(重复请求) --- -## 8. 核心数据结构 +## 8. DeleteObject 对归档数据的处理 -### 8.1 ArchiveScheduler +### 8.1 设计原则 + +磁带是追加写入介质,无法随机删除已写入的数据。DeleteObject 操作需分阶段处理: + +1. **即时阶段**:立即删除元数据和缓存中的数据 +2. **延迟回收阶段**:磁带上的物理空间通过"标记删除 + 磁带整理"回收 + +### 8.2 DeleteObject 流程 + +``` +DeleteObject 请求处理: + +1. Scheduler 接收 Gateway 转发的 DELETE 请求 +2. Scheduler → Metadata: 查询 ObjectMetadata +3. 根据对象状态分支处理: + + ── ColdPending(尚未归档): + 4a. Scheduler → Cache Worker: DeleteStaging(staging_id) // 删除暂存数据 + 5a. Scheduler → Metadata: DeleteObject // 删除元数据 + 6a. 完成。暂存数据和元数据均已清理 + + ── Cold(已归档到磁带): + 4b. Scheduler → Cache Worker: Delete(bucket, key) // 清理解冻缓存(如有) + 5b. Scheduler → Metadata: MarkObjectDeleted(bucket, key) // 标记元数据为 Deleted + └─ 不立即删除 ObjectMetadata,保留 archive_id/tape_id 供后续磁带整理参考 + 6b. Scheduler → Metadata: IncrementBundleDeleteCount(archive_id) + └─ 记录 ArchiveBundle 中已删除对象数 + 7b. 完成。磁带上的物理数据在后续整理时回收 +``` + +### 8.3 磁带空间回收策略 + +磁带空间不支持就地回收,需通过**磁带整理(Tape Compaction)**实现。 +当一盘磁带上的有效数据比例低于阈值时,触发整理: + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| `compaction_threshold` | 0.3 | 有效数据低于 30% 时触发整理 | +| `compaction_scan_interval_secs` | 86400 | 每天扫描一次 | + +**整理流程**: + +``` +1. 扫描 TapeInfo,计算每盘磁带的有效数据比例: + 有效比例 = (总写入 - 已删除对象大小) / 总容量 +2. 对有效比例 < compaction_threshold 的磁带: + a. 读取磁带上所有未删除对象 + b. 将有效对象重新聚合为新 ArchiveBundle + c. 写入新磁带 + d. 更新元数据(新 archive_id、tape_id) + e. 旧磁带标记为可复用或退役 +``` + +### 8.4 ObjectMetadata 状态扩展 + +为支持延迟删除,`StorageClass` 不需要新增状态。DeleteObject 直接从 `cf_objects` 中删除记录, +但在 `cf_deleted_objects` 中保留必要信息供磁带整理参考: + +```rust +pub struct DeletedObjectRecord { + pub bucket: String, + pub key: String, + pub archive_id: Uuid, + pub tape_id: String, + pub size: u64, + pub deleted_at: DateTime, +} +``` + +对应新增 Column Family: + +| CF 名称 | Key 格式 | Value 类型 | 说明 | +|---------|----------|-----------|------| +| `cf_deleted_objects` | `del:{archive_id}:{bucket}:{key}` | DeletedObjectRecord | 已删除对象记录,供磁带整理参考 | + +--- + +## 9. 核心数据结构 + +### 9.1 ArchiveScheduler ```rust pub struct ArchiveScheduler { @@ -348,7 +432,7 @@ pub struct ArchiveScheduler { | `config` | ArchiveConfig | 归档配置 | 聚合参数、块大小、缓冲等 | | `running` | AtomicBool | 运行状态标记 | 优雅停止扫描循环 | -### 8.2 RecallScheduler +### 9.2 RecallScheduler ```rust pub struct RecallScheduler { @@ -368,7 +452,7 @@ pub struct RecallScheduler { | `queue` | RecallQueue | 取回优先级队列 | 三级优先级 + 合并窗口 | | `config` | RecallConfig | 取回配置 | 队列大小、超时、合并窗口等 | -### 8.3 ArchiveBundle(归档包) +### 9.3 ArchiveBundle(归档包) 一批对象在磁带上的连续写入单元,是归档调度的基本粒度。 @@ -402,7 +486,7 @@ pub struct ArchiveBundle { | `created_at` | DateTime\ | 创建时间 | 聚合窗口计算 | | `completed_at` | Option\\> | 完成时间 | 审计与可观测性 | -### 8.4 BundleEntry(Bundle 内单个对象条目) +### 9.4 BundleEntry(Bundle 内单个对象条目) 描述一个对象在 Bundle 内的物理位置,用于取回时精确定位。 @@ -428,7 +512,7 @@ pub struct BundleEntry { | `tape_block_offset` | u64 | 对象在磁带上的块偏移(相对 FileMark) | `MTFSR(tape_block_offset)` 精确定位 | | `checksum` | String | SHA256 hex | 取回时校验数据完整性 | -### 8.5 ObjectHeader(磁带上对象头) +### 9.5 ObjectHeader(磁带上对象头) 写入磁带时,每个对象前写入 Header,采用**定长前缀 + 变长 bucket/key** 的混合格式。解析时先读定长部分获取 `bucket_len`/`key_len`,再按长度读取变长字符串。 @@ -462,7 +546,7 @@ pub struct ObjectHeader { | `flags` | u8 | 标志位(bit 0: 压缩, bit 1: 加密) | 预留,当前全 0 | | `reserved` | [u8; 16] | 保留字段 | 未来扩展,写入时全 0 | -### 8.6 ArchiveTask(归档任务) +### 9.6 ArchiveTask(归档任务) ```rust pub struct ArchiveTask { @@ -498,7 +582,7 @@ pub struct ArchiveTask { | `completed_at` | Option\\> | 完成时间 | 计算耗时 | | `error` | Option\ | 错误信息 | Failed 时记录原因 | -### 8.7 RecallTask(取回任务) +### 9.7 RecallTask(取回任务) ```rust pub struct RecallTask { @@ -548,7 +632,7 @@ pub struct RecallTask { | `completed_at` | Option\\> | 完成时间 | 计算耗时 | | `error` | Option\ | 错误信息 | 失败原因 | -### 8.8 RestoreTier(取回优先级) +### 9.8 RestoreTier(取回优先级) ```rust pub enum RestoreTier { @@ -564,7 +648,7 @@ pub enum RestoreTier { | `Standard` | 标准 | 3–5 小时 | 中 | | `Bulk` | 批量 | 5–12 小时 | 最低 | -### 8.9 TapeReadJob(磁带读取作业) +### 9.9 TapeReadJob(磁带读取作业) 取回合并后的执行单元,一个 TapeReadJob = 一次换带 + 一次顺序读取。 @@ -590,7 +674,7 @@ pub struct TapeReadJob { | `total_bytes` | u64 | 总字节数 | 监控 | | `priority` | RestoreTier | 作业优先级(取组内最高) | 驱动分配排序 | -### 8.10 ReadSegment(读取段) +### 9.10 ReadSegment(读取段) ```rust pub struct ReadSegment { @@ -606,7 +690,7 @@ pub struct ReadSegment { | `filemark` | u32 | 该 Bundle 的起始 FileMark(来自 `ArchiveBundle.filemark_start`) | `MTFSF(filemark)` 定位 | | `objects` | Vec\ | 段内对象列表,按 tape_block_offset 升序 | 顺序读取 | -### 8.11 ReadObject(待读取对象) +### 9.11 ReadObject(待读取对象) ```rust pub struct ReadObject { @@ -630,7 +714,7 @@ pub struct ReadObject { | `checksum` | String | SHA256 hex | 读取后校验 | | `expire_at` | DateTime\ | 解冻过期时间 | 写入缓存 xattr | -### 8.12 TapeWriteJob(磁带写入作业) +### 9.12 TapeWriteJob(磁带写入作业) ```rust pub struct TapeWriteJob { @@ -652,7 +736,7 @@ pub struct TapeWriteJob { | `bytes_written` | u64 | 已写字节 | 进度追踪 | | `objects_written` | u32 | 已写对象数 | 进度追踪 | -### 8.13 RecallQueue(取回优先级队列) +### 9.13 RecallQueue(取回优先级队列) ```rust pub struct RecallQueue { @@ -677,7 +761,7 @@ pub struct RecallQueue { - **出队/合并**:合并时从 `pending_by_tape` 按 tape 分组取出任务,结合优先级队列做排序 - **完成/取消**:任务完成或取消后,从 `pending_by_tape` 中移除对应 task_id -### 8.14 枚举汇总 +### 9.14 枚举汇总 ```rust pub enum ArchiveBundleStatus { @@ -706,7 +790,7 @@ pub enum RestoreStatus { --- -## 9. 模块结构 +## 10. 模块结构 ``` src/scheduler/ @@ -728,16 +812,16 @@ src/scheduler/ --- -## 10. 配置项 +## 11. 配置项 ```yaml scheduler: archive: - scan_interval_secs: 3600 + scan_interval_secs: 60 batch_size: 1000 min_archive_size_mb: 100 max_archive_size_mb: 10240 - aggregation_window_secs: 3600 + aggregation_window_secs: 300 block_size: 262144 write_buffer_mb: 64 target_throughput_mbps: 300 @@ -757,7 +841,7 @@ scheduler: --- -## 11. 依赖关系 +## 12. 依赖关系 **调度层主动依赖**: @@ -780,10 +864,89 @@ scheduler: --- -## 12. 实施要点 +## 13. 实施要点 - 归档与取回可独立扩缩容 - 每个磁带驱动对应一条顺序 I/O 流水线,避免多任务共享同一驱动导致 seek - 增加驱动可线性提升并发归档与取回能力 - 元数据需记录 `tape_block_offset`,供取回时按物理顺序排序 -- **调度层同时注入 MetadataClient + CacheWriteApi + TapeManager**,是系统中唯一的编排者 +- **调度层同时注入 MetadataClient + CacheWriteApi + StagingReadApi + TapeManager**,是系统中唯一的编排者 + +--- + +## 14. 多 Scheduler Worker 扩展(待设计) + +> **状态:占位章节,具体方案后续确定。** + +当前阶段 Scheduler Worker 以单实例运行,后续需支持多 Scheduler Worker 横向扩展。 +以下列出需要在后续设计中解决的核心问题: + +### 14.1 待解决问题 + +| 问题 | 描述 | 可能方向 | +|------|------|----------| +| **ColdPending 扫描分区** | 多个 Scheduler 同时扫描 ColdPending 会导致重复归档 | 按 bucket/key hash 分区、Metadata 侧支持 claim 机制 | +| **RecallTask 分配** | 多个 Scheduler 如何分配取回任务 | 基于 tape_id 亲和性分配、通过 Metadata 做分布式锁 | +| **驱动竞争** | 多个 Scheduler 对同一 Tape Worker 的驱动竞争 | Tape Worker 侧排队、Scheduler 间协商 | +| **主备 vs 对等** | 是采用主备模式还是对等模式 | 主备简单但切换慢、对等需要更复杂的协调 | +| **故障转移** | Scheduler 故障时任务如何重新分配 | InProgress 任务超时后由其他 Scheduler 接管 | +| **Cache Worker 亲和** | 每个 Scheduler 与同机 Cache Worker 绑定 | 通过 `paired_cache_worker_id` 保持亲和 | + +### 14.2 已有的接口预留 + +当前已在 `SchedulerWorkerInfo` 中预留了多 Scheduler 支持字段: + +- `is_active: bool` — 主备模式下标识活跃 Scheduler +- `paired_cache_worker_id: u64` — 同机 Cache Worker 绑定 + +### 14.3 约束条件 + +- 多 Scheduler 方案不得改变现有的 gRPC 接口协议 +- Gateway 可配置多个 Scheduler 地址,支持负载均衡 +- 磁带层和缓存层无需感知 Scheduler 数量变化 + +--- + +## 15. FileMark 使用精确定义 + +### 15.1 磁带上 ArchiveBundle 的物理结构 + +每个 ArchiveBundle 在磁带上的布局如下。**FileMark 仅在 Bundle 边界处写入**, +Bundle 内的对象之间不写 FileMark,通过块偏移定位: + +``` +磁带物理布局: + +[BOT] ... [FM_prev] [Bundle N 起始] [Obj1 Header][Obj1 Data][Obj2 Header][Obj2 Data]...[ObjN Data] [FM_end] ... + +说明: + - FM_prev: 前一个 Bundle 的结束 FileMark(同时也是当前 Bundle 的起始定位点) + - Bundle 数据: 对象连续写入,无 FileMark 分隔 + - FM_end: 当前 Bundle 写入完成后追加的 FileMark + +对于第一个 Bundle: + [BOT] [Obj1 Header][Obj1 Data]...[ObjN Data] [FM_0] + +后续 Bundle: + [FM_0] [Obj1 Header][Obj1 Data]...[ObjN Data] [FM_1] +``` + +### 15.2 filemark_start 与 filemark_end 含义 + +| 字段 | 含义 | 取回定位方式 | +|------|------|-------------| +| `filemark_start` | Bundle 起始位置前的 FileMark 编号(从 BOT 算起第几个 FM) | 从 BOT 执行 `MTFSF(filemark_start)` 跳到 Bundle 起始位置 | +| `filemark_end` | Bundle 结束后写入的 FileMark 编号 | 标记 Bundle 边界,下一个 Bundle 的 `filemark_start = 当前 filemark_end` | + +对于磁带上的第一个 Bundle,`filemark_start = 0`(表示从 BOT 开始,不需要跳过 FM)。 + +### 15.3 Bundle 内对象定位 + +Bundle 内的对象通过 `tape_block_offset`(相对于 Bundle 起始位置的块偏移)定位: + +``` +取回单个对象: +1. MTFSF(filemark_start) → 跳到 Bundle 起始 +2. MTFSR(tape_block_offset) → 在 Bundle 内按块偏移前进到目标对象 +3. read(object_size) → 读取对象数据 +``` diff --git a/docs/modules/08-observability.md b/docs/modules/08-observability.md index f6ed25f..359f4f3 100644 --- a/docs/modules/08-observability.md +++ b/docs/modules/08-observability.md @@ -714,33 +714,35 @@ async fn handle_get_object( Span::current().record("coldstore.storage_class", &meta.storage_class.as_str()); - if meta.storage_class == StorageClass::Cold { - match meta.restore_status { - Some(RestoreStatus::Completed) if meta.restore_expire_at > Some(Utc::now()) => { - // 从缓存读取 - let cached = state.cache.get(bucket, key, None).await?; - match cached { - Some(obj) => { - Span::current().record("cache.hit", true); - state.metrics.cache_hits.add(1, &[]); - state.metrics.s3_request_duration.record( - start.elapsed().as_secs_f64() * 1000.0, - &[KeyValue::new("method", "GET"), KeyValue::new("bucket", bucket)], - ); - Ok(build_response(obj, &meta)) - } - None => { - Span::current().record("cache.hit", false); - state.metrics.cache_misses.add(1, &[]); - Err(Error::ServiceUnavailable("cache miss, please restore again")) + // ColdStore 是纯冷归档系统,所有对象都是 Cold 或 ColdPending + match meta.storage_class { + StorageClass::ColdPending => { + Err(Error::InvalidObjectState) + } + StorageClass::Cold => { + match meta.restore_status { + Some(RestoreStatus::Completed) if meta.restore_expire_at > Some(Utc::now()) => { + let cached = state.cache.get(bucket, key, None).await?; + match cached { + Some(obj) => { + Span::current().record("cache.hit", true); + state.metrics.cache_hits.add(1, &[]); + state.metrics.s3_request_duration.record( + start.elapsed().as_secs_f64() * 1000.0, + &[KeyValue::new("method", "GET"), KeyValue::new("bucket", bucket)], + ); + Ok(build_response(obj, &meta)) + } + None => { + Span::current().record("cache.hit", false); + state.metrics.cache_misses.add(1, &[]); + Err(Error::ServiceUnavailable("cache miss, please restore again")) + } } } + _ => Err(Error::InvalidObjectState), } - _ => Err(Error::InvalidObjectState), } - } else { - // 热对象直接返回... - todo!() } } ``` diff --git a/docs/modules/09-admin-console.md b/docs/modules/09-admin-console.md index a52c24d..21f9563 100644 --- a/docs/modules/09-admin-console.md +++ b/docs/modules/09-admin-console.md @@ -22,6 +22,10 @@ ### 1.2 在架构中的位置 +Console **仅连接 Metadata 集群**,不直连 Scheduler Worker、Cache Worker 或 Tape Worker。 +所有运行时数据(缓存统计、驱动状态、队列深度等)由各 Worker 通过心跳上报到 Metadata, +Console 从 Metadata 读取。 + ``` ┌─────────────────────────────────────────────────────────────────┐ │ 浏览器 (Admin Console) │ @@ -33,7 +37,7 @@ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ ColdStore Admin API (Axum :8080) │ -│ 独立于 S3 API (:9000),共享同一进程内的服务层 │ +│ 独立进程,仅连接 Metadata 集群 │ │ ┌──────────┬───────────┬──────────┬──────────┬───────────┐ │ │ │ /cluster │ /buckets │ /objects │ /tapes │ /tasks │ │ │ │ /drives │ /cache │ /metrics │ /audit │ /config │ │ @@ -41,11 +45,30 @@ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ -│ │ MetadataService │ TapeManager │ CacheManager │ Metrics │ │ +│ │ MetadataClient (gRPC) │ │ +│ │ 所有数据来源于 Metadata 集群 │ │ │ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────────┬──────────────────────────────────┘ + │ gRPC (配置: metadata_addrs) + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Metadata 集群 (Raft + RocksDB) │ +│ - 对象/桶/任务元数据 │ +│ - Worker 注册信息 + 心跳上报的运行时数据 │ +│ - 磁带/驱动信息(由 Worker 心跳上报) │ +│ - 缓存统计(由 Cache Worker 心跳上报) │ └─────────────────────────────────────────────────────────────────┘ ``` +**关键设计决策**: + +- **Console 不直连任何 Worker**:与 DESIGN.md §2.1 保持一致,Console 仅连 Metadata +- **运行时数据来源**:各 Worker 每 5s 心跳上报自身状态到 Metadata(参见 03-metadata-cluster §3.10.9), + 包括缓存容量/命中率(CacheWorkerInfo)、驱动状态/当前磁带(TapeWorkerInfo)、 + 队列深度/活跃作业(SchedulerWorkerInfo) +- **Console 读取路径**:Console Admin API 通过 MetadataClient 查询 Metadata 中的 Worker 信息, + 获取缓存统计、磁带驱动状态等运行时数据 + --- ## 2. 技术选型 From 352e888d2155307a998a413438fa222b5bbefe59 Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Mon, 2 Mar 2026 22:23:11 +0800 Subject: [PATCH 5/7] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20pre-commit=20?= =?UTF-8?q?hook=20(fmt/clippy/check/test)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/pre-commit: 可提交的 hook 脚本,依次运行 cargo fmt --check、 clippy -D warnings、cargo check、cargo test - Makefile: 新增 install-hooks / lint / setup targets - proto lib.rs: 允许 tonic-build 生成代码的 doc_lazy_continuation lint 团队成员 clone 后执行 `make setup` 即可安装 hook。 Made-with: Cursor --- Makefile | 135 +++++++++------------------------------- crates/proto/src/lib.rs | 24 +++++++ scripts/pre-commit | 54 ++++++++++++++++ 3 files changed, 109 insertions(+), 104 deletions(-) create mode 100644 crates/proto/src/lib.rs create mode 100755 scripts/pre-commit diff --git a/Makefile b/Makefile index f98fb78..95b4450 100644 --- a/Makefile +++ b/Makefile @@ -1,133 +1,60 @@ -.PHONY: build fmt clippy run clean test help +.PHONY: build fmt clippy test clean check help install-hooks lint -# 默认目标 .DEFAULT_GOAL := help -# 项目名称 -PROJECT := coldstore - -# 构建模式(release 或 debug) BUILD_MODE ?= release -# 运行参数 -RUN_ARGS ?= - -# 构建项目 build: - @echo "构建项目 ($(BUILD_MODE))..." - @if [ "$(BUILD_MODE)" = "release" ]; then \ - cargo build --release; \ - else \ - cargo build; \ - fi + @echo "Building all crates ($(BUILD_MODE))..." + @if [ "$(BUILD_MODE)" = "release" ]; then cargo build --release; else cargo build; fi -# 快速构建(debug 模式) build-debug: @$(MAKE) BUILD_MODE=debug build -# 格式化代码 fmt: - @echo "格式化代码..." @cargo fmt --all - @echo "代码格式化完成" - -# 检查代码格式 fmt-check: - @echo "检查代码格式..." @cargo fmt --all -- --check - -# 运行 Clippy 代码检查 clippy: - @echo "运行 Clippy 代码检查..." @cargo clippy --all-targets --all-features -- -D warnings - @echo "Clippy 检查完成" - -# 运行 Clippy(允许警告) -clippy-allow: - @echo "运行 Clippy 代码检查(允许警告)..." - @cargo clippy --all-targets --all-features - -# 运行项目 -run: - @echo "运行项目..." - @cargo run --release -- $(RUN_ARGS) - -# 运行项目(debug 模式) -run-debug: - @echo "运行项目(debug 模式)..." - @cargo run -- $(RUN_ARGS) - -# 清理构建产物 -clean: - @echo "清理构建产物..." - @cargo clean - @echo "清理完成" - -# 运行测试 test: - @echo "运行测试..." @cargo test --all-features - @echo "测试完成" - -# 运行测试(显示输出) -test-verbose: - @echo "运行测试(显示输出)..." - @cargo test --all-features -- --nocapture - -# 检查代码(不构建) check: - @echo "检查代码..." @cargo check --all-targets --all-features - @echo "检查完成" - -# 完整检查(格式化 + Clippy + 测试) check-all: fmt-check clippy test - @echo "所有检查完成" +clean: + @cargo clean -# 开发前检查(格式化 + Clippy) -dev-check: fmt-check clippy - @echo "开发检查完成" +run-metadata: + @cargo run -p coldstore-metadata +run-gateway: + @cargo run -p coldstore-gateway +run-scheduler: + @cargo run -p coldstore-scheduler +run-cache: + @cargo run -p coldstore-cache +run-tape: + @cargo run -p coldstore-tape + +lint: fmt-check clippy check test + @echo "All lint checks passed." + +install-hooks: + @cp scripts/pre-commit .git/hooks/pre-commit + @chmod +x .git/hooks/pre-commit + @echo "Pre-commit hook installed." -# 安装开发工具 install-tools: - @echo "安装开发工具..." @rustup component add rustfmt clippy - @echo "开发工具安装完成" + @echo "protobuf-compiler required: apt install protobuf-compiler" -# 更新依赖 -update: - @echo "更新依赖..." - @cargo update - @echo "依赖更新完成" +setup: install-tools install-hooks + @echo "Development environment ready." -# 显示帮助信息 help: - @echo "ColdStore 项目 Makefile" - @echo "" - @echo "可用目标:" - @echo " build - 构建项目(release 模式)" - @echo " build-debug - 构建项目(debug 模式)" - @echo " fmt - 格式化代码" - @echo " fmt-check - 检查代码格式" - @echo " clippy - 运行 Clippy 代码检查(严格模式)" - @echo " clippy-allow - 运行 Clippy 代码检查(允许警告)" - @echo " run - 运行项目(release 模式)" - @echo " run-debug - 运行项目(debug 模式)" - @echo " clean - 清理构建产物" - @echo " test - 运行测试" - @echo " test-verbose - 运行测试(显示输出)" - @echo " check - 检查代码(不构建)" - @echo " check-all - 完整检查(格式化 + Clippy + 测试)" - @echo " dev-check - 开发前检查(格式化 + Clippy)" - @echo " install-tools - 安装开发工具(rustfmt, clippy)" - @echo " update - 更新依赖" - @echo " help - 显示此帮助信息" + @echo "ColdStore Workspace Makefile" @echo "" - @echo "示例:" - @echo " make build # 构建 release 版本" - @echo " make build-debug # 构建 debug 版本" - @echo " make fmt # 格式化代码" - @echo " make clippy # 运行 Clippy 检查" - @echo " make run # 运行项目" - @echo " make run RUN_ARGS='--help' # 带参数运行" - @echo " make check-all # 运行所有检查" + @echo "Build: build build-debug clean" + @echo "Quality: fmt fmt-check clippy test check check-all lint" + @echo "Run: run-metadata run-gateway run-scheduler run-cache run-tape" + @echo "Setup: setup install-tools install-hooks" diff --git a/crates/proto/src/lib.rs b/crates/proto/src/lib.rs new file mode 100644 index 0000000..f872e83 --- /dev/null +++ b/crates/proto/src/lib.rs @@ -0,0 +1,24 @@ +#[allow(clippy::doc_lazy_continuation)] +pub mod common { + tonic::include_proto!("coldstore.common"); +} + +#[allow(clippy::doc_lazy_continuation)] +pub mod metadata { + tonic::include_proto!("coldstore.metadata"); +} + +#[allow(clippy::doc_lazy_continuation)] +pub mod scheduler { + tonic::include_proto!("coldstore.scheduler"); +} + +#[allow(clippy::doc_lazy_continuation)] +pub mod cache { + tonic::include_proto!("coldstore.cache"); +} + +#[allow(clippy::doc_lazy_continuation)] +pub mod tape { + tonic::include_proto!("coldstore.tape"); +} diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..1b62bcd --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +failed=0 + +echo -e "${YELLOW}[pre-commit]${NC} Running checks..." + +# 1. cargo fmt -- check +echo -e "${YELLOW}[1/4]${NC} cargo fmt --check" +if ! cargo fmt --all -- --check 2>/dev/null; then + echo -e "${RED}✗ cargo fmt failed. Run 'cargo fmt --all' to fix.${NC}" + failed=1 +else + echo -e "${GREEN}✓ cargo fmt${NC}" +fi + +# 2. cargo clippy +echo -e "${YELLOW}[2/4]${NC} cargo clippy" +if ! cargo clippy --workspace --all-targets -- -D warnings 2>/dev/null; then + echo -e "${RED}✗ cargo clippy found warnings/errors.${NC}" + failed=1 +else + echo -e "${GREEN}✓ cargo clippy${NC}" +fi + +# 3. cargo check +echo -e "${YELLOW}[3/4]${NC} cargo check" +if ! cargo check --workspace 2>/dev/null; then + echo -e "${RED}✗ cargo check failed.${NC}" + failed=1 +else + echo -e "${GREEN}✓ cargo check${NC}" +fi + +# 4. cargo test +echo -e "${YELLOW}[4/4]${NC} cargo test" +if ! cargo test --workspace --no-fail-fast 2>/dev/null; then + echo -e "${RED}✗ cargo test failed.${NC}" + failed=1 +else + echo -e "${GREEN}✓ cargo test${NC}" +fi + +if [ "$failed" -ne 0 ]; then + echo -e "\n${RED}Pre-commit checks failed. Commit aborted.${NC}" + exit 1 +fi + +echo -e "\n${GREEN}All pre-commit checks passed.${NC}" From f80a567fd98d1fc82990afa91b0b1000b8fc76ac Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Mon, 2 Mar 2026 22:25:03 +0800 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=B8=BA?= =?UTF-8?q?=E5=A4=9A=20crate=20workspace=20=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将单体 src/ 结构迁移为 crates/ 多 crate workspace: 新增 crates: - proto: gRPC 接口定义 (5 个 .proto 文件) + tonic-build 生成 - common: 共享配置、错误处理、领域模型 - metadata: Metadata 节点 (Raft + RocksDB) 二进制 - gateway: S3 HTTP 接入层二进制 - scheduler: 归档/取回调度器二进制 - cache: Cache Worker 二进制 (HDD 暂存后端) - tape: Tape Worker 二进制 删除: - src/ 旧单体代码 - config/config.yaml.example (各 crate 内置默认配置) 更新: - Cargo.toml: workspace 根配置 + 共享依赖 - .gitignore, README.md, README_ZH.md, README_RUST.md Made-with: Cursor --- .gitignore | 30 +-- Cargo.toml | 81 +++---- README.md | 49 ++-- README_RUST.md | 170 +++----------- README_ZH.md | 54 ++--- config/config.yaml.example | 46 ---- crates/cache/Cargo.toml | 31 +++ crates/cache/src/backend.rs | 48 ++++ crates/cache/src/hdd.rs | 139 +++++++++++ crates/cache/src/lib.rs | 25 ++ crates/cache/src/main.rs | 19 ++ crates/cache/src/service.rs | 91 ++++++++ crates/common/Cargo.toml | 19 ++ crates/common/src/config.rs | 213 +++++++++++++++++ crates/common/src/error.rs | 69 ++++++ crates/common/src/lib.rs | 5 + crates/common/src/models.rs | 127 ++++++++++ crates/gateway/Cargo.toml | 30 +++ crates/gateway/src/handler.rs | 24 ++ crates/gateway/src/lib.rs | 27 +++ crates/gateway/src/main.rs | 19 ++ crates/gateway/src/protocol.rs | 52 +++++ crates/metadata/Cargo.toml | 29 +++ crates/metadata/src/lib.rs | 25 ++ crates/metadata/src/main.rs | 20 ++ crates/metadata/src/service.rs | 361 +++++++++++++++++++++++++++++ crates/proto/Cargo.toml | 14 ++ crates/proto/build.rs | 20 ++ crates/proto/proto/cache.proto | 226 ++++++++++++++++++ crates/proto/proto/common.proto | 262 +++++++++++++++++++++ crates/proto/proto/metadata.proto | 288 +++++++++++++++++++++++ crates/proto/proto/scheduler.proto | 199 ++++++++++++++++ crates/proto/proto/tape.proto | 197 ++++++++++++++++ crates/scheduler/Cargo.toml | 27 +++ crates/scheduler/src/lib.rs | 45 ++++ crates/scheduler/src/main.rs | 19 ++ crates/scheduler/src/service.rs | 107 +++++++++ crates/tape/Cargo.toml | 30 +++ crates/tape/src/lib.rs | 17 ++ crates/tape/src/main.rs | 15 ++ crates/tape/src/service.rs | 97 ++++++++ src/cache/manager.rs | 110 --------- src/cache/mod.rs | 3 - src/config.rs | 163 ------------- src/error.rs | 48 ---- src/lib.rs | 11 - src/main.rs | 39 ---- src/metadata/backend.rs | 191 --------------- src/metadata/mod.rs | 4 - src/metadata/service.rs | 50 ---- src/models.rs | 117 ---------- src/notification/mod.rs | 3 - src/notification/service.rs | 51 ---- src/s3/handler.rs | 73 ------ src/s3/mod.rs | 5 - src/s3/server.rs | 32 --- src/scheduler/archive.rs | 86 ------- src/scheduler/mod.rs | 5 - src/scheduler/recall.rs | 87 ------- src/tape/driver.rs | 30 --- src/tape/manager.rs | 39 ---- src/tape/mod.rs | 5 - 62 files changed, 3050 insertions(+), 1468 deletions(-) delete mode 100644 config/config.yaml.example create mode 100644 crates/cache/Cargo.toml create mode 100644 crates/cache/src/backend.rs create mode 100644 crates/cache/src/hdd.rs create mode 100644 crates/cache/src/lib.rs create mode 100644 crates/cache/src/main.rs create mode 100644 crates/cache/src/service.rs create mode 100644 crates/common/Cargo.toml create mode 100644 crates/common/src/config.rs create mode 100644 crates/common/src/error.rs create mode 100644 crates/common/src/lib.rs create mode 100644 crates/common/src/models.rs create mode 100644 crates/gateway/Cargo.toml create mode 100644 crates/gateway/src/handler.rs create mode 100644 crates/gateway/src/lib.rs create mode 100644 crates/gateway/src/main.rs create mode 100644 crates/gateway/src/protocol.rs create mode 100644 crates/metadata/Cargo.toml create mode 100644 crates/metadata/src/lib.rs create mode 100644 crates/metadata/src/main.rs create mode 100644 crates/metadata/src/service.rs create mode 100644 crates/proto/Cargo.toml create mode 100644 crates/proto/build.rs create mode 100644 crates/proto/proto/cache.proto create mode 100644 crates/proto/proto/common.proto create mode 100644 crates/proto/proto/metadata.proto create mode 100644 crates/proto/proto/scheduler.proto create mode 100644 crates/proto/proto/tape.proto create mode 100644 crates/scheduler/Cargo.toml create mode 100644 crates/scheduler/src/lib.rs create mode 100644 crates/scheduler/src/main.rs create mode 100644 crates/scheduler/src/service.rs create mode 100644 crates/tape/Cargo.toml create mode 100644 crates/tape/src/lib.rs create mode 100644 crates/tape/src/main.rs create mode 100644 crates/tape/src/service.rs delete mode 100644 src/cache/manager.rs delete mode 100644 src/cache/mod.rs delete mode 100644 src/config.rs delete mode 100644 src/error.rs delete mode 100644 src/lib.rs delete mode 100644 src/main.rs delete mode 100644 src/metadata/backend.rs delete mode 100644 src/metadata/mod.rs delete mode 100644 src/metadata/service.rs delete mode 100644 src/models.rs delete mode 100644 src/notification/mod.rs delete mode 100644 src/notification/service.rs delete mode 100644 src/s3/handler.rs delete mode 100644 src/s3/mod.rs delete mode 100644 src/s3/server.rs delete mode 100644 src/scheduler/archive.rs delete mode 100644 src/scheduler/mod.rs delete mode 100644 src/scheduler/recall.rs delete mode 100644 src/tape/driver.rs delete mode 100644 src/tape/manager.rs delete mode 100644 src/tape/mod.rs diff --git a/.gitignore b/.gitignore index 079c148..df2f2fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,8 @@ -# Rust -/target/ -**/*.rs.bk -*.pdb +/target +**/target Cargo.lock - -# IDE -.idea/ -.vscode/ *.swp *.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Config -config/config.yaml -!config/config.yaml.example - -# Logs +.env *.log -logs/ - -# Cache -/var/cache/coldstore/ - -# Test data -test_data/ +/data diff --git a/Cargo.toml b/Cargo.toml index b7d7946..6391457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,69 +1,64 @@ -[package] -name = "coldstore" +[workspace] +resolver = "2" +members = [ + "crates/proto", + "crates/common", + "crates/metadata", + "crates/gateway", + "crates/scheduler", + "crates/cache", + "crates/tape", +] + +[workspace.package] version = "0.1.0" edition = "2021" -authors = ["Your Name "] -description = "S3-compatible tape-based cold storage archive system" license = "MIT OR Apache-2.0" repository = "https://github.com/yourusername/coldstore" -[dependencies] +[workspace.dependencies] # Async runtime -tokio = { version = "1.35", features = ["full"] } +tokio = { version = "1", features = ["full"] } + +# gRPC +tonic = "0.12" +tonic-build = "0.12" +prost = "0.13" +prost-types = "0.13" -# HTTP server and S3 compatibility +# HTTP server (Gateway S3 layer) axum = "0.7" tower = "0.4" tower-http = { version = "0.5", features = ["cors", "trace"] } -http = "1.0" -hyper = "1.0" +http = "1" +hyper = "1" # Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" serde_yaml = "0.9" -# Database and metadata storage -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] } -# etcd-rs = "1.0" # 暂时注释,等需要时再添加 - # Error handling -anyhow = "1.0" -thiserror = "1.0" +anyhow = "1" +thiserror = "2" # Logging and observability tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -tracing-appender = "0.2" + +# Time and identity +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v4", "serde"] } # Configuration config = "0.14" -# Time and date -chrono = { version = "0.4", features = ["serde"] } -uuid = { version = "1.6", features = ["v4", "serde"] } +# Streaming +tokio-stream = "0.1" -# Checksum and hashing +# Checksum sha2 = "0.10" -crc32fast = "1.3" - -# Cache -lru = "0.12" - -# Tape and storage -# Note: These may need to be replaced with actual tape library bindings -# For now, we'll use placeholder dependencies - -# Message queue and notifications -# rabbitmq-stream-client = "0.12" # Optional: for MQ integration - -# Metrics (optional) -# prometheus = "0.13" - -[dev-dependencies] -tokio-test = "0.4" -mockall = "0.12" -[[bin]] -name = "coldstore" -path = "src/main.rs" +# Internal crates +coldstore-proto = { path = "crates/proto" } +coldstore-common = { path = "crates/common" } diff --git a/README.md b/README.md index 04523bc..bc805a6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ As object storage systems continue to grow in scale, the cost and energy consump The goal of this project is to build a system that is: -> **S3‑compatible, tape‑backed, and capable of automatic hot/cold tiering and on‑demand restore (thawing)** +> **S3‑compatible, tape‑backed pure cold archive with on‑demand restore (thawing)** The system targets government, enterprise, research, finance, and media workloads that require low cost, long‑term retention, and high reliability. @@ -15,7 +15,7 @@ The system targets government, enterprise, research, finance, and media workload ## 2. Design Goals * Maintain full S3 protocol transparency for upper‑layer applications, requiring no business logic changes -* Support automatic migration of object data to tape and other cold storage media +* All PutObject writes immediately queue for archival to tape * Provide controllable, observable, and extensible restore and rehydration capabilities * Support enterprise‑grade archive workflows involving offline tapes and human intervention * Achieve a practical engineering balance among performance, capacity, and reliability @@ -34,11 +34,7 @@ The system targets government, enterprise, research, finance, and media workload ### 3.2 Automatic Cold Data Tiering and Archival -Support policy‑based migration of object data from general‑purpose storage to cold storage media: - -* Lifecycle rules (time‑based) -* Access frequency (hot/cold identification) -* Object tags or bucket‑level policies +All objects written via PutObject are immediately marked as ColdPending and queued for archival to tape. ColdStore does not manage lifecycle policies—the decision of when to archive is made by the external hot storage system. After archival: @@ -144,7 +140,7 @@ Reliability and storage efficiency are configurable trade‑offs. The system adopts a layered, decoupled architecture: * **S3 Access Layer**: External object access interface -* **Metadata & Policy Management Layer**: Hot/cold state, lifecycle, and indexing +* **Metadata & Policy Management Layer**: Cold state and indexing * **Tiering & Scheduling Layer**: Core archival and restore orchestration * **Cache Layer**: Online cache for restored data * **Tape Management Layer**: Unified abstraction for tapes, drives, and libraries @@ -158,17 +154,17 @@ The system adopts a layered, decoupled architecture: * Handles PUT / GET / HEAD requests * Intercepts access to cold objects and evaluates state -* Can be implemented via MinIO or a custom S3 proxy +* Implemented via custom Axum S3 gateway #### Metadata Service -* Maintains object hot/cold state and archive locations +* Maintains object cold state and archive locations * Manages tape indexing and restore state machines -* Metadata is always stored on online storage (KV / RDB) +* Metadata is always stored on online storage (OpenRaft + RocksDB) #### Archive Scheduler -* Scans objects eligible for archival +* Processes ColdPending objects queued for archival * Performs batch aggregation and sequential tape writes * Coordinates tape drives for write operations @@ -196,10 +192,10 @@ The system adopts a layered, decoupled architecture: ### 5.1 Cold Data Archival Flow -1. Object is written via S3 -2. Lifecycle policy is triggered +1. Object is written via S3 (PutObject) +2. Object is immediately marked as ColdPending and queued for archival 3. Scheduler aggregates objects and writes them to tape -4. Metadata hot/cold state is updated +4. Metadata cold state is updated (ColdPending → Cold) 5. Online storage space is reclaimed ### 5.2 Cold Data Restore Flow @@ -230,10 +226,10 @@ This section translates the design into an implementable architecture using obje | Layer | Technology | Description | | ------------ | ---------------------------- | ------------------------------------------------- | -| S3 Access | MinIO / RustFS | S3‑compatible API, PUT/GET/HEAD/Restore semantics | -| Metadata | KV / RDB (etcd / PostgreSQL) | Hot/cold state, tape index, task state | +| S3 Access | Axum (custom S3 gateway) | S3‑compatible API, PUT/GET/HEAD/Restore semantics | +| Metadata | OpenRaft + RocksDB | Cold state, tape index, task state | | Scheduling | Custom Scheduler (Rust) | Core archive and recall logic | -| Cache | Local SSD / HDD + LRU | Restored data cache | +| Cache | SPDK Blobstore (NVMe) / HDD | Restored data cache | | Tape Access | LTFS / Vendor SDK / SCSI | LTO‑9 / LTO‑10 integration | | Notification | Webhook / MQ / Ticketing | Human collaboration | @@ -243,15 +239,12 @@ This section translates the design into an implementable architecture using obje #### 7.2.1 Cold Object Access Control +ColdStore is a pure cold archive: all PutObject writes go to ColdPending state immediately. There is no Hot/Warm storage. + * After archival, only metadata and placeholders remain online * Regular GET on cold objects returns an S3‑like `InvalidObjectState` * RestoreObject or extended APIs trigger recall scheduling -This can be implemented via: - -* MinIO Lifecycle / ILM extensions, or -* Direct integration into RustFS object access paths - --- ### 7.3 Metadata Model (Core) @@ -259,7 +252,7 @@ This can be implemented via: #### 7.3.1 Object Metadata * bucket / object / version -* `storage_class`: HOT / WARM / COLD +* `storage_class`: ColdPending / Cold * `archive_id` (archive bundle ID) * `tape_id` / `tape_set` * checksum / size @@ -280,8 +273,8 @@ This can be implemented via: **Write Path** -1. Object written to MinIO / RustFS -2. Lifecycle policy marks it as `COLD_PENDING` +1. Object written via PutObject +2. Immediately marked as ColdPending and queued for archival 3. Archive Scheduler aggregates objects 4. Sequential tape write (≥ 300 MB/s) 5. Metadata updated and online data released @@ -355,8 +348,8 @@ This can be implemented via: ### 7.10 Reusable and Replaceable Components -* S3: MinIO / RustFS -* Metadata: etcd / PostgreSQL +* S3: Custom Axum gateway +* Metadata: OpenRaft + RocksDB * Notification: Existing ticketing / alert systems * Tape drivers: Vendor SDKs diff --git a/README_RUST.md b/README_RUST.md index 9ea8f18..85c09c6 100644 --- a/README_RUST.md +++ b/README_RUST.md @@ -1,156 +1,48 @@ # ColdStore - Rust 实现 -基于 Rust 的 S3 兼容磁带冷存储归档系统。 +S3 兼容的纯冷归档磁带存储系统。 -## 项目结构 +## 项目定位 -``` -coldstore/ -├── Cargo.toml # 项目配置和依赖 -├── src/ -│ ├── main.rs # 主入口 -│ ├── lib.rs # 库入口 -│ ├── s3/ # S3 接入层 -│ │ ├── mod.rs -│ │ ├── server.rs # S3 HTTP 服务器 -│ │ └── handler.rs # S3 请求处理 -│ ├── metadata/ # 元数据服务 -│ │ ├── mod.rs -│ │ ├── service.rs # 元数据服务接口 -│ │ └── backend.rs # 后端实现(PostgreSQL/Etcd) -│ ├── scheduler/ # 调度器 -│ │ ├── mod.rs -│ │ ├── archive.rs # 归档调度器 -│ │ └── recall.rs # 取回调度器 -│ ├── cache/ # 缓存层 -│ │ ├── mod.rs -│ │ └── manager.rs # 缓存管理 -│ ├── tape/ # 磁带管理 -│ │ ├── mod.rs -│ │ ├── manager.rs # 磁带管理器 -│ │ └── driver.rs # 磁带驱动抽象 -│ ├── notification/ # 通知服务 -│ │ ├── mod.rs -│ │ └── service.rs # 通知服务实现 -│ ├── config/ # 配置管理 -│ │ └── mod.rs -│ ├── error/ # 错误处理 -│ │ └── mod.rs -│ └── models/ # 数据模型 -│ └── mod.rs -├── config/ -│ └── config.yaml.example # 配置示例 -└── README_RUST.md # 本文档 -``` +ColdStore 是**纯冷归档系统**(类似 AWS Glacier Deep Archive): +- 所有对象写入即标记为 ColdPending,自动排队归档到磁带 +- 不提供在线热存储层,不做生命周期管理 +- 外部热存储系统在需要冷归档时调用 ColdStore 的 PutObject API -## 快速开始 - -### 1. 安装 Rust - -确保已安装 Rust 工具链(推荐使用 rustup): - -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -### 2. 配置项目 - -复制配置示例文件: - -```bash -cp config/config.yaml.example config/config.yaml -``` - -根据实际情况编辑 `config/config.yaml`。 - -### 3. 设置数据库(如果使用 PostgreSQL) - -```bash -createdb coldstore -# 运行数据库迁移(待实现) -``` - -### 4. 构建项目 +## Workspace 结构 -```bash -cargo build --release -``` +| Crate | 类型 | 说明 | +|-------|------|------| +| coldstore-proto | lib | gRPC 协议定义(protobuf 生成) | +| coldstore-common | lib | 共享模型、错误、配置 | +| coldstore-metadata | bin | 元数据集群节点(Raft + RocksDB) | +| coldstore-gateway | bin | S3 HTTP 网关(无状态) | +| coldstore-scheduler | bin | 调度 Worker(业务中枢) | +| coldstore-cache | bin | 缓存 Worker(独立进程,HDD/SPDK) | +| coldstore-tape | bin | 磁带 Worker(独立物理节点) | -### 5. 运行 +## 组件间通信 -```bash -cargo run --release -``` +所有组件通过 gRPC 通信,协议定义在 crates/proto/proto/ 下: -或使用环境变量覆盖配置: +- common.proto: 共享枚举和数据结构 +- metadata.proto: MetadataService(元数据读写、集群管理、心跳) +- scheduler.proto: SchedulerService(Gateway 转发 S3 请求) +- cache.proto: CacheService(数据暂存和解冻缓存) +- tape.proto: TapeService(磁带读写和驱动管理) -```bash -COLDSTORE_SERVER_PORT=9001 cargo run --release -``` - -## 开发状态 - -当前项目处于初始骨架阶段,各模块的基础结构已搭建,但具体实现需要逐步完善: - -- ✅ 项目结构和模块划分 -- ✅ 配置管理框架 -- ✅ 错误处理框架 -- ✅ 数据模型定义 -- ⏳ S3 协议实现 -- ⏳ 元数据后端实现 -- ⏳ 归档调度器实现 -- ⏳ 取回调度器实现 -- ⏳ 磁带驱动集成 -- ⏳ 缓存层实现 -- ⏳ 通知系统集成 - -## 下一步开发计划 - -1. **完善 S3 协议支持** - - 实现 PUT/GET/HEAD 操作 - - 实现 RestoreObject API - - 添加认证和授权 - -2. **实现元数据后端** - - PostgreSQL 表结构设计 - - 数据库迁移脚本 - - Etcd 键值结构设计 - -3. **实现归档调度器** - - 对象扫描逻辑 - - 归档包聚合 - - 磁带写入流程 - -4. **实现取回调度器** - - 任务队列管理 - - 任务合并逻辑 - - 磁带读取流程 - -5. **集成磁带驱动** - - LTFS 集成 - - 或厂商 SDK 集成 - - 驱动抽象层完善 - -6. **完善缓存层** - - LRU/LFU 实现 - - TTL 管理 - - 容量限制和淘汰 +## 快速开始 -7. **测试和文档** - - 单元测试 - - 集成测试 - - API 文档 +前置条件: Rust 1.75+, protobuf-compiler -## 依赖说明 + make build-debug -主要依赖包括: +分别启动各组件: -- **tokio**: 异步运行时 -- **axum**: HTTP 服务器框架 -- **sqlx**: 异步 SQL 工具 -- **etcd-rs**: Etcd 客户端 -- **serde**: 序列化/反序列化 -- **tracing**: 结构化日志 + cargo run -p coldstore-metadata + cargo run -p coldstore-cache + cargo run -p coldstore-scheduler + cargo run -p coldstore-gateway ## 许可证 diff --git a/README_ZH.md b/README_ZH.md index a9055a0..f920a35 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -6,7 +6,7 @@ 本项目目标是构建一个: -> S3 协议兼容、支持磁带归档、具备自动冷热分层与解冻能力的冷存储系统 +> S3 协议兼容、支持磁带归档的纯冷归档系统,具备解冻能力 @@ -19,7 +19,7 @@ 对上层应用保持 S3 协议透明,无需业务改造 -支持对象数据自动流转至磁带等冷存储介质 +所有 PutObject 写入立即排队归档至磁带 提供可控、可观测、可扩展的解冻与回迁能力 @@ -47,14 +47,7 @@ 3.2 冷数据自动分层与归档 -支持基于策略将对象数据从通用存储介质自动迁移至冷存储介质: - -生命周期规则(时间维度) - -访问频率(冷热识别) - -对象标签 / Bucket 级策略 - +所有通过 PutObject 写入的对象立即标记为 ColdPending 并排队等待归档至磁带。ColdStore 不管理生命周期策略——何时归档由外部热存储系统决定。 冷数据归档后,对象元数据仍保持可见; @@ -195,7 +188,7 @@ TTL 失效策略 S3 接入层:对外提供对象访问接口 -元数据与策略管理层:管理冷热状态、生命周期与索引 +元数据与策略管理层:管理冷状态与索引 冷热调度层:归档与取回调度核心 @@ -217,12 +210,12 @@ S3 接入层 对冷对象访问进行拦截与状态判断; -可基于 MinIO / 自研 S3 Proxy 实现。 +基于自研 Axum S3 网关实现。 元数据服务 -维护对象冷热状态、归档位置、磁带索引; +维护对象冷状态、归档位置、磁带索引; 支持一致性校验与状态机管理; @@ -231,7 +224,7 @@ S3 接入层 归档调度器(Archive Scheduler) -扫描满足归档条件的对象; +处理排队等待归档的 ColdPending 对象; 进行批量聚合与顺序写入; @@ -272,16 +265,16 @@ S3 接入层 5.1 冷数据归档流程 -1. 对象写入 S3; +1. 对象通过 S3(PutObject)写入; -2. 生命周期策略命中; +2. 对象立即标记为 ColdPending 并排队等待归档; 3. 调度器聚合对象并写入磁带; -4. 更新元数据冷热状态; +4. 更新元数据冷状态(ColdPending → Cold); 5. 释放在线存储空间。 @@ -334,10 +327,10 @@ S3 接入层 层级 技术选型 说明 -S3 接入层 MinIO / RustFS 提供 S3 兼容接口,承载 PUT/GET/HEAD/Restore 语义 -元数据存储 KV / RDB(如 etcd / PostgreSQL) 管理冷热状态、磁带索引、任务状态 +S3 接入层 Axum(自研 S3 网关) 提供 S3 兼容接口,承载 PUT/GET/HEAD/Restore 语义 +元数据存储 OpenRaft + RocksDB 管理冷状态、磁带索引、任务状态 冷热调度 自研 Scheduler(Rust) 归档与取回核心逻辑,强业务定制 -缓存层 本地 HDD / SSD + LRU 解冻数据缓存,避免重复取带 +缓存层 SPDK Blobstore (NVMe) / HDD 解冻数据缓存,避免重复取带 磁带访问 LTFS / 厂商 SDK / SCSI 对接 LTO-9 / LTO-10 磁带库 通知系统 Webhook / MQ / 工单系统 离线磁带人工协同 @@ -362,14 +355,7 @@ RestoreObject 或扩展 API: -该逻辑可通过: - -MinIO 的 Lifecycle / ILM 扩展 - -或在 RustFS 的对象访问路径中直接注入判断 - - -实现。 +ColdStore 为纯冷归档系统:所有 PutObject 写入立即进入 ColdPending 状态,无 Hot/Warm 存储。 --- @@ -382,7 +368,7 @@ MinIO 的 Lifecycle / ILM 扩展 bucket / object / version -storage_class:HOT / WARM / COLD +storage_class:ColdPending / Cold archive_id(归档包 ID) @@ -414,13 +400,13 @@ restore_status / restore_expire_at 写入路径 -1. 对象正常写入 MinIO / RustFS; +1. 对象通过 PutObject 写入; -2. 生命周期策略命中,标记为 COLD_PENDING; +2. 立即标记为 ColdPending 并排队等待归档; -3. Archive Scheduler 扫描并聚合对象; +3. Archive Scheduler 聚合对象; 4. 顺序写入磁带(Streaming Write ≥ 300MB/s); @@ -549,9 +535,9 @@ Archive / Recall Scheduler 业务策略强,通用方案无法覆盖 7.10 可复用与可替换组件 -S3:MinIO / RustFS +S3:自研 Axum 网关 -KV:etcd / PostgreSQL +元数据:OpenRaft + RocksDB 通知:现有工单 / 告警系统 diff --git a/config/config.yaml.example b/config/config.yaml.example deleted file mode 100644 index 1abcc37..0000000 --- a/config/config.yaml.example +++ /dev/null @@ -1,46 +0,0 @@ -server: - host: "0.0.0.0" - port: 9000 - s3_endpoint: "http://localhost:9000" - -metadata: - backend: "Postgres" # 或 "Etcd" - postgres: - url: "postgresql://localhost/coldstore" - max_connections: 10 - etcd: - endpoints: - - "http://localhost:2379" - timeout_secs: 5 - -scheduler: - archive: - scan_interval_secs: 3600 # 1小时扫描一次 - batch_size: 1000 - min_archive_size_mb: 100 - target_throughput_mbps: 300 # 目标 300 MB/s - recall: - queue_size: 10000 - max_concurrent_restores: 10 - restore_timeout_secs: 3600 - min_restore_interval_secs: 300 # 5分钟 - -cache: - enabled: true - path: "/var/cache/coldstore" - max_size_gb: 100 - ttl_secs: 86400 # 24小时 - eviction_policy: "Lru" # 或 "Lfu", "Ttl" - -tape: - library_path: null # 磁带库路径,根据实际情况配置 - supported_formats: - - "LTO-9" - - "LTO-10" - replication_factor: 2 - verify_readability: true - -notification: - enabled: true - webhook_url: null # Webhook URL,可选 - mq_endpoint: null # 消息队列端点,可选 diff --git a/crates/cache/Cargo.toml b/crates/cache/Cargo.toml new file mode 100644 index 0000000..a2243cc --- /dev/null +++ b/crates/cache/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "coldstore-cache" +description = "ColdStore cache worker (independent binary, SPDK/HDD backend)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "coldstore-cache" +path = "src/main.rs" + +[dependencies] +coldstore-proto = { workspace = true } +coldstore-common = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +prost = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +config = { workspace = true } +sha2 = { workspace = true } + +# TODO: Phase 3 - SPDK backend +# async-spdk = { git = "https://github.com/madsys-dev/async-spdk" } + +tokio-stream = { workspace = true } diff --git a/crates/cache/src/backend.rs b/crates/cache/src/backend.rs new file mode 100644 index 0000000..37e0da5 --- /dev/null +++ b/crates/cache/src/backend.rs @@ -0,0 +1,48 @@ +use anyhow::Result; + +/// 缓存后端抽象 trait +/// +/// 当前实现: HddBackend (机械硬盘) +/// 目标实现: SpdkBlobBackend (SPDK Blobstore on NVMe) +#[tonic::async_trait] +pub trait CacheBackend: Send + Sync + 'static { + /// 写入对象数据,返回内部存储 ID + async fn write(&self, cache_key: &str, data: &[u8], xattrs: &CacheXattrs) -> Result; + + /// 读取对象数据 + async fn read(&self, storage_id: u64) -> Result>; + + /// 删除对象 + async fn delete(&self, storage_id: u64) -> Result<()>; + + /// 读取 xattrs + async fn read_xattrs(&self, storage_id: u64) -> Result; + + /// 列出所有存储对象(启动时重建索引用) + async fn list_all(&self) -> Result>; + + /// 可用容量 + async fn available_bytes(&self) -> Result; +} + +/// 缓存对象扩展属性 +#[derive(Debug, Clone)] +pub struct CacheXattrs { + pub bucket: String, + pub key: String, + pub version_id: Option, + pub size: u64, + pub expire_at: i64, + pub cached_at: i64, + pub checksum: Option, + pub content_type: Option, + pub etag: Option, + /// staging = 暂存数据 (PutObject), restored = 解冻数据 + pub category: CacheCategory, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CacheCategory { + Staging, + Restored, +} diff --git a/crates/cache/src/hdd.rs b/crates/cache/src/hdd.rs new file mode 100644 index 0000000..7738174 --- /dev/null +++ b/crates/cache/src/hdd.rs @@ -0,0 +1,139 @@ +use crate::backend::{CacheBackend, CacheCategory, CacheXattrs}; +use anyhow::Result; +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; +use tokio::fs; +use tracing::debug; + +pub struct HddBackend { + base_path: PathBuf, + max_size_bytes: u64, + next_id: AtomicU64, +} + +impl HddBackend { + pub async fn new(base_path: String, max_size_gb: u64) -> Result { + let base = PathBuf::from(&base_path); + fs::create_dir_all(base.join("staging")).await?; + fs::create_dir_all(base.join("restored")).await?; + fs::create_dir_all(base.join("meta")).await?; + Ok(Self { + base_path: base, + max_size_bytes: max_size_gb * 1024 * 1024 * 1024, + next_id: AtomicU64::new(1), + }) + } + + fn data_path(&self, id: u64, cat: CacheCategory) -> PathBuf { + let dir = match cat { + CacheCategory::Staging => "staging", + CacheCategory::Restored => "restored", + }; + self.base_path.join(dir).join(format!("{id}.dat")) + } + + fn meta_path(&self, id: u64) -> PathBuf { + self.base_path.join("meta").join(format!("{id}.json")) + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct XattrsJson { + bucket: String, + key: String, + version_id: Option, + size: u64, + expire_at: i64, + cached_at: i64, + checksum: Option, + content_type: Option, + etag: Option, + category: String, +} + +fn to_json(x: &CacheXattrs) -> XattrsJson { + XattrsJson { + bucket: x.bucket.clone(), + key: x.key.clone(), + version_id: x.version_id.clone(), + size: x.size, + expire_at: x.expire_at, + cached_at: x.cached_at, + checksum: x.checksum.clone(), + content_type: x.content_type.clone(), + etag: x.etag.clone(), + category: match x.category { + CacheCategory::Staging => "staging".into(), + CacheCategory::Restored => "restored".into(), + }, + } +} + +fn from_json(j: &XattrsJson) -> CacheXattrs { + CacheXattrs { + bucket: j.bucket.clone(), + key: j.key.clone(), + version_id: j.version_id.clone(), + size: j.size, + expire_at: j.expire_at, + cached_at: j.cached_at, + checksum: j.checksum.clone(), + content_type: j.content_type.clone(), + etag: j.etag.clone(), + category: if j.category == "staging" { + CacheCategory::Staging + } else { + CacheCategory::Restored + }, + } +} + +#[tonic::async_trait] +impl CacheBackend for HddBackend { + async fn write(&self, _key: &str, data: &[u8], xattrs: &CacheXattrs) -> Result { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + fs::write(self.data_path(id, xattrs.category), data).await?; + fs::write(self.meta_path(id), serde_json::to_vec(&to_json(xattrs))?).await?; + debug!(id, size = data.len(), "HDD write ok"); + Ok(id) + } + + async fn read(&self, id: u64) -> Result> { + let x = self.read_xattrs(id).await?; + Ok(fs::read(self.data_path(id, x.category)).await?) + } + + async fn delete(&self, id: u64) -> Result<()> { + let x = self.read_xattrs(id).await?; + let _ = fs::remove_file(self.data_path(id, x.category)).await; + let _ = fs::remove_file(self.meta_path(id)).await; + Ok(()) + } + + async fn read_xattrs(&self, id: u64) -> Result { + let raw = fs::read(self.meta_path(id)).await?; + let j: XattrsJson = serde_json::from_slice(&raw)?; + Ok(from_json(&j)) + } + + async fn list_all(&self) -> Result> { + let mut out = Vec::new(); + let mut rd = fs::read_dir(self.base_path.join("meta")).await?; + while let Some(e) = rd.next_entry().await? { + let n = e.file_name(); + let n = n.to_string_lossy(); + if let Some(s) = n.strip_suffix(".json") { + if let Ok(id) = s.parse::() { + if let Ok(x) = self.read_xattrs(id).await { + out.push((id, x)); + } + } + } + } + Ok(out) + } + + async fn available_bytes(&self) -> Result { + Ok(self.max_size_bytes) + } +} diff --git a/crates/cache/src/lib.rs b/crates/cache/src/lib.rs new file mode 100644 index 0000000..de0c370 --- /dev/null +++ b/crates/cache/src/lib.rs @@ -0,0 +1,25 @@ +pub mod backend; +pub mod hdd; +pub mod service; + +use anyhow::Result; +use coldstore_common::config::CacheConfig; +use tonic::transport::Server; +use tracing::info; + +pub async fn run(config: CacheConfig) -> Result<()> { + let addr = config.listen.parse()?; + + let cache_service = service::CacheServiceImpl::new(&config).await?; + + info!("Cache Worker 启动在 {}", config.listen); + + Server::builder() + .add_service( + coldstore_proto::cache::cache_service_server::CacheServiceServer::new(cache_service), + ) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/crates/cache/src/main.rs b/crates/cache/src/main.rs new file mode 100644 index 0000000..df0942b --- /dev/null +++ b/crates/cache/src/main.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use coldstore_common::config::CacheConfig; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "coldstore_cache=info".into()), + ) + .init(); + + info!("启动 ColdStore Cache Worker..."); + + let config = CacheConfig::default(); + + coldstore_cache::run(config).await +} diff --git a/crates/cache/src/service.rs b/crates/cache/src/service.rs new file mode 100644 index 0000000..3a538c9 --- /dev/null +++ b/crates/cache/src/service.rs @@ -0,0 +1,91 @@ +use crate::backend::CacheBackend; +use crate::hdd::HddBackend; +use anyhow::Result; +use coldstore_common::config::{CacheBackendConfig, CacheConfig}; +use coldstore_proto::cache::cache_service_server::CacheService; +use coldstore_proto::cache::*; +use tonic::{Request, Response, Status, Streaming}; + +pub struct CacheServiceImpl { + _backend: Box, + _config: CacheConfig, +} + +impl CacheServiceImpl { + pub async fn new(config: &CacheConfig) -> Result { + let backend: Box = match &config.backend { + CacheBackendConfig::Hdd { path, max_size_gb } => { + Box::new(HddBackend::new(path.clone(), *max_size_gb).await?) + } + CacheBackendConfig::Spdk { .. } => { + anyhow::bail!("SPDK backend not yet implemented") + } + }; + Ok(Self { + _backend: backend, + _config: config.clone(), + }) + } +} + +#[tonic::async_trait] +impl CacheService for CacheServiceImpl { + async fn put_staging( + &self, + _req: Request>, + ) -> std::result::Result, Status> { + todo!() + } + async fn put_restored( + &self, + _req: Request>, + ) -> std::result::Result, Status> { + todo!() + } + async fn delete( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + + type GetStream = tokio_stream::wrappers::ReceiverStream>; + async fn get( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn contains( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + + type GetStagingStream = + tokio_stream::wrappers::ReceiverStream>; + async fn get_staging( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_staging_keys( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn delete_staging( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn stats(&self, _req: Request<()>) -> std::result::Result, Status> { + todo!() + } +} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml new file mode 100644 index 0000000..97471e5 --- /dev/null +++ b/crates/common/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "coldstore-common" +description = "ColdStore shared types, models, and error handling" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +coldstore-proto = { workspace = true } +tonic = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +config = { workspace = true } +prost-types = { workspace = true } diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs new file mode 100644 index 0000000..02dbf41 --- /dev/null +++ b/crates/common/src/config.rs @@ -0,0 +1,213 @@ +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Gateway 配置 +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GatewayConfig { + pub listen: String, + pub scheduler_addrs: Vec, +} + +impl Default for GatewayConfig { + fn default() -> Self { + Self { + listen: "0.0.0.0:9000".to_string(), + scheduler_addrs: vec!["127.0.0.1:22001".to_string()], + } + } +} + +// --------------------------------------------------------------------------- +// Metadata 节点配置 +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetadataConfig { + pub node_id: u64, + pub listen: String, + pub cluster: String, + pub data_path: String, + pub rocksdb: RocksDbConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RocksDbConfig { + pub max_open_files: i32, + pub write_buffer_size_mb: u64, + pub max_background_jobs: i32, +} + +impl Default for MetadataConfig { + fn default() -> Self { + Self { + node_id: 1, + listen: "0.0.0.0:21001".to_string(), + cluster: "1:127.0.0.1:21001,2:127.0.0.1:21002,3:127.0.0.1:21003".to_string(), + data_path: "/var/lib/coldstore/metadata".to_string(), + rocksdb: RocksDbConfig { + max_open_files: 1024, + write_buffer_size_mb: 64, + max_background_jobs: 4, + }, + } + } +} + +// --------------------------------------------------------------------------- +// Scheduler Worker 配置 +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchedulerConfig { + pub listen: String, + pub metadata_addrs: Vec, + pub archive: ArchiveSchedulerConfig, + pub recall: RecallSchedulerConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ArchiveSchedulerConfig { + pub scan_interval_secs: u64, + pub batch_size: usize, + pub min_archive_size_mb: u64, + pub max_archive_size_mb: u64, + pub target_throughput_mbps: u64, + pub aggregation_window_secs: u64, + pub write_buffer_mb: u64, + pub block_size: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecallSchedulerConfig { + pub max_concurrent_restores: usize, + pub merge_window_secs: u64, + pub restore_timeout_secs: u64, + pub read_buffer_mb: u64, +} + +impl Default for SchedulerConfig { + fn default() -> Self { + Self { + listen: "0.0.0.0:22001".to_string(), + metadata_addrs: vec![ + "127.0.0.1:21001".to_string(), + "127.0.0.1:21002".to_string(), + "127.0.0.1:21003".to_string(), + ], + archive: ArchiveSchedulerConfig { + scan_interval_secs: 60, + batch_size: 1000, + min_archive_size_mb: 100, + max_archive_size_mb: 10240, + target_throughput_mbps: 300, + aggregation_window_secs: 300, + write_buffer_mb: 128, + block_size: 262144, + }, + recall: RecallSchedulerConfig { + max_concurrent_restores: 10, + merge_window_secs: 60, + restore_timeout_secs: 3600, + read_buffer_mb: 64, + }, + } + } +} + +// --------------------------------------------------------------------------- +// Cache Worker 配置 +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheConfig { + pub listen: String, + pub metadata_addrs: Vec, + pub backend: CacheBackendConfig, + pub default_ttl_secs: u64, + pub eviction_policy: String, + pub eviction_batch_size: usize, + pub eviction_low_watermark: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum CacheBackendConfig { + #[serde(rename = "hdd")] + Hdd { path: String, max_size_gb: u64 }, + #[serde(rename = "spdk")] + Spdk { + config_file: String, + bdev_name: String, + max_size_gb: u64, + cluster_size_mb: u32, + }, +} + +impl Default for CacheConfig { + fn default() -> Self { + Self { + listen: "0.0.0.0:23001".to_string(), + metadata_addrs: vec![ + "127.0.0.1:21001".to_string(), + "127.0.0.1:21002".to_string(), + "127.0.0.1:21003".to_string(), + ], + backend: CacheBackendConfig::Hdd { + path: "/var/lib/coldstore/cache".to_string(), + max_size_gb: 100, + }, + default_ttl_secs: 86400, + eviction_policy: "Lru".to_string(), + eviction_batch_size: 64, + eviction_low_watermark: 0.8, + } + } +} + +// --------------------------------------------------------------------------- +// Tape Worker 配置 +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TapeConfig { + pub listen: String, + pub metadata_addrs: Vec, + pub sdk_backend: String, + pub scsi: ScsiConfig, + pub library_device: Option, + pub supported_formats: Vec, + pub tape_hold_secs: u64, + pub drive_acquire_timeout_secs: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScsiConfig { + pub devices: Vec, + pub block_size: u32, + pub buffer_size_mb: u64, +} + +impl Default for TapeConfig { + fn default() -> Self { + Self { + listen: "0.0.0.0:24001".to_string(), + metadata_addrs: vec![ + "127.0.0.1:21001".to_string(), + "127.0.0.1:21002".to_string(), + "127.0.0.1:21003".to_string(), + ], + sdk_backend: "scsi".to_string(), + scsi: ScsiConfig { + devices: vec!["/dev/nst0".to_string()], + block_size: 262144, + buffer_size_mb: 64, + }, + library_device: None, + supported_formats: vec!["LTO-9".to_string(), "LTO-10".to_string()], + tape_hold_secs: 300, + drive_acquire_timeout_secs: 600, + } + } +} diff --git a/crates/common/src/error.rs b/crates/common/src/error.rs new file mode 100644 index 0000000..9c2c9c9 --- /dev/null +++ b/crates/common/src/error.rs @@ -0,0 +1,69 @@ +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum Error { + #[error("配置错误: {0}")] + Config(#[from] config::ConfigError), + + #[error("IO 错误: {0}")] + Io(#[from] std::io::Error), + + #[error("序列化错误: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("gRPC 错误: {0}")] + Grpc(#[from] tonic::Status), + + #[error("gRPC 传输错误: {0}")] + Transport(#[from] tonic::transport::Error), + + #[error("S3 协议错误: {code}: {message}")] + S3Protocol { code: String, message: String }, + + #[error("对象不存在: {bucket}/{key}")] + ObjectNotFound { bucket: String, key: String }, + + #[error("桶不存在: {0}")] + BucketNotFound(String), + + #[error("对象状态无效: {0}")] + InvalidObjectState(String), + + #[error("磁带离线: {0}")] + TapeOffline(String), + + #[error("驱动不可用: {0}")] + DriveUnavailable(String), + + #[error("缓存未命中: {bucket}/{key}")] + CacheMiss { bucket: String, key: String }, + + #[error("容量不足: {0}")] + InsufficientCapacity(String), + + #[error("状态流转无效: {from:?} -> {to:?}")] + InvalidStateTransition { from: String, to: String }, + + #[error("内部错误: {0}")] + Internal(String), +} + +impl From for tonic::Status { + fn from(err: Error) -> Self { + match &err { + Error::ObjectNotFound { .. } => tonic::Status::not_found(err.to_string()), + Error::BucketNotFound(_) => tonic::Status::not_found(err.to_string()), + Error::InvalidObjectState(_) => tonic::Status::failed_precondition(err.to_string()), + Error::TapeOffline(_) => tonic::Status::unavailable(err.to_string()), + Error::DriveUnavailable(_) => tonic::Status::unavailable(err.to_string()), + Error::CacheMiss { .. } => tonic::Status::not_found(err.to_string()), + Error::InsufficientCapacity(_) => tonic::Status::resource_exhausted(err.to_string()), + Error::InvalidStateTransition { .. } => { + tonic::Status::failed_precondition(err.to_string()) + } + _ => tonic::Status::internal(err.to_string()), + } + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs new file mode 100644 index 0000000..5921b69 --- /dev/null +++ b/crates/common/src/lib.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod error; +pub mod models; + +pub use error::{Error, Result}; diff --git a/crates/common/src/models.rs b/crates/common/src/models.rs new file mode 100644 index 0000000..66e9ad1 --- /dev/null +++ b/crates/common/src/models.rs @@ -0,0 +1,127 @@ +use serde::{Deserialize, Serialize}; + +/// 存储类别(纯冷归档:仅 ColdPending 和 Cold) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum StorageClass { + ColdPending, + Cold, +} + +/// 解冻状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RestoreStatus { + Pending, + WaitingForMedia, + InProgress, + Completed, + Expired, + Failed, +} + +/// 解冻优先级 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RestoreTier { + Expedited, + Standard, + Bulk, +} + +/// 磁带状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum TapeStatus { + Online, + Offline, + Error, + Retired, + Unknown, +} + +/// 驱动状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DriveStatus { + Idle, + InUse, + Loading, + Unloading, + Error, + Offline, +} + +/// 节点状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum NodeStatus { + Online, + Offline, + Draining, + Maintenance, +} + +/// Worker 类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkerType { + Scheduler, + Cache, + Tape, +} + +/// 归档包状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ArchiveBundleStatus { + Pending, + Writing, + Completed, + Failed, +} + +/// 归档任务状态 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ArchiveTaskStatus { + Pending, + InProgress, + Completed, + Failed, +} + +// --------------------------------------------------------------------------- +// 状态流转校验 +// --------------------------------------------------------------------------- + +impl RestoreStatus { + pub fn can_transition_to(&self, target: &RestoreStatus) -> bool { + matches!( + (self, target), + (RestoreStatus::Pending, RestoreStatus::InProgress) + | (RestoreStatus::Pending, RestoreStatus::WaitingForMedia) + | (RestoreStatus::Pending, RestoreStatus::Failed) + | (RestoreStatus::WaitingForMedia, RestoreStatus::Pending) + | (RestoreStatus::WaitingForMedia, RestoreStatus::Failed) + | (RestoreStatus::InProgress, RestoreStatus::Completed) + | (RestoreStatus::InProgress, RestoreStatus::Failed) + | (RestoreStatus::Completed, RestoreStatus::Expired) + ) + } +} + +impl ArchiveBundleStatus { + pub fn can_transition_to(&self, target: &ArchiveBundleStatus) -> bool { + matches!( + (self, target), + (ArchiveBundleStatus::Pending, ArchiveBundleStatus::Writing) + | (ArchiveBundleStatus::Writing, ArchiveBundleStatus::Completed) + | (ArchiveBundleStatus::Writing, ArchiveBundleStatus::Failed) + | (ArchiveBundleStatus::Failed, ArchiveBundleStatus::Pending) + ) + } +} + +impl ArchiveTaskStatus { + pub fn can_transition_to(&self, target: &ArchiveTaskStatus) -> bool { + matches!( + (self, target), + (ArchiveTaskStatus::Pending, ArchiveTaskStatus::InProgress) + | (ArchiveTaskStatus::InProgress, ArchiveTaskStatus::Completed) + | (ArchiveTaskStatus::InProgress, ArchiveTaskStatus::Failed) + | (ArchiveTaskStatus::Failed, ArchiveTaskStatus::Pending) + ) + } +} diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml new file mode 100644 index 0000000..a578dd2 --- /dev/null +++ b/crates/gateway/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "coldstore-gateway" +description = "ColdStore S3-compatible gateway (stateless)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "coldstore-gateway" +path = "src/main.rs" + +[dependencies] +coldstore-proto = { workspace = true } +coldstore-common = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +prost = { workspace = true } +axum = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +http = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +config = { workspace = true } +sha2 = { workspace = true } diff --git a/crates/gateway/src/handler.rs b/crates/gateway/src/handler.rs new file mode 100644 index 0000000..a072ca8 --- /dev/null +++ b/crates/gateway/src/handler.rs @@ -0,0 +1,24 @@ +use crate::GatewayState; +use axum::{routing::get, Router}; +use std::sync::Arc; + +pub fn router(state: Arc) -> Router { + Router::new() + .route("/health", get(health)) + // TODO: S3 API 路由 + // PUT /{bucket}/{key} → PutObject + // GET /{bucket}/{key} → GetObject + // HEAD /{bucket}/{key} → HeadObject + // DELETE /{bucket}/{key} → DeleteObject + // POST /{bucket}/{key}?restore → RestoreObject + // GET /{bucket} → ListObjects + // PUT /{bucket} → CreateBucket + // DELETE /{bucket} → DeleteBucket + // HEAD /{bucket} → HeadBucket + // GET / → ListBuckets + .with_state(state) +} + +async fn health() -> &'static str { + "OK" +} diff --git a/crates/gateway/src/lib.rs b/crates/gateway/src/lib.rs new file mode 100644 index 0000000..91185c7 --- /dev/null +++ b/crates/gateway/src/lib.rs @@ -0,0 +1,27 @@ +pub mod handler; +pub mod protocol; + +use anyhow::Result; +use coldstore_common::config::GatewayConfig; +use coldstore_proto::scheduler::scheduler_service_client::SchedulerServiceClient; +use tonic::transport::Channel; +use tracing::info; + +pub struct GatewayState { + pub scheduler: SchedulerServiceClient, +} + +pub async fn run(config: GatewayConfig) -> Result<()> { + let scheduler_addr = format!("http://{}", &config.scheduler_addrs[0]); + let scheduler = SchedulerServiceClient::connect(scheduler_addr).await?; + + let state = std::sync::Arc::new(GatewayState { scheduler }); + + let app = handler::router(state); + + let listener = tokio::net::TcpListener::bind(&config.listen).await?; + info!("S3 Gateway 启动在 {}", config.listen); + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs new file mode 100644 index 0000000..67b7a53 --- /dev/null +++ b/crates/gateway/src/main.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use coldstore_common::config::GatewayConfig; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "coldstore_gateway=info".into()), + ) + .init(); + + info!("启动 ColdStore S3 Gateway..."); + + let config = GatewayConfig::default(); + + coldstore_gateway::run(config).await +} diff --git a/crates/gateway/src/protocol.rs b/crates/gateway/src/protocol.rs new file mode 100644 index 0000000..cc8d62e --- /dev/null +++ b/crates/gateway/src/protocol.rs @@ -0,0 +1,52 @@ +//! S3 冷归档协议适配层 +//! +//! 职责: +//! - StorageClass 映射: 所有对象写入即 ColdPending +//! - RestoreObject 请求解析 (Days, Tier) +//! - x-amz-restore 响应头生成 +//! - 错误码映射 (InvalidObjectState, RestoreAlreadyInProgress 等) +//! - GET 行为控制 (冷对象需先 Restore) + +/// S3 错误码 +pub enum S3ErrorCode { + InvalidObjectState, + RestoreAlreadyInProgress, + GlacierExpeditedRetrievalNotAvailable, + NoSuchKey, + NoSuchBucket, +} + +impl S3ErrorCode { + pub fn as_str(&self) -> &'static str { + match self { + S3ErrorCode::InvalidObjectState => "InvalidObjectState", + S3ErrorCode::RestoreAlreadyInProgress => "RestoreAlreadyInProgress", + S3ErrorCode::GlacierExpeditedRetrievalNotAvailable => { + "GlacierExpeditedRetrievalNotAvailable" + } + S3ErrorCode::NoSuchKey => "NoSuchKey", + S3ErrorCode::NoSuchBucket => "NoSuchBucket", + } + } + + pub fn http_status(&self) -> u16 { + match self { + S3ErrorCode::InvalidObjectState => 403, + S3ErrorCode::RestoreAlreadyInProgress => 409, + S3ErrorCode::GlacierExpeditedRetrievalNotAvailable => 503, + S3ErrorCode::NoSuchKey => 404, + S3ErrorCode::NoSuchBucket => 404, + } + } +} + +/// 生成 x-amz-restore 响应头 +pub fn format_restore_header(ongoing: bool, expiry_date: Option<&str>) -> String { + if ongoing { + "ongoing-request=\"true\"".to_string() + } else if let Some(date) = expiry_date { + format!("ongoing-request=\"false\", expiry-date=\"{date}\"") + } else { + "ongoing-request=\"false\"".to_string() + } +} diff --git a/crates/metadata/Cargo.toml b/crates/metadata/Cargo.toml new file mode 100644 index 0000000..a83f3e4 --- /dev/null +++ b/crates/metadata/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "coldstore-metadata" +description = "ColdStore metadata cluster node (Raft + RocksDB)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "coldstore-metadata" +path = "src/main.rs" + +[dependencies] +coldstore-proto = { workspace = true } +coldstore-common = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +prost = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +config = { workspace = true } + +# TODO: Phase 2 - Raft + RocksDB +# openraft = "0.10" +# rocksdb = "0.22" diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs new file mode 100644 index 0000000..04a950e --- /dev/null +++ b/crates/metadata/src/lib.rs @@ -0,0 +1,25 @@ +pub mod service; + +use anyhow::Result; +use coldstore_common::config::MetadataConfig; +use tonic::transport::Server; +use tracing::info; + +pub async fn run(config: MetadataConfig) -> Result<()> { + let addr = config.listen.parse()?; + + let metadata_service = service::MetadataServiceImpl::new(&config).await?; + + info!("Metadata 节点 {} 启动在 {}", config.node_id, addr); + + Server::builder() + .add_service( + coldstore_proto::metadata::metadata_service_server::MetadataServiceServer::new( + metadata_service, + ), + ) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/crates/metadata/src/main.rs b/crates/metadata/src/main.rs new file mode 100644 index 0000000..fac6f5b --- /dev/null +++ b/crates/metadata/src/main.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use coldstore_common::config::MetadataConfig; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "coldstore_metadata=info".into()), + ) + .init(); + + info!("启动 ColdStore Metadata 节点..."); + + // TODO: 从配置文件加载 + let config = MetadataConfig::default(); + + coldstore_metadata::run(config).await +} diff --git a/crates/metadata/src/service.rs b/crates/metadata/src/service.rs new file mode 100644 index 0000000..bdf7aac --- /dev/null +++ b/crates/metadata/src/service.rs @@ -0,0 +1,361 @@ +use anyhow::Result; +use coldstore_common::config::MetadataConfig; +use coldstore_proto::common; +use coldstore_proto::metadata::metadata_service_server::MetadataService; +use coldstore_proto::metadata::*; +use tonic::{Request, Response, Status}; + +pub struct MetadataServiceImpl { + _config: MetadataConfig, + // TODO: Phase 2 — Raft + RocksDB + // raft: Arc>, + // store: Arc, +} + +impl MetadataServiceImpl { + pub async fn new(config: &MetadataConfig) -> Result { + // TODO: 初始化 Raft 集群和 RocksDB + Ok(Self { + _config: config.clone(), + }) + } +} + +#[tonic::async_trait] +impl MetadataService for MetadataServiceImpl { + // ── ObjectApi ── + + async fn put_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn get_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn get_object_version( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn delete_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn head_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_objects( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_storage_class( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_archive_location( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_restore_status( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn scan_cold_pending( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + // ── BucketApi ── + + async fn create_bucket( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn get_bucket( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn delete_bucket( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_buckets( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + // ── ArchiveApi ── + + async fn put_archive_bundle( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn get_archive_bundle( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_archive_bundle_status( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_bundles_by_tape( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn put_archive_task( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn get_archive_task( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_archive_task( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_pending_archive_tasks( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + // ── RecallApi ── + + async fn put_recall_task( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn get_recall_task( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_recall_task( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_pending_recall_tasks( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_recall_tasks_by_tape( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn find_active_recall( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + // ── TapeApi ── + + async fn put_tape( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn get_tape( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_tape( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_tapes( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_tapes_by_status( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + // ── ClusterApi ── + + async fn get_cluster_info( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + async fn register_scheduler_worker( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn deregister_scheduler_worker( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_online_scheduler_workers( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + async fn register_cache_worker( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn deregister_cache_worker( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_online_cache_workers( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + async fn register_tape_worker( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn deregister_tape_worker( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_online_tape_workers( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + + async fn update_worker_status( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn drain_worker( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + // ── HeartbeatApi ── + + async fn heartbeat( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } +} diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml new file mode 100644 index 0000000..b60920a --- /dev/null +++ b/crates/proto/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "coldstore-proto" +description = "ColdStore gRPC protocol definitions" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tonic = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } diff --git a/crates/proto/build.rs b/crates/proto/build.rs new file mode 100644 index 0000000..9e59f63 --- /dev/null +++ b/crates/proto/build.rs @@ -0,0 +1,20 @@ +fn main() -> Result<(), Box> { + let proto_files = &[ + "proto/common.proto", + "proto/metadata.proto", + "proto/scheduler.proto", + "proto/cache.proto", + "proto/tape.proto", + ]; + + tonic_build::configure() + .build_server(true) + .build_client(true) + .compile_protos(proto_files, &["proto/"])?; + + for file in proto_files { + println!("cargo:rerun-if-changed={file}"); + } + + Ok(()) +} diff --git a/crates/proto/proto/cache.proto b/crates/proto/proto/cache.proto new file mode 100644 index 0000000..bd03f1a --- /dev/null +++ b/crates/proto/proto/cache.proto @@ -0,0 +1,226 @@ +syntax = "proto3"; +package coldstore.cache; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +// =========================================================================== +// CacheService — Scheduler Worker → Cache Worker 接口 +// +// Cache Worker 是独立二进制,通过 gRPC 与 Scheduler 通信。 +// 缓存层是纯数据存储,不持有 MetadataClient。 +// +// 职责: +// - 暂存 PutObject 写入的数据(等待归档) +// - 存储解冻后的对象数据(供 GET 快速响应) +// - 缓存淘汰(LRU/LFU/TTL/容量) +// +// 当前后端: 机械硬盘 (HDD) +// 目标后端: SPDK Blobstore (NVMe) +// =========================================================================== +service CacheService { + + // ── 写入接口 (CacheWriteApi) ── + + // 暂存 PutObject 数据(写入即 ColdPending,等待归档调度器消费) + // client streaming: 第一个 chunk 携带元数据,后续为数据块 + rpc PutStaging(stream PutStagingRequest) returns (PutStagingResponse); + + // 写入解冻数据(从磁带取回后写入缓存) + // client streaming: 第一个 chunk 携带元数据,后续为数据块 + rpc PutRestored(stream PutRestoredRequest) returns (google.protobuf.Empty); + + // 删除缓存对象 + rpc Delete(DeleteRequest) returns (google.protobuf.Empty); + + // ── 读取接口 (CacheReadApi) ── + + // 读取缓存对象数据 + // server streaming: 第一个 chunk 携带元数据,后续为数据块 + rpc Get(GetRequest) returns (stream GetResponse); + + // 检查缓存对象是否存在且未过期 + rpc Contains(ContainsRequest) returns (ContainsResponse); + + // 读取暂存数据(归档调度器消费) + // server streaming: 第一个 chunk 携带元数据,后续为数据块 + rpc GetStaging(GetStagingRequest) returns (stream GetStagingResponse); + + // 列出所有暂存对象的 key(归档调度器扫描用) + rpc ListStagingKeys(ListStagingKeysRequest) returns (ListStagingKeysResponse); + + // 删除暂存数据(归档写入磁带成功后清理) + rpc DeleteStaging(DeleteStagingRequest) returns (google.protobuf.Empty); + + // ── 管理接口 ── + + // 获取缓存统计信息 + rpc Stats(google.protobuf.Empty) returns (CacheStats); +} + +// --------------------------------------------------------------------------- +// PutStaging (client streaming) — 暂存 PutObject 数据 +// --------------------------------------------------------------------------- + +message PutStagingRequest { + oneof payload { + PutStagingMeta meta = 1; + bytes data = 2; + } +} + +message PutStagingMeta { + string bucket = 1; + string key = 2; + optional string version_id = 3; + uint64 size = 4; + optional string checksum = 5; + optional string content_type = 6; + optional string etag = 7; +} + +message PutStagingResponse { + string staging_id = 1; +} + +// --------------------------------------------------------------------------- +// PutRestored (client streaming) — 写入解冻数据 +// --------------------------------------------------------------------------- + +message PutRestoredRequest { + oneof payload { + PutRestoredMeta meta = 1; + bytes data = 2; + } +} + +message PutRestoredMeta { + string bucket = 1; + string key = 2; + optional string version_id = 3; + uint64 size = 4; + optional string checksum = 5; + optional string content_type = 6; + optional string etag = 7; + google.protobuf.Timestamp expire_at = 8; +} + +// --------------------------------------------------------------------------- +// Get (server streaming) — 读取缓存对象 +// --------------------------------------------------------------------------- + +message GetRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +message GetResponse { + oneof payload { + CachedObjectMeta meta = 1; + bytes data = 2; + } +} + +message CachedObjectMeta { + uint64 size = 1; + google.protobuf.Timestamp expire_at = 2; + optional string content_type = 3; + optional string etag = 4; + optional string checksum = 5; +} + +// --------------------------------------------------------------------------- +// Contains — 检查缓存是否命中 +// --------------------------------------------------------------------------- + +message ContainsRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +message ContainsResponse { + bool exists = 1; + optional google.protobuf.Timestamp expire_at = 2; +} + +// --------------------------------------------------------------------------- +// Delete — 删除缓存对象 +// --------------------------------------------------------------------------- + +message DeleteRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +// --------------------------------------------------------------------------- +// Staging 操作 — 归档调度器使用 +// --------------------------------------------------------------------------- + +message GetStagingRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +message GetStagingResponse { + oneof payload { + StagingObjectMeta meta = 1; + bytes data = 2; + } +} + +message StagingObjectMeta { + string bucket = 1; + string key = 2; + optional string version_id = 3; + uint64 size = 4; + optional string checksum = 5; + optional string content_type = 6; + optional string etag = 7; + google.protobuf.Timestamp staged_at = 8; +} + +message ListStagingKeysRequest { + uint32 limit = 1; + optional string after = 2; +} + +message ListStagingKeysResponse { + repeated StagingKeyEntry entries = 1; + bool has_more = 2; +} + +message StagingKeyEntry { + string bucket = 1; + string key = 2; + optional string version_id = 3; + uint64 size = 4; + google.protobuf.Timestamp staged_at = 5; +} + +message DeleteStagingRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +// --------------------------------------------------------------------------- +// Stats — 缓存统计 +// --------------------------------------------------------------------------- + +message CacheStats { + uint64 total_capacity = 1; + uint64 used_capacity = 2; + uint64 object_count = 3; + uint64 staging_count = 4; + uint64 staging_bytes = 5; + uint64 restored_count = 6; + uint64 restored_bytes = 7; + uint64 hit_count = 8; + uint64 miss_count = 9; + uint64 evict_count = 10; + uint64 evict_bytes = 11; +} diff --git a/crates/proto/proto/common.proto b/crates/proto/proto/common.proto new file mode 100644 index 0000000..7d9a3b4 --- /dev/null +++ b/crates/proto/proto/common.proto @@ -0,0 +1,262 @@ +syntax = "proto3"; +package coldstore.common; + +import "google/protobuf/timestamp.proto"; + +// --------------------------------------------------------------------------- +// 枚举类型 +// --------------------------------------------------------------------------- + +enum StorageClass { + STORAGE_CLASS_UNSPECIFIED = 0; + COLD_PENDING = 1; // 写入即排队归档 + COLD = 2; // 已归档到磁带 +} + +enum RestoreStatus { + RESTORE_STATUS_UNSPECIFIED = 0; + RESTORE_PENDING = 1; + RESTORE_WAITING_FOR_MEDIA = 2; + RESTORE_IN_PROGRESS = 3; + RESTORE_COMPLETED = 4; + RESTORE_EXPIRED = 5; + RESTORE_FAILED = 6; +} + +enum RestoreTier { + RESTORE_TIER_UNSPECIFIED = 0; + EXPEDITED = 1; + STANDARD = 2; + BULK = 3; +} + +enum TapeStatus { + TAPE_STATUS_UNSPECIFIED = 0; + TAPE_ONLINE = 1; + TAPE_OFFLINE = 2; + TAPE_ERROR = 3; + TAPE_RETIRED = 4; + TAPE_UNKNOWN = 5; +} + +enum DriveStatus { + DRIVE_STATUS_UNSPECIFIED = 0; + DRIVE_IDLE = 1; + DRIVE_IN_USE = 2; + DRIVE_LOADING = 3; + DRIVE_UNLOADING = 4; + DRIVE_ERROR = 5; + DRIVE_OFFLINE = 6; +} + +enum NodeStatus { + NODE_STATUS_UNSPECIFIED = 0; + NODE_ONLINE = 1; + NODE_OFFLINE = 2; + NODE_DRAINING = 3; + NODE_MAINTENANCE = 4; +} + +enum WorkerType { + WORKER_TYPE_UNSPECIFIED = 0; + WORKER_SCHEDULER = 1; + WORKER_CACHE = 2; + WORKER_TAPE = 3; +} + +enum ArchiveBundleStatus { + ARCHIVE_BUNDLE_STATUS_UNSPECIFIED = 0; + BUNDLE_PENDING = 1; + BUNDLE_WRITING = 2; + BUNDLE_COMPLETED = 3; + BUNDLE_FAILED = 4; +} + +enum ArchiveTaskStatus { + ARCHIVE_TASK_STATUS_UNSPECIFIED = 0; + ARCHIVE_TASK_PENDING = 1; + ARCHIVE_TASK_IN_PROGRESS = 2; + ARCHIVE_TASK_COMPLETED = 3; + ARCHIVE_TASK_FAILED = 4; +} + +// --------------------------------------------------------------------------- +// 核心数据结构 +// --------------------------------------------------------------------------- + +message ObjectMetadata { + string bucket = 1; + string key = 2; + optional string version_id = 3; + uint64 size = 4; + string checksum = 5; + optional string content_type = 6; + optional string etag = 7; + StorageClass storage_class = 8; + optional string archive_id = 9; + optional string tape_id = 10; + repeated string tape_set = 11; + optional uint64 tape_block_offset = 12; + optional RestoreStatus restore_status = 13; + optional google.protobuf.Timestamp restore_expire_at = 14; + google.protobuf.Timestamp created_at = 15; + google.protobuf.Timestamp updated_at = 16; +} + +message ArchiveBundle { + string id = 1; + string tape_id = 2; + repeated string tape_set = 3; + repeated BundleEntry entries = 4; + uint64 total_size = 5; + uint32 filemark_start = 6; + uint32 filemark_end = 7; + optional string checksum = 8; + ArchiveBundleStatus status = 9; + google.protobuf.Timestamp created_at = 10; + optional google.protobuf.Timestamp completed_at = 11; +} + +message BundleEntry { + string bucket = 1; + string key = 2; + optional string version_id = 3; + uint64 size = 4; + uint64 offset_in_bundle = 5; + uint64 tape_block_offset = 6; + string checksum = 7; +} + +message TapeInfo { + string id = 1; + optional string barcode = 2; + string format = 3; + TapeStatus status = 4; + optional string location = 5; + uint64 capacity_bytes = 6; + uint64 used_bytes = 7; + uint64 remaining_bytes = 8; + repeated string archive_bundle_ids = 9; + optional google.protobuf.Timestamp last_verified_at = 10; + uint32 error_count = 11; + google.protobuf.Timestamp registered_at = 12; +} + +message RecallTask { + string id = 1; + string bucket = 2; + string key = 3; + optional string version_id = 4; + string archive_id = 5; + string tape_id = 6; + repeated string tape_set = 7; + uint64 tape_block_offset = 8; + uint64 object_size = 9; + string checksum = 10; + RestoreTier tier = 11; + uint32 days = 12; + google.protobuf.Timestamp expire_at = 13; + RestoreStatus status = 14; + optional string drive_id = 15; + uint32 retry_count = 16; + google.protobuf.Timestamp created_at = 17; + optional google.protobuf.Timestamp started_at = 18; + optional google.protobuf.Timestamp completed_at = 19; + optional string error = 20; +} + +message ArchiveTask { + string id = 1; + string bundle_id = 2; + string tape_id = 3; + optional string drive_id = 4; + uint32 object_count = 5; + uint64 total_size = 6; + uint64 bytes_written = 7; + ArchiveTaskStatus status = 8; + uint32 retry_count = 9; + google.protobuf.Timestamp created_at = 10; + optional google.protobuf.Timestamp started_at = 11; + optional google.protobuf.Timestamp completed_at = 12; + optional string error = 13; +} + +message BucketInfo { + string name = 1; + google.protobuf.Timestamp created_at = 2; + optional string owner = 3; + bool versioning_enabled = 4; + uint64 object_count = 5; + uint64 total_size = 6; +} + +// --------------------------------------------------------------------------- +// 集群元数据 +// --------------------------------------------------------------------------- + +message SchedulerWorkerInfo { + uint64 node_id = 1; + string addr = 2; + NodeStatus status = 3; + optional google.protobuf.Timestamp last_heartbeat = 4; + bool is_active = 5; + uint64 pending_archive_tasks = 6; + uint64 pending_recall_tasks = 7; + uint64 active_jobs = 8; + uint64 paired_cache_worker_id = 9; +} + +message CacheWorkerInfo { + uint64 node_id = 1; + string addr = 2; + NodeStatus status = 3; + optional google.protobuf.Timestamp last_heartbeat = 4; + string bdev_name = 5; + uint64 total_capacity = 6; + uint64 used_capacity = 7; + uint64 blob_count = 8; + uint32 io_unit_size = 9; +} + +message TapeWorkerInfo { + uint64 node_id = 1; + string addr = 2; + NodeStatus status = 3; + optional google.protobuf.Timestamp last_heartbeat = 4; + repeated DriveEndpoint drives = 5; + optional LibraryEndpoint library = 6; +} + +message DriveEndpoint { + string drive_id = 1; + string device_path = 2; + string drive_type = 3; + DriveStatus status = 4; + optional string current_tape = 5; +} + +message LibraryEndpoint { + string device_path = 1; + uint32 slot_count = 2; + uint32 import_export_count = 3; + uint32 drive_count = 4; +} + +message ClusterInfo { + string cluster_id = 1; + repeated MetadataNodeInfo metadata_nodes = 2; + repeated SchedulerWorkerInfo scheduler_workers = 3; + repeated CacheWorkerInfo cache_workers = 4; + repeated TapeWorkerInfo tape_workers = 5; + optional uint64 leader_id = 6; + uint64 term = 7; + uint64 committed_index = 8; +} + +message MetadataNodeInfo { + uint64 node_id = 1; + string addr = 2; + string raft_role = 3; + optional google.protobuf.Timestamp last_heartbeat = 4; + NodeStatus status = 5; +} diff --git a/crates/proto/proto/metadata.proto b/crates/proto/proto/metadata.proto new file mode 100644 index 0000000..89c0716 --- /dev/null +++ b/crates/proto/proto/metadata.proto @@ -0,0 +1,288 @@ +syntax = "proto3"; +package coldstore.metadata; + +import "common.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +// =========================================================================== +// MetadataService — 元数据集群对外统一接口 +// +// 消费方: +// - Scheduler Worker: 全部业务读写(唯一业务入口) +// - Console: 集群管理操作 +// - 三类 Worker: 心跳注册 +// =========================================================================== +service MetadataService { + + // ── ObjectApi ── + + rpc PutObject(coldstore.common.ObjectMetadata) returns (google.protobuf.Empty); + rpc GetObject(GetObjectRequest) returns (coldstore.common.ObjectMetadata); + rpc GetObjectVersion(GetObjectVersionRequest) returns (coldstore.common.ObjectMetadata); + rpc DeleteObject(DeleteObjectRequest) returns (google.protobuf.Empty); + rpc HeadObject(HeadObjectRequest) returns (coldstore.common.ObjectMetadata); + rpc ListObjects(ListObjectsRequest) returns (ListObjectsResponse); + rpc UpdateStorageClass(UpdateStorageClassRequest) returns (google.protobuf.Empty); + rpc UpdateArchiveLocation(UpdateArchiveLocationRequest) returns (google.protobuf.Empty); + rpc UpdateRestoreStatus(UpdateRestoreStatusRequest) returns (google.protobuf.Empty); + rpc ScanColdPending(ScanColdPendingRequest) returns (ScanColdPendingResponse); + + // ── BucketApi ── + + rpc CreateBucket(coldstore.common.BucketInfo) returns (google.protobuf.Empty); + rpc GetBucket(GetBucketRequest) returns (coldstore.common.BucketInfo); + rpc DeleteBucket(DeleteBucketRequest) returns (google.protobuf.Empty); + rpc ListBuckets(google.protobuf.Empty) returns (ListBucketsResponse); + + // ── ArchiveApi ── + + rpc PutArchiveBundle(coldstore.common.ArchiveBundle) returns (google.protobuf.Empty); + rpc GetArchiveBundle(GetArchiveBundleRequest) returns (coldstore.common.ArchiveBundle); + rpc UpdateArchiveBundleStatus(UpdateArchiveBundleStatusRequest) returns (google.protobuf.Empty); + rpc ListBundlesByTape(ListBundlesByTapeRequest) returns (ListBundlesByTapeResponse); + rpc PutArchiveTask(coldstore.common.ArchiveTask) returns (google.protobuf.Empty); + rpc GetArchiveTask(GetArchiveTaskRequest) returns (coldstore.common.ArchiveTask); + rpc UpdateArchiveTask(coldstore.common.ArchiveTask) returns (google.protobuf.Empty); + rpc ListPendingArchiveTasks(google.protobuf.Empty) returns (ListArchiveTasksResponse); + + // ── RecallApi ── + + rpc PutRecallTask(coldstore.common.RecallTask) returns (google.protobuf.Empty); + rpc GetRecallTask(GetRecallTaskRequest) returns (coldstore.common.RecallTask); + rpc UpdateRecallTask(coldstore.common.RecallTask) returns (google.protobuf.Empty); + rpc ListPendingRecallTasks(google.protobuf.Empty) returns (ListRecallTasksResponse); + rpc ListRecallTasksByTape(ListRecallTasksByTapeRequest) returns (ListRecallTasksResponse); + rpc FindActiveRecall(FindActiveRecallRequest) returns (FindActiveRecallResponse); + + // ── TapeApi ── + + rpc PutTape(coldstore.common.TapeInfo) returns (google.protobuf.Empty); + rpc GetTape(GetTapeRequest) returns (coldstore.common.TapeInfo); + rpc UpdateTape(coldstore.common.TapeInfo) returns (google.protobuf.Empty); + rpc ListTapes(google.protobuf.Empty) returns (ListTapesResponse); + rpc ListTapesByStatus(ListTapesByStatusRequest) returns (ListTapesResponse); + + // ── ClusterApi ── + + rpc GetClusterInfo(google.protobuf.Empty) returns (coldstore.common.ClusterInfo); + rpc RegisterSchedulerWorker(coldstore.common.SchedulerWorkerInfo) returns (google.protobuf.Empty); + rpc DeregisterSchedulerWorker(DeregisterWorkerRequest) returns (google.protobuf.Empty); + rpc ListOnlineSchedulerWorkers(google.protobuf.Empty) returns (ListSchedulerWorkersResponse); + rpc RegisterCacheWorker(coldstore.common.CacheWorkerInfo) returns (google.protobuf.Empty); + rpc DeregisterCacheWorker(DeregisterWorkerRequest) returns (google.protobuf.Empty); + rpc ListOnlineCacheWorkers(google.protobuf.Empty) returns (ListCacheWorkersResponse); + rpc RegisterTapeWorker(coldstore.common.TapeWorkerInfo) returns (google.protobuf.Empty); + rpc DeregisterTapeWorker(DeregisterWorkerRequest) returns (google.protobuf.Empty); + rpc ListOnlineTapeWorkers(google.protobuf.Empty) returns (ListTapeWorkersResponse); + rpc UpdateWorkerStatus(UpdateWorkerStatusRequest) returns (google.protobuf.Empty); + rpc DrainWorker(DrainWorkerRequest) returns (google.protobuf.Empty); + + // ── HeartbeatApi ── + + rpc Heartbeat(HeartbeatRequest) returns (google.protobuf.Empty); +} + +// --------------------------------------------------------------------------- +// Request / Response 消息 +// --------------------------------------------------------------------------- + +// Object + +message GetObjectRequest { + string bucket = 1; + string key = 2; +} + +message GetObjectVersionRequest { + string bucket = 1; + string key = 2; + string version_id = 3; +} + +message DeleteObjectRequest { + string bucket = 1; + string key = 2; +} + +message HeadObjectRequest { + string bucket = 1; + string key = 2; +} + +message ListObjectsRequest { + string bucket = 1; + optional string prefix = 2; + optional string marker = 3; + uint32 max_keys = 4; +} + +message ListObjectsResponse { + repeated coldstore.common.ObjectMetadata objects = 1; + optional string next_marker = 2; + bool is_truncated = 3; +} + +message UpdateStorageClassRequest { + string bucket = 1; + string key = 2; + coldstore.common.StorageClass storage_class = 3; +} + +message UpdateArchiveLocationRequest { + string bucket = 1; + string key = 2; + string archive_id = 3; + string tape_id = 4; + repeated string tape_set = 5; + uint64 tape_block_offset = 6; +} + +message UpdateRestoreStatusRequest { + string bucket = 1; + string key = 2; + coldstore.common.RestoreStatus status = 3; + optional google.protobuf.Timestamp expire_at = 4; +} + +message ScanColdPendingRequest { + uint32 limit = 1; +} + +message ScanColdPendingResponse { + repeated coldstore.common.ObjectMetadata objects = 1; +} + +// Bucket + +message GetBucketRequest { + string name = 1; +} + +message DeleteBucketRequest { + string name = 1; +} + +message ListBucketsResponse { + repeated coldstore.common.BucketInfo buckets = 1; +} + +// Archive + +message GetArchiveBundleRequest { + string id = 1; +} + +message UpdateArchiveBundleStatusRequest { + string id = 1; + coldstore.common.ArchiveBundleStatus status = 2; +} + +message ListBundlesByTapeRequest { + string tape_id = 1; +} + +message ListBundlesByTapeResponse { + repeated string bundle_ids = 1; +} + +message GetArchiveTaskRequest { + string id = 1; +} + +message ListArchiveTasksResponse { + repeated coldstore.common.ArchiveTask tasks = 1; +} + +// Recall + +message GetRecallTaskRequest { + string id = 1; +} + +message ListRecallTasksByTapeRequest { + string tape_id = 1; +} + +message ListRecallTasksResponse { + repeated coldstore.common.RecallTask tasks = 1; +} + +message FindActiveRecallRequest { + string bucket = 1; + string key = 2; +} + +message FindActiveRecallResponse { + optional coldstore.common.RecallTask task = 1; +} + +// Tape + +message GetTapeRequest { + string tape_id = 1; +} + +message ListTapesResponse { + repeated coldstore.common.TapeInfo tapes = 1; +} + +message ListTapesByStatusRequest { + coldstore.common.TapeStatus status = 1; +} + +// Cluster + +message DeregisterWorkerRequest { + uint64 node_id = 1; +} + +message ListSchedulerWorkersResponse { + repeated coldstore.common.SchedulerWorkerInfo workers = 1; +} + +message ListCacheWorkersResponse { + repeated coldstore.common.CacheWorkerInfo workers = 1; +} + +message ListTapeWorkersResponse { + repeated coldstore.common.TapeWorkerInfo workers = 1; +} + +message UpdateWorkerStatusRequest { + coldstore.common.WorkerType worker_type = 1; + uint64 node_id = 2; + coldstore.common.NodeStatus status = 3; +} + +message DrainWorkerRequest { + coldstore.common.WorkerType worker_type = 1; + uint64 node_id = 2; +} + +// Heartbeat + +message HeartbeatRequest { + coldstore.common.WorkerType worker_type = 1; + uint64 node_id = 2; + oneof payload { + SchedulerHeartbeat scheduler = 3; + CacheHeartbeat cache = 4; + TapeHeartbeat tape = 5; + } +} + +message SchedulerHeartbeat { + uint64 pending_archive_tasks = 1; + uint64 pending_recall_tasks = 2; + uint64 active_jobs = 3; +} + +message CacheHeartbeat { + uint64 used_capacity = 1; + uint64 blob_count = 2; +} + +message TapeHeartbeat { + repeated coldstore.common.DriveEndpoint drives = 1; +} diff --git a/crates/proto/proto/scheduler.proto b/crates/proto/proto/scheduler.proto new file mode 100644 index 0000000..440fa17 --- /dev/null +++ b/crates/proto/proto/scheduler.proto @@ -0,0 +1,199 @@ +syntax = "proto3"; +package coldstore.scheduler; + +import "common.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +// =========================================================================== +// SchedulerService — Gateway → Scheduler Worker 接口 +// +// Gateway 将所有 S3 请求转发给 Scheduler Worker。 +// Scheduler 是唯一业务中枢,负责编排元数据、缓存和磁带操作。 +// =========================================================================== +service SchedulerService { + + // 上传对象:写入即 ColdPending,数据暂存缓存层,等待归档调度 + // 使用 client streaming 支持大对象 + rpc PutObject(stream PutObjectRequest) returns (PutObjectResponse); + + // 下载对象:冷对象需已解冻,从缓存层读取 + // 使用 server streaming 返回大对象数据 + rpc GetObject(GetObjectRequest) returns (stream GetObjectResponse); + + // 查询对象元数据(HEAD) + rpc HeadObject(HeadObjectRequest) returns (HeadObjectResponse); + + // 删除对象 + rpc DeleteObject(DeleteObjectRequest) returns (google.protobuf.Empty); + + // 发起解冻请求(RestoreObject) + rpc RestoreObject(RestoreObjectRequest) returns (RestoreObjectResponse); + + // 列出对象 + rpc ListObjects(ListObjectsRequest) returns (ListObjectsResponse); + + // 桶操作 + rpc CreateBucket(CreateBucketRequest) returns (google.protobuf.Empty); + rpc DeleteBucket(DeleteBucketRequest) returns (google.protobuf.Empty); + rpc HeadBucket(HeadBucketRequest) returns (google.protobuf.Empty); + rpc ListBuckets(google.protobuf.Empty) returns (ListBucketsResponse); +} + +// --------------------------------------------------------------------------- +// PutObject (client streaming) +// 第一个 chunk 携带元数据,后续 chunk 携带数据 +// --------------------------------------------------------------------------- + +message PutObjectRequest { + oneof payload { + PutObjectMeta meta = 1; + bytes data = 2; + } +} + +message PutObjectMeta { + string bucket = 1; + string key = 2; + uint64 content_length = 3; + optional string content_type = 4; + optional string checksum_sha256 = 5; +} + +message PutObjectResponse { + string etag = 1; + string version_id = 2; +} + +// --------------------------------------------------------------------------- +// GetObject (server streaming) +// 第一个 chunk 携带元数据,后续 chunk 携带数据 +// --------------------------------------------------------------------------- + +message GetObjectRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +message GetObjectResponse { + oneof payload { + GetObjectMeta meta = 1; + bytes data = 2; + } +} + +message GetObjectMeta { + uint64 content_length = 1; + optional string content_type = 2; + string etag = 3; + coldstore.common.StorageClass storage_class = 4; + optional string restore_info = 5; + google.protobuf.Timestamp last_modified = 6; +} + +// --------------------------------------------------------------------------- +// HeadObject +// --------------------------------------------------------------------------- + +message HeadObjectRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +message HeadObjectResponse { + uint64 content_length = 1; + optional string content_type = 2; + string etag = 3; + coldstore.common.StorageClass storage_class = 4; + optional string restore_info = 5; + google.protobuf.Timestamp last_modified = 6; +} + +// --------------------------------------------------------------------------- +// DeleteObject +// --------------------------------------------------------------------------- + +message DeleteObjectRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; +} + +// --------------------------------------------------------------------------- +// RestoreObject +// --------------------------------------------------------------------------- + +message RestoreObjectRequest { + string bucket = 1; + string key = 2; + optional string version_id = 3; + uint32 days = 4; + coldstore.common.RestoreTier tier = 5; +} + +message RestoreObjectResponse { + // 202 = accepted (新请求), 200 = already restored (延长) + uint32 status_code = 1; +} + +// --------------------------------------------------------------------------- +// ListObjects +// --------------------------------------------------------------------------- + +message ListObjectsRequest { + string bucket = 1; + optional string prefix = 2; + optional string marker = 3; + optional string delimiter = 4; + uint32 max_keys = 5; +} + +message ListObjectsResponse { + string bucket = 1; + optional string prefix = 2; + optional string marker = 3; + optional string next_marker = 4; + uint32 max_keys = 5; + bool is_truncated = 6; + repeated ObjectEntry contents = 7; + repeated CommonPrefix common_prefixes = 8; +} + +message ObjectEntry { + string key = 1; + google.protobuf.Timestamp last_modified = 2; + string etag = 3; + uint64 size = 4; + string storage_class = 5; +} + +message CommonPrefix { + string prefix = 1; +} + +// --------------------------------------------------------------------------- +// Bucket 操作 +// --------------------------------------------------------------------------- + +message CreateBucketRequest { + string bucket = 1; +} + +message DeleteBucketRequest { + string bucket = 1; +} + +message HeadBucketRequest { + string bucket = 1; +} + +message ListBucketsResponse { + repeated BucketEntry buckets = 1; +} + +message BucketEntry { + string name = 1; + google.protobuf.Timestamp creation_date = 2; +} diff --git a/crates/proto/proto/tape.proto b/crates/proto/proto/tape.proto new file mode 100644 index 0000000..1450674 --- /dev/null +++ b/crates/proto/proto/tape.proto @@ -0,0 +1,197 @@ +syntax = "proto3"; +package coldstore.tape; + +import "common.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +// =========================================================================== +// TapeService — Scheduler Worker → Tape Worker 接口 +// +// Tape Worker 独立部署于挂载磁带驱动和带库的物理节点上。 +// 通过 gRPC 接收 Scheduler 的读写指令。 +// 纯硬件抽象层,不持有 MetadataClient。 +// =========================================================================== +service TapeService { + + // ── 数据读写 ── + + // 写入归档包到磁带(顺序写入) + // client streaming: 第一个 chunk 携带写入元数据,后续为数据块 + // 返回写入结果(filemark 位置、校验和等) + rpc WriteBundle(stream WriteBundleRequest) returns (WriteBundleResponse); + + // 从磁带读取归档包(或单个对象) + // 返回 server streaming 数据 + rpc ReadBundle(ReadBundleRequest) returns (stream ReadBundleResponse); + + // ── 驱动管理 ── + + // 列出所有磁带驱动 + rpc ListDrives(google.protobuf.Empty) returns (ListDrivesResponse); + + // 获取指定驱动状态 + rpc GetDriveStatus(GetDriveStatusRequest) returns (coldstore.common.DriveEndpoint); + + // 分配驱动(锁定驱动供独占使用) + rpc AcquireDrive(AcquireDriveRequest) returns (AcquireDriveResponse); + + // 释放驱动 + rpc ReleaseDrive(ReleaseDriveRequest) returns (google.protobuf.Empty); + + // ── 磁带操作 ── + + // 加载磁带到驱动(带库场景) + rpc LoadTape(LoadTapeRequest) returns (google.protobuf.Empty); + + // 从驱动卸载磁带 + rpc UnloadTape(UnloadTapeRequest) returns (google.protobuf.Empty); + + // 回卷磁带 + rpc Rewind(RewindRequest) returns (google.protobuf.Empty); + + // 定位到指定 filemark + rpc SeekToFilemark(SeekToFilemarkRequest) returns (google.protobuf.Empty); + + // 获取磁带状态(剩余空间、当前位置等) + rpc GetTapeMediaStatus(GetTapeMediaStatusRequest) returns (TapeMediaStatus); + + // 磁带库清点(inventory) + rpc Inventory(google.protobuf.Empty) returns (InventoryResponse); +} + +// --------------------------------------------------------------------------- +// WriteBundle (client streaming) — 写入归档包 +// --------------------------------------------------------------------------- + +message WriteBundleRequest { + oneof payload { + WriteBundleMeta meta = 1; + bytes data = 2; + } +} + +message WriteBundleMeta { + string drive_id = 1; + string bundle_id = 2; + uint64 total_size = 3; + uint32 object_count = 4; + uint32 block_size = 5; +} + +message WriteBundleResponse { + string drive_id = 1; + string bundle_id = 2; + uint64 bytes_written = 3; + uint32 filemark_start = 4; + uint32 filemark_end = 5; + optional string checksum = 6; + bool success = 7; + optional string error = 8; +} + +// --------------------------------------------------------------------------- +// ReadBundle (server streaming) — 读取归档包/对象 +// --------------------------------------------------------------------------- + +message ReadBundleRequest { + string drive_id = 1; + // 定位方式: filemark 或 block offset + oneof location { + uint32 filemark = 2; + uint64 block_offset = 3; + } + uint64 length = 4; +} + +message ReadBundleResponse { + oneof payload { + ReadBundleMeta meta = 1; + bytes data = 2; + } +} + +message ReadBundleMeta { + uint64 total_size = 1; + optional string checksum = 2; +} + +// --------------------------------------------------------------------------- +// 驱动管理 +// --------------------------------------------------------------------------- + +message ListDrivesResponse { + repeated coldstore.common.DriveEndpoint drives = 1; +} + +message GetDriveStatusRequest { + string drive_id = 1; +} + +message AcquireDriveRequest { + optional string preferred_drive_id = 1; + optional string required_tape_id = 2; + uint32 priority = 3; + uint32 timeout_secs = 4; +} + +message AcquireDriveResponse { + string drive_id = 1; + optional string current_tape = 2; +} + +message ReleaseDriveRequest { + string drive_id = 1; +} + +// --------------------------------------------------------------------------- +// 磁带操作 +// --------------------------------------------------------------------------- + +message LoadTapeRequest { + string tape_id = 1; + string drive_id = 2; + optional string slot_id = 3; +} + +message UnloadTapeRequest { + string drive_id = 1; + optional string target_slot_id = 2; +} + +message RewindRequest { + string drive_id = 1; +} + +message SeekToFilemarkRequest { + string drive_id = 1; + uint32 filemark = 2; +} + +message GetTapeMediaStatusRequest { + string drive_id = 1; +} + +message TapeMediaStatus { + string drive_id = 1; + optional string tape_id = 2; + coldstore.common.TapeStatus tape_status = 3; + uint64 capacity_bytes = 4; + uint64 used_bytes = 5; + uint64 remaining_bytes = 6; + uint64 current_position = 7; + uint32 current_filemark = 8; + bool is_write_protected = 9; +} + +message InventoryResponse { + repeated SlotInfo slots = 1; +} + +message SlotInfo { + string slot_id = 1; + optional string tape_id = 2; + bool is_drive = 3; + optional string drive_id = 4; + bool is_import_export = 5; +} diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml new file mode 100644 index 0000000..8f7fd8b --- /dev/null +++ b/crates/scheduler/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "coldstore-scheduler" +description = "ColdStore scheduler worker (business orchestration hub)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "coldstore-scheduler" +path = "src/main.rs" + +[dependencies] +coldstore-proto = { workspace = true } +coldstore-common = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +prost = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +config = { workspace = true } + +tokio-stream = { workspace = true } diff --git a/crates/scheduler/src/lib.rs b/crates/scheduler/src/lib.rs new file mode 100644 index 0000000..bba2c2b --- /dev/null +++ b/crates/scheduler/src/lib.rs @@ -0,0 +1,45 @@ +pub mod service; + +use anyhow::Result; +use coldstore_common::config::SchedulerConfig; +use coldstore_proto::cache::cache_service_client::CacheServiceClient; +use coldstore_proto::metadata::metadata_service_client::MetadataServiceClient; +use coldstore_proto::tape::tape_service_client::TapeServiceClient; +use tonic::transport::{Channel, Server}; +use tracing::info; + +pub struct SchedulerState { + pub metadata: MetadataServiceClient, + pub cache: Option>, + pub tape: Option>, + pub config: SchedulerConfig, +} + +pub async fn run(config: SchedulerConfig) -> Result<()> { + let addr = config.listen.parse()?; + + let metadata_addr = format!("http://{}", &config.metadata_addrs[0]); + let metadata = MetadataServiceClient::connect(metadata_addr).await?; + + let state = std::sync::Arc::new(SchedulerState { + metadata, + cache: None, + tape: None, + config: config.clone(), + }); + + let scheduler_service = service::SchedulerServiceImpl::new(state); + + info!("Scheduler Worker 启动在 {}", config.listen); + + Server::builder() + .add_service( + coldstore_proto::scheduler::scheduler_service_server::SchedulerServiceServer::new( + scheduler_service, + ), + ) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/crates/scheduler/src/main.rs b/crates/scheduler/src/main.rs new file mode 100644 index 0000000..2c850b2 --- /dev/null +++ b/crates/scheduler/src/main.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use coldstore_common::config::SchedulerConfig; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "coldstore_scheduler=info".into()), + ) + .init(); + + info!("启动 ColdStore Scheduler Worker..."); + + let config = SchedulerConfig::default(); + + coldstore_scheduler::run(config).await +} diff --git a/crates/scheduler/src/service.rs b/crates/scheduler/src/service.rs new file mode 100644 index 0000000..91934e7 --- /dev/null +++ b/crates/scheduler/src/service.rs @@ -0,0 +1,107 @@ +use crate::SchedulerState; +use coldstore_proto::scheduler::scheduler_service_server::SchedulerService; +use coldstore_proto::scheduler::*; +use std::sync::Arc; +use tonic::{Request, Response, Status, Streaming}; + +pub struct SchedulerServiceImpl { + _state: Arc, +} + +impl SchedulerServiceImpl { + pub fn new(state: Arc) -> Self { + Self { _state: state } + } +} + +#[tonic::async_trait] +impl SchedulerService for SchedulerServiceImpl { + async fn put_object( + &self, + _request: Request>, + ) -> std::result::Result, Status> { + // 1. 从 stream 接收元数据和数据 + // 2. 写入 Metadata (PutObject, storage_class = ColdPending) + // 3. 将数据暂存到 Cache Worker (PutStaging) + // 4. 返回 ETag + todo!() + } + + type GetObjectStream = + tokio_stream::wrappers::ReceiverStream>; + + async fn get_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + // 1. 查询 Metadata (HeadObject) + // 2. 检查 storage_class 和 restore_status + // 3. 若 Cold + Completed: 从 Cache Worker 读取数据 + // 4. 若 Cold + 未解冻: 返回 InvalidObjectState + // 5. Stream 返回数据 + todo!() + } + + async fn head_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + // 查询 Metadata, 生成 x-amz-restore 头信息 + todo!() + } + + async fn delete_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + // 1. 删除 Metadata 中的对象 + // 2. 清理 Cache Worker 中的缓存/暂存数据 + todo!() + } + + async fn restore_object( + &self, + _request: Request, + ) -> std::result::Result, Status> { + // 1. 检查对象是否为 Cold 状态 + // 2. 线性读检查是否已有进行中的 Restore + // 3. 创建 RecallTask 写入 Metadata + // 4. 返回 202 Accepted + todo!() + } + + async fn list_objects( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn create_bucket( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn delete_bucket( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn head_bucket( + &self, + _request: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_buckets( + &self, + _request: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } +} diff --git a/crates/tape/Cargo.toml b/crates/tape/Cargo.toml new file mode 100644 index 0000000..f21fcce --- /dev/null +++ b/crates/tape/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "coldstore-tape" +description = "ColdStore tape worker (SCSI tape drive management)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "coldstore-tape" +path = "src/main.rs" + +[dependencies] +coldstore-proto = { workspace = true } +coldstore-common = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +prost = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +config = { workspace = true } + +# TODO: Phase 4 - Linux SCSI ioctl +# nix = { version = "0.29", features = ["ioctl"] } + +tokio-stream = { workspace = true } diff --git a/crates/tape/src/lib.rs b/crates/tape/src/lib.rs new file mode 100644 index 0000000..793d262 --- /dev/null +++ b/crates/tape/src/lib.rs @@ -0,0 +1,17 @@ +pub mod service; + +use anyhow::Result; +use coldstore_common::config::TapeConfig; +use tonic::transport::Server; +use tracing::info; + +pub async fn run(config: TapeConfig) -> Result<()> { + let addr = config.listen.parse()?; + let svc = service::TapeServiceImpl::new(&config)?; + info!("Tape Worker started on {}", config.listen); + Server::builder() + .add_service(coldstore_proto::tape::tape_service_server::TapeServiceServer::new(svc)) + .serve(addr) + .await?; + Ok(()) +} diff --git a/crates/tape/src/main.rs b/crates/tape/src/main.rs new file mode 100644 index 0000000..5f6e098 --- /dev/null +++ b/crates/tape/src/main.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use coldstore_common::config::TapeConfig; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "coldstore_tape=info".into()), + ) + .init(); + info!("Starting ColdStore Tape Worker..."); + coldstore_tape::run(TapeConfig::default()).await +} diff --git a/crates/tape/src/service.rs b/crates/tape/src/service.rs new file mode 100644 index 0000000..7432f48 --- /dev/null +++ b/crates/tape/src/service.rs @@ -0,0 +1,97 @@ +use coldstore_common::config::TapeConfig; +use coldstore_proto::common; +use coldstore_proto::tape::tape_service_server::TapeService; +use coldstore_proto::tape::*; +use tonic::{Request, Response, Status, Streaming}; + +pub struct TapeServiceImpl { + _config: TapeConfig, +} + +impl TapeServiceImpl { + pub fn new(config: &TapeConfig) -> anyhow::Result { + Ok(Self { + _config: config.clone(), + }) + } +} + +#[tonic::async_trait] +impl TapeService for TapeServiceImpl { + async fn write_bundle( + &self, + _req: Request>, + ) -> std::result::Result, Status> { + todo!() + } + + type ReadBundleStream = + tokio_stream::wrappers::ReceiverStream>; + async fn read_bundle( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + + async fn list_drives( + &self, + _req: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } + async fn get_drive_status( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn acquire_drive( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn release_drive( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn load_tape( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn unload_tape( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn rewind( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn seek_to_filemark( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn get_tape_media_status( + &self, + _req: Request, + ) -> std::result::Result, Status> { + todo!() + } + async fn inventory( + &self, + _req: Request<()>, + ) -> std::result::Result, Status> { + todo!() + } +} diff --git a/src/cache/manager.rs b/src/cache/manager.rs deleted file mode 100644 index 0cd5525..0000000 --- a/src/cache/manager.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::error::Result; -use lru::LruCache; -use std::num::NonZeroUsize; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::RwLock; - -pub struct CacheManager { - cache: Arc>>, - base_path: PathBuf, - max_size_bytes: u64, - ttl_secs: u64, -} - -struct CachedObject { - path: PathBuf, - size: u64, - cached_at: chrono::DateTime, -} - -impl CacheManager { - pub fn new(base_path: PathBuf, max_size_gb: u64, ttl_secs: u64) -> Result { - // 创建缓存目录 - std::fs::create_dir_all(&base_path)?; - - // 估算最大缓存项数(假设平均对象大小 10MB) - let estimated_max_items = - (max_size_gb * 1024 * 1024 * 1024 / (10 * 1024 * 1024)).max(1000) as usize; - - let cache = Arc::new(RwLock::new(LruCache::new( - NonZeroUsize::new(estimated_max_items).unwrap(), - ))); - - Ok(Self { - cache, - base_path, - max_size_bytes: max_size_gb * 1024 * 1024 * 1024, - ttl_secs, - }) - } - - /// 获取缓存对象 - pub async fn get(&self, key: &str) -> Result>> { - let mut cache = self.cache.write().await; - - if let Some(cached) = cache.get(key) { - // 检查是否过期 - let now = chrono::Utc::now(); - let age = now.signed_duration_since(cached.cached_at); - - if age.num_seconds() < self.ttl_secs as i64 { - // 从文件系统读取 - let data = std::fs::read(&cached.path)?; - return Ok(Some(data)); - } else { - // 过期,移除 - cache.pop(key); - } - } - - Ok(None) - } - - /// 写入缓存 - pub async fn put(&self, key: &str, data: Vec) -> Result<()> { - // TODO: 实现缓存写入逻辑 - // 1. 检查缓存大小限制 - // 2. 写入文件系统 - // 3. 更新 LRU 缓存索引 - // 4. 如果超过限制,执行淘汰 - - let file_path = self.base_path.join(format!("{}.cache", key)); - std::fs::write(&file_path, &data)?; - - let cached = CachedObject { - path: file_path, - size: data.len() as u64, - cached_at: chrono::Utc::now(), - }; - - let mut cache = self.cache.write().await; - cache.put(key.to_string(), cached); - - Ok(()) - } - - /// 删除缓存 - pub async fn evict(&self, key: &str) -> Result<()> { - let mut cache = self.cache.write().await; - - if let Some(cached) = cache.pop(key) { - let _ = std::fs::remove_file(&cached.path); - } - - Ok(()) - } - - /// 清理过期缓存 - pub async fn cleanup_expired(&self) -> Result { - // TODO: 实现过期缓存清理 - let mut cache = self.cache.write().await; - let now = chrono::Utc::now(); - let mut removed = 0; - - // 注意:LRU 缓存不支持直接遍历,需要特殊处理 - // 这里简化处理,实际应该维护一个过期时间索引 - - Ok(removed) - } -} diff --git a/src/cache/mod.rs b/src/cache/mod.rs deleted file mode 100644 index 3941121..0000000 --- a/src/cache/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod manager; - -pub use manager::CacheManager; diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index e59f570..0000000 --- a/src/config.rs +++ /dev/null @@ -1,163 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - pub server: ServerConfig, - pub metadata: MetadataConfig, - pub scheduler: SchedulerConfig, - pub cache: CacheConfig, - pub tape: TapeConfig, - pub notification: NotificationConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ServerConfig { - pub host: String, - pub port: u16, - pub s3_endpoint: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MetadataConfig { - pub backend: MetadataBackend, - pub postgres: Option, - pub etcd: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum MetadataBackend { - Postgres, - Etcd, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PostgresConfig { - pub url: String, - pub max_connections: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EtcdConfig { - pub endpoints: Vec, - pub timeout_secs: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SchedulerConfig { - pub archive: ArchiveSchedulerConfig, - pub recall: RecallSchedulerConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ArchiveSchedulerConfig { - pub scan_interval_secs: u64, - pub batch_size: usize, - pub min_archive_size_mb: u64, - pub target_throughput_mbps: u64, // 目标吞吐量 MB/s -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecallSchedulerConfig { - pub queue_size: usize, - pub max_concurrent_restores: usize, - pub restore_timeout_secs: u64, - pub min_restore_interval_secs: u64, // 最小取回间隔(5分钟) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CacheConfig { - pub enabled: bool, - pub path: PathBuf, - pub max_size_gb: u64, - pub ttl_secs: u64, - pub eviction_policy: EvictionPolicy, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum EvictionPolicy { - Lru, - Lfu, - Ttl, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TapeConfig { - pub library_path: Option, - pub supported_formats: Vec, // e.g., ["LTO-9", "LTO-10"] - pub replication_factor: u32, - pub verify_readability: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NotificationConfig { - pub enabled: bool, - pub webhook_url: Option, - pub mq_endpoint: Option, -} - -impl Config { - pub fn load() -> crate::error::Result { - let config_path = - std::env::var("COLDSTORE_CONFIG").unwrap_or_else(|_| "config/config.yaml".to_string()); - - let settings = config::Config::builder() - .add_source(config::File::with_name(&config_path).required(false)) - .add_source(config::Environment::with_prefix("COLDSTORE")) - .build()?; - - Ok(settings.try_deserialize()?) - } -} - -impl Default for Config { - fn default() -> Self { - Self { - server: ServerConfig { - host: "0.0.0.0".to_string(), - port: 9000, - s3_endpoint: "http://localhost:9000".to_string(), - }, - metadata: MetadataConfig { - backend: MetadataBackend::Postgres, - postgres: Some(PostgresConfig { - url: "postgresql://localhost/coldstore".to_string(), - max_connections: 10, - }), - etcd: None, - }, - scheduler: SchedulerConfig { - archive: ArchiveSchedulerConfig { - scan_interval_secs: 3600, - batch_size: 1000, - min_archive_size_mb: 100, - target_throughput_mbps: 300, - }, - recall: RecallSchedulerConfig { - queue_size: 10000, - max_concurrent_restores: 10, - restore_timeout_secs: 3600, - min_restore_interval_secs: 300, // 5分钟 - }, - }, - cache: CacheConfig { - enabled: true, - path: PathBuf::from("/var/cache/coldstore"), - max_size_gb: 100, - ttl_secs: 86400, // 24小时 - eviction_policy: EvictionPolicy::Lru, - }, - tape: TapeConfig { - library_path: None, - supported_formats: vec!["LTO-9".to_string(), "LTO-10".to_string()], - replication_factor: 2, - verify_readability: true, - }, - notification: NotificationConfig { - enabled: true, - webhook_url: None, - mq_endpoint: None, - }, - } - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 7a2f206..0000000 --- a/src/error.rs +++ /dev/null @@ -1,48 +0,0 @@ -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Error, Debug)] -pub enum Error { - #[error("配置错误: {0}")] - Config(#[from] config::ConfigError), - - #[error("IO 错误: {0}")] - Io(#[from] std::io::Error), - - #[error("数据库错误: {0}")] - Database(#[from] sqlx::Error), - - #[error("序列化错误: {0}")] - Serialization(#[from] serde_json::Error), - - #[error("S3 协议错误: {0}")] - S3(String), - - #[error("元数据错误: {0}")] - Metadata(String), - - #[error("调度器错误: {0}")] - Scheduler(String), - - #[error("磁带操作错误: {0}")] - Tape(String), - - #[error("缓存错误: {0}")] - Cache(String), - - #[error("通知错误: {0}")] - Notification(String), - - #[error("对象不存在")] - ObjectNotFound, - - #[error("对象状态无效: {0}")] - InvalidObjectState(String), - - #[error("磁带离线: {0}")] - TapeOffline(String), - - #[error("内部错误: {0}")] - Internal(String), -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index e8fb948..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod cache; -pub mod config; -pub mod error; -pub mod metadata; -pub mod models; -pub mod notification; -pub mod s3; -pub mod scheduler; -pub mod tape; - -pub use error::{Error, Result}; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index bc90555..0000000 --- a/src/main.rs +++ /dev/null @@ -1,39 +0,0 @@ -use coldstore::config::Config; -use coldstore::error::Result; -use tracing::info; - -#[tokio::main] -async fn main() -> Result<()> { - // 初始化日志 - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "coldstore=info".into()), - ) - .init(); - - info!("启动 ColdStore 冷存储系统..."); - - // 加载配置 - let _config = Config::load()?; - info!("配置加载完成"); - - // TODO: 初始化各个服务组件 - // - S3 接入层 - // - 元数据服务 - // - 归档调度器 - // - 取回调度器 - // - 缓存层 - // - 磁带管理 - // - 通知服务 - - info!("ColdStore 系统启动完成"); - - // 运行主循环 - tokio::signal::ctrl_c() - .await - .expect("无法注册 Ctrl+C 处理器"); - - info!("收到停止信号,正在关闭..."); - Ok(()) -} diff --git a/src/metadata/backend.rs b/src/metadata/backend.rs deleted file mode 100644 index d4016da..0000000 --- a/src/metadata/backend.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::config::MetadataConfig; -use crate::error::Result; -use crate::models::ObjectMetadata; - -pub enum MetadataBackend { - Postgres(PostgresBackend), - Etcd(EtcdBackend), -} - -impl MetadataBackend { - pub async fn from_config(config: &MetadataConfig) -> Result { - match config.backend { - crate::config::MetadataBackend::Postgres => { - let pg_config = config.postgres.as_ref().ok_or_else(|| { - crate::error::Error::Config(config::ConfigError::Message( - "PostgreSQL 配置缺失".to_string(), - )) - })?; - Ok(Self::Postgres( - PostgresBackend::new(pg_config.url.clone()).await?, - )) - } - crate::config::MetadataBackend::Etcd => { - let etcd_config = config.etcd.as_ref().ok_or_else(|| { - crate::error::Error::Config(config::ConfigError::Message( - "Etcd 配置缺失".to_string(), - )) - })?; - Ok(Self::Etcd( - EtcdBackend::new(etcd_config.endpoints.clone()).await?, - )) - } - } - } - - pub async fn get_object(&self, bucket: &str, key: &str) -> Result { - match self { - Self::Postgres(backend) => backend.get_object(bucket, key).await, - Self::Etcd(backend) => backend.get_object(bucket, key).await, - } - } - - pub async fn put_object(&self, metadata: ObjectMetadata) -> Result<()> { - match self { - Self::Postgres(backend) => backend.put_object(metadata).await, - Self::Etcd(backend) => backend.put_object(metadata).await, - } - } - - pub async fn delete_object(&self, bucket: &str, key: &str) -> Result<()> { - match self { - Self::Postgres(backend) => backend.delete_object(bucket, key).await, - Self::Etcd(backend) => backend.delete_object(bucket, key).await, - } - } - - pub async fn list_objects( - &self, - bucket: &str, - prefix: Option<&str>, - max_keys: Option, - ) -> Result> { - match self { - Self::Postgres(backend) => backend.list_objects(bucket, prefix, max_keys).await, - Self::Etcd(backend) => backend.list_objects(bucket, prefix, max_keys).await, - } - } - - pub async fn update_storage_class( - &self, - bucket: &str, - key: &str, - storage_class: crate::models::StorageClass, - ) -> Result<()> { - match self { - Self::Postgres(backend) => { - backend - .update_storage_class(bucket, key, storage_class) - .await - } - Self::Etcd(backend) => { - backend - .update_storage_class(bucket, key, storage_class) - .await - } - } - } -} - -pub struct PostgresBackend { - pool: sqlx::PgPool, -} - -impl PostgresBackend { - pub async fn new(url: String) -> Result { - let pool = sqlx::PgPool::connect(&url).await?; - // TODO: 运行数据库迁移 - Ok(Self { pool }) - } - - pub async fn get_object(&self, bucket: &str, key: &str) -> Result { - // TODO: 实现 PostgreSQL 查询 - todo!() - } - - pub async fn put_object(&self, metadata: ObjectMetadata) -> Result<()> { - // TODO: 实现 PostgreSQL 插入/更新 - todo!() - } - - pub async fn delete_object(&self, bucket: &str, key: &str) -> Result<()> { - // TODO: 实现 PostgreSQL 删除 - todo!() - } - - pub async fn list_objects( - &self, - bucket: &str, - prefix: Option<&str>, - max_keys: Option, - ) -> Result> { - // TODO: 实现 PostgreSQL 列表查询 - todo!() - } - - pub async fn update_storage_class( - &self, - bucket: &str, - key: &str, - storage_class: crate::models::StorageClass, - ) -> Result<()> { - // TODO: 实现 PostgreSQL 更新 - todo!() - } -} - -pub struct EtcdBackend { - // TODO: 实现 Etcd 客户端 - // client: etcd_rs::Client, - endpoints: Vec, -} - -impl EtcdBackend { - pub async fn new(endpoints: Vec) -> Result { - // TODO: 实现 Etcd 连接 - // let client = etcd_rs::Client::connect(etcd_rs::ClientConfig { - // endpoints, - // auth: None, - // tls: None, - // }) - // .await - // .map_err(|e| crate::error::Error::Internal(format!("Etcd 连接失败: {}", e)))?; - - Ok(Self { endpoints }) - } - - pub async fn get_object(&self, bucket: &str, key: &str) -> Result { - // TODO: 实现 Etcd 查询 - todo!() - } - - pub async fn put_object(&self, metadata: ObjectMetadata) -> Result<()> { - // TODO: 实现 Etcd 写入 - todo!() - } - - pub async fn delete_object(&self, bucket: &str, key: &str) -> Result<()> { - // TODO: 实现 Etcd 删除 - todo!() - } - - pub async fn list_objects( - &self, - bucket: &str, - prefix: Option<&str>, - max_keys: Option, - ) -> Result> { - // TODO: 实现 Etcd 列表查询 - todo!() - } - - pub async fn update_storage_class( - &self, - bucket: &str, - key: &str, - storage_class: crate::models::StorageClass, - ) -> Result<()> { - // TODO: 实现 Etcd 更新 - todo!() - } -} diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs deleted file mode 100644 index e82e6de..0000000 --- a/src/metadata/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod backend; -pub mod service; - -pub use service::MetadataService; diff --git a/src/metadata/service.rs b/src/metadata/service.rs deleted file mode 100644 index 6e1e91a..0000000 --- a/src/metadata/service.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::error::Result; -use crate::metadata::backend::MetadataBackend; -use crate::models::ObjectMetadata; - -pub struct MetadataService { - backend: MetadataBackend, -} - -impl MetadataService { - pub fn new(backend: MetadataBackend) -> Self { - Self { backend } - } - - /// 获取对象元数据 - pub async fn get_object(&self, bucket: &str, key: &str) -> Result { - self.backend.get_object(bucket, key).await - } - - /// 创建或更新对象元数据 - pub async fn put_object(&self, metadata: ObjectMetadata) -> Result<()> { - self.backend.put_object(metadata).await - } - - /// 删除对象元数据 - pub async fn delete_object(&self, bucket: &str, key: &str) -> Result<()> { - self.backend.delete_object(bucket, key).await - } - - /// 列出对象 - pub async fn list_objects( - &self, - bucket: &str, - prefix: Option<&str>, - max_keys: Option, - ) -> Result> { - self.backend.list_objects(bucket, prefix, max_keys).await - } - - /// 更新对象存储类别 - pub async fn update_storage_class( - &self, - bucket: &str, - key: &str, - storage_class: crate::models::StorageClass, - ) -> Result<()> { - self.backend - .update_storage_class(bucket, key, storage_class) - .await - } -} diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index eb418fb..0000000 --- a/src/models.rs +++ /dev/null @@ -1,117 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// 存储类别 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum StorageClass { - Hot, - Warm, - Cold, - ColdPending, -} - -/// 解冻状态 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum RestoreStatus { - Pending, - InProgress, - Completed, - Expired, - Failed, -} - -/// 磁带状态 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum TapeStatus { - Online, - Offline, - Unknown, - Error, -} - -/// 对象元数据 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ObjectMetadata { - pub bucket: String, - pub object_key: String, - pub version: Option, - pub storage_class: StorageClass, - pub archive_id: Option, - pub tape_id: Option, - pub tape_set: Option>, - pub checksum: String, - pub size: u64, - pub restore_status: Option, - pub restore_expire_at: Option>, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -/// 归档包(Archive Bundle) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ArchiveBundle { - pub id: Uuid, - pub tape_id: String, - pub object_keys: Vec, - pub total_size: u64, - pub created_at: DateTime, - pub status: ArchiveBundleStatus, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ArchiveBundleStatus { - Pending, - Writing, - Completed, - Failed, -} - -/// 磁带信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TapeInfo { - pub id: String, - pub format: String, // e.g., "LTO-9", "LTO-10" - pub status: TapeStatus, - pub location: Option, - pub capacity_bytes: Option, - pub used_bytes: Option, - pub archive_bundles: Vec, - pub last_verified_at: Option>, -} - -/// 取回任务 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecallTask { - pub id: Uuid, - pub object_key: String, - pub bucket: String, - pub archive_id: Uuid, - pub tape_id: String, - pub status: RestoreStatus, - pub priority: u32, - pub created_at: DateTime, - pub started_at: Option>, - pub completed_at: Option>, - pub error: Option, -} - -/// 归档任务 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ArchiveTask { - pub id: Uuid, - pub object_keys: Vec, - pub archive_bundle_id: Uuid, - pub status: ArchiveTaskStatus, - pub created_at: DateTime, - pub started_at: Option>, - pub completed_at: Option>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ArchiveTaskStatus { - Pending, - InProgress, - Completed, - Failed, -} diff --git a/src/notification/mod.rs b/src/notification/mod.rs deleted file mode 100644 index 9508f0f..0000000 --- a/src/notification/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod service; - -pub use service::NotificationService; diff --git a/src/notification/service.rs b/src/notification/service.rs deleted file mode 100644 index 9daed65..0000000 --- a/src/notification/service.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::error::Result; -use crate::models::TapeInfo; - -pub struct NotificationService { - webhook_url: Option, - mq_endpoint: Option, -} - -impl NotificationService { - pub fn new(webhook_url: Option, mq_endpoint: Option) -> Self { - Self { - webhook_url, - mq_endpoint, - } - } - - /// 发送离线磁带通知 - pub async fn notify_offline_tape( - &self, - tape: &TapeInfo, - archive_ids: Vec, - ) -> Result<()> { - // TODO: 实现通知逻辑 - // 1. 构建通知消息 - // 2. 通过 Webhook 或 MQ 发送 - // 3. 记录通知历史 - - tracing::warn!( - "磁带 {} 离线,需要人工介入。归档包: {:?}", - tape.id, - archive_ids - ); - - if let Some(url) = &self.webhook_url { - // TODO: 发送 HTTP 请求 - } - - if let Some(endpoint) = &self.mq_endpoint { - // TODO: 发送 MQ 消息 - } - - Ok(()) - } - - /// 发送取回任务完成通知 - pub async fn notify_restore_completed(&self, task_id: uuid::Uuid) -> Result<()> { - // TODO: 实现完成通知 - tracing::info!("取回任务 {} 已完成", task_id); - Ok(()) - } -} diff --git a/src/s3/handler.rs b/src/s3/handler.rs deleted file mode 100644 index c23a54f..0000000 --- a/src/s3/handler.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::error::{Error, Result}; -use crate::metadata::MetadataService; -use crate::models::{ObjectMetadata, RestoreStatus, StorageClass}; - -pub struct S3Handler { - metadata: MetadataService, -} - -impl S3Handler { - pub fn new(metadata: MetadataService) -> Self { - Self { metadata } - } - - /// 处理 PUT Object 请求 - pub async fn put_object(&self, bucket: &str, key: &str, data: Vec) -> Result<()> { - // TODO: 实现对象上传逻辑 - // 1. 写入热存储 - // 2. 创建元数据记录 - // 3. 标记为 HOT 状态 - todo!() - } - - /// 处理 GET Object 请求 - pub async fn get_object(&self, bucket: &str, key: &str) -> Result> { - // TODO: 实现对象下载逻辑 - // 1. 检查对象状态 - // 2. 如果是 COLD 且未解冻,返回 InvalidObjectState - // 3. 如果是 COLD 但已解冻,从缓存读取 - // 4. 如果是 HOT,直接从热存储读取 - - let metadata = self.metadata.get_object(bucket, key).await?; - - match metadata.storage_class { - StorageClass::Cold => { - if let Some(status) = metadata.restore_status { - match status { - RestoreStatus::Completed => { - // 从缓存读取 - todo!("从缓存读取") - } - _ => { - return Err(Error::InvalidObjectState( - "对象处于冷存储状态,需要先解冻".to_string(), - )); - } - } - } else { - return Err(Error::InvalidObjectState( - "对象处于冷存储状态,需要先解冻".to_string(), - )); - } - } - _ => { - // 从热存储读取 - todo!("从热存储读取") - } - } - } - - /// 处理 Restore Object 请求 - pub async fn restore_object(&self, bucket: &str, key: &str, days: u32) -> Result<()> { - // TODO: 实现解冻逻辑 - // 1. 检查对象是否为 COLD 状态 - // 2. 创建取回任务 - // 3. 提交到取回调度器 - todo!() - } - - /// 处理 HEAD Object 请求 - pub async fn head_object(&self, bucket: &str, key: &str) -> Result { - self.metadata.get_object(bucket, key).await - } -} diff --git a/src/s3/mod.rs b/src/s3/mod.rs deleted file mode 100644 index 1458e32..0000000 --- a/src/s3/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod handler; -pub mod server; - -pub use handler::S3Handler; -pub use server::S3Server; diff --git a/src/s3/server.rs b/src/s3/server.rs deleted file mode 100644 index e2778b3..0000000 --- a/src/s3/server.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::error::Result; -use crate::s3::handler::S3Handler; -use axum::Router; - -pub struct S3Server { - handler: S3Handler, -} - -impl S3Server { - pub fn new(handler: S3Handler) -> Self { - Self { handler } - } - - pub fn router(&self) -> Router { - Router::new() - // TODO: 添加 S3 API 路由 - // .route("/:bucket/:object", get(get_object).put(put_object)) - // .route("/:bucket", get(list_objects)) - // .route("/:bucket/:object?restore", post(restore_object)) - .route("/health", axum::routing::get(|| async { "OK" })) - } - - pub async fn start(&self, addr: &str) -> Result<()> { - let app = self.router(); - let listener = tokio::net::TcpListener::bind(addr).await?; - - tracing::info!("S3 服务器启动在 {}", addr); - axum::serve(listener, app).await?; - - Ok(()) - } -} diff --git a/src/scheduler/archive.rs b/src/scheduler/archive.rs deleted file mode 100644 index efdb915..0000000 --- a/src/scheduler/archive.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::error::Result; -use crate::metadata::MetadataService; -use crate::models::{ArchiveBundle, ArchiveTask, ObjectMetadata}; -use crate::tape::TapeManager; -use tokio::time::{interval, Duration}; - -pub struct ArchiveScheduler { - metadata: MetadataService, - tape_manager: TapeManager, - scan_interval: Duration, - batch_size: usize, - min_archive_size_mb: u64, - target_throughput_mbps: u64, -} - -impl ArchiveScheduler { - pub fn new( - metadata: MetadataService, - tape_manager: TapeManager, - scan_interval_secs: u64, - batch_size: usize, - min_archive_size_mb: u64, - target_throughput_mbps: u64, - ) -> Self { - Self { - metadata, - tape_manager, - scan_interval: Duration::from_secs(scan_interval_secs), - batch_size, - min_archive_size_mb, - target_throughput_mbps, - } - } - - /// 启动归档调度器 - pub async fn start(&self) -> Result<()> { - let mut interval = interval(self.scan_interval); - - loop { - interval.tick().await; - - if let Err(e) = self.scan_and_archive().await { - tracing::error!("归档调度器错误: {}", e); - } - } - } - - /// 扫描并归档符合条件的对象 - async fn scan_and_archive(&self) -> Result<()> { - // TODO: 实现归档逻辑 - // 1. 扫描 COLD_PENDING 状态的对象 - // 2. 按 archive_id 聚合 - // 3. 创建归档任务 - // 4. 调度磁带写入 - // 5. 更新元数据状态 - - tracing::info!("开始扫描待归档对象..."); - // 示例:获取待归档对象列表 - // let pending_objects = self.metadata.list_objects_by_storage_class( - // StorageClass::ColdPending, - // self.batch_size, - // ).await?; - - // 聚合对象到归档包 - // 创建归档任务 - // 执行磁带写入 - - Ok(()) - } - - /// 创建归档包 - async fn create_archive_bundle(&self, objects: Vec) -> Result { - // TODO: 实现归档包创建逻辑 - todo!() - } - - /// 执行归档任务 - async fn execute_archive_task(&self, task: ArchiveTask) -> Result<()> { - // TODO: 实现归档任务执行 - // 1. 获取磁带驱动 - // 2. 顺序写入磁带 - // 3. 验证写入完整性 - // 4. 更新元数据 - todo!() - } -} diff --git a/src/scheduler/mod.rs b/src/scheduler/mod.rs deleted file mode 100644 index db2ea35..0000000 --- a/src/scheduler/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod archive; -pub mod recall; - -pub use archive::ArchiveScheduler; -pub use recall::RecallScheduler; diff --git a/src/scheduler/recall.rs b/src/scheduler/recall.rs deleted file mode 100644 index 543b4b5..0000000 --- a/src/scheduler/recall.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::cache::CacheManager; -use crate::error::Result; -use crate::metadata::MetadataService; -use crate::models::{RecallTask, RestoreStatus}; -use crate::tape::TapeManager; -use std::collections::HashMap; -use tokio::sync::mpsc; - -pub struct RecallScheduler { - metadata: MetadataService, - tape_manager: TapeManager, - cache_manager: CacheManager, - task_queue: mpsc::UnboundedSender, - max_concurrent: usize, -} - -impl RecallScheduler { - pub fn new( - metadata: MetadataService, - tape_manager: TapeManager, - cache_manager: CacheManager, - max_concurrent: usize, - ) -> Self { - let (tx, _rx) = mpsc::unbounded_channel(); - - Self { - metadata, - tape_manager, - cache_manager, - task_queue: tx, - max_concurrent, - } - } - - /// 获取任务队列发送端(用于提交任务) - pub fn task_sender(&self) -> mpsc::UnboundedSender { - self.task_queue.clone() - } - - /// 提交取回任务 - pub async fn submit_restore(&self, bucket: &str, key: &str, days: u32) -> Result { - // TODO: 实现取回任务提交 - // 1. 检查对象状态 - // 2. 创建取回任务 - // 3. 加入队列 - // 4. 合并同磁带的任务 - todo!() - } - - /// 启动取回调度器 - pub async fn start(&self) -> Result<()> { - // TODO: 实现取回调度逻辑 - // 1. 从队列获取任务 - // 2. 检查磁带状态(在线/离线) - // 3. 在线:直接调度读取 - // 4. 离线:发送通知 - // 5. 执行磁带读取 - // 6. 写入缓存 - // 7. 更新元数据状态 - - // 注意:需要使用接收端,这里需要重新设计 - // 暂时留空,等实现时再完善 - loop { - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - // TODO: 实现任务处理逻辑 - } - } - - /// 执行取回任务 - async fn execute_recall(&self, task: RecallTask) -> Result<()> { - // TODO: 实现取回执行逻辑 - // 1. 检查磁带状态 - // 2. 如果离线,发送通知并等待 - // 3. 从磁带读取数据 - // 4. 写入缓存 - // 5. 更新对象状态为已解冻 - todo!() - } - - /// 合并同磁带的任务 - async fn merge_tasks_by_tape(&self, tasks: Vec) -> Vec { - // TODO: 实现任务合并逻辑 - // 按 tape_id 和 archive_id 分组 - // 合并为批量读取任务 - todo!() - } -} diff --git a/src/tape/driver.rs b/src/tape/driver.rs deleted file mode 100644 index 9a6f4a4..0000000 --- a/src/tape/driver.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::error::Result; - -pub struct TapeDriver { - drive_id: String, -} - -impl TapeDriver { - pub fn new(drive_id: String) -> Self { - Self { drive_id } - } - - /// 写入数据到磁带 - pub async fn write(&self, data: Vec) -> Result<()> { - // TODO: 实现磁带写入 - // 使用 LTFS 或厂商 SDK - todo!() - } - - /// 从磁带读取数据 - pub async fn read(&self, offset: u64, length: u64) -> Result> { - // TODO: 实现磁带读取 - todo!() - } - - /// 定位到指定位置 - pub async fn seek(&self, position: u64) -> Result<()> { - // TODO: 实现磁带定位 - todo!() - } -} diff --git a/src/tape/manager.rs b/src/tape/manager.rs deleted file mode 100644 index 5dff098..0000000 --- a/src/tape/manager.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::error::Result; -use crate::models::{TapeInfo, TapeStatus}; - -pub struct TapeManager { - // TODO: 实现磁带管理逻辑 - // - 磁带库抽象 - // - 驱动管理 - // - 磁带状态跟踪 -} - -impl TapeManager { - pub fn new() -> Self { - Self {} - } - - /// 获取磁带信息 - pub async fn get_tape(&self, tape_id: &str) -> Result { - // TODO: 实现磁带信息查询 - todo!() - } - - /// 更新磁带状态 - pub async fn update_tape_status(&self, tape_id: &str, status: TapeStatus) -> Result<()> { - // TODO: 实现状态更新 - todo!() - } - - /// 获取可用的磁带驱动 - pub async fn get_available_drive(&self) -> Result> { - // TODO: 实现驱动分配 - todo!() - } - - /// 检查磁带是否在线 - pub async fn is_tape_online(&self, tape_id: &str) -> Result { - let tape = self.get_tape(tape_id).await?; - Ok(tape.status == TapeStatus::Online) - } -} diff --git a/src/tape/mod.rs b/src/tape/mod.rs deleted file mode 100644 index 70c5533..0000000 --- a/src/tape/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod driver; -pub mod manager; - -pub use driver::TapeDriver; -pub use manager::TapeManager; From cb0da38f54696fe74321ab68ea4228be79ee3b11 Mon Sep 17 00:00:00 2001 From: GatewayJ <835269233@qq.com> Date: Mon, 2 Mar 2026 22:30:25 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix(ci):=20=E5=AE=89=E8=A3=85=20protoc=20?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E5=96=84=20GitHub=20Actions=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 protobuf-compiler 安装步骤,修复 tonic-build 找不到 protoc 的错误 - 使用 dtolnay/rust-toolchain 安装 stable + rustfmt/clippy - 添加 cargo cache 加速构建 - 增加 fmt check 和 clippy 检查步骤 - 触发分支增加 doc Made-with: Cursor --- .github/workflows/rust.yml | 41 +++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..0ddfd8e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,7 +2,7 @@ name: Rust on: push: - branches: [ "main" ] + branches: [ "main", "doc" ] pull_request: branches: [ "main" ] @@ -10,13 +10,40 @@ env: CARGO_TERM_COLOR: always jobs: - build: - + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: cargo fmt + run: cargo fmt --all -- --check + + - name: cargo clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: cargo build + run: cargo build --workspace --verbose + + - name: cargo test + run: cargo test --workspace --verbose