ClickHouse 以四種方式把 C++ 隔離出 Postgres 擴充套件
ClickHouse Engineering Blog(Philip Dubé)· 2026-09-30
Postgres 擴充套件如果直接在 backend 裡呼叫 C++ 函式庫,一次 palloc 失敗就可能讓整個叢集進入 crash recovery。ClickHouse 團隊回顧自家四個擴充套件,逐一把 C++ 關在 Postgres 的錯誤處理機制之外:pg_clickhouse 改寫成純 C,pg_re2 用 try/catch 封死邊界,pg_chdb 拆成獨立行程,pg_stat_ch 則仍在重構中。
這篇文章發表於 2026-09-30,並附上 pg_stat_ch 的 PR #126 與 PR #129 作為實例。
原本的問題
Postgres 用 MemoryContext arena 配置器,配合 palloc/pfree,出錯時靠 PG_TRY/PG_CATCH 底下的 setjmp/longjmp 跳出。C++ 則用 new/delete、建構與解構函式,配置失敗丟出 std::bad_alloc。兩套機制互不知道對方存在。
文章指出:longjmp 會跳過 C++ 的清理,跳過帶有非平凡解構函式的自動物件屬於未定義行為。反方向也一樣危險:沒人接住的 C++ 例外會讓行程 abort,後果是整個叢集的 crash recovery,而不只是一個連線失敗。
文章舉的典型寫法如下,cstring_to_text_with_len 內部的 palloc 一旦失敗,就會 longjmp 繞過 s 的解構函式:
auto s = std::string(col->AsStrict<ColumnString>()->At(row));
ret = PointerGetDatum(cstring_to_text_with_len(s.data(), s.size()));四種隔離方式
| 擴充套件 | 做法 | C++ 還在 backend 嗎 |
|---|---|---|
pg_clickhouse | 以純 C 的 clickhouse-c 取代 clickhouse-cpp,Postgres 專屬邏輯放在 pg-clickhouse-c | 否 |
pg_re2 | C++ 只留在 re2_wrapper.cpp,try/catch 內不呼叫 Postgres 的 C 程式碼 | 是,但被封在邊界內 |
pg_chdb | libchdb 放進獨立的 helper 執行檔 | 否 |
pg_stat_ch | 背景 worker 隔離,未捕捉例外改走 ereport(FATAL) 而非 abort() | 是,文章稱這只是緩解 |
pg_re2 的邊界寫法最值得照抄。re2_wrapper.cpp 在編譯 pattern 時捕捉 std::bad_alloc,把失敗轉成 NULL 與錯誤緩衝區內容,讓 C 端自己決定要不要 ereport:
try {
pat = new re2_pattern(default_opts(),
re2::StringPiece(pattern, pattern_len));
...
return pat;
}
catch (std::bad_alloc &) {
strlcpy(errbuf, "out of memory", errbuf_size);
delete pat;
return NULL;
}例外不會穿過語言邊界,C++ 程式碼也不碰 palloc,兩種失敗模式各自在自己那一側處理完。
判斷該用哪一種,關鍵在 C++ 程式碼是否需要碰 Postgres 的資料結構。pg_clickhouse 連線與資料轉換都貼近 Postgres 型別,所以重寫成純 C 並保留傳輸層無關的設計;pg_chdb 依賴的是整個嵌入式引擎,無法重寫,只能整包移出 backend;pg_re2 的介面窄到只剩編譯與比對,一層薄封裝就夠。介面越窄,越適合留在同一個行程。
還沒解完的部分:pg_stat_ch
pg_stat_ch 是四個之中最晚處理的。PR #126「Prevent double frees when retrying interrupted dequeues」修的是共享記憶體佇列的問題:worker 被中斷後,若替代 worker 帶著保留的共享記憶體重啟,可能對已釋放的資源再釋放一次。修法是先清除 slot 的參照指標,再釋放記憶體,並把可能出錯的 DSA attach 提前到清除參照之前。
PR #129「Encapsulation」打算把 C++ 的 exporter 放進 src/exporter,不依賴 Postgres,由 bgworker.c 負責 C/C++ 邊界,涉及 32 個檔案。兩個 PR 截至我們抓取時都還是 draft/open 狀態,審查中仍有關於例外安全與缺少中斷復原測試的意見,所以這一套做法尚未定案。
影響範圍
- 在 Postgres 擴充套件裡連結 C++ 函式庫的作者:檢查每一處 C++ 物件存活期間是否可能呼叫
palloc、ereport或其他會longjmp的 Postgres API。上面那兩行就是檢查對象。 - 使用背景 worker 並在共享記憶體放佇列的擴充套件:worker 重啟後是否會重複釋放?可參考 PR #126 的順序,先清參照再釋放。
- 無法改寫的第三方 C++ 函式庫:文章的兩條出路是
pg_re2式的薄封裝,或pg_chdb式的獨立行程。後者多一層行程間通訊,代價文章未量化。 - 運維端:若擴充套件在 backend 內有 C++ 未捕捉例外,表現是整個 postmaster 的 crash recovery,而不是單一 session 報錯。
文章沒有提供效能數據,也沒有說明各方案的遷移成本,只描述了各擴充套件目前採用的設計。pg_stat_ch 用 ereport(FATAL) 取代 abort() 的做法,作者自己也標為緩解而非保證。
原始來源:ClickHouse 部落格、pg_stat_ch PR #126、pg_stat_ch PR #129、pg_re2 re2_wrapper.cpp