shelf-project/shelf

GitHub: shelf-project/shelf

Shelf 是一个用 Rust 编写的、面向 Trino on Iceberg 的行组粒度读取缓存,通过伪装 S3 端点以零侵入方式将 p50 延迟减半、基础设施故障率降至接近零。

Stars: 4 | Forks: 0

# Shelf

Shelf — a wooden shelf holding metadata.json, manifests, footers and row groups, sitting between Trino and S3

一个行组粒度、计划感知且原生支持 Iceberg 的 Trino 读取缓存。使用 Rust 编写,Apache 2.0 许可证,采用失败开放 (fail-open) 设计。

CI Release License Container SBOM Stars

## 什么是 Shelf? Trino 在 Iceberg 上会对 S3 进行大量小且重复的读取操作 —— `metadata.json`、manifest 文件、Parquet footer 以及行组本身。这些字节在不同快照之间不会发生改变,但如果没有缓存,每个查询都会在 HTTP 中一次又一次地向 S3 询问相同的问题,这不仅带来高昂的延迟,还要不断付费。 Shelf 是一个小巧的 Rust 进程,它伪装成 S3。只需告诉 Trino `s3.endpoint=http://shelfd:9092` —— 仅需一行配置 —— Shelf 要么直接从其自身的 DRAM + NVMe 缓存中提供字节服务,要么将请求代理到真实的 S3,从响应中学习,并将其记住以备下次使用。

Three tiers — counter (Trino heap), pantry (Shelf), grocery store (S3)

## 为什么选择 Shelf - **行组粒度。** 缓存键为 `sha256(etag || offset || length || rg_ordinal)`。Shelf 缓存的是 64 KB 的 Parquet footer 或单个 4 MB 的行组,而不是整个 512 MB 的文件。对一个 3 GB 的表执行 `LIMIT 10` 查询,只需缓存 **~1.78 MB** —— 即经过 Iceberg + Parquet 裁剪后保留下来的字节范围,绝不多缓存。 - **Iceberg 时钟新鲜度。** S3 ETag 是每个缓存键的一部分。新快照 → 新文件 → 新 ETag → 新键。旧的条目会变成无法访问的孤立数据,Foyer 会在容量不足时将其驱逐。**无需 TTL,无需失效队列,杜绝脏读类 Bug。** - **计划感知预取。** Trino 协调器插件在规划器仍在分配 splits 时,就会预热文件和 footer 字节。行组预取由插件观察驱动;不依赖于已被移除的 `SplitCompletedEvent`。 - **跨副本共享。** 一个集群,多个 Trino 副本,共享一个预热好的工作集。不再为每个副本支付冷启动的代价。 - **无共识协议。** 成员关系由 K8s headless service 管理。Pin 列表和租户配额是版本化的 S3 ConfigMap。没有 Raft,没有 etcd。 - **失败开放。** 如果某个 Shelf pod 不可达,Trino 插件(在可用时使用 read-path SPI)会透过断路器直接回退访问 S3 —— 绝不会因为缓存未命中而阻塞查询。 - **v1 版本支持 HTTP/2。** 只有一种协议,只需调整一个连接池。Arrow Flight 被推迟到 v1.x 版本,具体取决于实测的 EKS 吞吐量。

Funnel showing how a 3 GB table narrows to a 1.5 GB partition, then a 512 MB file, a 4 MB row group, two column chunks of 1.4 MB, with 1.78 MB cached at the bottom

## 工作原理 一个 Rust 二进制文件,每个 pod 一个容器,每个缓存节点一个 pod。两个 Foyer 池共享磁盘 —— 诸如 manifest 和 footer 这样的小型数据保留在 DRAM 中,大型行组数据则溢出到 NVMe。

Architecture diagram — Trino cluster on the left, Shelf in the middle (S3-shim, Foyer engine, metadata pool, rowgroup pool), AWS S3 on the right

`:9092` 上的 S3 shim 通过 HTTP 接受 `GetObject` 和 `HeadObject` 请求,并按设计忽略 SigV4 签名(Shelf 是一个 sidecar,而不是多租户网关)。它支持后缀字节范围(`bytes=-100`)和开放式范围(`bytes=0-`) —— 这正是 Trino 原生 S3 客户端用于 Parquet/Avro footer 读取的请求形式。 Foyer 引擎是一个混合了 DRAM + 磁盘的缓存。这两个池之所以不同,是因为它们的访问模式不同: | 池 | 存储位置 | 优化目标 | 典型条目 | |---|---|---|---| | **metadata** | 仅 DRAM | 高请求率,小条目 | Iceberg manifest,Parquet footer | | **rowgroup** | DRAM + NVMe 溢出 | 高字节量,范围读取 | Parquet 列块 | 如果将它们混合在一个池中,会导致淘汰策略相互冲突。 ## Iceberg 时钟技巧 每个缓存最终都会面临同一个问题:*当数据发生变化时会发生什么?* 大多数缓存使用 TTL、失效队列或写入时的 pub/sub 来回答这个问题。Shelf 没有使用这些机制 —— 它仅使用由 ETag 构成的缓存键。

Two Yellow-Pages-style books on a shelf — old greyed out as orphan, new highlighted as current, connected by an arrow labeled INSERT → new file → new ETag → new cache key

当 Iceberg 提交新的快照时,写入器会生成一个带有 **新** ETag 的 **新** Parquet 文件。新文件的任何字节范围的缓存键在数学上都不同于旧文件的缓存键。旧条目并没有被“失效” —— 它只是变得不可达,成为一个由 Foyer 在容量不足时最终驱逐的孤立条目。 ## 实际影响 以下数据来自一个运行约 25 万查询/天的四副本 Trino-on-Iceberg 集群,记录了从先前的缓存层迁移到单 Shelf 金丝雀副本后的第一个小时内的表现:

Before/after comparison — left side red showing 94 percent failure rate, 147 metadata errors, 38 bad-data errors, 5.74 second p50 latency; right side green showing 5.7 percent, zero, zero, 2.05 seconds

| 指标 | 之前 | 之后 | Δ | |---|---:|---:|---:| | 基础设施故障率 | 94.0 % | 5.7 % | **−94 %** | | `ICEBERG_INVALID_METADATA` | 147 | 0 | **−100 %** | | `ICEBERG_BAD_DATA` (损坏的 Parquet) | 38 | 0 | **−100 %** | | `ICEBERG_CANNOT_OPEN_SPLIT` | 111 | 13 | −88 % | | p50 读取实际耗时 | 5.74 s | 2.05 s | **−64 %** | 这些是来自 `trino_queries` 事件监听器表和 `shelfd:9090/metrics` 的实测集群数据 —— 而非供应商的基准测试。复现脚本和完整的方法论详见 [docs/](./docs/)。 ### 持续结果(第二个副本,营业时间对比) 第二个副本被迁移,并测量了完整的 7 天基线(直连 S3)与迁移后时期的数据对比,数据仅筛选营业时间(上午 9 点至晚上 9 点),并排除了迁移当天以避免冷缓存污染: | 指标 | 直连 S3 (7 天基线) | Shelf (迁移后) | 差异 | |--------|---------------------------|---------------------|-------| | **p50 实际耗时** | 2.34 s | 1.12 s | **−52 %** | | p95 实际耗时 | 1,112 s | 1,107 s | ~相同 | | p99 实际耗时 | 1,272 s | 1,232 s | −3 % | | 平均 CPU 时间 | 197.7 s | 152.4 s | **−23 %** | | 平均规划时间 | 0.46 s | 0.37 s | −19 % | | `ICEBERG_BAD_DATA` 错误 | 10 | **0** | **完全消除** | | `CLUSTER_OUT_OF_MEMORY` | 62 | 9 | −85 % | **预热曲线:** | 阶段 | p50 实际耗时 | Iceberg 基础设施错误 | |-------|----------|---------------------| | 0–6 小时 (冷缓存) | 3.3 s | 5 | | 6–12 小时 (预热中) | 85.1 s* | 0 | | 12 小时+ (已预热) | **2.87 s** | 40 | \* 受计划内批处理 ETL 影响而膨胀,并非缓存性能问题。 持续观测的数据证实:**p50 延迟减半**,CPU 时间下降 23%(Worker 花在阻塞 I/O 上的时间减少),并且由连接池饱和时代理字节截断引起的 `ICEBERG_BAD_DATA` “损坏的 Parquet” 故障类别 —— 在基于 ETag 的内容寻址缓存下,结构性根除。且未引入任何新的故障类别。 ## 快速入门 在笔记本电脑上从零开始到实现首次缓存命中,使用 k3d + MinIO 只需 ≤ 10 分钟: → [docs/quickstart/](./docs/quickstart/index.md) ### 或者:通过 LLM 代理安装(无需 Trino / Helm / K8s 专业知识) 将以下提示词投喂给 Cursor / Claude / 通用代理并放入您的代码库中: 该技能将引导代理检测您的 Trino 集群,使用合理的默认值通过 Helm 安装 Shelf,对 S3 shim 进行冒烟测试,修改 Trino 的 `s3.endpoint`,验证迁移结果,并在收到信号时执行回滚。用户只需确认涉及集群变更的步骤;其余工作均由代理完成。 ## 架构 应用了 ADR 覆盖的 BLUEPRINT 面向用户摘要: → [docs/architecture.md](./docs/architecture.md) 完整的标准设计:[BLUEPRINT.md](./BLUEPRINT.md)。 ## “Trino 很慢 —— 我应该使用 Shelf 吗?” 如果您想弄清楚 Shelf 是否是解决您特定瓶颈的正确答案(或者正确的修复方法应该是 JVM 调优、查询重写、Alluxio、原生 `fs.cache` 还是“什么都不做”): → [docs/discovery/trino-is-slow.md](./docs/discovery/trino-is-slow.md) —— 按症状分类的决策树,优先执行“在缓存前先进行性能分析”这一步骤。 → [docs/discovery/alternatives.md](./docs/discovery/alternatives.md) —— Shelf 与 Alluxio、Warp Speed 以及原生方案的对比,详细说明了各项权衡。 → [docs/discovery/trino-upstream-strategy.md](./docs/discovery/trino-upstream-strategy.md) —— Shelf 将如何随着时间的推移,与 Trino OSS 合作推进原生 blob-cache 集成 ([trinodb/trino#29184](https://github.com/trinodb/trino/pull/29184))。 ## Shelf 还适用于哪些场景? 抛开 Trino 和 Iceberg 的背景不谈,Shelf 是一个**兼容 S3 API 的字节范围缓存**,具有基于 ETag 的新鲜度机制。只要客户端对相同的 S3 兼容对象进行重复的字节范围读取,并且您可以控制它们访问的 S3 endpoint,它就能发挥作用: - **Spark / Presto / Impala / Dremio / Drill on Iceberg, Delta, or Hudi** —— 相同的 `s3.endpoint` 覆盖模式,相同的收益。 - **ClickHouse 外部表和 S3 磁盘** —— 对 Parquet 和 ClickHouse 原生格式的重复字节范围读取。 - **共享 Notebook 环境中的 DuckDB / Polars / Lance** —— 一份预热好的副本即可服务所有分析师。 - **S3 上的 AI/ML 训练数据集** (PyTorch `webdataset`、Mosaic StreamingDataset、parquet/lance shards) —— 这是典型的“共享储藏室”场景,多个 Worker 在跨周期训练时重新读取相同的 shards。 - **S3 上的 Cloud-Optimized GeoTIFFs、Zarr、HDF5** —— 专为字节范围读取设计的格式,正好切中 Shelf 的甜蜜点。 以下场景不适用:S3 endpoint 不允许您重定向的封闭式托管服务(Snowflake、BigQuery、Athena、Redshift),或者每次读取都是唯一的场景。 ## 许可证 Copyright 2026 The Shelf Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with the License. You may obtain a copy of the License at . See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for details.
标签:Apache 2.0, DNS解析, DRAM, Fail-open, Gradle集成, HTTP工具, Iceberg, NVMe, Parquet, Rust, S3, S3兼容, Trino, 代理服务, 可视化界面, 基础设施, 大数据, 子域名突变, 对象存储, 延迟优化, 开源项目, 性能优化, 数据湖, 检测绕过, 目录扫描, 缓存, 网络安全审计, 网络流量审计, 行组级缓存, 读取加速, 通知系统