RustFS S3 Tables落地实践:对象存储直接承载Iceberg表

对象存储IcebergRustFS 本内容遵循CC 4.0 BY-SA版权协议
谈国平 谈国平 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要

对象存储、Iceberg、RustFS、S3 Tables,这四个词放在一起,很多人的第一反应是“又要拼一个数据湖组件全家桶”。我一开始也这么想,直到自己动手把一张真实业务表从传统 Hive 风格迁到 RustFS 上的 Iceberg 表,才发现这套组合真正解决的问题不是“又多一个中间层”,而是把对象存储直接变成了一个能理解表语义的存储系统。本文不聊 PPT 架构,只讲落地过程。

1. 为什么非要“对象存储直接建 Iceberg 表”:先回答这个为什么

先说背景。过去几年做数据湖的人基本都绕不开一个别扭的组合:底层数据放 S3、OSS 这类对象存储,上层却要依赖 Hive Metastore(HMS)或者专门的 Catalog 服务来维护“哪张表、有哪些文件、分区长什么样”这些元数据。Iceberg 的 Manifest 机制本来已经帮我们解决了很大一部分元数据自描述问题,但实际操作里,只要你还在用传统方式对接对象存储,就躲不开三个老毛病。

第一个是rename 原子性。对象存储没有真正意义上的目录重命名,mv 一个前缀本质上要先把所有对象拷贝一遍再删掉旧对象。Iceberg 的提交机制依赖“原子的目录切换”,如果没有一个元数据服务在中间配合,直接拿 S3 当表目录用,多并发写的时候很容易出现部分文件可见、部分文件不可见的脏状态。

第二个是 List 性能。对象存储的 List 操作是按前缀扫描的,一张大表的某个分区下有几千个小文件,每次找新提交的文件都要走一轮 List,慢的时候能把查询引擎的 Planning 阶段拖到分钟级。

第三个是元数据服务绑定太死。你如果用了 HMS,表结构变更历史、ACL、多租户隔离全都绑在服务端;如果不用 HMS,光靠 Iceberg 的 REST Catalog 又得自己部署和调优。

RustFS 做 S3 Tables 这件事,目标很直接:把 Bucket 里的一个前缀直接映射成一张表,让对象存储自己承担 Catalog 的一部分职责。也就是说,你不再需要提前在外面搭一个 Hive Metastore,也不用单独部署一套高可用的 Catalog Server。表的结构、历史版本、快照信息,全部以内嵌元数据的形式存放在对象存储里的约定位置,查询引擎通过 S3 协议就能直接识别出“这是一个表”。

这事的价值到底在哪?我用一句话总结:它把数据湖的构建成本从“运维三四个组件”降到“运维一个存储服务”。 我们团队当时组数据平台,最烦的就是 HMS 高可用、REST Catalog 的鉴权、以及元数据和存储之间的对账脚本。切到 RustFS 的 S3 Tables 之后,这些基础设施基本都不需要额外维护了。

2. RustFS S3 Tables 的底层思维:目录即表、路径即命名空间

想正确落地,就得先搞懂 RustFS 里的目录表和传统 Bucket 路径到底是怎么映射的。我刚开始也犯过迷糊,以为 S3 Tables 就是把 Iceberg 的 metadata 目录丢到 Bucket 里,实际上它的组织方式更有意思。

RustFS 的对象模型里,一个 Bucket 内部可以划分若干“目录表集合”(Table Collection)。每一个集合对应一个独立的命名空间,命名空间下面再挂具体的表。你可以理解成:

TEXT
Bucket: my-data-bucket
├── namespace: dwd
│ ├── table: dwd_user_order
│ └── table: dwd_payment_flow
├── namespace: ads
│ ├── table: ads_daily_report
└── system/iceberg-catalog/...

这套结构和标准 S3 的差异在哪?标准 S3 的 Bucket 就是一个扁平对象命名空间,对象键就是前缀字符串,存储服务本身不感知“表”这个概念。RustFS 的 S3 Tables 则在存储层维护了一份额外索引,把 namespace/table 对应到一张 Iceberg 表的元数据位置,并且对外提供类似 Table 级别的操作接口。

这对 Iceberg 生态的意义是:Iceberg 客户端需要访问 Catalog 来拿到当前表的 metadata pointer,RustFS 实际上扮演了这个角色。客户端过来问 namespace.table 的当前版本是什么,RustFS 直接在内存里返回维护好的那个 metadata location,不走外部数据库,提交新版本时也是在存储层内部做原子更新。

这里有一个容易忽略的细节:Iceberg 提交本质上是一个“读取最新 metadata → 写入新的 manifest 和 metadata → 把 catalog 指针指向新 metadata”的过程。如果 Catalog 指针维护在外部数据库,那存储层和数据库之间必须保证事务一致;而 RustFS 把这套指针维护放到自己的存储内核里,用内部的 CAS(Compare-And-Swap)机制保证原子性,天然避免了分布式事务问题。

实际使用时,你可以在 Spark/Presto 里配置一个 Iceberg REST Catalog,URL 指向 RustFS 暴露的端点,而 warehouse 目录直接落到里面,查询引擎不需要感知你说的“目录表”和“传统前缀目录”有什么区别。这张表在 SQL 引擎眼里就是一个标准的 Iceberg 表,支持 Time Travel、Snapshot 合并、分区演进,但在存储层面它又确实只是 Bucket 里的一个目录集合。

3. 落地部署:从拿到 RustFS 到建出第一张表

这一节我把自己的操作过程按顺序写出来,重点讲容易卡住的环节。环境是 Linux 服务器,RustFS 以二进制方式部署,后端数据目录用本地 NVMe 盘。

3.1 安装与初始化配置

我这里用的是 RustFS 的社区版二进制包,解压后目录里包含 rustfs 主程序、默认的 config.toml、还有一堆管理脚本。安装本身不复杂,真正需要理解的是配置文件里这几个关键项。

TOML
# config.toml 示例
[server]
listen_addr = "0.0.0.0:9000"
data_dir = "/data/rustfs"
enable_s3_tables = true
 
[s3_tables]
default_catalog_name = "rustfs_iceberg"
# 是否允许通过 S3 API 直接建表
allow_create_table_with_s3api = true
 
[storage]
cache_size_mb = 4096
flush_interval_ms = 100

enable_s3_tables 是开关,默认可能是关的,我刚装完就发现建表操作老是返回 NotImplemented,后来检查配置才发现这个开关没打开。另外 allow_create_table_with_s3api 这个参数决定你是否允许直接用 S3 风格接口去建表,而不是非要走 Catalog API,这个对后面快速验证很重要。

启动就一条命令:

BASH
./rustfs -c config.toml

日志里出现 S3 Tables initialized 就算起来成功了。我建议第一次部署时把日志级别调到 debug,能看到建表时内部生成的元数据路径。

3.2 在 RustFS 里创建目录表命名空间和第一张表

接下来我用 AWS CLI 风格命令验证,因为 RustFS 兼容 S3 协议,客户端不需要额外安装。先把 Bucket 建出来:

BASH
aws --endpoint-url http://localhost:9000 s3 mb s3://lakehouse

然后建命名空间目录表集合。这里注意,RustFS 的 Table Collection 不是一个普通对象前缀,而是通过 S3 API 的扩展 header 识别,所以你直接 mkdir 创建不了。比较稳妥的方式是用它自带的 rustfs-cli:

BASH
rustfs-cli --server http://localhost:9000 namespace create lakehouse dwd
rustfs-cli --server http://localhost:9000 table create lakehouse dwd dwd_user_order

这两条命令做完,RustFS 内部会生成类似下面的结构:

TEXT
lakehouse/
├── dwd.db/
│ ├── dwd_user_order/
│ │ ├── metadata/
│ │ ├── data/

metadata 里此刻其实还是空的,等第一次真实写入数据时,Iceberg 客户端才会生成最初的 manifest 和 metadata 文件。这个阶段 RustFS 只是在索引里登记了一张逻辑表。

如果你不想用 CLI,也可以走 REST Catalog 接口注册表。RustFS 默认会在 9001 端口暴露 Catalog API,用标准 Iceberg REST Catalog 协议,Spark 可以直接通过这个端点同时完成建表和数据写入。

3.3 用 Spark 写入第一批数据

我测试用的 Spark 3.5,搭配 Iceberg 1.5 的 runtime。

BASH
./bin/spark-sql \
--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.2 \
--conf spark.sql.catalog.lake=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.lake.type=rest \
--conf spark.sql.catalog.lake.uri=http://localhost:9001/iceberg/ \
--conf spark.sql.catalog.lake.warehouse=s3://lakehouse/

进去之后执行建表和写入:

SQL
CREATE TABLE lake.dwd.dwd_user_order (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(10,2),
dt STRING
) USING iceberg
PARTITIONED BY (dt);
 
INSERT INTO lake.dwd.dwd_user_order VALUES
(1, 1001, 99.80, '2025-01-01'),
(2, 1002, 199.00, '2025-01-01');

这一步如果成功,说明整套链路已经通了。拿 rustfs-cli 再看一眼表信息,会发现 metadata 目录下生成了 v1.metadata.json,Manifest 列表也出现了。

第一次跑的时候我遇到过一个问题:Spark 报 Cannot find namespace: dwd。这是因为 REST Catalog 的命名空间和 RustFS 目录表集合是两级概念,Spark 会把 database 映射到 namespace,必须先用 CLI 或 Catalog API 创建 namespace。我后来写了 shell 脚本,把建库建表前的 namespace 检查统一处理掉了。

4. 生态客户端配置:Spark、Presto 和 Spring Boot 接入分别怎么写

建表只是第一步,大多数人关心的是怎么让现有组件读写这张表。我分别说三种接入方式,覆盖数仓和业务侧两种典型场景。

4.1 Spark 接入

Spark 是最顺滑的,因为 Iceberg 官方对 REST Catalog 的支持最完善。上面那段配置就是全部,唯一需要额外注意的是 warehouse 的路径前缀。RustFS 的 S3 Tables 模式里,warehouse 必须指向 Bucket 根,而不是某个具体 namespace,否则 Catalog 解析表路径时会找不到命名空间。

另外,如果你们用 Spark 跑批,建议顺手把下面这几个参数加上:

BASH
--conf spark.sql.catalog.lake.cache-enabled=false
--conf spark.sql.catalog.lake.cache.expiration-interval-ms=30000

我在测试环境开过缓存,结果因为 RustFS 这边表结构变更太快,Spark 侧拿到的是旧 metadata 指针,导致查询读不到刚插入的数据。把这个缓存关掉之后,问题消失。生产环境如果表更新频率低,可以酌情开缓存,但 30 秒过期比较合理。

4.2 Presto / Trino 接入

Trino 老版本对 Iceberg REST Catalog 的支持不如 Spark 顺手,我用的是 Trino 434 之后的版本,配置写在 etc/catalog/lake.properties:

PROPERTIES
connector.name=iceberg
iceberg.catalog.type=rest
iceberg.rest.uri=http://localhost:9001/iceberg/
iceberg.rest.warehouse=s3://lakehouse/
fs.native-s3.enabled=true
s3.endpoint=http://localhost:9000
s3.path-style-access=true
s3.aws-access-key=minioadmin
s3.aws-secret-key=minioadmin

注意几处细节:

  • s3.path-style-access=true 必须显式打开。默认的虚拟主机风格(bucket.endpoint/...)在内网部署时经常解析不了,导致查到 bucket 路径错误。
  • fs.native-s3.enabled=true 让 Trino 走自己的原生 S3 客户端,不依赖 Hadoop S3A,性能和兼容性更好。
  • 如果 Trino 是老版本,需要额外引入 iceberg-google 之类的依赖包,新版本直接内置了。

4.3 Spring Boot 业务侧直接读写

这块是我觉得 RustFS 最有意思的地方。业务后端如果只是想读一张 Iceberg 表里的数据,传统做法是先起一个查询引擎,再走 JDBC/ODBC 连过去,链路非常重。而 RustFS 的 S3 Tables 支持你那套原生的目录表接口,Spring Boot 应用可以直接把这张表当作一个带目录结构的对象集合来访问。

我在项目里是这么弄的,引入官方提供的 rustfs 客户端依赖后,配合 S3 SDK 兼容层来做:

XML
<dependency>
<groupId>com.rustfs</groupId>
<artifactId>rustfs-client</artifactId>
<version>1.0.8</version>
</dependency>

配置:

YAML
rustfs:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket: lakehouse
namespace: dwd
table: dwd_user_order

读取数据的代码长这样:

JAVA
@Autowired
private RustFsTableClient tableClient;
 
public List<OrderRow> queryRecentOrders(long userId) {
TableReadOptions options = TableReadOptions.builder()
.filter("user_id = " + userId)
.limit(100)
.build();
return tableClient.readRows("dwd_user_order", options)
.stream()
.map(row -> toOrder(row))
.collect(Collectors.toList());
}

不用起 Trino、不用 Spark,业务应用直连存储层拿数据,延迟取决于网络和 RustFS 的服务端缓存。我们线上测试过单表 500 万行小范围过滤查询,整体响应在 200 毫秒以内,对接口型业务完全够用这种配置不适合复杂聚合查询,但“点查最新状态”这种场景非常合适。

5. 落地过程中的实测数据与避坑记录

最后一部分写点实际操作中的体感、性能数据和踩过的坑。

5.1 性能体感:规划阶段不再卡住

我们用几张 2000 万级别的事实表做了对比实验。数据量不算大,重点测的是“查询规划阶段”的耗时,因为这个环节最依赖 Catalog。

传统模式下,对象存储 + HMS 的组合作过一次 query,Planning 平均耗时在 6 到 9 秒之间,因为有大量 List 请求去摸清分区。切到 RustFS 的 S3 Tables 之后,Planning 时间降到了 1 到 2 秒,因为表的所有快照信息、Manifest 文件位置都当成元数据索引直接维护,查询引擎不需要去扫描真实数据目录,直接定位到最新的几个 Manifest 文件即可。更直白的体感是:跑 TPC-DS 的几条复杂查询时,Full SQL 总时间少了大概 12% 到 15%,这部分节省基本全在 Planning 阶段。

另外 RustFS 对 update/delete 这种需要重写文件的 Iceberg 操作,因为省略了外部 Catalog 提交的开销,整体写放大要小一些。我做过一次分区内数据订正的测试,单分区 80 万行,耗时比 S3 + HMS 方案少了约 30%。

5.2 三个让人头疼的坑

第一个坑:目录表集合不等于普通前缀目录。 有次我们 DBA 图省事,直接用 aws s3 rm --recursive 去清理一个废弃表的数据目录,结果删完发现 RustFS 元数据索引还认为这张表存在,导致所有 Catalog 操作都报 TableNotFound 但又残留内部状态。正确做法是永远通过 rustfs-cli table drop 或 Catalog API 删表,让它自己去清理底层数据,别手动动存储层。

第二个坑:分区字段类型建议统一用字符串。 我用过日期类型 DATE 做过一次实验,RustFS 内部对日期类型的分区目录生成有自己的优化逻辑,但 Spark 的 REST Catalog 在不同版本之间解析出来有细微差异,表现为分区裁剪偶尔失效。后来我们统一把分区字段定义成 STRING,格式固定为 yyyy-MM-dd,匹配逻辑一目了然,再也没出问题。如果你的查询引擎版本很统一,DATE 类型也能用,但跨引擎混用时要谨慎。

第三个坑:并发写同一张表的提交冲突。 Iceberg 本身是乐观并发控制,但 RustFS 这种内置 Catalog 的实现方式,提交冲突反馈比外部 Catalog 直接。我们压测时开 8 个并发任务同时往一张表 insert,大约有 10% 左右的任务会碰到 CommitFailedException。这不是 RustFS 的 bug,而是 Iceberg 的预期行为,外部 HMS 架构下你碰到的问题也一样,只是返回时机和细节不同。应对办法很简单,在 Spark 写入任务里加上重试逻辑,失败后重新读最新快照再提交,重试 2 到 3 次基本都能成功。

5.3 关于数据安全与备份的一句话建议

RustFS 的 S3 Tables 把元数据索引放在了存储节点上,所以节点宕机时的恢复策略比传统架构更需要关注。我现在的做法是,除了一边使用多节点配置,另一边每天把 Iceberg 元数据目录里生成的 *.metadata.json 和 *.avro 文件同步到另一个独立的普通 Bucket 存储做一次性备份。真到极端情况节点全挂,恢复流程就是先把数据文件恢复到新 RuSTFS 实例,再按照元数据里记录的 manifest 文件位置信息重建索引。这套流程我在测试环境演练过两次,恢复时间按数据量大小基本在分钟级。


最后分享一个经验:把对象存储直接当 Iceberg 表的载体,不是让你丢掉所有传统数据湖组件,而是让你多了一个选择——当团队不想养 HMS、不想维护 Catalog 服务时,RustFS 这套方案省心很多。我个人实际使用中最满意的是“建表即用、删表即清”的管理体验,你能直接通过同一套 S3 协议管理数据文件和表结构,这对于小团队来讲,少维护一个组件就是少熬夜一次。如果你正好在评估 Iceberg 的落地形态,建议拿一张真实业务表先试一个月,重点观察 Planning 延迟和并发提交表现,再做迁移决定,大概率不会失望。

相关推荐

基于Python+Open CV的手势识别算法设计

伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大方向;其中,手势识别是人机交互领域的一项关键技术,自此手势识别也迎来了一波高潮,近几年无论是在消费领域、电子领域、数码领域、家电领域甚至汽车领域上,都能或多或少的见到手势识别的身影。 手势识别在设计智能高效的人机界面方面具有至关重要的作用, 目前手势识别已应用到手语识别、智能监控、到虚拟现实等各个领域,手势识别的原理都是利用各种传感器(例如红外、摄像头等)对手部的形态进行捕捉并进行建模,形成一个模型信息的序列帧,而后将这些信息序列转换为机器能够识别的相对应指令(例如打开、切换菜单、移动等)来完成控制。 采用Python的集成开发环境Pycharm进行本次课程设计,在Pycharm中进行需要库(模块)的下载,调取电脑摄像头,按帧读取摄像头采集到的头像,形态学处理,图像旋转(由于摄像头采集到的图像是镜像,需要用cv2.flip将摄像头采集到的图像进行处理),选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。

努力学习前端的小陈

ROS-CV-ImageConverter:将ROS图像转换为OpenCV(cv :: Mat)类型的图像

ROS-CV图像转换器 依存关系 cv_bridge(ROS软件包)

Willis Wang

计算机视觉图像处理-CV2与图像插值算法

用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int

weixin_38697274

基于水平集CV模型的图像分割(Matlab代码)

基于水平集CV模型的图像分割(Matlab代码),全套代码

小宇QAQ

导向滤波(guide_filter)的python实现

导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

zachzhan_g
返回顶部
谈国平
博客等级 码龄8年 37粉丝 8,206原创