Databricks 用 REGISTER 加 UNREGISTER 让表在不搬一个文件的前提下换目录;DuckDB 的纯 Java 表函数把摄入从 3 分 37 秒压到 1 分 27 秒;一条物理优化器规则让 DataFusion 全量排序快 6.9 到 28.8 倍;Gojek 把队列隔离粒度推到失败真正发生的那一层
本期聚焦 10 月 5 日的四件事:Iceberg REST 规范里的 REGISTER 与 UNREGISTER 组合成一次完整的目录交接流程,并在 Unity Catalog 上开私有预览;DuckDB 用户来稿对比四种 Java 摄入路径,用户定义表函数以 1 分 27 秒胜出;ArrowMetal 0.4.0 用一个 DataFusion 物理优化器规则把全量排序搬到 Apple Silicon GPU;Gojek 把支付管道的 bulkhead 隔离推到「银行 × 转账类型 × 优先级」。
10 月 5 日这天,数据基建的动静集中在同一件事上:把原本隐含的边界显式化,并且显式到可以用一个端点、一条规则或一条 lane 来表达。Databricks 把「这张表归哪个目录管」拆成 REGISTER 与 UNREGISTER 两个动作,让表在对象存储里一个字节都不动的前提下换掉管理它的 catalog,前提是旧目录先显式交出指针。DuckDB 的一篇用户来稿把四种 Java 摄入路径摆在同一份 48 万行数据上比了一次,最后胜出的是把数据源写成引擎内一个算子的用户定义表函数,而不是任何现成的连接器。ArrowMetal 0.4.0 给 DataFusion 注册了一条物理优化器规则,只接管全量排序这一种算子,其余节点照旧,并用一张量出来的接管表和 HANDBACK 报告把「什么时候不该上 GPU」也写进产物。Gojek 把支付管道的隔离维度从「优先级」细到「银行 × 转账类型 × 优先级」,因为下游会以四种不同方式失败,粒度必须跟失败对齐。
REGISTER 加 UNREGISTER:表不搬一个文件,管理它的目录换一个
事件概述
10 月 5 日,Databricks 博客刊出 Ryan Blue 与 Marco Kroll 的文章,标题直指目录锁定(catalog lock-in)。文章要解决的问题很具体:开放表格式让数据放在客户自己的桶里、能被 Spark 与 Trino 等多个引擎读,但真正决定这张表当前状态的是 catalog。想换 catalog 的时候,DROP TABLE 不能用,因为它连数据和元数据一起删。
Iceberg REST 规范里本来就有 REGISTER 端点:给一个新表名和一份已存在的 metadata 位置,catalog 写一条映射记录,不复制、不移动、不改写任何文件。缺的是另一半——让旧 catalog 干净地退出对这张表的管理。Databricks 把 UNREGISTER 端点贡献进了 Apache Iceberg REST catalog 规范(该端点随 Iceberg 1.12.0 进入规范,Vol.81 已就发布本身做过展开),这次是把规范端点接成一次可执行的交接流程,并在 Unity Catalog 上开私有预览。
技术解析
- REGISTER 只是写一条映射:客户端发一个 POST,带上目标表名与已有 metadata 的 URI;catalog 做基本校验后写一条记录,把表名指向那份
metadata.json。数据文件不参与。 - 只用 REGISTER 会造出 split-brain:Iceberg 的各个 catalog 互相不通气。REGISTER 会在新 catalog 里加一条记录,旧 catalog 那边则完全保持活跃。两边都认为自己是这张表唯一的所有者,两边都不报错;第一次写入就把表 fork 成两条历史,查询结果不一致,而且丢数据是静默的。
- UNREGISTER 返回的是指针,不是一份删除确认:请求是一个空 POST,目标是表资源本身——
POST <uc-iceberg-rest-base>/v1/{prefix}/namespaces/{namespace}/tables/{table}/unregister。响应体给出metadata-location与完整的 IcebergTableMetadata对象。这个返回值就是下一个 catalog 接管提交协调所需的确切指针。 - 三步交接,任意时刻只有一个管理 catalog:先 UNREGISTER 把表从 Catalog A 摘掉(文件原地不动),拿到最新 metadata 位置,再用 REGISTER 交给 Catalog B。文章也写明,一次生产迁移还需要停写、改作业指向这些运维步骤,会在后续文章里展开。
- 为什么必须「显式放弃」而不是「直接接管」:这是文章里最有工程价值的一点。旧目录不明确交出控制权,就没有办法区分「一次失败之后表是仍注册、已注销、还是两边都在写」这三种状态。UNREGISTER 把「谁在协调提交」这件事从约定变成了一次有返回值的调用。
行业影响
开放表格式解决的是引擎可换,REGISTER 与 UNREGISTER 解决的是治理层可换。对已经跑了 Unity Catalog、Polaris、Gravitino、Nessie 中不止一个的组织来说,迁移成本从「导出再导入」降到「改一条注册记录」,跨目录的实验和多活架构第一次有了干净的退路。代价也随之而来:split-brain 从教科书上的理论风险变成操作风险,因为现在真的搬得动了,误操作的概率不再为零。
另一个值得记下的观察点是规范与产品的时间差。厂商把能力贡献进中立的 Apache 规范,再在自己的托管服务上以私有预览首发,是 2026 年 lakehouse 厂商反复使用的路径。规范保证互操作,产品提供运维,两者之间那几个月就是竞争优势的来源。对使用者来说,判断一项能力到底「可以用」还是「只是写进了规范」,要看它在自己那个 catalog 上有没有可调用的端点,而不是看规范里有没有条目。
相关链接
- Unlocking Data Portability: Preventing Catalog Lock-in with REGISTER and UNREGISTER APIs(Databricks Blog, 2026-10-05)
- Apache Iceberg 1.12.0 Release(Iceberg Blog, 2026-10-05)
- REST Spec: Add unregister table endpoint(PR #16400)
- Apache Iceberg 1.12.0: What’s New, Breaking Changes, and Upgrade Guide(Dremio Blog)
DuckDB 摄入实测:四种 Java 路径摆在 48 万行上,表函数 1 分 27 秒胜出
事件概述
10 月 5 日,DuckDB 工程博客刊出一篇用户来稿(Geertjan Wielenga、Alex Kasko)。起因很日常:一家公司把客户的漏洞数据存成 MongoDB 里的文档集合,分析页越用越慢,某些场景的查询停在几十秒,Excel 导出 5 万条要 2 到 3 分钟。方案是把一年数据搬进与 MongoDB 同机部署的 DuckDB,硬指标是查询 1 秒内返回、5 万条导出 10 秒内完成。
真正难的不是查询而是把数据装进去。作者把四种路径在同一份样本上跑了一遍:mongo 社区扩展、标准 JDBC 批量插入、Java Appender、用户定义表函数。样本是脱敏后的 485,168 行、24 个字段,其中 description 与 references 两个 VARCHAR 的中位长度分别是 5,000 与 2,500 字符,最长到 32,000,这让很多操作在数据上格外昂贵。
技术解析
- 基线是 mongo 社区扩展,55 秒:
INSTALL mongo FROM community加ATTACH就能用,GROUP BY 与 COUNT 查询在 100 毫秒内,5 万条导出 5 秒内。团队最后没选它,理由是社区扩展没有「未来兼容」保证——DuckDB 发版很快,一旦某个扩展不再被更新、不再能构建,就会被卡在旧版本上。唯一稳妥的用法是自己构建,等于维护一份 C++ 内部 fork。 - 标准 JDBC
executeBatch,3 分 37 秒(其中最后一步CREATE TABLE ... AS ... ORDER BY约 30 秒)。DuckDB 没有原生的批量插入,executeBatch是在 Java 层实现的:语句每批只 prepare 一次,但批内每条记录仍走一次数据库级INSERT,逐行开销躲不掉。 - Appender,2 分 02 秒:
Connection#createAppender是真正的批处理,Java 侧攒满 2,048 行组成 DataChunk 后一次性 flush。比 JDBC 快一倍,在真实数据上仍达不到要求。 - 用户定义表函数,1 分 27 秒:注册方式是
DuckDBFunctions.tableFunction().withName("vuln_import").withParameters(LocalDate.class, LocalDate.class).withFunction(new ImportFunction()).register(conn),然后CREATE TABLE staging.vuln1 AS FROM vuln_import('2026-02-13'::DATE, '2026-08-13'::DATE)。bind里声明结果列,init里用info.setMaxThreads(8)声明支持并发apply,apply每次最多写output.capacity()(2,048)行,返回 0 表示源已耗尽。 - 并行还差一个开关:
SET preserve_insertion_order = FALSE。不设成 FALSE,DuckDB 为了保证读取顺序会把apply限制在单线程。既然排序是导入之后单独做的后处理,保持插入顺序没有必要。 - 最后落地的是非并行版本:受磁盘空间限制,作者把
ORDER BY直接放进同一条CREATE TABLE,省掉中间拷贝,实测与 mongo 扩展持平。两条附带的经验也对体积有帮助——排序显著减小数据库文件,SET storage_compatibility_version = 'latest'同样显著减小文件。
行业影响
嵌入式分析里最贵的往往不是查询而是摄入,这篇来稿的价值在于把四类路径的开销摊在同一份数据上,而不是只报一个最好看的数字。最值得注意的是它的选型逻辑:最快的那条路被放弃了,因为「能不能一直编出来」被当成硬约束。C++ 扩展与主版本的耦合会把一次常规的引擎升级变成项目,这个代价不写在任何 benchmark 里。
把数据源写成引擎内的一个算子,与本期第一条在结构上是同一类动作——把「谁负责这件事」从外部工具移进离数据更近的那一层。表函数换来的不只是时间,而是摄入管道可以用团队本来就在用的语言维护,并且能复用到其他没有现成扩展的内部数据源上。
相关链接
- Importing Data using Java Table Functions(DuckDB Blog, 2026-10-05)
- staticlibs/duckdb_java_data_import(示例代码仓库)
- DuckDB Table Functions in Java(DuckDB Blog, 2026-08-25)
- Chunked Query Results in the DuckDB Java Driver(DuckDB Blog, 2026-08-21)
一条物理优化器规则:DataFusion 的全量排序搬到 Apple Silicon GPU,快 6.9 到 28.8 倍
事件概述
ArrowMetal 0.4.0 于 10 月 2 日发布,其中 datafusion-arrowmetal 这个 Rust crate 提供了一条 DataFusion 物理优化器规则,把符合条件的全量排序换到 Apple Silicon 的 GPU 上执行。10 月 5 日出版的 Data Engineering Weekly #290 收录了这篇技术分析。
设计上最干净的一点是它不试图成为另一个引擎。DataFusion 分两轮做优化——先逻辑规则,再对真正执行的算子跑物理优化器规则,而这份规则列表是开放的。注册之后,SQL、计划器、其余算子依旧是 DataFusion 的,EXPLAIN 里 SortExec 变成 MetalExec,并产出一份报告说明接管了哪些节点、放过了哪些、原因是什么。
技术解析
- 接管条件是量出来的表,不是开关:全量排序从 250,000 行起接管,因为那是每种 key 类型都领先的最小规模(冷启动 2.64 倍、热 6.9 倍)。规模越大领先越多——200 万行 20.3 到 26.1 倍,5,000 万行 19.0 到 26.6 倍,此时 DataFusion 单独跑需要 1.58 到 1.86 秒,加规则后是 61 到 88 毫秒。在 DataFusion 自己的 Parquet reader 之上,Float64 排 1,000 万与 5,000 万行是 10.8 到 16.5 倍,含字符串列的文件是 3.6 到 5.5 倍,解码时间算在规则的时间内。
- CPU 时间才是真正的差距:5,000 万行 16 分区排序,DataFusion 墙钟 1,581 毫秒、CPU 6,104 毫秒;加规则后墙钟 69.74 毫秒、CPU 126 毫秒。GPU 的时间不计入 CPU 时间,统一内存也不需要把列拷过一条总线。69.74 毫秒里,7.31 等 6,104 个输入批次、10.21 写入 Metal buffer、48.21 是排序本身。
- Top-k 是诚实的反例:
ORDER BY ... LIMIT 100在每个规模上都更慢,0.14 到 0.41 倍。原因是结构性的——DataFusion 的 TopK 在输入流式到达时就保留每分区最好的 100 行,5,000 万行的整个查询只要 6.21 毫秒,而MetalExec光收集和导入就要 7.72 加 9.83 毫秒,GPU 还没开始干活。过滤同理。两者默认关闭,ArrowMetalConfig::all()才打开。 - 聚合要判两次:规则在计划期能看到聚合的行数,看不到组数,而同一个
count(*)在 100 万组时领先、200 组时落后。于是计划期按形状(聚合族、key 类型、是否直读MemTable)查一次表,运行期先读前 262,144 行、用分层抽样加 Chao1 偏差校正估计组数再查一次;不划算就把已读的批次和剩余数据一起流回 DataFusion 自己的聚合,报告里记一行HANDBACK。 - 正确性用差分网格兜底:460 条查询、24 种表配置、0 到 20,000 行、空值比例 0 / 0.1 / 1.0、1 分区与 3 分区,逐位比对,13,632 组查询 0 不一致。浮点按 IEEE 754 totalOrder、字符串按字节比较,与 DataFusion 的语义对齐。
- 限制写得很直白:
MetalExec先收集输入再执行,没有自己的 spill 路径,批次计入 DataFusion 的内存池,被拒绝就交还;单进程一次只能有一个 GPU 作业,进程首条 GPU 查询要编译 pipeline(1,000 万行时 42 到 66 毫秒);聚合同样只在MemTable扫描且 ≥5,000 万行时接管,join 默认不接管;版本锁死在 DataFusion 55.1.0 与 arrow-rs 59.3.0,仅 macOS 加 Apple Silicon。所有数字来自一台 M4 Max。
行业影响
把 GPU 加速做成引擎里一条可插拔的物理规则,是把异构算力引入分析栈最省力的方式:SQL、计划器、其余算子都还是原引擎的,收益与风险一并被限制在单个节点上,出问题也是一个节点的回退而不是一次迁移。
更值得抄走的是方法论。这条规则没有把「上 GPU」当成一个全局开关,而是维护一张实测的接管表,并要求每条放过或交还都给出理由。这让「要不要把某个算子放到异构算力上」从信仰变成可审计的决策——包括明确承认 TopK 和过滤在这套硬件上就是更慢。数据源侧的引擎在往 GPU 走的时候,缺的往往不是算子实现,而是这样一份边界清楚的接管判据。
相关链接
- ArrowMetal v0.4.0 release(GitHub, 2026-10-02)
- DataFusion on the Apple Silicon GPU: One Optimizer Rule, the Same SQL, Sorts 6.9x to 28.8x Faster
- datafusion-arrowmetal(crates.io)
- Data Engineering Weekly #290(2026-10-05)
Gojek 的按银行分道:把隔离粒度推到失败真正发生的那一层
事件概述
Gojek 工程团队公开了支付管道的四次架构演进。规模是每天 300 万到 450 万笔转账、峰值 400 RPS,下游是各家银行——一个完全不受控的外部依赖。目标不是快,是保证一家出问题的银行不会拖住发往其他银行的转账。最终方案是把隔离粒度推到「银行 × 转账类型 × 优先级」,每个组合一条独立 lane。这篇文章由 10 月 5 日出版的 Data Engineering Weekly #290 收录。
技术解析
- v1 同步直连:用户请求一路阻塞到银行再返回。用户延迟等于银行延迟;一家银行变慢,请求线程堆积,线程池耗光之后整个应用挂掉,而不只是那家银行的转账失败。
- v2 异步加两个 topic(priority / bulk):批量流量不再饿死优先流量。剩下的两个问题都在「共享」上——所有银行共用一个 topic,流量大的银行会挤住流量小的银行,而且今天热的是 A、明天可能是 B;想按银行调吞吐也做不到,因为只有一把全局旋钮。
- v3 每个「银行 × 转账类型 × 优先级」一条 lane:每个组合都有自己的 Kafka topic 和自己的消费者服务部署。举例:Bank A — Intrabank — Priority 这个 topic 有 36 个分区,配 2 个 pod、每 pod 18 个消费线程;需要吞吐时只扩这一条 lane。
- 维度是失败模式决定的:银行会以四种方式失败——整体宕机(连接被拒或 5xx)、尾延迟(接受请求但挂着,最坏挂到 150 秒超时)、各家各样的限流、峰值 10 到 15 倍的突发。四种失败对应的隔离维度不同,所以维度落在三个字段的笛卡尔积上,而不是只按银行切。
- 指标选对了:最关键的指标是 queue wait time,一笔转账在被消费前要等多久。改造前这个数字恰好在最要紧的场景下爆掉,改造后是平的。
行业影响
「如果多个不受控的下游共用一份容量,你就把它们的命运耦合在一起了」——这句结论对 Kafka 消费者组、Flink slot、Spark executor、数据库连接池全都成立。一个下游的抖动变成全局尾延迟,往往不是因为它慢,而是因为它慢的时候还占着别人要用的那份资源。
代价也必须一起看。lane 的数量是三个维度的笛卡尔积,运维对象从「一个消费者服务」变成几十个部署,监控、告警、扩容策略都要按 lane 维度重新组织。这一步通常要到真的被某一家下游拖垮之后才划算,而不是提前做的设计练习。判断标准可以是那个 queue wait time:如果它在某一个下游出问题时整体抬升,共享容量就已经是瓶颈了。
相关链接
- Per-Bank Consumer Lanes: How We Solved Head-of-Line Blocking with the Bulkhead Pattern(Gojek Engineering)
- Data Engineering Weekly #290(2026-10-05)
编者评
本期四件事在回答同一个问题:边界应该划在哪里。Databricks 把「这张表归谁管」做成两个端点,边界从「谁能读」移到「谁在协调提交」,代价是 split-brain 从纸面风险变成操作风险。DuckDB 那条把「哪段代码负责搬数据」从外部连接器和社区扩展移进引擎内的一个算子,换来的是长期可维护性与一点性能。ArrowMetal 那条把「哪种算力执行哪个算子」做成一条可插拔的物理规则,并且用一张实测接管表和 HANDBACK 报告把「什么时候不该下推」也写进了产物。Gojek 那条把「容量归谁用」切成更细的 lane,细到真正会失败的那一层。
四件事的共同点不是更快,也不是更开放,而是把原本隐含的边界显式化。显式到一个端点、一条规则、一条 lane 就能表达之后,边界才可以被审计、被回滚、被写进故障演练手册。反过来,凡是还停留在「大家都这么约定」的边界——谁负责删文件、谁负责保证顺序、谁在慢的时候让出资源——迟早会以一次静默的分叉或一次全局尾延迟的方式把账算回来。