iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Software Development

1+1+1>3 ~ Spark 與 DataFusion、Comet 效能煉金術 ~系列 第 23

Day 23 Comet protobuf vs Substrait:自訂 IR 還是跨引擎標準

  • 分享至 

  • xImage
  •  

昨天 D22 講的是 Comet 決定不接手某個 operator 時的診斷子系統,理由怎麼被記錄下來,今天講另一半:Comet 決定接手之後,這個 operator 要怎麼翻成 Rust 看得懂

這是 D15 六接點的第二個,protobuf IR。看起來是純技術題,其實藏了一個很大的選擇。Gluten 走 Substrait,一個社群主導的跨引擎標準。Comet 自己刻一份 protobuf。表面上像重複造輪子,實際上兩邊在解不同的問題。

Substrait 是什麼

Substrait 是一份跨引擎的關聯代數規格,用 protobuf 定義。目標是任何引擎都能吃、都能吐。

舉個例子。SELECT a + b FROM t WHERE c > 10 這個查詢,翻成 Substrait 大概是一棵這樣的樹:

ProjectRel (a + b)
  +- FilterRel (c > 10)
       +- ReadRel (t)

ProjectRelFilterRelReadRel 都是規格裡定義好的 message,+> 對到標準函式庫裡的 addgt。理論上你在 Ibis 產出這棵樹,DuckDB、Velox、DataFusion 誰都能接手去跑。

Gluten 是 Substrait 最大的實用案例。Spark plan 攔截下來後轉成 Substrait,交給 Velox 或 ClickHouse 執行。理論上抽掉 Velox 改插 ClickHouse,Spark 到 Substrait 的那段轉換不用重寫。

Comet 自訂 protobuf 是什麼

Comet 選了完全相反的路。native/proto/src/proto/ 底下四個檔案(operator.protoexpr.prototypes.protopartitioning.proto),每個 Spark operator、每個表達式都有對應的 message。JVM 側把 physical plan 翻成這些 message,過 JNI 送到 Rust 側,DataFusion 再 build 成 ExecutionPlan

這份 protobuf 沒有任何跨引擎的野心。訊息定義貼著 Spark 語義寫,需要什麼欄位就開什麼欄位,不會為了「將來也許 Velox 也吃」而做通用抽象。

差別在哪

面向 Substrait Comet protobuf
目標 跨引擎的關聯代數標準 Spark 到 DataFusion 的傳送格式
覆蓋範圍 全引擎共通子集 Spark 語義照抄
演進節奏 社群討論、標準委員會 Comet 自己決定
抽換 native 後端 轉換層不用重寫 綁死 DataFusion
使用者 Velox、Gluten、DuckDB、Ibis 等 只有 Comet

一句話:Substrait 是通用 IR,Comet protobuf 是專用管道。

Gluten 買 Substrait 付什麼代價

通用性不是免費,而且代價比想像中重。Gluten 不是「用 Substrait 加幾個 extension」,是直接 fork 了 Substrait 的 proto 檔,base 版本鎖在 v0.23.0,然後自己往裡面加東西。看幾個具體的:

Spark 有的 operator,Substrait 沒有,就自己加一個 Rel。 Spark 的 ExpandGROUPING SETS / CUBE 會用到)在 Substrait 裡沒有對應,Gluten 加了 ExpandRel。同樣被加進去的還有 WindowRelGenerateRelTopNRelWriteRel

語義對不上,就改上游的定義。 Substrait 原本只有一個 JOIN_TYPE_SEMI,但 Spark 分左右,所以 Gluten 把它拆成 JOIN_TYPE_LEFT_SEMIJOIN_TYPE_RIGHT_SEMI。型別那邊也加了一個 Substrait 沒有的 Nothing

Gluten 自己的文件寫得很清楚:能推回上游的就推,推不動的才考慮 AdvancedExtension。所以 extension 是退路,不是主要手段。

fork 之後跟不上上游,會撞車。 這是最好的例子。Gluten 早期把 bucket_spec 接在 WriteRel 的 field 7 上,因為那時候 7 是空的。上游 v0.98.0 後來把 field 7 指派給 common,號碼直接撞上。Gluten 只好把 bucket_spec 搬到 1000,並且立下新規矩:自己加的欄位一律從 1000 起跳,避開上游會用到的區間。

這件事很說明問題。你 fork 了一份標準,你就同時失去了標準的好處,又背上了追上游的義務。

好處當然還是有。Substrait 的生態互通不是假的,Gluten 押的是「Spark 之外也有需求,這個投資會攤下去」。只是帳單比廣告上寫的長。

Comet 為什麼不需要通用 IR

Comet 要橋接的兩端從一開始就寫死了:上游只有 Spark,下游只有 DataFusion。沒有「將來把 DataFusion 抽掉改插 Velox」的故事,也沒有「Ibis 也能吐 Comet plan」的野心。

在這個範圍內,Substrait 的通用性沒有回報。要付的代價全都要付,收到的好處用不到。

反過來,自訂 protobuf 在這個範圍內幾乎零阻力。Spark 加一個新運算子,Comet 加一個 message;Spark 改一個語義行為,Comet 改對應 message,不用等社群開會,也不用擔心撞到別人的 field 號碼。目前 Comet 有二十幾個 operator serde、幾百個表達式全部照 Spark 語義刻進去,這件事在 Substrait 上只會更慢。

抽象層級的取捨

如果只能記一件事,就記這個:這份 IR 要服務幾種引擎

  • 服務多種 = native 後端抽換得掉,但要背通用抽象的重量,還要追上游
  • 只服務一種 = 後端抽換不掉,但演進沒摩擦

Substrait 押通用,Gluten 收到多引擎可攜性,付 fork 與對齊的稅。Comet 押貼身,收到簡潔與速度,付「這份 IR 沒有別人能用」的封閉性。

這不是誰對誰錯,是對「這份 IR 要服務幾種引擎」的判斷不同。

結論

Substrait 是通用 IR,Comet protobuf 是專用管道。差別不在技術優劣,在這份 IR 要服務幾種引擎。Gluten 押跨引擎攤成本,代價是 fork 一份標準之後還要追上游;Comet 押 Spark 專用求速度,代價是這份 IR 只有自己能用。

明天 D24 談 Arrow C Data Interface。protobuf 傳的是 plan,資料還沒動。當 Rust 算完,Arrow batch 要怎麼跨語言傳回 JVM 而不複製?這是六接點的第三個。

那就明天見~

參考資料


上一篇
Day 22 Comet 的 fallback 診斷子系統:SupportLevel、TreeNodeTag 與 strict 模式
下一篇
Day 24 Comet codegen dispatcher:getSupportLevel、canHandle 與修法時機
系列文
1+1+1>3 ~ Spark 與 DataFusion、Comet 效能煉金術 ~26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言