ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

一条 AI 管线靠 S3 Tables 省了 40% 基础设施成本,自建侧把这三笔账算明白

一条 AI 管线靠 S3 Tables 省了 40% 基础设施成本,自建侧把这三笔账算明白

事情是这样的。

前两天我在 AWS 的官方博客上刷到一个案例,看完第一反应是不信。Seltz,一家给 AI agent 做网络搜索的公司,它的 Knowledge Pipeline 处理几十亿份文档,每个数据加工阶段都要写出几百万个小文件,换到 Amazon S3 Tables 之后,基础设施成本省了 40%。

40%。一个存储行业的老兵看到这个数字,心里会同时冒出两句话,一句是卧槽,一句是真的假的。

但这次我倾向于信。因为这个数字的口径写得特别老实,AWS 原文写的是相比自管管线省 40% 基础设施成本,并且专门补了一句,不含工程人力。行业里太多降本故事靠把人力成本藏起来凑数,能把边界主动讲清楚的,反而更像真的。

信归信,我心里还有个更大的疑问。Seltz 是托管用户,它付钱让 AWS 替它管账本。那同样是这三笔开销,落到我们这种自建侧的工程师头上,长什么样,到底贵在哪,省下来到底是什么东西。

文档给不了这个手感。所以我自己跑了一遍。

先交代环境,数字能信几成你自己判断。我在自己电脑上起了一个 RustFS 1.0.4 单机实例,Windows 本地盘,客户端走 localhost,引擎用的 PyIceberg 0.11.1,连 RustFS 内置的 Iceberg REST 目录。RustFS 是一个 Apache 2.0 协议的 S3 兼容对象存储,兼容到 S3 Tables 这种控制面 API 都能对上,这也是我能把这套东西搬回家跑的原因。

然后我把同一份 100 万行的数据,写成了两张表。表 A 模拟 Seltz 那种 AI 管线的真实节奏,200 次追加,每次只写 5 千行,攒出 100 万行大约 5.3 MB 的 Parquet。表 B 图省事,同样的 100 万行一次写完,做对照。一个缩比声明放在前面,Seltz 是几百万文件的量级,我这里只有 200 个,税的形状是一样的,税额别直接换算。

写完打开桶一看,第一笔账就来了。

表 A,600 个对象。表 B,3 个。

我当时就愣了一下,数据明明一模一样,对象数差了 200 倍。拆开看更难受,表 A 的 600 个里,200 个是数据文件,另外 400 个全是元数据。Iceberg 每提交一次追加,就会在桶里落一对 manifest 和 manifest-list,200 次写入就是 400 个元数据对象,一共 2.1 MB。表 A 的数据文件才 5.3 MB,账本抄出了数据本身 39% 的体积。

你存的每一个小文件,都在元数据层再抄一遍账。这就是第一笔,小文件税的数据层。

本来看到这就想写结论了,结果扫描的数字告诉我,这才收了一半。

全表扫描这 100 万行,表 A 三次实测 1.74 到 1.85 秒,表 B 稳定在 0.125 秒左右。14.6 倍。每个数据文件一次 S3 GET 的开销,就是这 14.6 倍的主体,好理解。

真正让我坐直的是过滤扫描。我取 id 在最后一段的 1 万行,按 Iceberg 的设计,文件裁剪靠 Parquet 统计信息跳过无关文件,200 个文件应该裁到只剩两三个,扫起来应该飞快。裁剪确实生效了,真的只剩 2 个文件,实测 0.796 秒。表 B 同样条件,0.131 秒。

还是差 6 倍。

多出来的时间花在哪,我盯着火焰图想了一会儿才想明白,裁剪这个动作本身,要先读几百个元数据对象做引用分析。账本太厚,翻账本就要半天。所以小文件税收两层,第一层是数据文件的 GET 次数,第二层是每次扫描前都要付的元数据读放大。AWS 案例里托管 compaction 为什么值钱,因为它合并的不只是数据文件,还有你后面每一次扫描都要背的那本厚账。

写到这你可能会想,那我把写入合并成大批量不就完了。朋友,真实世界的 AI 管线做不到,Seltz 的每一个加工阶段就是会产生几百万次小写入,这是业务形状,不是谁手滑。

所以问题变成,账本已经厚了,怎么办。

第三笔账在提交上。表 A 的 200 次追加总共跑了 198 秒,单次提交 p50 是 0.97 秒,最慢 2.2 秒。这个时间花在哪的,每次只有几百 KB 的 Parquet,写盘根本用不了一秒。它花在 Iceberg 的提交协议上,每次追加都要校验基准元数据,写新的 manifest,然后原子更新元数据指针。两个客户端同时提交的时候,后到的那个必须重读元数据解决冲突。高频小批写入的真正单价,是每次约 1 秒的元数据提交,不是数据本身。

这 1 秒攒出来的另一个东西叫快照。200 次追加后,表 A 攒了 200 个快照、402 个元数据文件。这时候我调服务端的维护端点做一次 dry-run 规划,想看看能回收多少。结果等我的不是报告,是等待。表 B 那边,1 个快照 3 个对象,0.14 秒返回。表 A 这边,629.8 秒,十多分钟。后面的删除计划更夸张,1404 秒。

维护规划要读所有快照的引用关系,快照拖得越多,它自己越慢。这是个会滚雪球的债,越不维护,越维护不动。我盯着那十几分钟的进度条,突然就理解了托管侧那句话,快照过期是自动的。自动的意思是,有人在你看不见的地方替你还这笔债。

终于等到规划结果。402 个元数据文件里,200 个是删除候选,保留的 202 个是当前快照还引用着的,200 个数据文件零候选,判定都是对的。接着真删,DELETE 作业报告回来,deleted_metadata_file_count = 200。

删干净了,对吧。

我顺手 list 了一下桶。

600 个对象,一个没少。

去翻服务端日志,本机 Windows 后台清理任务在报 OS 权限错误,error 5,物理删除根本没落地。报告说删了 200 个,桶里真真切切一个都没动。这一下给我整不会了,缓了半天只能接受这个事实,作业报告说了删,不等于桶里真没了,复核只能靠自己的 list。当然要公平地说一句,这是 Windows 本地盘的表现,Linux 生产部署上是不是同样,得单独验过才知道,我这边只敢说实测如此。

账三我原本没计划,是被前两笔账逼出来的。既然报告不可全信,我就想试试更脏的场景,作业写崩留下的孤立文件。往表 B 的数据前缀直接塞了 3 个大约 3 MB 的 Parquet 残件,共 8.9 MB,然后调维护。

这回维护的反应快得很,即时把这 3 个识别成了清理候选,cleanup_candidate_object_count = 3。识别到了,然后呢,deletable_object_count = 0,一个没删。

拦着它的是安全窗口。刚写进来的对象一律不动,防止把还在途的写入误判成垃圾删掉。这个设计我认,真删了你哭都来不及。但代价就是回收要等窗口过了再跑一次维护,而维护多快你已经在上面看到了。再加上官方文档明确写了一条,普通桶的生命周期过期规则对表桶直接禁用,因为生命周期策略不认识 Iceberg 的引用关系,删到当前快照还引用着的文件,表就废了。

写到这里,三笔账就都对上了。对完我回头看 Seltz 那 40%,突然觉得这个数字没那么神奇了。

它省的不是硬盘的钱。数据躺在盘上的成本,托管和自建差不了太多。它省的是三件事的自动化,小文件合并不用你排程,快照过期不用你还债,孤立清理不用你复核,外加一本不用你自己管的账本,Iceberg REST 目录做进了存储内核,控制面和数据面共用 9000 端口,单独的 catalog 服务进程和它的元数据库直接省掉。

而自建侧,这三件事全是显式维护操作,默认关闭,没有内置定时,什么时候跑、跑多勤,是你的运维排程问题。账二已经说明了跑多勤有讲究,快照积压会拖慢维护本身,高频小批写入的表,维护排程要跟着写入节奏走。

想自己跑一遍验证的,核心就三步。建桶并启用表桶,curl 要 7.76 以上才支持--aws-sigv4,

exportAWS_ACCESS_KEY_ID="<your-access-key>"exportAWS_SECRET_ACCESS_KEY="<your-secret-key>"exportAWS_DEFAULT_REGION="us-east-1"aws --endpoint-url"http://localhost:9000"s3api create-bucket--bucketmy-bucketcurl--fail-with-body--silent--show-error\--aws-sigv4"aws:amz:us-east-1:s3"\--user"$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"\--requestPUT"http://localhost:9000/iceberg/v1/buckets/my-bucket"

PyIceberg 连 REST 目录的时候,warehouse填桶名,不是 S3 URI,REST 签名和 S3 文件访问的凭证要分开配两套。有个坑我踩了,提前说,PyIceberg 0.11.1 带 JSON body 的请求,建 namespace、提交这类,会被服务端以缺少x-amz-content-sha256头拒绝,因为它的 SigV4 适配器只给空 body 设了这个头,得在 botocore 的SigV4Auth.add_auth前把请求体的 SHA256 补进头里再签名。计算引擎侧把 catalog 指到/iceberg就行,Spark 的最小配置长这样,

spark.sql.catalog.demo = org.apache.iceberg.spark.SparkCatalog spark.sql.catalog.demo.type = rest spark.sql.catalog.demo.uri = http://localhost:9000/iceberg spark.sql.catalog.demo.warehouse = my-bucket spark.sql.catalog.demo.rest.sigv4-enabled = true spark.sql.catalog.demo.rest.signing-name = s3

能力边界也交代清楚,支持 Iceberg 格式 v1 和 v2,默认 v2,暂存式建表、删表时清除数据和格式 v3 不在支持范围,没有 SQL 执行引擎,查询靠 Spark、DuckDB、Trino 这些引擎接 catalog,Seltz 的用法就是这个形态,事务范围限于单表,也不声明完整兼容 AWS S3 Tables 控制面。细节在 docs.rustfs.com 的 S3 Tables 文档,PyIceberg 实操有官方的 PyIceberg 集成教程,Seltz 案例原文在 AWS Storage Blog。

最后把不成熟的经验掏出来,1、高频小批写入的表,把维护排程当写入链路的一部分,追加多勤维护就得多勤,快照债会拖慢维护本身。2、每次维护后自己 list 桶复核回收量,报告计数和磁盘状态要分开看,我的 200 就是这么露馅的。3、别拿生命周期策略清理表桶,会被跳过,也别想着绕过安全窗口,它是防误删的最后一道闸。4、读密集场景给表留 compaction 窗口,14.6 倍的扫描差距是这篇实测里最直接的性能账。

一开始我也不知道跑这些能不能跑出什么名堂,反正实验数据都在,你复现的时候数字跟我差个百分之十几都很正常,比值和对象数的变化才是能信的部分。

回到开头那个 40%。

算完这三笔账我反而觉得,这个数字诚实得很。它从头到尾没说省的是硬盘,它省的是那本你不用再自己翻的账。做自建的朋友都懂,真正把人耗住的从来不是存数据,是存数据这件事衍生出来的那一堆账本,元数据、快照、引用关系、回收窗口,每一本都得有人翻。

Seltz 们把钱交给 AWS 翻。我们至少可以,先把账看明白。

以上实验的脚本和全部原始数据都在我的仓库里,复现遇到坑欢迎来聊。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进