Pipelines 與 R2 SQL:把事件落地成資料湖
⚠️ Pipelines、R2 SQL、R2 Data Catalog 三者目前都是 open beta,且官方文件有多處互相矛盾。本章會逐一標明哪些是實測、哪些是文件、哪些是文件沒說的。查證日期 2026-08-01。
第 22 章結束在一個明確的缺口上:Analytics Engine 會抽樣,而且只留 3 個月。它能回答「上週前 10 大來源國家」,但回答不了「這個 API key 昨天 14:32 做了什麼」,也回答不了「今年 Q1 對比去年 Q1」。
LinkForge 若要往 B2B SaaS 走,這兩個問題遲早會變成合約義務 —— 稽核軌跡與長期報表。這需要的是把每一筆事件原封不動落地,然後用一個能掃描 TB 級資料的引擎去查。這就是 Cloudflare Data Platform 的位置。
它由三個獨立產品組成,可以分開用也可以串起來用:
- Pipelines —— 把事件流從 Worker 或 HTTP 端點,經過 SQL 轉換,寫進 R2。
- R2 Data Catalog —— 內建在 R2 bucket 上的 Apache Iceberg 目錄,讓 Spark / DuckDB / Trino 這些引擎能直接讀。
- R2 SQL —— Cloudflare 自家的 serverless 查詢引擎,直接查 R2 Data Catalog 裡的 Iceberg 表。
23.1 先講清楚:這一章的內容有多穩定
Section titled “23.1 先講清楚:這一章的內容有多穩定”這是本系列第一次整章都在講 beta 產品,所以先把風險攤開。
(一)2025-09-25 有一次架構重寫。 舊的 Pipelines 是「一個 pipeline 物件 + --r2-bucket 參數」;新的是 Streams / Pipelines / Sinks 三物件模型。2025-09-25 之前建立的 pipeline 被官方稱為 legacy,「會持續運作到 Pipelines GA 為止」,但「新功能只會出現在新架構上」。官方沒有給 EOL 日期。
實測 wrangler pipelines --help(4.118.0),舊模型的痕跡還在:
wrangler pipelines update <pipeline> Update a pipeline configuration (legacy pipelines only) [open beta]每一個子指令都標著 [open beta]。
(二)Worker binding 的鍵名在 2026-05 改過名。 這是本章最容易踩的一個坑,23.3 詳述。
(三)文件與文件之間互相矛盾。 至少三處:landing page 說「目前不會被收費」,pricing page 列出費率且沒有任何 beta 標示;pricing changelog 公告了 Workers Free 方案的 1 GB 額度,pricing page 的表格裡沒有這一欄;官方唯一講 binding 設定的頁面用的還是舊鍵名。
(四)官方從未說明底層引擎。 網路上(包括本系列早期的大綱草稿)流傳 Pipelines 建在 Arroyo 之上。翻遍官方文件找不到任何一處提到 Arroyo。本章不做這個宣稱。
所以本章的定位是:幫你判斷現在該不該用,以及如果要用,哪些地方文件會騙你。
23.2 三物件模型
Section titled “23.2 三物件模型” HTTP POST ─┐ ├─► Stream ──► Pipeline (SQL) ──► Sink ──► R2 / R2 Data Catalog Worker send() ─┘| 物件 | 是什麼 | 官方定義 |
|---|---|---|
| Stream | 入口 | 「持久化、有緩衝的佇列,透過 HTTP 端點或 Worker binding 接收事件」 |
| Pipeline | 轉換 | 「用 SQL 轉換連接 stream 與 sink,在寫入儲存前修改事件」 |
| Sink | 出口 | Iceberg 表(R2 Data Catalog)或 R2 上的 Parquet / JSON 檔 |
一條完整的鏈需要三個物件都建立。wrangler pipelines setup 是互動式的一次建全套,實測 CLI 表面:
wrangler pipelines setup Interactive setup for a complete pipelinewrangler pipelines create <pipeline> Create a new pipelinewrangler pipelines list / get / deletewrangler pipelines update <pipeline> (legacy pipelines only)wrangler pipelines streams create / list / get / deletewrangler pipelines sinks create / list / get / delete注意 streams 與 sinks 都沒有 update 子指令。加上下一節會提到的「pipeline SQL 建立後不可修改」,這個平台目前的心智模型是:所有設定都是 immutable,要改就刪掉重建。
Stream
Section titled “Stream”npx wrangler pipelines streams create ch23-clicks --schema-file schema.json實測 --help 的完整選項:
--schema-file Path to JSON file containing stream schema--http-enabled Enable HTTP endpoint [default: true]--http-auth Require authentication [default: true]--cors-origin CORS origin [array]兩個 default 值得記下來,因為官方的 manage-streams 頁面只用 dashboard 截圖說明,從來沒寫出這兩個預設值:HTTP 端點預設開啟,而且預設要求驗證。
schema 是可選的。給了就會驗證與強制執行,不給就接受任何合法 JSON。schema 檔長這樣:
{ "fields": [ { "name": "tenant_id", "type": "string", "required": true }, { "name": "slug", "type": "string", "required": true }, { "name": "country", "type": "string", "required": false }, { "name": "latency_ms","type": "int32", "required": false }, { "name": "ts", "type": "timestamp", "required": true } ]}支援型別:string、int32、int64、float32、float64、bool、timestamp(RFC 3339 或 Unix 數字)、json、binary(base64)、list(需 items)、struct(巢狀 fields)。
這裡有一個必須加粗的行為。 官方 manage-streams 頁面寫著:「不符合已定義 schema 的事件在 ingestion 時會被接受,但在處理階段會被丟棄。」
也就是說:你 POST 進去會拿到 200,資料卻永遠不會出現在 sink 裡。沒有 dead letter queue,沒有錯誤回應。唯一能看見的地方是 metrics —— GraphQL dataset
pipelinesUserErrorsAdaptiveGroups,錯誤類型有missing_field、type_mismatch、parse_failure、null_value。對照第 19 章:Queues 有 DLQ,訊息失敗會進死信佇列讓你重放。Pipelines 目前沒有等價機制。所以「上線第一週每天檢查 error metrics」不是建議,是必要作業。
HTTP 攝入
Section titled “HTTP 攝入”POST https://{stream-id}.ingest.cloudflare.comAuthorization: Bearer <API_TOKEN>Content-Type: application/json
[{"tenant_id":"t1","slug":"abc","ts":"2026-08-01T05:00:00Z"}]body 是 JSON 陣列,不是 NDJSON。NDJSON 只出現在 R2 sink 的輸出格式裡,兩者不要搞混。
Pipeline(SQL 轉換)
Section titled “Pipeline(SQL 轉換)”npx wrangler pipelines create ch23-pipeline --sql-file transform.sqlINSERT INTO ch23_sinkSELECT tenant_id, slug, upper(country) AS country, latency_ms, tsFROM ch23_clicksWHERE tenant_id IS NOT NULL;一個檔案裡可以放多個 INSERT INTO ... SELECT ... FROM ...,用分號隔開 —— 這就是 fan-out(一份 stream 寫進多個 sink)的做法。
官方明確寫出、而且非常重要的一句話:
「Pipeline SQL 建立後無法修改。要改變 SQL 轉換,你必須刪除並重建這條 pipeline。」
實務上這代表 SQL 轉換要當成 schema migration 等級的變更來對待,走 CI/CD 而不是手改(第 40 章會處理這件事)。
關於「只支援 stateless 轉換」這個說法,本章必須誠實。 官方 SELECT 文法參考給的完整文法只有:
[WITH with_query [, ...]] SELECT select_expr [, ...] FROM from_item [WHERE condition]加上 UNNEST(「只能出現在 SELECT 子句」、「每個 SELECT 只能 unnest 一個陣列」)。GROUP BY、HAVING、ORDER BY、LIMIT、JOIN、DISTINCT、UNION、window function 全都不在文法裡。pricing 頁面另外有一句旁證:定價「涵蓋 stateless 轉換(例如 filter、reshape、unnest、cast、compute)」,而聚合這類 stateful 操作「未來可能另外計價」。
但是 —— 官方從來沒有明說這些子句「不支援」。 所以本章的說法是:
官方文件所記載的文法只允許
WITH/SELECT/FROM/WHERE/UNNEST。要做聚合,請在下游(R2 SQL 或你自己的引擎)做,不要指望在 pipeline 裡做。
這是刻意的措辭差異。「文法只記載了這些」和「這些一定會被 parser 拒絕」是兩件事,我沒有帳號可以實測後者。
兩種類型,實測 wrangler pipelines sinks create --help 的完整選項與預設值:
--type [required] [choices: "r2", "r2-data-catalog"]--bucket [required]--format [choices: "json", "parquet"] [default: "parquet"]--compression [choices: "uncompressed","snappy","gzip","zstd","lz4"] [default: "zstd"]--target-row-group-size Target row group size for parquet format--path The base prefix in your bucket where data will be written--partitioning Time partition pattern (r2 sinks only)--roll-size Roll file size in MB--roll-interval Roll file interval in seconds [default: 300]--access-key-id / --secret-access-key (留空則自動建立 R2 credentials)--namespace / --table / --catalog-token (r2-data-catalog 必填)幾個文件與 CLI 對得起來、但值得單獨拉出來的點:
--format的預設是 parquet,不是 json。json產出的是 newline-delimited JSON。--partitioning的說明字串直接寫著 (r2 sinks only) —— R2 Data Catalog sink 沒有這個選項(Iceberg 自己管分割)。預設 pattern 是year=%Y/month=%m/day=%d,用 strftime 指示字元。--roll-interval預設 300 秒。R2 sink 最小 10 秒,R2 Data Catalog sink 最小 60 秒,官方理由是「避免壓實(compaction)問題」。- R2 Data Catalog sink 無法對既有的 Iceberg 表建立。 官方原文:「sink 會建立指定的 namespace 與 table(若不存在)。Sinks cannot be created for existing Iceberg tables.」如果你已經有一張 Iceberg 表想灌進去,目前做不到。
關於 exactly-once:官方只在 sinks 總覽頁說了一句「Sinks 提供 exactly-once 投遞保證,確保事件不會重複也不會遺失」。沒有任何一頁定義這個保證的範圍 —— 是端到端(含 HTTP 重送、含 Worker 重試),還是只保證 sink 寫入端?本章不替它背書。你的 Worker 若在 send() 失敗後自行重試,重複的責任在你這邊,不在 sink。
23.3 Worker binding:本章實測最有價值的一段
Section titled “23.3 Worker binding:本章實測最有價值的一段”鍵名改過,而唯一講 binding 的文件頁還是舊的
Section titled “鍵名改過,而唯一講 binding 的文件頁還是舊的”{ "pipelines": [ { "binding": "EVENTS", "stream": "ch23-clicks" } ]}注意:最外層的陣列仍然叫 pipelines,改的是裡面那個鍵:pipeline → stream。
查 node_modules/wrangler/config-schema.json,這件事寫得清清楚楚:
{ "binding": { "type": "string" }, "stream": { "type": "string", "description": "Id of the Stream to bind" }, "pipeline": { "type": "string", "description": "Id of the Stream to bind", "deprecated": "Use `stream` instead." }, "remote": { "type": "boolean", "description": "Whether the pipeline should be remote or not in local development" }}實測用舊鍵,wrangler 每一次指令都會警告:
▲ WARNING Processing wrangler.jsonc configuration: - The "pipeline" field in "pipelines[1]" bindings is deprecated. Use "stream" instead.問題在於:官方唯一一頁講「如何從 Worker 寫入 stream」的文件(/pipelines/streams/writing-to-streams/),到今天用的還是 "pipeline" 舊鍵,而且沒有任何棄用註記。 改名這件事只出現在 changelog 裡。所以一個照著文件做的讀者,會得到一個每次 build 都跳警告的專案,而且不知道為什麼。
(旁證:官方的 Bluesky firehose 範例頁用的是新鍵 "stream"。同一份文件站的兩頁互相矛盾。)
schema 與 validator 不一致
Section titled “schema 與 validator 不一致”schema 的 required 只有 ["binding"]。所以照著 JSON schema 的編輯器自動完成,你會以為 { "binding": "X" } 是合法的。實測:
✘ ERROR Processing wrangler.jsonc configuration: - "pipelines[2]" bindings must have a string "stream" field but got {"binding":"NO_TARGET"}.JSON schema(給編輯器看的)比 wrangler 的執行期 validator 寬鬆。 這是本系列第三種「三個真相來源互相不符」的情況:第 15 章是型別比 runtime 窄、第 21 章是文件比型別窄,這裡是 schema 比 validator 寬。遇到設定問題時,以 wrangler deploy --dry-run 的實際輸出為準,不要以編輯器的紅線為準。
remote 支援:文件沒說,schema 說了
Section titled “remote 支援:文件沒說,schema 說了”官方的 bindings-per-env 支援矩陣 列出了 AI、Assets、Analytics Engine、Browser、D1、DO、Containers、Email、Hyperdrive、Images、KV、Queues、R2、Rate Limiting、Service Bindings、Vectorize、Workflows —— Pipelines 整個不在表上。沒有任何一頁提到 wrangler dev 對 Pipelines 的行為。
但 schema 裡有 remote,而且描述就是「Whether the pipeline should be remote or not in local development」。實測加上 "remote": true,wrangler deploy --dry-run 完全不報錯(對照第 22 章的 Analytics Engine 加 remote 會出警告)。
結論:Pipelines 支援 remote binding,只是沒有寫進文件。
本地 binding 的真身:它是一個 Fetcher
Section titled “本地 binding 的真身:它是一個 Fetcher”這是本章最有趣的發現。把 binding 的原型鏈整條印出來:
{ "typeofBinding": "object", "hasSend": true, "typeofSend": "function", "sendSource": "[object JsRpcProperty]", "protoChain": [ { "ctor": "Fetcher", "keys": [] }, { "ctor": "Fetcher", "keys": ["fetch", "connect", "constructor"] }, { "ctor": "Object", "keys": ["constructor", "hasOwnProperty", ...] } ]}三件事:
- 原型是
Fetcher—— 就是第 18 章 service binding 的那個類別。 - 原型上根本沒有
send。 原型只有fetch和connect。 String(binding.send)是"[object JsRpcProperty]"。
也就是說:Pipelines binding 是建在 Workers RPC 之上的(第 18 章)。send 不是一個真的方法,是一個 RPC proxy property。這解釋了它的所有行為,也帶來一個具體的陷阱 —— 任何屬性存取都會產生一個看起來像函式的 proxy:
{ "typeofTypo": "function", "typoSource": "[object JsRpcProperty]", "callTypo": { "threwName": "TypeError", "threw": "TypeError: The RPC receiver does not implement the method \"sendd\"." }, "callFetch": { "threwName": "Error", "threw": "Error: Handler does not export a fetch() function." }}env.EVENTS.sendd 的 typeof 是 "function","sendd" in env.EVENTS 也是 true。只有真的呼叫下去才會炸。所以 typeof x.send === "function" 這種 feature detection 對這個 binding 完全無效 —— 它對任何名字都回 true。
順帶一提,binding.fetch() 丟 Handler does not export a fetch() function,證實遠端接收者是純 RPC entrypoint。
send() 在本地接受任何東西
Section titled “send() 在本地接受任何東西”型別是:
export interface Pipeline<T extends PipelineRecord = PipelineRecord> { send(records: T[]): Promise<void>;}export type PipelineRecord = Record<string, unknown>;實測七個探針,全部 resolve、全部回 undefined:
| 探針 | 結果 |
|---|---|
send([{...}]) 正常 | ok |
send(50 筆) | ok |
send([]) 空陣列 | ok |
send({a:1}) 非陣列 | ok |
send() 不給參數 | ok |
send([{a:{b:[1,2,{c:"d"}]}}]) 巢狀 | ok |
send([{fn: () => 1}]) 不可序列化 | ok |
而 .wrangler/state/v3/ 底下只有 cache、observability、workflows —— 沒有 pipelines 目錄,什麼都沒有存下來。
這和第 22 章的 Analytics Engine 是同一個病:本地 binding 是不驗證、不儲存的 no-op。差別在於 Analytics Engine 至少有一個自己的類別 LocalAnalyticsEngineDataset,Pipelines 連那個都沒有,就是一個空的 RPC receiver。
實務對策:既然 remote: true 可用(即使文件沒寫),開發 Pipelines 就該用它。 這是本章唯一能真正驗證資料有沒有進去的方法。
型別產生需要登入
Section titled “型別產生需要登入”實測 wrangler types 在未登入時的警告:
▲ WARNING Not authenticated - using generic types for pipeline bindings. Run `wrangler login` to enable typed pipeline bindings.未登入時退回泛型:
EVENTS: import("cloudflare:pipelines").Pipeline<import("cloudflare:pipelines").PipelineRecord>;登入而且 stream 有 schema 時,wrangler 會去讀 schema 產生具名型別(PascalCase + Record 後綴),例如 Cloudflare.Ch23ClicksRecord。這代表 stream schema 是型別安全的來源,值得為此宣告 schema,而不只是為了驗證。
一個 runtime 裡的化石
Section titled “一個 runtime 裡的化石”worker-configuration.d.ts 的 cloudflare:pipelines 模組裡還有這個:
export abstract class PipelineTransformationEntrypoint<Env, I, O> { public run(records: I[], metadata: PipelineBatchMetadata): Promise<O[]>;}export type PipelineBatchMetadata = { pipelineId: string; pipelineName: string };這是舊模型的 Worker-based 轉換:一個 Worker 類別接收一批 records、回傳轉換後的 records。新架構的轉換是 SQL,這個類別在現行文件裡完全找不到。它還留在 workerd 的型別裡,因為 legacy pipeline 還在運作。
不要用它。 它屬於 2025-09-25 之前的模型,會隨著 legacy pipeline 一起消失。
23.4 R2 Data Catalog
Section titled “23.4 R2 Data Catalog”R2 Data Catalog 是「直接內建在 R2 bucket 裡的受管 Apache Iceberg 資料目錄」,對外暴露標準的 Iceberg REST catalog 介面。
npx wrangler r2 bucket catalog enable linkforge-lake回傳兩個東西:Warehouse 名稱與 Catalog URI。之後任何支援 Iceberg REST catalog 的引擎都能接:
from pyiceberg.catalog.rest import RestCatalog
catalog = RestCatalog( name="linkforge", warehouse=WAREHOUSE, uri=CATALOG_URI, token=R2_TOKEN, # R2 API token, Admin Read & Write)官方有 Trino、DuckDB、PyIceberg、Snowflake、Spark(Python / Scala)、StarRocks 的分別說明頁。這是整個 Cloudflare Data Platform 最有戰略價值的一點:資料以開放格式(Iceberg on Parquet)存在你自己的 R2 bucket 裡,不是專有格式。要離開這個平台,把 bucket 同步走就行;第 12 章講過 R2 沒有 egress 費用,所以連搬走都不用付出口費。這在 vendor lock-in 的權衡上是很不一樣的立場。
實測 CLI:
wrangler r2 bucket catalog enable / disable / get <bucket>wrangler r2 bucket catalog compaction 自動壓實維護作業wrangler r2 bucket catalog snapshot-expiration 自動快照過期Compaction 是 opt-in 的
Section titled “Compaction 是 opt-in 的”串流寫入必然產生大量小檔案,小檔案會殺死查詢效能。R2 Data Catalog 提供受管壓實,但預設不開:
npx wrangler r2 bucket catalog compaction enable linkforge-lake \ --target-size 128 --token $R2_CATALOG_TOKEN--target-size 範圍 64–512 MB。官方建議值:延遲敏感 64–128、串流攝入 128–256、OLAP 256–512。
Beta 期間的限制值得記下來:「每張表每小時最多壓實 2 GB 的檔案。」 如果你的攝入速率高於這個,小檔案會持續累積。以 --roll-interval 300(預設 5 分鐘)計算,一天會產生 288 個檔案;資料量大時很快就會撞到這個上限。
另外兩點:只支援 Parquet;孤兒檔案不會被清理。快照過期是免費的。
23.5 R2 SQL
Section titled “23.5 R2 SQL”⚠️ 首先更正一個常見錯誤:文件不在
/r2/sql/(那會 404),而在/r2-sql/。
R2 SQL 是「Cloudflare 的 serverless 分散式分析查詢引擎,用來查詢儲存在 R2 Data Catalog 的 Apache Iceberg 表」。官方沒有指名底層用了哪個第三方引擎。
export WRANGLER_R2_SQL_AUTH_TOKEN="<token>"npx wrangler r2 sql query <WAREHOUSE> "SELECT * FROM linkforge.clicks LIMIT 10"實測 CLI 簽章 —— 兩個必填位置參數,沒有任何指令專屬的旗標:
POSITIONALS warehouse R2 Data Catalog warehouse name [required] query The SQL query to execute [required]HTTP API:
POST https://api.sql.cloudflarestorage.com/api/v1/accounts/{ACCOUNT_ID}/r2-sql/query/{BUCKET_NAME}Authorization: Bearer ${WRANGLER_R2_SQL_AUTH_TOKEN}{ "query": "SELECT * FROM namespace.table_name limit 10;" }注意這個 host 不是 api.cloudflare.com —— 是獨立的 api.sql.cloudflarestorage.com。
能力已經比 2025 年的說法強很多
Section titled “能力已經比 2025 年的說法強很多”如果你讀過 2025 年的 R2 SQL 教學說「不支援 JOIN、不支援聚合」,那個資訊已經過時。 現行 SQL 參考記載的是:
SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY(ASC/DESC、多欄位)、LIMITJOIN:INNER、LEFT、RIGHT、FULL OUTER、CROSS- CTE、
FROM/IN/EXISTS子查詢、EXPLAIN - 有獨立的聚合函式參考頁
明確不支援的(這一份清單很值得抄下來):
| 不支援 | 備註 |
|---|---|
OFFSET | LIMIT 只吃整數,預設 500 |
| LATERAL derived table | FROM 子查詢不能引用其他 FROM 欄位 |
nullable 欄位上的 NOT IN 子查詢 | |
子查詢裡的 SELECT DISTINCT | |
| 巢狀(括號)join | |
具名 WINDOW 宣告 | |
INSERT / UPDATE / DELETE 與所有 DDL | 「R2 SQL 是查詢引擎,不是資料庫。No writes.」 |
| Parquet 以外的格式 |
還有一個很特別的設計:部分函式會在執行前做預算檢查,掃描量過大就直接回 400 —— MEDIAN、PERCENTILE_CONT、ARRAY_AGG、STRING_AGG、window function 都在這一類。這是我在其他查詢引擎上沒看過的做法:不是跑到一半超時,是根本不讓你跑。
另外一個小細節:now() / current_time() 「量化到 10ms 邊界並強制 UTC」。
限制:官方沒有 limits 頁
Section titled “限制:官方沒有 limits 頁”第 22 章的 Analytics Engine 至少有一張七行的 limits 表。R2 SQL 完全沒有 limits 頁面。 limitations-best-practices 是純散文,沒有任何數字:沒有查詢逾時秒數、沒有掃描量上限、沒有結果大小上限、沒有並行數。
唯二的量化事實來自 pricing 頁:每次查詢最低計費 10 MB 掃描量,以及上面那個 400 預算門檻。
所以:任何宣稱 R2 SQL 有某個查詢逾時或掃描上限的教學(包括別處看到的),都不是從官方文件來的。 本章不臆測。
Token 權限:文件自己講不清楚
Section titled “Token 權限:文件自己講不清楚”query-data 頁列了三項要求,而且同一頁內部就自相矛盾 —— 文字說「R2 Data Catalog(read-only)」,對應的權限群組卻寫 Workers R2 Data Catalog Write。
get-started 頁則簡單得多:建一個 Admin Read & Write 的 R2 API token,「這也包含了 R2 SQL Read 權限」。
實務建議:照 get-started 走,用 Admin Read & Write。
23.6 限制與計費
Section titled “23.6 限制與計費”Pipelines limits(官方整張表就這五行)
Section titled “Pipelines limits(官方整張表就這五行)”| 項目 | 上限 |
|---|---|
| 每帳號 streams 數 | 20 |
| 每次攝入請求的 payload | 5 MB |
| 每個 stream 的攝入速率 | 5 MB/s |
| 每帳號 sinks 數 | 20 |
| 每帳號 pipelines 數 | 20 |
可透過 Limit Increase Request Form 申請提高。
文件沒有記載的:單筆 record 大小上限、單次 send() 的筆數上限、schema 欄位數上限、一條 pipeline 的 SQL 敘述數上限。唯一相關的數字是 5 MB 的攝入 payload 上限,而文件並沒有說這條是否適用於 send()。
「每帳號 20 個 stream」這條對多租戶設計有直接影響:你不能一個租戶開一個 stream。正確做法是所有租戶共用一條 stream,tenant_id 當成欄位,在 sink 的分割或查詢時再切開。
計費:兩份官方頁面互相矛盾
Section titled “計費:兩份官方頁面互相矛盾”Pipelines:
- Landing page(
/pipelines/):「Pipelines 處於 open beta,任何有 Workers Paid 方案的開發者都能開始使用。」以及「目前,除了標準的 R2 儲存與操作費用之外,你不會因為使用 Pipelines 而被收費。」 - Pricing changelog:「Billing is not yet enabled. 我們會在開始收費前至少提前 30 天通知。」並公告 Workers Free 每個維度每月 1 GB、Paid 每月 50 GB。
- Pricing 頁(
/pipelines/platform/pricing/):直接列出費率 —— Streams 攝入免費、SQL 轉換 $0.04/GB、Sink 寫出 R2-JSON $0.03/GB、Parquet/Iceberg $0.06/GB。沒有 beta 標示、沒有「尚未開始計費」的說明、而且表格裡只有 Workers Paid 一欄,changelog 公告的 Free 方案 1 GB 額度不見了。
R2 SQL 是完全一樣的模式:landing page 說 open beta 且「不會被收費」,pricing 頁列出 $0.0025/GB 掃描($2.50/TB)、每月含 10 GB、每次查詢最低計費 10 MB、失敗的查詢不計費、EXPLAIN/SHOW/DESCRIBE 免費 —— 沒有 beta 或「尚未計費」的字樣。
R2 Data Catalog 也一樣:public beta 且「不會被收費」,pricing 頁列出每百萬 catalog 操作 $9.00、壓實處理 $0.005/GB、每百萬物件 $2.00。
三個產品、三組矛盾、同一個模式。
本章的立場:以 landing page 的「open beta,尚未計費」為現況,以 pricing 頁的費率為未來的成本估算依據。查證日期 2026-08-01,讀者請自行重新確認。
架構決策上要注意的是:這三個產品的費率不是零,而且 Pipelines 的 SQL 轉換與 sink 寫出是分開計價的。一份資料經過轉換再寫成 Parquet,是 $0.04 + $0.06 = $0.10/GB。以 LinkForge 每天 1 億次點擊、每筆 200 bytes 估算,每天 20 GB、每月 600 GB,扣掉含量 50 GB 之後大約 $55/月。這個數量級要事先算清楚。
23.7 現在該不該用?
Section titled “23.7 現在該不該用?”綜合前面所有內容,我的判斷框架如下。這不是官方立場 —— 官方沒有任何一頁比較這幾個產品。
該用的情境:
- 你需要稽核軌跡:逐筆、不抽樣、可長期保存。Analytics Engine(第 22 章)做不到這件事。
- 你需要開放格式的資料湖:Iceberg on Parquet 存在自己的 R2 bucket,可以被 Spark / DuckDB / Trino 讀,將來要搬走沒有 egress 費用。
- 你的分析團隊已經在用 Iceberg 生態系。
- 你需要超過 3 個月的歷史資料。
先別用的情境:
- 你只需要儀表板與趨勢 —— Analytics Engine 更簡單、更便宜(目前免費)、查詢延遲更低。
- 你需要即時查詢 —— sink 的 roll interval 預設 300 秒、R2 Data Catalog sink 最小 60 秒,資料落地本來就有分鐘級延遲。
- 你需要在攝入階段做聚合 —— 文法不允許。
- 你無法承受 open beta 的變動。 這個產品在 2025-09 整個重寫過一次,binding 鍵名在 2026-05 改過名,計費隨時可能開啟。
三個產品的分工,放進 LinkForge 的架構:
| 需求 | 用什麼 |
|---|---|
| 即時點擊計數 | Durable Object 計數器(第 14 章) |
| 儀表板趨勢、前 N 名 | Analytics Engine(第 22 章) |
| 逐筆稽核、長期歸檔 | Pipelines → R2 Data Catalog(本章) |
| 臨時的深度分析查詢 | R2 SQL(本章) |
| 精確計費用量 | DO 計數器 + 每日對帳寫入 D1 |
同一筆點擊事件同時寫進 Analytics Engine 和 Pipelines 是完全合理的 —— 前者負責快而粗,後者負責慢而全。兩邊各自的成本模型都是線性的,加起來也還是可預測的。
23.8 本章實測結論彙整
Section titled “23.8 本章實測結論彙整”| # | 結論 | 影響 |
|---|---|---|
| 1 | config-schema.json 把 pipeline 標為 deprecated: "Use \stream` instead.”` | 鍵名改名證據確鑿 |
| 2 | 用舊鍵每次指令都出警告,訊息文字已記錄於本章 | 官方唯一的 binding 文件頁仍用舊鍵,照做會一直看到警告 |
| 3 | schema 只要求 binding,validator 要求 stream 是字串 | schema 比 validator 寬鬆,以 --dry-run 為準 |
| 4 | schema 有 remote 欄位且 --dry-run 接受 | Pipelines 支援 remote binding,文件完全沒說(且不在支援矩陣裡) |
| 5 | 本地 binding 的原型鏈是 Fetcher,send 是 [object JsRpcProperty] | Pipelines 建在 Workers RPC(第 18 章)之上 |
| 6 | 任何屬性名的 typeof 都是 "function";env.X.sendd(...) 才丟 The RPC receiver does not implement the method "sendd" | feature detection 對這個 binding 無效 |
| 7 | binding.fetch() 丟 Handler does not export a fetch() function | 遠端是純 RPC entrypoint |
| 8 | send() 對不給參數、非陣列、空陣列、不可序列化值全部靜默成功 | 本地無驗證 |
| 9 | .wrangler/state/v3/ 下沒有 pipelines 目錄 | 本地不持久化 —— 開發必須用 remote: true |
| 10 | wrangler types 未登入時警告並退回 Pipeline<PipelineRecord> | 具名型別需要登入 + stream schema |
| 11 | runtime 型別仍保留 PipelineTransformationEntrypoint(舊的 Worker 轉換模型) | 現行文件已無此物,不要使用 |
| 12 | CLI 每個子指令都標 [open beta];pipelines update 標 (legacy pipelines only) | 舊模型仍在,但只維護不演進 |
| 13 | streams / sinks 都沒有 update 子指令;pipeline SQL 建立後不可改 | 全平台 immutable,改動 = 刪除重建 |
| 14 | --http-enabled 與 --http-auth 預設都是 true | 文件從未寫出這兩個預設值 |
| 15 | --format 預設 parquet;--partitioning 標註 (r2 sinks only) | Data Catalog sink 沒有分割選項 |
| 16 | 不符合 schema 的事件「攝入時被接受、處理時被丟棄」,且沒有 DLQ | 只能靠 metrics 發現;對照 Queues 有 DLQ |
| 17 | R2 Data Catalog sink 無法對既有 Iceberg 表建立 | 既有資料湖無法直接接上 |
| 18 | R2 SQL 文件在 /r2-sql/ 而非 /r2/sql/(後者 404) | — |
| 19 | R2 SQL 現已支援 JOIN / GROUP BY / HAVING / CTE | 2025 年「不支援聚合」的說法已過時 |
| 20 | R2 SQL 沒有 limits 頁;無任何逾時或掃描上限數字 | 引用這類數字的教學都不是來自官方 |
| 21 | 部分函式(MEDIAN、window 等)掃描量過大時執行前回 400 | 預算門檻,不是逾時 |
| 22 | Compaction 是 opt-in;beta 期間每張表每小時上限 2 GB | 高攝入速率下小檔案會累積 |
| 23 | 三個產品的 landing page 說「不會被收費」,pricing 頁列出費率且無 beta 標示 | 需標註查證日期並自行確認 |
| 24 | 每帳號 20 個 stream / sink / pipeline | 不能一租戶一 stream;共用 stream + tenant_id 欄位 |
| 25 | 官方文件從未提及 Arroyo 或任何底層引擎 | 網路上的說法無官方來源 |
23.9 動手練習
Section titled “23.9 動手練習”- 用
wrangler pipelines setup建一條完整的鏈,然後在 Worker 裡設"remote": true,用wrangler dev實際把資料送進去 —— 對照 23.3 的本地 no-op 行為。 - 故意送一筆缺少 required 欄位的事件,確認 HTTP 回 200,然後從 GraphQL
pipelinesUserErrorsAdaptiveGroups找出missing_field計數。這是驗證「靜默丟棄」的唯一方法。 - 對同一份 LinkForge 點擊資料,分別用 Analytics Engine 的
sum(_sample_interval)(第 22 章)與 R2 SQL 的count(*)算一次日點擊數,比較抽樣造成的誤差。 - 開啟 compaction,觀察
--target-size 128前後 R2 SQL 同一個查詢的掃描量(用EXPLAIN,免費)。 - 用 PyIceberg 從本機連上 R2 Data Catalog 讀同一張表,驗證「開放格式、可攜」這個賣點。
- Pipelines · Streams · Manage streams · Writing to streams(⚠️ 此頁仍使用已棄用的
pipeline鍵) - Pipelines 物件 · Manage pipelines
- Sinks · R2 sink · R2 Data Catalog sink
- SQL reference — SELECT · SQL data types
- Wrangler commands · Legacy pipelines
- Metrics · Limits · Pricing
- binding 改名 changelog · pricing changelog
- R2 SQL · SQL reference · Query data · Limitations & best practices · Pricing
- R2 Data Catalog · Table maintenance · Pricing
- 設定 schema 來源:
node_modules/wrangler/config-schema.json;型別來源:wrangler types產生的worker-configuration.d.ts
第 23 章結束了 Part 4(資料與分析)。下一章起進入 Part 5:前端與全端整合 —— 第 24 章從 React Router v7 的 framework mode 開始,把 LinkForge 的後台介面接上來。