ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

S3对象存储上生产:务必做的3组读压测,避开冷读、并发、List线上坑

S3对象存储上生产:务必做的3组读压测,避开冷读、并发、List线上坑

读密集业务的上线事故,很少发生在峰值吞吐上,更多发生在没人测过的角落:服务重启后的第一批请求、并发上量之后的长尾、某个业务线程里的一次全量 List。官方 benchmark 和第三方跑分回答的是「这套软件在别人的数据形状上多快」,回答不了「在你的桶里多快」。这篇给三组成本不高的自查测试,每组都是拿自己的数据形状去问存储,动作就是跑命令、看数字、对预算,一个下午能全部跑完。示例全部以 RustFS 演示,方法适用于 MinIO 在内的任何 S3 兼容存储。

开始前先把客户端配好。下文的s3rustfs是 mc 的别名,第一次用先设好,后面所有命令都走它:

mc alias set s3rustfs http://rustfs-host:9000 YOURACCESSKEY YOURSECRETKEY

三组测试的分工一图看全,后面每节展开一组:

benchmark 替代不了自查

跑分类的测试有个共同点:数据形状是均匀的。对象大小固定、前缀结构平坦、并发节奏由压测工具说了算。真实的读密集负载没有这么干净,小对象点读占大头,前缀有局部性,并发来自业务自己的节奏。跑分能证明软件路径没问题,但证明不了你的前缀设计不会拖慢 List,也证明不了你的对象大小不会让冷读变贵。

三组自查的目的,是把「跑分没问题,应该也没问题」换成「这三项我拿自己的数据测过」。每组的产出都是数字加一条业务侧的预算线:冷热差、并发平台点、List 耗时。数字和预算线对上了,上生产才有依据。

第一组:冷读,缓存热起来之前有多慢

读路径上有缓存层的存储系统,热数据的读延迟和冷数据回源的延迟可能差出量级,这个差距官方通常不给数字,因为它取决于你的对象大小和网络。测法很直接,同一个前缀拉两遍,比时间:

time rclone copy s3rustfs:bucket/cold-prefix-a /tmp/empty-1 --transfers 1 time rclone copy s3rustfs:bucket/cold-prefix-a /tmp/empty-2 --transfers 1

一个容易踩的坑:目标目录要保证不存在,rclone 复制时会保留源对象的修改时间,目录已有时重跑会被判定为「没变化」直接跳过传输,计时测的就不再是下载而是文件比对。每轮测试前把/tmp/empty-1、/tmp/empty-2删掉重来。

两个时间的差就是回源成本的真实大小。做的时候有几个容易翻车的地方。前缀要真的没读过,新造一批数据,这步要是偷懒拿热前缀测,差值会小得让你以为自己缓存放得很大,白忙一场;冷数据造起来也省事,本地随机生成一批不同大小的对象用 mc cp 传上去,大小分布照着生产的来。单次计时会被网络抖动污染,多来几轮看分布,重点盯最差的那几轮。最后对照的是业务超时预算,接口超时 3 秒,冷读一批对象就得装进这条线里,绝对值漂不漂亮反而不要紧。

写目标选/tmp或内存盘这类快路径也有讲究:客户端自己写盘太慢会把测量的水搅浑,你以为在测回源,其实卡在测试机的磁盘上。想要干净的数字,要么换更快的落盘路径,要么直接> /dev/null丢弃写入只留读。

这组测试还顺带回答一个上线后必问的问题:服务重启之后,读延迟多久能回到正常水位。答案是第一遍和第二遍时间差收敛的速度,自己测出来比猜靠谱。

真正的压力场景是两组条件叠加:缓存刚清空,又来一波并发。可以简单模拟一下,重启服务后立刻用--transfers 32拉一个冷前缀,看错误数和最差延迟。很多线上事故的形态就是这个组合:维护窗口刚结束,调度系统把积压任务一起放出来,缓存没热、并发先到。这个数字如果难看,值得给重启后的恢复期设计一个预热或限流步骤,而不是让业务来当预热器。

第二组:并发读,加压之后看吞吐和长尾

单流快说明不了并发行为。读放大之后,瓶颈可能从网络挪到磁盘或 CPU,位置不同,表现完全不同。工具二选一:快速粗测用 rclone 拉并发:

rclone copy s3rustfs:bucket/mixed-prefix /dev/shm/out --transfers 32 --checkers 32

要更规范的数字用 warp,MinIO 团队开源的 S3 基准工具,get 模式只压读路径,README 里的标准用法:

warp get --host=rustfs-host:9000 --access-key=YOURKEY --secret-key=YOURSECRET --duration=3m --concurrent=32 --obj.size=1MiB

--obj.size=1MiB只是示例值,压测前改成贴近业务真实的对象大小,大小分布照生产的来,否则测出来的并发平台点会失真。

先看吞吐曲线。并发往上加,吞吐跟着涨,涨到某个点就平了,那个位置就是这套部署的舒适并发数,warp 默认并发 20,--concurrent可以调。再看错误和长尾:重试和错误数应该是 0,p99 和均值的差距有没有越压越大,长尾一旦拉开,多半是哪里开始排队了。业务客户端数要是远超变平点,出路是拆前缀分流量或者加节点,硬顶是顶不住的。

warp 跑完会在当前目录留下压缩的结果文件,用warp analyze拆开看每个操作的延迟分布,比盯一个总吞吐数字有用。压完单端点还可以把多个端点逗号分隔传给--host,模拟客户端分散到多节点入口的行为,README 里对这两种用法都有示例。注意 warp 会先上传一批对象作为测试池再开压,测完记得清掉它建的桶,别让基准数据混进你的生产数据。

第三组:大桶 List,元数据路径的隐藏成本

S3 的 ListObjectsV2 按页返回,一页最多 1000 个对象,百万级对象的桶全量扫一遍就是上千次分页请求,元数据路径的成本从这里来。而这个问题在常规压测里最容易漏,因为压测工具通常只压 GET 不压 List,业务代码里那句「列出目录下所有文件再逐个处理」上线后才会暴露。测法:

time mc ls --recursive s3rustfs:bucket > /dev/null time mc ls --recursive s3rustfs:bucket/2026/10/ > /dev/null

warp 也有 list 模式,但它会自己造一批对象再列,数据形状归它管;你要测的是自己桶的形状,所以主命令用 mc 计时,warp 的结果当参照就好。全量列一遍的耗时是一个数,按前缀收窄后再列是另一个数,两个数的降幅能直接告诉你前缀设计有没有起作用。如果业务里存在「全量扫桶再过滤」的代码路径,这组数字大概率会让你去改代码:按前缀取数,或客户端维护索引,比压存储端有用得多。

这组测试的真正产出其实是一份前缀设计评审。把业务里所有 List 调用列出来,每个都问一句:能不能用更窄的前缀,能不能用更少的页。前缀设计是上传时定下的,等读不动了再改就要迁移数据,这是三组测试里唯一一个「测出来晚了就改不动」的项目,所以哪怕前两组全绿,这组也建议在上传路径定型之前跑。

环境怎么搭,测试才不作废

测试结果可不可信,基本看环境搭得对不对。数据形状要同形,对象大小分布、前缀层级、数量级都照生产的样子造,规模可以缩,形状不能缩,缩十倍之后前缀深度和大小分布都得保住,一百个对象的桶测不出百万对象的 List 行为。还得想清楚单机到底能回答什么,docker-compose 单机环境测的是软件路径和操作手感,签名、HTTP、对象组装、元数据操作都在,但多节点扩展性它答不了,冷读和 List 两组的单机数据有参考价值,并发那组的数字只在同拓扑下作数。客户端也要同形,业务用什么 SDK 就用什么测,mc 和 rclone 是测量工具,替不了业务路径,SDK 自己的重试和超时逻辑跟测量工具是两回事,最后拿业务客户端跑一遍冒烟才算闭环。

测试机的位置也别忽略:和生产环境隔着一个公网或跨机房链路,测出来的延迟就掺了链路成本,条件允许就把测试机放进同内网段,条件不允许就把链路延迟单独测出来记账上。

最后把每组数字记下来,存好时间戳,连同当时的对象规模和并发参数一起记。升级 RustFS 版本之后把三组重跑一遍,对比两组数字,这就是你自己的回归基线,比任何外部跑分都贴身。

数字有了,跟什么比

三组数字都测出来了,还有件事悬着:跟什么比?说实话,别跟 benchmark 比,跟业务自己的数字比。冷读那组对接口超时和批处理窗口,最差轮次也要装得进去。并发那组对业务的真实客户端规模,舒适并发点要盖得住它,盖不住就得拆分或扩容。List 那组对业务里扫桶调用的频率和容忍度,单次耗时乘以调用频率,就是它在业务线程里占掉的时间份额。这三个数字产品侧都有现成的,测试之前先问出来,别等测完再拍脑袋定线。

这些测试防的是什么

回头看,冷读挡的是重启后第一批请求超时,并发挡的是上量之后吞吐顶不住,List 挡的是藏在业务代码里的全量扫描悄悄拖慢整个服务。三组全绿再谈上线;哪组不绿,先查前缀设计、缓存策略和业务取数,最后才轮到怀疑存储本身。顺手把三条主命令各存成一个脚本,参数化前缀和并发数,以后扩容、升级、换硬件都直接复跑。这套自查不给你任何性能承诺,它能保证的只有一件事:变更窗口里不用靠猜。

RELATED READING

延伸阅读

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