news 2026/10/14 15:13:14

Apache OpenDAL RFC-2083 深度解析:Writer 流式写入(sink)API 的设计、落地与演进

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Apache OpenDAL RFC-2083 深度解析:Writer 流式写入(sink)API 的设计、落地与演进
  • 数据存储
  • 后端

【免费下载链接】opendal

Apache OpenDAL: One Layer, All Storage.

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

本文围绕 OpenDAL 的 RFC-2083(writer_sink_api)展开,系统讲解为什么对象存储抽象层需要流式写入能力、sinkAPI 的原始设计(copy_from/pipe_from)如何把用户数据直接“管道”进底层服务,以及该设计在仓库中真实落地后经历的重构:从oio::Write::sink被移除(RFC-3017),到最终演化为今天Writer::into_sink()、into_bytes_sink()、into_futures_async_write()这套零拷贝 Sink/AsyncWrite 适配器。读完本文,你将理解 OpenDAL 写入路径的完整历史脉络,并知道当前版本下应使用哪些 API 实现流式上传。

背景:为什么需要流式写入

RFC 文档(2083_writer_sink_api.md)开宗明义:当时的 OpenDAL 不支持流式上传,用户必须先把数据加载进内存,再分块调用write:

let bs = balabala(); w.write(bs).await?; let bs = daladala(); w.write(bs).await?; ... w.close().await?;

这种模式有两个主要缺点(RFC Summary 与 Motivation 章节):

  • 内存占用高:数据必须先完整驻留内存。RFC 明确引用了 GitHub issue #1821 中用户报告的内存问题;
  • 性能低:因为用户数据必须先被缓冲到内存中,才能发送到网络。

因此 RFC 提议让 OpenDAL 提供一个 API,允许用户把一个 stream 或 reader 直接传入 writer,跳过“全量驻留内存”这一步。

RFC 原始 API 设计:copy_from与pipe_from

RFC 在 Guide-level explanation 中提出的Writer公共 API 有两个入口:

impl Writer { pub async fn copy_from<R>(&mut self, size: u64, r: R) -> Result<()> where R: futures::AsyncRead + Send + Sync + 'static; pub async fn pipe_from<S>(&mut self, size: u64, s: S) -> Result<()> where S: futures::TryStream + Send + Sync + 'static Bytes: From<S::Ok>; }

二者分别面向两类数据源:

  • copy_from接受任意futures::AsyncRead源(例如异步文件、HTTP 响应体),size参数声明数据源的总长度;
  • pipe_from接受任意futures::TryStream,且要求流中的项S::Ok能转换为Bytes,适合已经是分块流形态的数据(例如上游对象存储的 Reader 分块流)。

RFC 给出的使用示例展示了流式写入 5 TiB 文件的场景:

// Start writing the 5 TiB file. let w = op.writer_with( OpWrite::default() .with_content_length(5 * 1024 * 1024 * 1024 * 1024)); let r = balabala(); // Send to network directly without in-memory buffer. w.copy_from(size, r).await?; // repeat... ... // Close the write once we are ready! w.close().await?;

注意OpWrite::with_content_length的用法:提前声明文件的总长度,使得底层服务(例如 S3 分片上传)可以在不缓冲整份数据的情况下规划上传策略。RFC 同时承诺:“The underlying services will handle this stream in the most efficient way possible”——即把“如何最高效地消费这个流”的决定权交给各 service 实现。

底层设计:oio::Write::sink

Reference-level explanation 给出了该 RFC 的核心机制:在原始层oio::Writetrait 上新增一个sink方法:

#[async_trait] pub trait Write: Unpin + Send + Sync { async fn sink(&mut self, size: u64, s: Box<dyn futures::TryStream<Ok=Bytes> + Send + Sync>) -> Result<()>; }

(见 2083_writer_sink_api.md 的原始签名)

设计要点:

  1. 统一归一为字节流。OpenDAL 把用户传入的 reader 或 stream 都转换成Box<dyn TryStream<Ok=Bytes> + Send + Sync>形式的字节流,再交给 service。这样 service 侧只需面对一种输入形态,降低了 trait 的复杂度;
  2. 原生流式 vs 回退缓冲。RFC 说明:支持流式上传的服务可以直接消费这个流;不支持的服务则可以用反复调用write的方式把整个流逐块写入,即 sink 对不支持的 service 有天然的回退路径。

content_length与sinksize 的区别

Rationale 章节回答了一个关键问题:OpWrite::content_length与sinkAPI 的size参数有什么差异?

  • OpWrite::content_length声明的是待写入文件的总长度;
  • sink的size参数表示本次传入的 reader/stream 的长度。

当content_length恰好等于某次 sink 传入的size时,某些服务可以优化为“单次请求写完全部内容”(例如一个 PUT 请求)而不是分片上传。这两个参数的语义区分是 service 层做上传策略优化的依据。

落地情况:sink 曾真实进入 OpenDAL 的 Raw API

RFC 并非停留在纸面。从 core/CHANGELOG.md 可以确认其完整的落地轨迹:

  • feat(core): Add Write::sink API(PR #2440):oio::Write::sink进入核心;
  • 随后多个服务逐个实现原生 sink:feat: Implement sink for services s3(PR #2508),以及 azblob、gcs、oss、obs、cos、webdav 等服务的 sink 实现(PR #2574/#2576/#2577/#2578/#2587/#2622);
  • feat: Add retry for Writer::sink operation(PR #2896):为 sink 操作增加了重试支持,这正是 RFC “Future possibilities” 章节预告的方向;
  • refactor: Relax bounds on Writer::{sink, copy}(PR #3027):放宽了公共 API 的 trait bound。

core/core/src/docs/upgrade.md 中 “Upgrade to v0.38” 一节也印证了这一点:OpenDAL 在 v0.38 为 Raw API 添加了Write::sink,这对依赖 raw API 的实现者是破坏性变更,官方给出的快速适配方案是让自定义Write实现对sink()返回Unsupported错误——这恰好与 RFC 中“不支持流式的服务返回错误、由框架回退”的设计相衔接。

后续演进:oio::Write::sink(copy_from)被移除

sink API 在实现中暴露了问题。后续 RFC-3017(3017_remove_write_copy_from.md,2023-09-06)记录了这一转折:sinkAPI 落地后曾更名为oio::Write::copy_from,签名变为:

pub trait Write: Unpin + Send + Sync { async fn copy_from(&mut self, size: u64, src: oio::Reader) -> Result<u64>; }

RFC-3017 指出了它的两大局限:

  • 与既有的缓冲(buffering)和重试(retry)机制不兼容:sink 的流式语义难以被 buffering/retry layer 统一拦截处理;
  • 对 reader 强加所有权要求:src: oio::Reader是值传递,每次写操作都需要重新创建 reader,使用上被复杂化。

理想形态copy_from(&mut self, size: u64, src: &mut impl oio::Read)在当时的 Rust 与 hyper 约束下无法做到 object-safe 且被 hyper 的impl Stream + 'static要求所限制。因此 RFC-3017 决定移除oio::Write::copy_from,保留Writer::sink()/Writer::copy()等公共入口,但内部改用futures::io::copy(&mut r, w)基于AsyncWrite实现流式传输——即在 service 内部通过通用的异步写抽象完成 reader 到 writer 的管道,而不是让 service 直接消费原始流。

当前仓库状态:sink 思想在公共 API 上的最终形态

从当前仓库源码看,oio::Writetrait(api.rs)已经没有sink方法,核心方法只剩write、copy_from(注意:此处的copy_from语义已变为“把已存在对象的某个范围原生拷贝进 writer”,受Capability::write_can_copy_from能力位控制,见 api.rs 的注释)、close和abort。

RFC-2083 想解决的“流式写入”需求,如今由Writer上的三类 Sink/AsyncWrite 适配器承载,定义于 writer.rs:

API返回类型定位源码位置
into_sink()BufferSink(实现futures::Sink<Buffer>)零拷贝,复用底层 Bytes,无额外复制writer.rs#L303
into_bytes_sink()FuturesBytesSink(实现futures::Sink<Bytes>)零拷贝writer.rs#L418
into_futures_async_write()FuturesAsyncWriter(实现futures::AsyncWrite)需要 ownedBuffer,存在一次额外拷贝writer.rs#L361

典型用法(源自 writer.rs 的文档示例):

use bytes::Bytes; use futures::SinkExt; use opendal_core::Operator; use opendal_core::Result; async fn test(op: Operator) -> Result<()> { let mut s = op.writer("hello.txt").await?.into_sink(); let bs = "Hello, World!".as_bytes(); s.send(Buffer::from(bs)).await?; s.close().await?; Ok(()) }
use futures::io::AsyncWriteExt; use opendal_core::Operator; use opendal_core::Result; async fn test(op: Operator) -> Result<()> { let mut w = op.writer("hello.txt").await?.into_futures_async_write(); let bs = "Hello, World!".as_bytes(); w.write_all(bs).await?; w.close().await?; Ok(()) }

此外Writer还提供write_from(buf: impl Buf)(writer.rs#L162)用于从bytes::Buf高效写入:对单块连续Bytes走零拷贝快路径,对Chain等多块缓冲区则逐块提取,其中Bytes块的提取只是引用计数递增、不发生数据复制。这套组合——零拷贝 Sink 适配器 + 可链式配置的 writer 构建器(如.concurrent(8).chunk(256),见 writer.rs 文档示例)——在功能上承接了 RFC-2083 “把流直接送进 writer、由底层以最高效方式处理”的初衷,同时绕开了当初 sink trait 方法与 buffering/retry 层冲突的问题。

对应的实现与单测分别位于 buffer_sink.rs、futures_bytes_sink.rs、futures_async_writer.rs,而Writer自身的行为验证见 writer.rs 的 tests 模块。

RFC 中预留的开放问题

回到 RFC 原文的 “Future possibilities” 章节,其中两项也都有了后续:

  1. sink 的重试:RFC 指出“sink API 本身无法重试,但可以在流被拉取(next)遇到可重试错误时,借助backon一类 crate 重新调用next来重试”。CHANGELOG 中的feat: Add retry for Writer::sink operation(PR #2896)表明这一方向在 sink 存续期间曾落地;sink 移除后,重试职责回归到统一的 retry layer 与AsyncWrite路径上。
  2. blocking 支持:RFC 明确“We will add async support first”——先实现异步版本,阻塞版本后置。这一点从当前仓库结构看仍然成立:oio写入体系与Writer适配器全部基于异步。

小结

RFC-2083 是 OpenDAL 写入路径设计史中承上启下的一份提案:它确立了“用户侧流式源 + service 侧最高效消费 + 不支持时回退缓冲”的流式写入模型,并真实经历了从oio::Write::sink落地(S3、azblob、GCS、OSS、OBS、COS、WebDAV 等服务原生支持)、增加重试,到因 buffering/retry 兼容性与 reader 所有权问题被 RFC-3017 移除、重构为基于AsyncWrite的实现。对今天的用户而言,需要流式写入时不再寻找sink/pipe_from这类 API,而是直接使用Writer::into_sink()/into_bytes_sink()/into_futures_async_write()这组零拷贝或低开销适配器;而在需要“把已有对象范围合并写入”的场景下,Writer::copy_from(path, range)(受Capability::write_can_copy_from能力位约束、单次最多 5 GiB)提供了服务端的原生拷贝路径。理解这一演进脉络,有助于在升级 OpenDAL 版本、阅读旧代码或自行实现 service 时,准确对应历史文档与当前代码之间的 API 差异。

  • 数据存储
  • 后端

【免费下载链接】opendal

Apache OpenDAL: One Layer, All Storage.

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 15:12:44

链队列的四大状态契约:语义正确性与资源可控性

1. 为什么链队列不是“链表队列”那么简单&#xff1f;“Python描述数据结构之链队列篇”——这个标题乍看平平无奇&#xff0c;像极了教材目录里被翻烂的一页。但我在某高校数据结构实训课带过三届学生后发现&#xff1a;92%的人能写出链表&#xff0c;85%的人能写出数组队列&…

作者头像 李华
网站建设 2026/10/14 15:12:40

商业与开源并行:解开腾讯WorkBuddy与Octop双布局之谜

最近我在技术社区里留意到一个很有意思的现象&#xff1a;腾讯的 WorkBuddy 刚在应用商店上线没多久&#xff0c;那边 Octop 开源的官方公告也发了出来&#xff0c;底下的评论直接炸了锅。很多人第一反应都是同一个问题——"腾讯是不是自己打自己&#xff1f;家里已经有了…

作者头像 李华
网站建设 2026/10/14 15:12:40

Java实战:用Gin Rummy游戏练手牌型识别与AI对战

简介&#xff1a;一份基于Java的经典杜松子酒接龙&#xff08;Gin Rummy&#xff09;纸牌游戏实现&#xff0c;遵循传统二人对战规则&#xff0c;玩家可与电脑AI进行完整对局&#xff0c;适合Java学习者、游戏开发爱好者及需要课程设计参考的高校学生。项目完整覆盖了发牌、出牌…

作者头像 李华
网站建设 2026/10/14 15:12:16

短视频旁白怎么写?适合AI配音的文案结构与写作技巧

同一段文字&#xff0c;放在文章里读起来很顺&#xff0c;转换成短视频旁白后却可能显得生硬。句子太长、信息太密、缺少停顿&#xff0c;都会影响AI配音的听感。如果平时使用文字转语音制作视频&#xff0c;写文案时就不能只考虑“看起来通顺”&#xff0c;还要考虑“听起来是…

作者头像 李华
网站建设 2026/10/14 15:11:53

MCP实战:构建AI操控Excel的后端服务全解析

前段时间有个做运营的朋友抱着一摞Excel表格找我&#xff0c;说他们想用AI把这些表“管起来”——不是问一句“这个表有多少行”这种回答&#xff0c;而是像让AI替班干活的形态&#xff1a;你说一句“把销售明细里华东区订单金额大于5000的提取出来&#xff0c;按省份汇总&…

作者头像 李华