☰
Apache OpenDAL RFC-3017 解析:为何移除 `oio::Write::copy_from` 及其设计演进
2026/10/13 3:05:13 网站建设 项目流程
  • 数据存储
  • 后端

【免费下载链接】opendal

Apache OpenDAL: One Layer, All Storage.

项目地址:https://gitcode.com/gh_mirrors/op/opendal
点击查看免费下载

导读

本文以 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 存在以下限制:

  1. 与现有的缓冲(buffering)和重试(retry)机制不兼容:copy_from一次吃掉整个 reader,无法融入 OpenDAL 面向 chunk 的缓冲流水线与可重试语义。
  2. 对 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 useAsyncWriteoffered 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 previousoio::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 的迁移动作非常明确(详见 升级指南):

  1. 删除旧版oio::Write::copy_from(&mut self, size, src: oio::Reader)的实现;
  2. 需要流式写入的用户侧代码,改用Writer暴露的AsyncWrite(例如into_sink()/into_bytes_sink(),见 writer.rs)自行驱动拷贝;
  3. 若后续要支持原生服务端拷贝,则在新签名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.

项目地址:https://gitcode.com/gh_mirrors/op/opendal
点击查看免费下载
上一篇:SymPy 数值计算实战:从 subs/evalf 到 lambdify、ufuncify 与代码生成
下一篇:mold 项目中 TBB 并发队列类 concurrent_queue 与 concurrent_bounded_queue 完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询