Tutti Runtime 配置、资源与装配流程

状态:当前实现设计收束

本文基于当前生产代码,描述 TuttiRuntime 从读取 YAML 配置,到创建并持有 StorageRuntime 的完整流程。配置示例以 examples/tutti_runtime 为准。

1. 范围

本文覆盖:

  • tutti/config/parser 如何将 YAML 翻译为 tutti/config/spec 定义的 TuttiRuntimeSpec

  • tutti/config/spec 如何提供默认值并验证静态配置;

  • TuttiRuntime 如何消费已验证的 spec,创建和初始化 Resource;

  • NVMe Resource 如何与 daemon 交互、持有 granted allocation,并分别向 Resolver factory 和 DataPath factory 提供运行期资源视图;

  • Resolver 和 DataPath 的 factory 接口、输入来源和输出所有权;

  • TuttiRuntime 如何构造 RuntimeComponents,调用 StorageRuntime::create()

  • TuttiRuntimeStorageRuntime 和各组件的所有权、失败回滚及销毁顺序。

本文不覆盖:

  • daemon 自身的初始化、配置和内部实现;

  • Resolver 和 DataPath factory 返回实例后的内部算法;

  • StorageRuntime 内部 registry、target、memory、submit、wait、progress 等 I/O 接口和实现;

  • DataPath 的实际 I/O 初始化细节。本文只确认该初始化由谁触发,以及失败时由谁回滚。

2. 总体模型

配置到 Runtime 的主链路如下:

YAML file
  |
  | parse_tutti_runtime_config(path)
  v
TuttiRuntimeSpec(缺省字段已经取得 spec 中定义的默认值)
  |
  | TuttiRuntimeSpec::validate()
  v
静态有效的装配图
  |
  | Resource factory + Resource::initialize()
  v
已授予的运行期 Resource
  |
  +-------------------------+
  |                         |
  | resolver resource view  | datapath resource view
  v                         v
Resolver factory        DataPath factory
  |                         |
  | resolver instance       | instance + initialize config
  +-------------+-----------+
                |
                | RuntimeComponents(借用组件指针)
                v
       StorageRuntime::create()
                |
                v
       RUNNING TuttiRuntime

该模型中有三个必须分开的数据层次:

数据层次

典型内容

产生者

生命周期

期望配置

accelerator、resource selection、请求队列数、组件 ID、类型、引用关系、调优参数

YAML/parser,或调用方直接构造 spec

创建期 value object

运行期事实

allocation ID、PCI BDF、字符/块设备路径、namespace、BAR0、MDTS、实际 granted queues

daemon,经 NVMe Resource 获取和验证

Resource lease 生命周期

装配结果

Resolver/DataPath 实例、路由 scheme、DataPath key、DataPathConfig

两个 factory 和 TuttiRuntime

TuttiRuntime/StorageRuntime 生命周期

静态 spec 不保存 daemon grant,daemon grant 也不回写 spec。二者只在 Resource 和组件 factory 的边界上汇合。

3. 模块职责总览

模块

主要输入

主要输出

核心职责

不负责

Config parser

YAML 路径和 YAML 节点

Result<TuttiRuntimeSpec>

YAML 加载、结构检查、字段转换、具体 config variant 构造

完整 spec 语义、硬件检查、对象创建

Config spec

C++ spec value

Status、debug string

默认值、字段约束、引用和拓扑校验、contract 校验

YAML、RPC、硬件探测、运行期对象

Resource factory

ResourceSpecResourceCreateContext

未初始化的 Resource

按 config variant 选择 concrete Resource

初始化顺序和 Runtime 所有权

NVMe Resource

NVMe 请求、runtime accel ID、daemon client

初始化后的 allocation、两类 Resource view

daemon 发现/申请、grant 验证、lease 释放、信息投影

Resolver/DataPath 构造、StorageRuntime 装配

Resolver factory

ResolverSpec、Resource、BackendSpec、DataPath key

StorageTargetResolver

校验关系,消费 resolver view,创建对应 Resolver

Resolver 的 Runtime 使用、对象长期所有权

DataPath factory

DataPathSpec、Resource、BackendSpec、runtime accel ID

CreatedDataPath

校验关系,消费 datapath view,创建 DataPath 和初始化配置

调用 DataPath::initialize()、对象长期所有权

TuttiRuntime

config path 或 TuttiRuntimeSpec

完整的 TuttiRuntime

编排、环境校验、组件所有权、StorageRuntime 创建和整体销毁

重复实现 parser/spec/factory 规则

StorageRuntime

RuntimeConfigRuntimeComponents

StorageRuntime

接收路由绑定并初始化 DataPath;销毁时停止 DataPath

拥有 Resource、Resolver、DataPath 或理解 concrete backend spec

BackendSpec 是装配关系,不对应一个运行期 backend 对象。它在创建期把一个 Resource、一个 Resolver 和一个 DataPath 连接起来,并携带三者共享的关系参数,例如 striped backend 的 stripe_unit

4. Config parser 边界

4.1 公共入口

Parser 对外只提供一个入口:

Result<TuttiRuntimeSpec> parse_tutti_runtime_config(
    const std::string& path);

实现位于 tutti/config/parser/tutti_runtime_config_parser.cppYAML::Node、yaml-cpp 异常和 YAML 路径错误都留在 parser 私有实现中,不进入 spec、Resource 或 Runtime 接口。

TuttiRuntime::create(config_path) 先调用该入口,成功后将返回的 spec 交给 TuttiRuntime::create(spec)。Parser 自身不调用 spec.validate(),所以其成功语义是 “YAML 已经可以完整翻译为 C++ spec”,而不是“该 spec 已经可以创建 Runtime”。完整 语义验证由 TuttiRuntime::create(spec) 统一触发。

4.2 Parser 负责的检查

Parser 负责所有依赖 YAML 表示的信息:

  • 文件存在且可由 yaml-cpp 加载;

  • root、storage、单个声明和 config 等节点是预期的 mapping/sequence/scalar;

  • required field 存在,unknown field 被拒绝;

  • YAML 标量可以转换到目标 C++ 整数范围;

  • selection 等字符串可以翻译成 spec enum;

  • 根据 typecontract 构造正确的 std::variant 分支;

  • 错误消息携带类似 storage.resources[0].allocation 的 YAML 路径。

Parser 不判断以下语义:

  • ID 是否重复、backend 引用是否存在;

  • Resource、Resolver 和 DataPath 类型能否组成一个合法 backend;

  • scheme 是否重复;

  • accelerator 是否真实存在;

  • daemon 是否能满足资源请求;

  • factory 或具体组件是否在当前 build 中可用。

4.3 分发边界

顶层 parser 解析公共字段后,按声明类型分发到私有 parser:

YAML 声明

分发键

目标 config variant

Resource

type: nvme

NvmeResourceConfig

Resource

type: memory

MemoryResourceConfig

Resolver

type: local-file

LocalFileResolverConfig

Resolver

type: striped-file

StripedFileResolverConfig

Resolver

type: memfs

MemfsResolverConfig

DataPath

type: local-nvme

LocalNvmeDataPathConfig

DataPath

type: striped-local-nvme

StripedLocalNvmeDataPathConfig

DataPath

type: memfs

MemfsDataPathConfig

Backend relation

contract: ext4-local-nvme

Ext4LocalNvmeBackendConfig

Backend relation

contract: striped-local-nvme

StripedLocalNvmeBackendConfig

Backend relation

contract: memfs

MemfsBackendConfig

这种分发让 YAML 解析逻辑与 spec 类型一一对应。新增组件类型时,应同时增加其 spec config、私有 parser、spec validator 和 factory 分支,而不是在 TuttiRuntime 中解析 组件字段。

5. Config spec 边界

5.1 Spec 是唯一静态配置模型

顶层结构为:

struct TuttiRuntimeSpec {
    AcceleratorSpec accelerator;
    RuntimeSpec runtime;
    StorageSpec storage;

    Status validate() const;
    Result<std::string> to_debug_string() const;
};

StorageSpec 分别保存 ResourceSpecResolverSpecDataPathSpecBackendSpec 数组。前三者是组件声明,BackendSpec 通过字符串 ID 引用它们:

BackendSpec
  resource  ----> ResourceSpec.id
  resolver  ----> ResolverSpec.id
  datapath  ----> DataPathSpec.id
  contract  ----> 合法的类型、scheme、cardinality 组合
  config    ----> 关系自身的参数

Spec 是普通 C++ value object。调用方可以绕过 YAML 直接构造,因此所有静态语义必须由 TuttiRuntimeSpec::validate() 独立保证,不能只依赖 parser 的防御性检查。

5.2 默认值归属

默认值定义在 spec header 的成员初始化器中,parser 对 optional field 只在 YAML 出现时 覆盖它。这保证 YAML 和程序化构造共享同一套默认值。

当前主要默认值如下:

字段

默认值来源

当前默认值

accelerator.profile

AcceleratorSpec

编译期 TUTTI_COMPILED_ACCELERATOR_PROFILE

runtime.accel_id

RuntimeSpec

编译期 TUTTI_DEFAULT_ACCEL_ID;HOST 为 -1,加速器 profile 为 0

NvmeAllocationSpec.selection

NVMe resource spec

Allowed;当前 YAML parser 仍要求显式提供该字段,因此该默认值主要服务程序化构造

NvmeDataPathTuning.threads_per_block

NVMe DataPath spec

16

NVMe DataPath 其余 tuning

NVMe DataPath spec

0;具体语义由 factory/concrete DataPath 解释

striped stripe_unit

striped backend spec

512 KiB

provider.endpointqueues_per_controller、memory capacity_bytes 等字段没有可用的业务 默认值;parser 将其设为 required,spec validator 也会拒绝空值或零值。

例如 examples/tutti_runtime/tutti_striped.yaml 显式设置 stripe_unit: 65536;如果省略 该 optional field,parser 构造 StripedLocalNvmeBackendConfig 时会保留 spec 中的 512 KiB 默认值。

5.3 validate() 的职责

TuttiRuntimeSpec::validate() 是静态配置语义的权威入口,当前依次保证:

  1. accelerator profile 只能是 HOST、CUDA、MUSA 或 MACA,且与 accel_id 的 host/ accelerator 语义一致;

  2. 每个声明的 ID、type、scheme、variant 和具体字段有效;

  3. 同类声明 ID 唯一;

  4. 每个 backend 引用的 Resource、Resolver、DataPath 都存在;

  5. backend contract 与 Resource type、Resolver type/scheme、DataPath type 和资源数量匹配;

  6. Resolver scheme 格式有效且全局唯一;

  7. 一个 Resource 不会被两个不同 DataPath 消费;

  8. 每个声明都可以从某个 backend 到达,不允许未使用声明;

  9. 当前产品约束为恰好一个 backend。

“恰好一个 backend”加上“所有声明可达”意味着当前一个 TuttiRuntimeSpec 实际上只会 装配一个 Resource、一个 Resolver 和一个 DataPath。数据结构仍使用 vector,保留了未来 扩展装配图的表达能力,但当前 validator 不允许多 backend Runtime。

当前 contract 矩阵为:

Contract

Resource

Resolver

Scheme

DataPath

Resource cardinality

ext4-local-nvme

nvme

local-file

file

local-nvme

1

striped-local-nvme

nvme

striped-file

striped

striped-local-nvme

至少 2

memfs

memory

memfs

memfs

memfs

1

其中 striped contract 还要求 stripe_unit 非零且按 4096 字节对齐。

这些检查只基于配置即可完成。daemon 实际返回几个 slice、每个 slice 的队列数和设备 元数据是否有效,属于 Resource 初始化后的动态校验。

5.4 Debug 输出

to_debug_string() 先再次调用 validate(),再输出包含默认值在内的确定性配置文本。 TuttiRuntimeCreateOptions::spec_debug_logger 可在创建任何 Resource 之前接收这份文本。 因此日志表达的是将被消费的 effective spec,而不是原始 YAML 文本。

6. TuttiRuntime 创建入口与环境校验

公共入口有两种:

static Result<std::unique_ptr<TuttiRuntime>> create(
    const std::string& config_path,
    TuttiRuntimeCreateOptions options = {});

static Result<std::unique_ptr<TuttiRuntime>> create(
    config::TuttiRuntimeSpec spec,
    TuttiRuntimeCreateOptions options = {});

两条路径最终都进入同一个 create_with_options_()。创建顺序为:

  1. spec.validate()

  2. 生成并可选记录 spec debug string;

  3. 校验 Runtime 环境;

  4. 创建并初始化所有 Resource;

  5. BackendSpec 解析关系,分别调用 Resolver 和 DataPath factory;

  6. 将实例注册到 TuttiRuntime 的 ownership registry,同时构造 RuntimeComponents

  7. 调用 StorageRuntime::create(RuntimeConfig, RuntimeComponents)

  8. 保存 StorageRuntime,将 TuttiRuntime 状态置为 RUNNING

环境校验不同于 spec 校验。它比较 accelerator.profile 和实际编译 profile,并在 accel_id != -1 时查询当前 accelerator backend 的 device count,拒绝超出范围的 device ID。这些结果依赖当前 build 和机器,不能放入 host-only spec validator。

7. Resource 边界

7.1 通用接口

Resource 抽象提供生命周期、诊断信息和两个面向消费者的窄视图:

class Resource {
public:
    virtual const ResourceCapabilities& capabilities() const = 0;
    virtual Status initialize() = 0;
    virtual Status shutdown() = 0;
    virtual ResourceInfo info() const = 0;
    virtual Result<std::unique_ptr<const ResourceView>>
        get_resolver_view() const = 0;
    virtual Result<std::unique_ptr<const ResourceView>>
        get_datapath_view() const = 0;
};

通用创建入口为:

Result<std::unique_ptr<Resource>> create_resource(
    const config::ResourceSpec& spec,
    const ResourceCreateContext& context);

输入来源如下:

输入

来源

含义

ResourceSpec

spec.storage.resources

Resource ID、type 和具体资源请求

ResourceCreateContext.runtime_accel_id

spec.runtime.accel_id

该 Resource 归属的 Runtime accelerator

resource_factory.cppResourceSpec.config 的 variant 分发到 NVMe 或 memory Resource。factory 只构造 CREATED 状态对象;TuttiRuntime 随后调用 Resource::initialize(),并验证 Resource ID/type/capability/state 与 spec 一致,最后才 接管所有权。

这里的 tutti::Resource 是 TuttiRuntime 装配层的资源抽象,不是 RuntimeComponents::resources 所使用的 ResourceProvider SPI。前者负责兑现 daemon 资源并为两个组件 factory 提供 view;后者是 StorageRuntime 调用 DataPath::initialize() 时传入的通用 provider。当前 TuttiRuntime 不设置 RuntimeComponents::resources,StorageRuntime 会使用其内部默认 provider。换言之, NVMe daemon grant 在 DataPath factory 构造实例时已经完成消费,不会继续穿透到 StorageRuntime。

7.2 NVMe Resource 的静态请求

examples/tutti_runtime/tutti_local_nvme.yaml 为例,静态 NVMe 请求只包含:

  • daemon endpoint;

  • selection mode:allowedexplicitstriped

  • explicit/striped 模式下期望的 daemon device ID;

  • 每个 controller 请求的 queue 数;

  • runtime.accel_id,由顶层 Runtime 配置提供。

PCI BDF、设备节点路径、mount/view 路径、namespace、block size、BAR0 size、MDTS 和实际 grant queue 数都不属于 YAML。

7.3 NVMe Resource 初始化与 daemon grant

NVMe Resource 的 concrete factory 使用 provider.endpoint 创建 NvmeResourceClient,但真正的 daemon 交互发生在 NvmeResource::initialize()

list_accelerators()
  -> 确认 runtime.accel_id 存在并有 view_root

list_nvme_resources()
  -> 确认请求设备存在、可用且 ACL 允许该 accelerator

acquire_nvme_slices(accel_id, selection, device_ids, queues)
  -> RuntimeNvmeAllocation { allocation_id, slices[] }

validate_allocation_metadata()
  -> ResourceState::INITIALIZED

每个 granted slice 包含:

device_id, accel_id, allowed_accel_ids,
pci_bdf, chrdev_path, block_path,
backing_mount_path, view_path,
namespace_id, logical_block_size,
bar0_size, max_data_size, granted_queues

Resource 会校验 allocation ID、slice 数量/顺序、accelerator 和 ACL、所有必要路径、 namespace、block size、BAR0、MDTS、queue grant,以及多 slice block size 一致性。 如果 grant 已取得但后续验证失败,Resource 立即 release allocation。

daemon client 将 allocation handle 保存在 client 内部;release(allocation_id) 通过销毁 对应 handle 释放 lease。NvmeResource::shutdown() 和析构函数最终都会走该释放路径。

7.4 面向 factory 的两种投影

同一份 daemon grant 不直接暴露给所有消费者。NVMe Resource 投影成两种 view:

View

提供字段

消费者

NvmeResolverResourceView

device ID、PCI BDF、block path、mount/view path、namespace、logical block size

Resolver factory

NvmeDataPathResourceView

device ID、PCI BDF、accel ID、character device path、namespace、logical block size、BAR0、MDTS、granted queues

DataPath factory

这两个 view 只能在 Resource 为 INITIALIZED 时取得,并以 value snapshot 的形式返回。 Resolver 无法通过其 view 取得 BAR0 或字符设备,DataPath 也不依赖文件系统 mount 信息。该隔离是 Resource 模块的主要边界,而不仅是类型转换便利。

8. Resolver factory 边界

8.1 接口和输入来源

struct ResolverCreateContext {
    const Resource& resource;
    const config::BackendSpec& relation;
    std::string data_path_key;
};

Result<std::unique_ptr<StorageTargetResolver>> create_resolver(
    const config::ResolverSpec& spec,
    const ResolverCreateContext& context);

输入

来源

用途

ResolverSpec

backend 的 resolver ID 在 storage.resolvers 中解析

选择 Resolver 类型和其静态 config

Resource&

backend 的 resource ID 在已初始化 Resource registry 中解析

获取 resolver resource view

BackendSpec

当前装配关系

校验 contract,并读取 stripe_unit 等关系参数

data_path_key

被引用的 DataPathSpec.id

写入 Resolver 产生的 target,使其能路由到同一个 DataPath binding

Factory 会防御性确认 relation 确实引用传入的 Resolver、Resource 和 DataPath key,再按 ResolverSpec.config variant 分发。它通过通用 Resource::get_resolver_view() 取得 snapshot,并 dynamic_cast 到 concrete view;类型不匹配即创建失败。

8.2 NVMe Resolver factory 消费的数据

local-file 组合要求一个 NVMe slice,构造 Resolver 时消费:

  • pci_bdf

  • namespace_id

  • logical_block_size

  • block_path

  • DataPathSpec.id 形成的 DataPath key。

striped-file 组合要求至少两个 slice。Factory 为每个 slice 构造 local-file shard,再用:

  • 所有 shard Resolver;

  • BackendSpec.config.stripe_unit

  • DataPathSpec.id 形成的 DataPath key;

构造 striped Resolver。

Factory 的输出是未被 StorageRuntime 拥有的 unique_ptrTuttiRuntime 将其注册到 resolvers_ registry,并向 RuntimeComponents 只提供:

ResolverBinding{
    .scheme = ResolverSpec.scheme,
    .resolver = borrowed_pointer,
};

9. DataPath factory 边界

9.1 接口和输入来源

struct DataPathCreateContext {
    const Resource& resource;
    const config::BackendSpec& relation;
    std::int32_t runtime_accel_id = -1;
};

struct CreatedDataPath {
    std::unique_ptr<DataPath> instance;
    DataPathConfig initialize_config;
};

Result<CreatedDataPath> create_data_path(
    const config::DataPathSpec& spec,
    const DataPathCreateContext& context);

输入

来源

用途

DataPathSpec

backend 的 datapath ID 在 storage.datapaths 中解析

选择 concrete DataPath 并提供 tuning

Resource&

已初始化 Resource registry

获取 datapath resource view

BackendSpec

当前装配关系

校验 DataPath 与 contract/Resource 的连接

runtime_accel_id

spec.runtime.accel_id

创建 context 中保留 Runtime 身份;当前 factory 尚未直接读取该字段,NVMe concrete binding 来自已验证 slice view

与 Resolver factory 一样,DataPath factory 先防御性检查 relation,再调用 Resource::get_datapath_view() 并检查 concrete view 类型。

9.2 NVMe DataPath factory 消费的数据

local-nvme 组合要求一个 slice。构造 DataPath 时,factory 合并:

  • spec tuning:cache capacity、threads per block、in-flight/batch 上限等;

  • daemon grant:character device path、BAR0 size、accel ID、granted queues、namespace、 logical block size、MDTS、PCI BDF。

striped-local-nvme 组合要求至少两个 slice。Factory 为每个 slice 构造 device descriptor, 并取所有 slice max_data_size 的最小值作为 effective MDTS。threads_per_block 不能超过 任何 slice 的实际 granted queues。

因此,诸如“请求 32 个 queue,但 daemon 只 grant 16 个,而 tuning 要求 32 个线程”的 冲突无法仅靠 spec 判断,应由 DataPath factory 在静态 tuning 与动态 grant 汇合处拒绝。

9.3 Factory 不初始化 DataPath

DataPath factory 返回 CreatedDataPath,其中:

  • instanceTuttiRuntime 持有;

  • initialize_configDataPathBinding 传给 StorageRuntime

Factory 不调用 DataPath::initialize()。该调用发生在 StorageRuntime::create(RuntimeConfig, RuntimeComponents) 内。这条边界保证 StorageRuntime 在注册完整路由后,以统一方式执行 DataPath 生命周期初始化和失败回滚。

10. 装配为 StorageRuntime

10.1 Backend relation 的解析

通过 spec validation 后,TuttiRuntime 可以把每个 BackendSpec 当作静态有效的装配 关系。当前实现按 ID 查找三个声明和已初始化 Resource:

BackendSpec relation
  +-- relation.resource -> ResourceSpec + initialized Resource
  +-- relation.resolver -> ResolverSpec
  +-- relation.datapath -> DataPathSpec

随后先创建 Resolver,再创建 DataPath,并把两者存入各自 registry。这里不创建 BackendInstance;relation 完成 factory 参数传递和 binding key 对齐后即完成使命。

最关键的不变量是:

BackendSpec.datapath
    == ResolverCreateContext.data_path_key
    == DataPathBinding.key

Resolver 解析 target 后给出的 recommended DataPath key,必然命中同一 relation 所绑定 的 DataPath 实例。

10.2 提交边界

TuttiRuntime 最终构造以下等价数据。实际代码使用 C++17 成员赋值和 push_back()

RuntimeConfig runtime_config;
runtime_config.accel_id = spec.runtime.accel_id;
runtime_config.profile_name = spec.accelerator.profile;

RuntimeComponents components;
components.resolvers.push_back(
    {resolver_spec.scheme, resolver_borrowed_pointer});
components.data_paths.push_back(
    {data_path_spec.id,
     data_path_borrowed_pointer,
     created_data_path.initialize_config});

然后调用:

StorageRuntime::create(
    std::move(runtime_config),
    std::move(components));

RuntimeComponents 是 in-process assembly contract,只包含 SPI 指针和路由信息,不包含 concrete NVMe、filesystem 或 backend spec 类型。其 resources 字段在此保持 nullptr;它不接收也不拥有 tutti::Resource

RuntimeConfig.max_terminal_results 没有对应的 TuttiRuntime spec 字段,因此此处保留 StorageRuntime 自身的默认值 64。这也说明 TuttiRuntimeSpec 目前只向 StorageRuntime 下传 accelerator 身份,不镜像其所有内部配置。

10.3 StorageRuntime 创建边界

本文只依赖 StorageRuntime::create() 的以下契约:

  • StorageRuntime 不拥有 Resolver 或 DataPath,调用方必须让它们至少存活到 StorageRuntime::shutdown() 完成;

  • create 先校验 RuntimeConfig 和 binding 完整性/唯一性;

  • create 检查 DataPath accelerator binding 与 Runtime 一致;

  • create 使用每个 binding 的 DataPathConfig 调用 DataPath::initialize()

  • 如果一个 DataPath 初始化失败,create shutdown 已初始化的 DataPath,并且不返回 StorageRuntime 实例;

  • create 成功后,Resolver/DataPath 指针由 StorageRuntime 借用,实际对象继续由 TuttiRuntime 持有。

StorageRuntime 如何使用 Resolver 打开 target,以及如何向 DataPath 提交 I/O,不在本文 范围内。

11. 所有权与生命周期

11.1 成功创建后的所有权

application
  owns unique_ptr<TuttiRuntime>
        |
        +-- owns unique_ptr<StorageRuntime>
        +-- owns Resource registry
        +-- owns Resolver registry
        +-- owns DataPath registry

StorageRuntime
  borrows Resolver* and DataPath*

Resolver/DataPath instances
  were constructed from Resource view snapshots

Resource
  owns daemon client and active allocation lease

TuttiRuntime::storage_runtime() 返回 borrowed pointer。调用方不能单独销毁它,也不能让 它超出 TuttiRuntime 生命周期。

11.2 创建失败回滚

创建过程具有事务语义:任何阶段失败都不向调用方返回半初始化 TuttiRuntime

  • Resource factory 或 initialize 失败:当前 Resource 被 shutdown;此前已注册 Resource 由临时 TuttiRuntime 析构逆序 shutdown;

  • NVMe allocation 已取得但 grant 校验失败:NVMe Resource 当场 release;

  • Resolver/DataPath factory 或 registry 失败:已创建实例由 unique_ptr 或 registry 释放,已初始化 Resource 随 Runtime 回滚;

  • StorageRuntime::create() 中 DataPath 初始化失败:StorageRuntime 先 shutdown 已初始化 DataPath,随后临时 TuttiRuntime 销毁组件和 Resource;

  • Resource、Resolver、DataPath 和 StorageRuntime factory 调用抛出的异常都会在 TuttiRuntime 创建边界转换为 Status

11.3 正常 shutdown 和销毁顺序

TuttiRuntime::~TuttiRuntime() 调用幂等的 shutdown()。当前顺序为:

1. StorageRuntime::shutdown(0)
2. destroy StorageRuntime
3. destroy Resolver instances(逆注册顺序)
4. destroy DataPath instances(逆注册顺序)
5. Resource::shutdown()(逆初始化顺序)
6. destroy Resource;NVMe lease/client 在此之前已经释放
7. TuttiRuntime state = STOPPED

即使某一步返回错误,TuttiRuntime 也记录第一个错误并继续后续清理,避免 Resource lease 因前序清理失败而无人释放。必须先销毁 StorageRuntime,因为它借用 Resolver 和 DataPath;必须最后 shutdown Resource,因为两个 concrete 组件都是由该 Resource 的 grant 构造出来的。

StorageRuntime::shutdown(0) 是非阻塞 drain:仍有 in-flight I/O 时可能返回 TIMEOUT。 当前 TuttiRuntime::shutdown() 即使收到 TIMEOUT,也会继续销毁 StorageRuntime、组件并 释放 Resource lease。因此“调用 shutdown”本身并不等于“已经完成 drain”;应用必须在 销毁 TuttiRuntime 前结束并回收全部 I/O,再显式检查 TuttiRuntime::shutdown() 的 返回值。否则借用组件和底层设备资源可能在未完成操作仍存在时被释放。这是当前创建/ 销毁边界的前置条件,本文不扩展其 I/O drain 策略。

12. 两个 NVMe 示例的端到端数据流

12.1 Local NVMe

examples/tutti_runtime/tutti_local_nvme.yaml 声明:

Resource:  example-nvme / nvme / explicit device 0 / request 32 queues
Resolver:  example-file-resolver / local-file / scheme=file
DataPath:  example-local-nvme-datapath / local-nvme / threads=32
Backend:   ext4-local-nvme,引用以上三个 ID

创建时的数据流为:

runtime.accel_id = 0
  + ResourceSpec request
  -> daemon acquire
  -> one granted NVMe slice
       | resolver view: BDF, block path, NSID, block size
       |   -> LocalFileResolver(data_path_key =
       |                        "example-local-nvme-datapath")
       |
       | datapath view: chrdev, BAR0, accel, queues, NSID, block size, MDTS, BDF
       |   + DataPathSpec tuning
       |   -> LocalNvmeDataPath + DataPathConfig{"local_nvme"}
       v
RuntimeComponents:
  resolver["file"] -> resolver instance
  datapath["example-local-nvme-datapath"] -> datapath instance

12.2 Striped Local NVMe

examples/tutti_runtime/tutti_striped.yaml 声明两个 device、striped Resolver/DataPath 和 stripe_unit: 65536。它与 local 流程的差异只在 factory 内:

  • NVMe Resource 要求 daemon 按请求顺序返回两个 slice;

  • Resolver factory 为两个 slice 构造 file shard,并用 backend 的 stripe unit 构造 StripedResolver

  • DataPath factory 为两个 slice 构造 device descriptor,以最小 MDTS 作为共同上限, 构造 StripedDataPath

  • binding key 使用 YAML 中的 DataPath ID,scheme 使用 striped

TuttiRuntime 的编排、所有权、StorageRuntime 创建和销毁流程与 local 模式完全相同。

13. 边界结论

当前实现可以收束为以下规则:

  1. Parser 只负责 YAML 到 spec 的忠实、类型安全翻译;

  2. Spec 定义所有静态字段和默认值,并独立保证整个装配图合法;

  3. Resource 将静态资源请求兑现为动态 grant,并独占外部 lease 生命周期;

  4. Resource 分别向 Resolver 和 DataPath factory 暴露最小必要的运行期视图;

  5. Resolver/DataPath factory 是静态 spec 与动态 grant 的汇合点,只负责实例化;

  6. BackendSpec 是关系,不是运行期对象;

  7. TuttiRuntime 是唯一编排者和所有者,负责把 owned components 转换为 StorageRuntime 借用的 bindings;

  8. StorageRuntime 的边界从 create(RuntimeConfig, RuntimeComponents) 开始,在 shutdown() 和对象销毁后结束;

  9. 销毁严格遵循 StorageRuntime、组件、Resource/lease 的依赖顺序。

14. 代码索引

主题

代码位置

TuttiRuntime 公共 API

tutti/include/tutti/tutti_runtime.h

创建和装配编排

tutti/tutti_runtime/tutti_runtime_create.cpp

shutdown 和 ownership registry

tutti/tutti_runtime/tutti_runtime.cpp

Parser 公共 API

tutti/config/tutti_runtime_config_parser.h

Parser 顶层分发

tutti/config/parser/tutti_runtime_config_parser.cpp

Spec 顶层结构

tutti/config/tutti_runtime_spec.h

Spec 验证

tutti/config/spec/tutti_runtime_spec.cpp

Resource SPI 和 factory

tutti/include/tutti/resource.htutti/resource/resource_factory.cpp

NVMe Resource 和 daemon adapter

tutti/resource/nvme/nvme_resource.cpptutti/resource/nvme/nvme_resource_grpc.cpp

Resolver factory

tutti/resolvers/resolver_factory.htutti/resolvers/resolver_factory.cpp

DataPath factory

tutti/data_paths/data_path_factory.htutti/data_paths/data_path_factory.cpp

StorageRuntime 创建/销毁契约

tutti/include/tutti/storage_runtime.h

配置示例

examples/tutti_runtime/*.yaml