- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
导读
本文以 Apache OpenDAL 的 RFC-3017:Remove Write Copy From 为核心,剖析 OpenDAL 在 2023 年基于oio::Writetrait 的copy_from(reader)流式写入 API 的完整生命周期:它因何而生(RFC-2083)、因何被移除(与缓冲、重试机制不兼容,且受 Rust 与 Hyper 的 API 约束无法达到理想签名)、移除后Writer又如何退回基于AsyncWrite的实现路径。结合当前仓库源码,读者还能看到这一决策如何为后来的 RFC-8109 铺路——copy_from最终以“源路径 + 范围”的新签名回归,并落地为 S3UploadPartCopy等原生服务端拷贝。读完本文,你将理解 OpenDAL 内部写入接口的演进逻辑、trait 设计中对象安全与所有权约束的权衡,以及升级到新版本时手动实现 Service/Layer 需要注意的迁移点。
一、背景:RFC-2083 为什么引入 sink / copy_from
OpenDAL 早期不支持流式上传。用户必须先把数据全部加载进内存,再分片写入Writer:
let bs = balabala(); w.write(bs).await?; let bs = daladala(); w.write(bs).await?; // ... w.close().await?;这种模式有两个明显缺陷(详见 RFC-2083: Writer Sink API):
- 内存占用高:数据必须先整体驻留内存;
- 性能低:用户数据必须先在客户端缓冲,才能通过网络发送。
为解决该问题,RFC-2083 提议在Writer上新增一个sink方法,允许用户把 reader 或 stream 直接交给 writer,由底层服务以最高效的方式处理。该方法最初命名为sink,后来更名为copy_from,并下沉到内部 traitoio::Write中。
理想中的使用方式
RFC-2083 设想用户可以直接以流式方式上传大文件:
// 开始写一个 5 TiB 的文件。 let w = op.writer_with( OpWrite::default() .with_content_length(5 * 1024 * 1024 * 1024 * 1024)); let r = balabala(); // 直接发送到网络,无需内存缓冲。 w.copy_from(size, r).await?; // ... // 一切就绪后关闭写入! w.close().await?;需要区分两个语义(RFC-2083 的 Rationale 中专门解释):OpWrite::content_length表示要写入文件的总长度,而copy_from的size参数表示传入 reader/stream 的长度。当两者相等时,某些服务可以优化为单请求写入全部内容。
二、被移除的 API 签名与其三大局限
RFC-3017 中被移除的oio::Write::copy_from签名如下:
pub trait Write: Unpin + Send + Sync { /// Copies data from the given reader to the writer. /// /// # Behavior /// /// - `Ok(n)` indicates successful writing of `n` bytes. /// - `Err(err)` indicates a failure, resulting in zero bytes written. /// /// A situation where `n < size` may arise; the caller should then transmit the remaining bytes until the full amount is written. async fn copy_from(&mut self, size: u64, src: oio::Reader) -> Result<u64>; }RFC 明确指出该 API 存在以下限制:
- 与现有的缓冲(buffering)和重试(retry)机制不兼容:
copy_from一次吃掉整个 reader,无法融入 OpenDAL 面向 chunk 的缓冲流水线与可重试语义。 - 对 reader 施加所有权要求:
src按值传入(oio::Reader是Box<dyn ReadDyn>类型擦除对象),用户每次写操作都必须重新创建 reader,使用不便。
更关键的是,RFC 还论证了一个“理想签名”在 Rust 与 Hyper 的 API 限制下根本无法实现:
pub trait Write: Unpin + Send + Sync { async fn copy_from(&mut self, size: u64, src: &mut impl oio::Read) -> Result<u64>; }两个硬性约束:
- Rust 的对象安全限制:若要 trait 保持对象安全(object safe,才能
Box<dyn Write>类型擦除),就不允许 trait 方法中出现impl oio::Read这类泛型参数; - Hyper 的
'static限制:Hyper 不接受引用类型的请求体,要求impl Stream + 'static,因此无法用&mut引用把 reader 喂给底层 HTTP 传输。
结论是:既然理想设计在当前约束下不可行,RFC-3017 的提案就是先从oio::Writetrait 中移除copy_from,等待更恰当的设计出现。
三、Guide-level:Writer::sink()与Writer::copy()保留,但改用AsyncWrite
RFC 指出,用户可见的Writer::sink()与Writer::copy()方法不会被删除,只是其内部实现从“调用oio::Write::copy_from”改为“基于futures::AsyncWrite的通用拷贝”。
当时的实现变更示意:
pub async fn copy<R>(&mut self, size: u64, read_from: R) -> Result<u64> where R: futures::AsyncRead + Send + Sync + Unpin + 'static, { if let State::Idle(Some(w)) = &mut self.state { let r = Box::new(oio::into_streamable_read( oio::into_read_from_file(read_from, 0, size), 64 * 1024, )); - w.copy_from(size, r).await + futures::io::copy(&mut r, w).await } else { unreachable!( "writer state invalid while copy, expect Idle, actual {}", self.state ); } }要点解析:
R: futures::AsyncRead + Send + Sync + Unpin + 'static:约束来自 Hyper 对请求体的要求——必须'static,不能借用;oio::into_read_from_file/oio::into_streamable_read:把用户的AsyncRead包装为 OpenDAL 内部的流式读取对象,缓冲块大小为64 * 1024(64 KiB);futures::io::copy(&mut r, w):通用拷贝逻辑直接写到AsyncWrite(writer 本身实现futures::AsyncWrite),从而绕开 trait 层面的copy_from。
换言之:接口保留,机制替换。流式拷贝能力没有被删除,只是从“服务实现必须提供的 trait 方法”变成了“框架层用通用AsyncWrite组装出来的能力”,从而解除对每个 service 实现的侵入式要求。
四、Reference-level:oio::Write::copy_from被移除
在实现层面,RFC-3017 的动作非常干脆:从oio::Writetrait 中删除copy_from方法。服务与层(Service/Layer)不再需要、也不再可能实现该方法。
这一决策的直接后果被记录在 升级指南(v0.40 的 Raw API 章节):
RFC-3017 从
oio::Writetrait 中移除了copy_fromAPI。手动实现 services 和 layers 的用户应删除该 API。
五、Drawbacks:流式上传能力让位给AsyncWrite
RFC 坦承该提案的代价:
The deprecation eliminates the ability to stream data uploads. A viable alternative is to directly use
AsyncWriteoffered byWriter.
即:移除后不再有“把整个 reader 一次性交给 writer”的流式上传能力;可行的替代方案是直接使用Writer提供的AsyncWrite接口自行驱动拷贝。对于仍想“一条语句完成流式上传”的用户,体验会有所回退——这正是后来重新设计copy_from的动机之一。
六、Future Possibilities:RFC-3017 留下的伏笔
RFC 在 Future Possibilities 中写道:条件成熟时,可引入Writer::copy_from(r: &dyn AsyncRead)之类的工具函数。
从当前仓库源码结构看,这一伏笔以另一种形态落地了。RFC-8109(8109_writer_copy_from.md)在 2026 年重新引入Writer::copy_from,但签名彻底改变——不再接受拥有所有权的 reader,而是接受源路径与范围:
impl Writer { pub async fn copy_from( &mut self, path: &str, range: impl Into<BytesRange>, ) -> Result<()>; pub async fn copy_from_options( &mut self, path: &str, options: ReadOptions, ) -> Result<()>; }RFC-8109 的 Prior Art 章节明确承认了 RFC-3017 的教训:
RFC-3017 removed the previous
oio::Write::copy_from(Reader)because owning a stateful reader conflicted with buffering and retry. This proposal passes an immutable path and read arguments instead, so fallback reads and native requests can be recreated safely.
正是因为 RFC-3017 移除了“按值持有有状态 reader”的设计,RFC-8109 才能用“不可变路径 + 读参数”安全地重建该能力——fallback 读取与原生请求都可以随时重新创建,不再与缓冲、重试机制冲突。
七、源码验证:新copy_from在当前仓库中的落地形态
从当前仓库源码结构看,RFC-8109 的 design 已经实现,我们可以逐层验证其与 RFC-3017 移除内容的关系。
7.1 内部 trait:oio::Write的copy_from已回归为新签名
在 core/core/src/raw/oio/write/api.rs 中,oio::Writetrait 当前包含:
pub trait Write: Unpin + Send + Sync { /// Write the entire buffer into the writer. fn write(&mut self, bs: Buffer) -> impl Future<Output = Result<()>> + MaybeSend; /// Copy one absolute bounded source range into this writer. fn copy_from( &mut self, _path: &str, _args: OpRead, _range: BytesRange, ) -> impl Future<Output = Result<()>> + MaybeSend { async { Err(Error::new( ErrorKind::Unsupported, "writer doesn't support native copy", )) } } fn close(&mut self) -> impl Future<Output = Result<Metadata>> + MaybeSend; fn abort(&mut self) -> impl Future<Output = Result<()>> + MaybeSend; }与 RFC-3017 移除的旧签名相比,新的copy_from:
- 接收
path + OpRead + BytesRange而非size + Reader——不持有任何有状态 reader; - 提供默认实现,默认返回
ErrorKind::Unsupported,服务只需在真正支持原生拷贝时覆写; - 仅接受绝对有界范围(从 multipart_write.rs 的实现可见,
is_suffix()或size().is_none()的范围会直接报Unexpected错误)。
7.2 公开 API:Writer::copy_from/copy_from_options
在 core/core/src/types/write/writer.rs 中,公开 API 的实现要点:
copy_from内部把range包装成默认ReadOptions后委托给copy_from_options;copy_from_options会对源路径执行normalize_path与validate_path(EntryMode::FILE)校验,目录路径会返回ErrorKind::IsADirectory错误;- 单次调用上限 5 GiB,更大的逻辑范围需要应用拆成多次有序调用;
- 底层经过
WriteContext组合后的服务栈(Servicer/OperationContext)解析源路径。
7.3 能力标记:Capability::write_can_copy_from
core/core/src/types/capability.rs 中定义了write_can_copy_from: bool,默认false。它报告组合后的服务能否对符合条件的范围执行原生(服务端)拷贝;为false时,公开操作退化为“读后写”(streaming fallback)。例如 S3 后端 在未启用 checksum 算法时才声明该能力:write_can_copy_from: checksum_algorithm.is_none()。
7.4 原生实现:S3 的UploadPartCopy与多部分写入
S3 服务端的copy_from落到UploadPartCopy:
- 请求构造见 core/services/s3/src/core.rs(
s3_upload_part_copy_request); - writer 侧执行见 core/services/s3/src/writer.rs,错误上下文标记为
ServiceOperation("UploadPartCopy"); - 多部分写入器在 multipart_write.rs 中把
copy_from调度为MultipartInput::Copy { path, args, range },与本地MultipartInput::Write(bytes)共享同一 upload ID、part 编号与 abort/complete 状态——这正是 RFC-8109 所述“本地上传与远端拷贝在同一个有序 part 队列中调度”。
7.5 行为测试:交错写入验证字节序语义
core/tests/behavior/async_write.rs 中的test_writer_copy_from_interleaved完整验证了新 API 的契约:先写"header",再copy_from源对象的1024..2048区间,接着用copy_from_options携带if_match条件拷贝2048..14MiB与15MiB..后缀区间,最后写"footer"并close,最终断言目标文件字节序列与“本地字节 + 源区间按调用顺序拼接”的期望完全一致。这与 RFC-3017 时代“一次性喂入整个 reader”的用法形成鲜明对比。
八、对开发者的迁移启示
如果你在手动实现 OpenDAL 的 Service 或 Layer,RFC-3017 的迁移动作非常明确(详见 升级指南):
- 删除旧版
oio::Write::copy_from(&mut self, size, src: oio::Reader)的实现; - 需要流式写入的用户侧代码,改用
Writer暴露的AsyncWrite(例如into_sink()/into_bytes_sink(),见 writer.rs)自行驱动拷贝; - 若后续要支持原生服务端拷贝,则在新签名
copy_from(&mut self, path: &str, args: OpRead, range: BytesRange)下覆写,并如实设置Capability::write_can_copy_from;层(Layer)若会改变路径、路由或字节内容,则必须清除该能力标记以退回流式 fallback(契约细节见 RFC-8109)。
总结
RFC-3017 是一次典型的“减法式重构”:它没有简单地砍掉流式上传能力,而是把该能力从服务实现必须提供的 trait 方法中剥离出来,降级为框架层基于AsyncWrite的通用组装,从而绕开 Rust 对象安全与 Hyper'static约束下的死结。其遗留的“未来可能性”在 RFC-8109 中得到回应——copy_from以“路径 + 范围”的不可变描述形式回归,并借助write_can_copy_from能力标记与 S3UploadPartCopy等原生操作实现了服务端拷贝优化。理解这段演进,有助于读者把握 OpenDAL 内部写入流水线的设计哲学:公共契约描述字节序列,底层机制决定传输路径。
- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
相关推荐
Apache OpenDAL 中 RFC-5556:write 操作返回 Metadata 的设计与实现解析
Apache OpenDAL 中 RFC 5556:write 操作返回 Metadata 的设计与实现解析 在 Apache OpenDAL 中,一次写入操作
数据存储后端Apache OpenDAL RFC-6189:移除原生阻塞支持,统一走 block_on 的阻塞 API 设计
Apache OpenDAL RFC 6189:移除原生阻塞支持,统一走 block_on 的阻塞 API 设计 Apache OpenDAL 长期同时维护原生
数据存储后端Apache OpenDAL 服务配置统一化:从 Builder 到 Config 结构体的演进设计(RFC 3197 深度解读)
Apache OpenDAL 服务配置统一化:从 Builder 到 Config 结构体的演进设计(RFC 3197 深度解读) 导读 本文以 Apache
数据存储后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考