- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
Apache OpenDAL 的opendal-service-foyer服务把 foyer 展开,完整讲解三种配置方式(预配置缓存、Builder 自动配置、URI 驱动)以及全部可调参数,并结合仓库源码剖析其懒加载初始化、墓碑删除与能力集实现,帮助你把它正确接入项目并理解其边界(数据可能被逐出、不支持 list、仅异步)。
Foyer 服务在 OpenDAL 中的定位
foyer 是一个支持内存与磁盘两级缓存的混合缓存库。OpenDAL 的 Foyer 服务把它当作一个易失性(volatile)KV 存储后端来使用:write写入的键值对会进入 foyer 缓存,当缓存容量达到上限时,数据可能随时被逐出(evict)。因此该服务适合存放可重建、可容忍丢失的热点数据,而不是持久化业务数据。
在 OpenDAL 中启用该服务有两种方式:
- 通过
opendal门面(facade)crate 启用,推荐大多数应用使用:
cargo add opendal --features services-foyer启用后,服务以opendal::services::Foyer对外暴露(见 core/Cargo.toml 中的services-foyer = ["dep:opendal-service-foyer"]特性定义,以及 core/src/lib.rs 中pub use opendal_service_foyer::*的导出)。
- 直接使用拆分后的 crate:
cargo add opendal-core opendal-service-foyer依赖版本以仓库实际为准:opendal-service-foyer依赖foyer = "0.22.3"与opendal-core 0.59.4(见 core/services/foyer/Cargo.toml)。
能力集(Capabilities)
根据配置与底层 foyer 引擎,该服务暴露以下能力(见 core/services/foyer/src/docs.md):
| 操作 | 支持 | 说明 |
|---|---|---|
stat | ✅ | 返回键对应对象的元数据 |
read | ✅ | 支持范围读取 |
write | ✅ | 支持空内容写入(write_can_empty) |
delete | ✅ | 删除键 |
list | ❌ | 不支持,foyer 不提供高效的键遍历 |
blocking | ❌ | 不支持,foyer 是纯异步库 |
这一能力集在源码中有明确对应:FoyerBackend::new中通过Capability { read, write, write_can_empty, delete, stat, shared, .. }声明支持项(见 backend.rs),而list与copy、rename、presign、create_dir均直接返回ErrorKind::Unsupported(见 backend.rs)。
构建 Operator 之后,可以用以下代码检查实际生效的能力集:
let op = Operator::new(Foyer::new().memory(64 * 1024 * 1024))?; let info = op.info(); let cap = info.capability(); assert!(cap.read && cap.write && cap.delete && cap.stat); assert!(!cap.list);配置参数总览
Foyer 服务可以用两种途径配置:
- 预配置缓存(Pre-configured cache):传入一个完全配置好的
HybridCache实例,获得最大控制权; - 自动配置缓存(Auto-configured cache):通过 Builder 方法配置内存与磁盘缓存,由服务在首次访问时懒加载初始化。
使用 Builder API 且不提供预构建缓存时,可用参数如下(默认值同时由 docs.md 与 core.rs 中的常量定义确认):
| 参数 | 含义 | 默认值 |
|---|---|---|
name | 缓存实例名称,用于标识服务 | 无(回退为"foyer") |
root | 后端根路径,所有操作都相对该路径进行 | / |
memory | 内存缓存容量(字节) | 1 GiB(1024 * 1024 * 1024) |
disk_path | 磁盘缓存目录路径,设置后启用混合缓存 | 未设置(纯内存) |
disk_capacity | 磁盘缓存总容量(字节),仅当设置disk_path时生效 | 无 |
disk_file_size | 单个缓存文件大小(字节),仅当设置disk_path时生效 | 1 MiB(1024 * 1024) |
recover_mode | 启动时的恢复模式:"none"(默认)、"quiet"或"strict" | "none" |
shards | 缓存分片数,用于并发访问 | 1 |
其中name、root、disk_capacity、disk_file_size与recover_mode的字段注释、合法取值和"仅当disk_path设置时生效"的约束,均定义在 config.rs 的FoyerConfig结构体中。recover_mode的解析在 core.rs 中实现:非法值会直接返回ErrorKind::ConfigInvalid错误。
方式一:通过预配置缓存(最大控制权)
当你需要精细控制 foyer 的引擎(例如指定块引擎、设备、恢复策略)时,可以先自己构建HybridCache,再通过Foyer::new().cache(cache)交给服务:
use opendal::Operator; use opendal_service_foyer::Foyer; use opendal_service_foyer::FoyerKey; use opendal_service_foyer::FoyerValue; use foyer::{BlockEngineConfig, DeviceBuilder, FsDeviceBuilder, HybridCacheBuilder}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // 创建 foyer HybridCache,完全掌控其配置 let device = FsDeviceBuilder::new("/tmp/foyer") .with_capacity(1024 * 1024 * 1024) .build()?; let cache = HybridCacheBuilder::new() .memory(64 * 1024 * 1024) // 64MB 内存缓存 .storage() .with_engine_config(BlockEngineConfig::new(device)) .build() .await?; // 创建 Operator let op = Operator::new(Foyer::new().cache(cache))?; // 像使用其他 OpenDAL Operator 一样使用 op.write("key", "value").await?; let value = op.read("key").await?; Ok(()) }在源码层面,Foyer::cache方法把传入的HybridCache包装进Arc并保存在FoyerBuilder.cache字段(见 backend.rs);Builder::build时会通过FoyerCore::with_cache将其写入OnceLock(见 backend.rs 与 core.rs)。一旦提供了预构建缓存,服务不会再自行初始化缓存,memory、disk_path等自动配置参数将被忽略。
方式二:通过 Builder 自动配置(混合缓存)
不提供预构建缓存时,FoyerCore会在第一次真正访问时懒加载初始化缓存(见 core.rs 的get_cache)。下面的示例同时配置了内存与磁盘,构成内存 + 磁盘的混合缓存:
use opendal::Operator; use opendal_service_foyer::Foyer; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // 创建带混合缓存(内存 + 磁盘)的 Operator let op = Operator::new( Foyer::new() .memory(64 * 1024 * 1024) // 64MB 内存 .disk_path("/tmp/foyer_cache") // 启用磁盘缓存 .disk_capacity(1024 * 1024 * 1024) // 1GB 磁盘 .disk_file_size(1024 * 1024) // 每个文件 1MB .recover_mode("quiet") // 重启后恢复 .shards(4) // 4 个分片提升并发 )?; op.write("key", "value").await?; let value = op.read("key").await?; Ok(()) }自动配置的底层逻辑(core.rs)是:
- 内存容量取
memory或默认 1 GiB; - 分片数取
shards或默认 1; - 恢复模式取
recover_mode或默认RecoverMode::None; - 若设置了
disk_path,则用FsDeviceBuilder构建磁盘设备(容量取disk_capacity),再以disk_file_size(默认 1 MiB)作为块大小组装BlockEngineConfig; - 最终调用
HybridCacheBuilder::new().memory(..).with_shards(..).storage().with_recover_mode(..)[.with_engine_config(..)].build()构建缓存。
因此disk_path是混合缓存的总开关:设置了它,冷数据才会在内存写满后自动迁移到磁盘。
方式三:通过 URI 构建(scheme 驱动)
Foyer 服务注册的 URI scheme 是foyer(常量FOYER_SCHEME,见 lib.rs)。使用 URI 前需要先注册服务:
use opendal::Operator; use opendal::OperatorRegistry; use opendal_service_foyer::register_foyer_service; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { register_foyer_service(OperatorRegistry::get()); // 纯内存缓存 let op = Operator::from_uri("foyer:///?memory=67108864")?; // 混合缓存 let op = Operator::from_uri( "foyer:///cache?memory=67108864&disk_path=/tmp/foyer&disk_capacity=1073741824" )?; Ok(()) }URI 的解析逻辑在FoyerConfig::from_uri(见 config.rs):
- 查询字符串中的参数(
memory、disk_path、disk_capacity、disk_file_size、recover_mode、shards、name)会全部并入配置 map; - URI 的路径段会被当作
root。例如foyer:///cache?..中/cache即根路径,所有键都相对cache前缀存储;foyer:///则根路径为/。
这一点由 config.rs 中的单元测试直接验证:foyer:///cache?name=test&memory=67108864会解析出root = "cache"、name = "test"、memory = 64 MiB;foyer:///?recover_mode=quiet&shards=4会解析出recover_mode = "quiet"、shards = 4。register_foyer_service仅对Operator::from_uri/Operator::via_iter这类 scheme 驱动构建是必需的;直接用Operator::new(Foyer::new()..)构建则无需注册(见 lib.rs 与 README.md)。
源码级原理:从写入到读取的完整调用链
理解服务内部如何操作 foyer 缓存,有助于判断其行为边界。服务内部五个核心模块分别是core(缓存持有与懒加载)、writer、reader、deleter与backend(能力与装配)。
键值类型的编码:FoyerKey与FoyerValue
foyer 缓存要求键值类型实现Code编解码 trait。服务自带的FoyerKey(路径字符串)与FoyerValue(包装 OpenDALBuffer)都直接实现了Code(见 lib.rs):编码时先写入 8 字节小端长度前缀再写入内容,estimated_size返回8 + 长度。这样设计使服务不依赖 foyer 的 serde/bincode 特性,键值序列化完全由 OpenDAL 侧掌控。
懒加载缓存与墓碑删除
FoyerCore用std::sync::OnceLock<Arc<HybridCache>>持有缓存,第一次get/insert/remove时才初始化(见 core.rs),因此从 URI 或配置构建的 Operator 在真正访问前不会付出构建缓存的成本。
删除语义值得注意:foyer 的HybridCache::remove是异步逐出,键被删除后缓存条目可能仍短暂可见。为提供确定性的删除语义,服务维护了一个deleted_keys: HashSet<FoyerKey>墓碑集合——remove时把键加入墓碑(core.rs),get时先检查墓碑,命中则直接视为不存在(core.rs);insert成功后再把键从墓碑中移除(core.rs)。这也意味着进程重启后墓碑消失,而磁盘上若残留了已删除条目且恢复模式为quiet/strict,删除的键可能"复活"——这正是易失性缓存 + 磁盘恢复组合下的固有行为。
写入、读取与删除的实现
- 写入:
FoyerWriter把分片数据累积进oio::QueueBuf,close()时一次性收集成Buffer并调用core.insert;路径以/结尾时返回目录元数据,否则返回文件元数据(见 writer.rs)。 - 读取:
FoyerReader在open()时按build_abs_path(root, path)拼接绝对键,从缓存取出整个Buffer,再用BytesRange切片支持范围读取;键不存在返回NotFound(见 reader.rs)。 - 删除:
FoyerDeleter走oio::OneShotDelete,把相对路径拼接 root 后调用core.remove(见 deleter.rs)。
测试用例对这些行为做了完整验证(见 lib.rs):test_basic_operations覆盖写、读、stat、删、删后读失败的完整闭环;test_range_read验证10..20范围切片正确、越界范围返回ErrorKind::RangeNotSatisfied、空范围返回空 Buffer;test_hybrid_cache_via_config验证 Builder 自动配置路径下写读删均正常。
使用注意事项(Notes)
数据易失性
Foyer 是缓存而非持久存储。当缓存达到容量上限时,数据可能在任何时刻被逐出。不要把它当作可靠存储使用,只适合存放可重建的数据。
混合缓存的冷热迁移
配置了disk_path后,内存缓存写满时冷数据会自动迁移到磁盘;未配置disk_path时,超过内存容量的数据直接逐出,不会落盘。这是 foyer 内存 + 磁盘两级缓存的核心价值:在内存性能与磁盘成本之间取得平衡。
恢复模式(RecoverMode)
recover_mode控制服务重启后如何处理磁盘上已有的缓存数据:
| 取值 | 行为 |
|---|---|
"none" | 不恢复磁盘数据(易失性缓存),默认值 |
"quiet" | 恢复数据,跳过任何损坏条目 |
"strict" | 恢复数据,遇到任何损坏即失败 |
非法取值会在缓存初始化时返回ConfigInvalid错误(见 core.rs)。
不支持 list 与 blocking
- 无 list:foyer 不提供高效的键遍历,
list操作不可用,因此该服务无法作为目录树式文件系统使用,它是纯粹的按键寻址 KV 存储; - 仅异步:foyer 是纯异步库,
blocking系列操作不被支持(见 docs.md)。
适用场景与建议
从仓库中的 foyer 集成 RFC 看,foyer 这类混合缓存的价值在于:云对象存储的请求延迟通常比本地内存/磁盘高一到数个数量级,且按请求计费,引入内存 + 磁盘两级缓存可以在性能与成本之间取得更好平衡,并借助 foyer 的请求去重(同一键并发访问仅一次真正落盘)与缓存未命中自动回填能力降低开销。Foyer 服务把这些能力以 OpenDAL 统一 API 暴露,适合以下场景:
- 作为短生命周期会话缓存或热点数据加速层,例如给不可变对象(不更新、不覆盖的数据)做读取加速;
- 需要统一存储访问接口、希望用
Operator::from_uri或一行 Builder 配置即可获得内存/磁盘两级缓存的场景; - 数据可丢失、可重建,且不需要
list遍历、不需要同步调用的应用。
如果只是想在现有存储后端之上透明地缓存读写(而非把 foyer 当作独立后端),可以关注 OpenDAL 的 FoyerLayer 缓存层(opendal-layer-foyer,FoyerLayer),它把 foyer 作为 layer 叠在任意服务之上;而本文介绍的 Foyer 服务则把 foyer 本身作为存储后端。两者定位不同,可按需选择。
小结
- Foyer 服务把高性能混合缓存库 foyer 封装为 OpenDAL 的易失性 KV 后端,支持
stat/read(含范围读)/write/delete,不支持list与blocking; - 三种配置方式:预配置
HybridCache(最大控制权)、Builder 自动配置(memory/disk_path/disk_capacity/disk_file_size/recover_mode/shards)、URI scheme 驱动(路径段即root); - 默认值:内存 1 GiB、单文件 1 MiB、分片 1、恢复模式
none; - 底层由
FoyerCore懒加载缓存,并用墓碑集合提供确定性删除;数据在容量满时会被逐出,切勿当作持久存储。
- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
相关推荐
Apache OpenDAL 集成 foyer 混合缓存:FoyerLayer 实现原理与实战指南
Apache OpenDAL 集成 foyer 混合缓存:FoyerLayer 实现原理与实战指南 本文围绕 OpenDAL 的 RFC 文档 6370_foy
数据存储后端Apache OpenDAL Foyer 混合缓存层(FoyerLayer)完全指南:原理、接入与实战配置
Apache OpenDAL Foyer 混合缓存层(FoyerLayer)完全指南:原理、接入与实战配置 导读 Apache OpenDAL 的 Foyer
数据存储后端Apache OpenDAL Cacache 服务深度指南:基于内容寻址缓存的本地存储后端
Apache OpenDAL Cacache 服务深度指南:基于内容寻址缓存的本地存储后端 Apache OpenDAL 的 opendal service c
数据存储后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考