russellromney/honker

Simon Willison's Weblog

View Original ↗
AI 導讀 technology infrastructure 重要性 3/5

honker:20+ SQL 函數讓 SQLite 跑起生產級佇列與 Kafka 式持久串流,1ms 輪詢近即時

  • 1ms stat 輪詢感知 .db-wal 變動,不執行完整 SQL 查詢就能實現近即時通知
  • 交易式發件匣保證:訊息與業務資料原子 commit,進程崩潰也不會丟失訊息
  • 一個 .db 檔跑佇列、持久串流、SQL 廣播三合一,不需要外部 Redis 或 Kafka

1ms 的輪詢頻率、不到十行 Python 程式碼,honker 把 PostgreSQL 的 pub/sub(發布/訂閱)機制搬進了 SQLite——這個 Rust 擴充套件附帶超過 20 個自訂 SQL 函數,讓本來只做嵌入式儲存的輕量資料庫,也能跑生產等級的訊息佇列與事件串流。Simon Willison 在部落格上評為「設計相當紮實」,背後核心靠的是 transactional outbox pattern(交易式發件匣)——一個在 Postgres 世界被反覆驗證有效的可靠訊息模式。

SQLite 版 Postgres 廣播語意:honker 的定位

PostgreSQL(開源關聯式資料庫)原生提供 NOTIFY/LISTEN 機制,讓不同資料庫連線之間可以互相廣播訊息:當一個連線執行 NOTIFY channel_name,所有正在 LISTEN 的連線會即時收到通知。這個機制讓 Postgres 可以不依賴外部 message broker(訊息代理,如 RabbitMQ 或 Kafka)就實現輕量的事件驅動架構,是許多 Postgres-based 系統的骨幹設施。

SQLite 沒有這個功能。它設計上是嵌入式資料庫,以函式庫形式直接運行在應用程式進程內,原本就不支援跨進程的即時通知。honker(russellromney/honker)是用 Rust 編寫的 SQLite 擴充套件(extension),目標是在 SQLite 上重現同等的廣播語意,並提供 Python 等語言的綁定(binding,讓其他語言呼叫 Rust 程式庫的橋接層),讓開發者能以高階 API 操作佇列與串流,而不必自行實作底層機制。

訊息佇列與 Kafka 式持久串流的雙 API 設計

honker 提供兩套截然不同的抽象介面,對應不同的使用場景。

第一套是訊息佇列(queue),語意接近傳統工作佇列:生產者呼叫 .enqueue() 把任務推入佇列,消費者在獨立的 worker 進程裡以非同步迭代方式 claim 取件,處理完呼叫 .ack() 標記完成。claim 方法支援具名 worker(如 "worker-1"),用來追蹤哪個 worker 正在處理哪筆任務,防止重複消費。整套 Python API 的設計極為精簡——建立連線、建立佇列、推入、取出、確認,每個步驟僅需一行程式碼。

第二套是持久事件串流(durable stream),語意更接近 Apache Kafka(分散式事件串流平台):訊息不會在被讀取後消失,不同的 consumer 可以從串流的任意偏移量(offset)開始讀取,彼此完全獨立互不干擾。最關鍵的設計是,stream.publish() 可以帶入 tx=tx 參數,把發布動作綁定在現有的資料庫交易(transaction)內——以更新使用者名稱為例,UPDATE users 語句與發布事件包在同一個 transaction 區塊裡,只要 UPDATE 成功 commit,事件才進串流;UPDATE 失敗回滾,事件也同步取消,兩者原子一致,這正是 transactional outbox pattern 的核心保證。

超過 20 個自訂 SQL 函數:從查詢層直接廣播

除了語言層 API,honker 還在 SQLite 內建超過 20 個自訂 SQL 函數,讓開發者可以直接在 SQL 查詢中操作通知與串流,不需要切換到應用程式層:

SELECT notify('orders', '{"id": 42}');
SELECT honker_stream_read_since('orders', 0, 1000);

notify() 對指定頻道廣播一則 JSON 訊息;honker_stream_read_since() 從串流的某個偏移量開始讀取最多 N 筆事件,第三個參數是批次上限。後者對熟悉 Kafka consumer offset 機制的開發者會非常眼熟——本質上就是「從第 X 筆開始、最多取 N 筆」的視窗查詢。能從 SQL 層直接操作,意味著 honker 不限於 Python 使用者,任何能執行 SQLite 查詢的工具或語言都能接入,工具鏈相容性相當廣泛。

WAL 模式 + 1ms stat 輪詢的近即時通知底層

honker 要求開啟 WAL 模式(Write-Ahead Logging,預寫日誌模式)。WAL 是 SQLite 的一種日誌策略,與預設的 DELETE 模式不同,WAL 在寫入時先把資料記錄至 .db-wal 暫存檔,讀寫操作可並行進行而互不阻塞,適合多 worker 並發的生產場景。

利用 WAL 的特性,honker 的 worker 採用 stat 系統呼叫1ms 監測一次 .db-wal 檔案是否有變動——有新資料寫入時,.db-wal 的修改時間戳(mtime)就會更新,worker 隨即察覺並去讀新資料。這個設計巧妙繞開了定期執行完整 SQL 查詢的開銷:stat 呼叫極為輕量,每毫秒觸發一次也不會對系統造成明顯負擔。從資料寫入到 worker 察覺,延遲最多 1ms,在不引入任何額外基礎設施的前提下,已相當接近即時通知的效果。

Transactional Outbox Pattern 在 SQLite 的落地意義

Simon Willison 特別強調,honker 實作了 transactional outbox pattern(交易式發件匣模式),並推薦 Brandur Leach 的文章《Transactionally Staged Job Drains in Postgres》作為這個模式的最佳解說。

這個模式解決的是一個經典的分散式系統問題:若應用程式先寫資料庫、再推訊息到 message broker,在「寫完資料庫但還沒推訊息」的瞬間若進程崩潰,訊息就會永久遺失。解法是把「要發出的訊息」也寫入同一個資料庫交易——業務資料與訊息要嘛一起 commit,要嘛一起 rollback,在原子層面徹底消除丟訊息的可能性。honker 把這套保證帶進 SQLite,讓開發者在不依賴 Redis、Kafka 等外部基礎設施的情況下,也能做到分散式系統等級的訊息可靠性,是這個函式庫最具工程說服力的設計決策。

下次評估是否引入 Kafka 或 Redis 之前,先問一句:workload 真的超出一個 .db 檔加 honker 的能力範圍了嗎?

Abstract

russellromney/honker "Postgres NOTIFY/LISTEN semantics" for SQLite, implemented as a Rust SQLite extension and various language bindings to help make use of it. The design of this looks very solid. It lets you write Python code for queues that looks like this: import honker db = honker.open("app.db") emails = db.queue("emails") emails.enqueue({"to": "[email protected]"}) # Consume (in a worker process) async for job in emails.claim("worker-1"): send(job.payload) job.ack() And Kafka-style durable streams like this: stream = db.stream("user-events") with db.transaction() as tx: tx.execute("UPDATE users SET name=? WHERE id=?", [name, uid]) stream.publish({"user_id": uid, "change": "name"}, tx=tx) async for event in stream.subscribe(consumer="dashboard"): await push_to_browser(event) It also adds 20+ custom SQL functions including these two: SELECT notify('orders', '{"id":42}'); SELECT honker_stream_read_since('orders', 0, 1000); The extension requires WAL mode, and workers can poll the .db-wal file with a stat call every 1ms to get as close to real-time as possible without the expense of running a full SQL query. honker implements the transactional outbox pattern, which ensures items are only queued if a transaction successfully commits. My favorite explanation of that pattern remains Transactionally Staged Job Drains in Postgres by Brandur Leach. It's great to see a new implementation of that pattern for SQLite. Via Show HN Tags: databases, postgresql, sqlite, rust