資料與儲存 2026 年 10 月 1 日

2026-10-01 — ClickHouse 以四種方式把 C++ 隔離出 Postgres 擴充套件

primary=https://clickhouse.com/blog/memory-safety-postgres-extensions-c-cpp primary=https://github.com/ClickHouse/pg_stat_ch/pull/126 primary=https://github.com/ClickHouse/pg_stat_ch/pull/129 primary=https://github.com/ClickHouse/pg_re2/blob/main/src/re2_wrapper.cpp

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_re2C++ 只留在 re2_wrapper.cpp,try/catch 內不呼叫 Postgres 的 C 程式碼是,但被封在邊界內
pg_chdblibchdb 放進獨立的 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


End of article
0
Would love your thoughts, please comment.x
()
x