PGKeeper 上線:Figma 為什麼自己刻一個資料庫看門員,從 PgBouncer 換掉的工程決策

Figma 用 Go 寫 PGKeeper 取代 PgBouncer——admission control 的 priority semaphore + CoDel + fair sharing 樹、連線管理三件套、3× peak 壓測 + 自動 flip back 的 rollout 路徑。設計師讀完會發現:每個工程 tradeoff,在 design system 都有對應版本。

PGKeeper: Building the bouncer we needed for Postgres | Figma 官方文章封面
Illustrations by Pete Gamlen · Figma Engineering Blog

你打開 Figma 文件那一秒,畫布跑出來、最近編輯記錄秒讀、留言 dot 同時亮起。這些動作背後是同一條路徑——應用層問 PostgreSQL 要資料、PostgreSQL 回。

中間擋著的這層叫 connection pooler。它存在的目的很單純:每個連線在 Postgres 上是獨立 process,建立一次很貴,爆量一次資料庫直接被拖垮。pooler 把連線復用、把流量塑形,是 Figma 後端最低調但最關鍵的一塊。

Figma 過去用的是業界最常見的 PgBouncer。這篇文章是 storage 團隊宣布他們把 PgBouncer 換掉了——換成自己用 Go 寫的 PGKeeper。從決定要換、到完整 rollout,他們公開了一份很值得讀的工程紀錄:怎麼評估、怎麼刻、怎麼安全地把 production 流量切過去。

SOURCE · FIGMA OFFICIAL BLOG
PGKeeper: Building the bouncer we needed for Postgres
Lihao He · Tim Goh · Mehant Baid · 2026-05-04
read original →
PGKEEPER · Q4 2025 IMPACT
20+
PGKeeper 在 2025 年第四季擋下 20 起以上原本會造成使用者可見的故障。從全面 rollout 之後,Figma 資料庫 SLO(以核心使用體驗可用性衡量)持續維持在 99.99% 以上。
本文目錄 · TABLE OF CONTENTS
  1. 01PgBouncer 的天花板:為什麼夠用變不夠用
  2. 02自建 vs 改現成:Figma 為什麼選自己寫
  3. 03連線管理:把連線當資產護住
  4. 04Admission control:流量塞爆時誰先過
  5. 05安全 rollout:怎麼把流量無痛切過去
  6. 06整體觀察:工程決策也是設計決策

Figma 過去幾年產品線拓得很開——Make、Weave、FigJam、Slides、Sites 一個接一個上線。每多一個產品,資料庫層的負載就多一份。Storage 團隊先是把 Postgres 做了水平 sharding 跟 vertical sharding,再用一個叫 DBProxy 的 query router 把分片邏輯藏在 application code 之外。

DBProxy 後面接的,就是這篇文章主角的位置——connection pooler。每台 Postgres 機器配一組 pooler replica,n 對 1 的關係。這層的工作很乾脆:把 application 來的請求轉成一個個 Postgres connection,盡量復用、盡量別讓資料庫處理大量短連線。

01/05 · SECTION
PgBouncer 的天花板:為什麼夠用變不夠用
擴展性、載量管理、連線管理、可延伸性 — 四個方向同時撞牆

PgBouncer 是業界用了十幾年的標準解。Figma 早期跑得也很順。問題是流量等級拉到一個量級之後,原本可以接受的設計選擇變成了硬天花板。

第一個是擴展性。PgBouncer 是單執行緒架構,垂直擴展頂多到一個程度。團隊試過用更多 replica 水平擴出去,結果 load distribution 不均,效能反而衰退。第二個是 load management。PgBouncer 沒有優先級概念,也沒有 backpressure 機制;當流量爆衝(突發從 20K QPS 跳到 45K QPS),critical 的使用者請求跟低優先級的批次工作排同一條 queue,沒辦法分流。第三個是連線管理。Postgres 連線本來就是 process-per-connection,建立成本高;PgBouncer 沒有節流機制,一場 incident 後的恢復過程,本身會造成第二波 churn 把 Postgres 再壓垮一次。第四個是可延伸性。要把 observability、feature flag、admission control 這些 infrastructure 標配加上去,得改 PgBouncer 的核心 code path——團隊算過,這條路的長期維護成本太高。

▸ TRY THIS
設計決策也常面對「夠用變不夠用」的時刻。下次遇到一個你接了三年、現在覺得卡卡的元件、或一個被加了太多 variant 的 component,先列一張表寫四欄:擴展性、可維護、彈性、團隊熟悉度。哪一欄踩線了,就是該重做的訊號——跟 storage 團隊評估 PgBouncer 同一個邏輯。
02/05 · SECTION
自建 vs 改現成:Figma 為什麼選自己寫
DBProxy 內建?PGCat fork?最後刻一個 Go service

自建之前,團隊先排除了兩條路。

第一條是把 connection pooling 做到 DBProxy 裡。看似省事,數字對不上——Postgres 一台 instance 大約配 100 條連線,但 DBProxy 是 stateless、有上百個 replica。要把 100 個有限資源切給上百個 replica,要嘛超發、要嘛得跨 replica 做複雜協調,兩個都不可行。

第二條是用社群現成的 PGCat。PGCat 是 multi-threaded 的 Go 實作,剛好補上 PgBouncer 垂直擴展的缺。問題是 observability、feature flag、admission control 一樣得深度改 core,這些改動上游不會收,等於要長期維護一支 fork——跟改 PgBouncer 同一個坑。

剩下一條路就是自己寫。PGKeeper 是一個 Go service,建在 PGX 這套成熟的 Postgres toolkit 上。它對外用 gRPC,每筆 query 帶著 metadata(traffic tier、user type、來源),讓 server 端可以看著訊號做決策。位置上,它接在 DBProxy 跟 Postgres 之間,每個 PGKeeper 替一台 Postgres 服務、每個 instance 配多個 replica 跨 Kubernetes cluster 做高可用。

Option A — Fork PGCat

對外能講「我們用社群方案」,對內得長期維護一支 fork。每次 upstream 改、自己也得追,自己加的 observability / feature flag 還要不斷重新合併。

Option B — 自己刻 PGKeeper

從 PGX 開始疊,第一天就把 observability、feature flag、admission control 設計進核心 code path。三個都不會被 fork 維護成本卡住。

▸ TRY THIS
這個決策結構很值得設計師抄起來用:「現成方案夠不夠用 → 不夠的話,是改?fork?還是重寫?」改的部分若會深入到 core,就要算 long-term maintenance 成本。下次團隊在討論要不要 fork 一套外部 design system,可以套同一張決策表。
03/05 · SECTION
連線管理:把連線當資產護住
Pool warming、token bucket、bounded exhaust、auto rollback、context cancellation
PGKeeper 請求處理流程:DBProxy → Admission control → Connection pool → PostgreSQL
PGKeeper 請求處理三階段:admission control → connection pool → query execution,每筆 query 都帶 metadata 進來。

每筆請求進到 PGKeeper 會走三個階段——admission control、connection pool、query execution。最有看頭的是中間這層 connection pool,因為 Postgres 連線本身就是貴的東西。

第一個機制是 pool warming。新的 PGKeeper 起來時連線池是空的,第一波請求得邊建連線邊處理,延遲飆很高。Pool warming 在 production 流量還沒進來之前,就先把連線預先建好,把這段熱機時間移到使用者看不見的地方。

第二個機制是建立跟拆除都做 rate limit。Pool warming 解的是穩態,但一旦遇到大規模 churn,重建連線本身就會把 Postgres 壓垮。PGKeeper 用 token bucket 把建立速度節流。拆除也一樣——團隊在 PostgreSQL 13.21 升 13.22 的時候才發現,pool shutdown 那一波關閉連線會讓資料庫 CPU 飽和,所以拆除這側也加上節流。

剩下三個都是「讓連線可以乾淨還回去」的細節。Bounded exhaust:client 沒讀完整個 result set 時,連線會殘留 server-side state,PGKeeper 會主動 drain 最多 100 row(P99 query 回傳少於 5 row,這 100 個閾值已經夠 cover 大多數)。Auto rollback:連線釋放時還在 transaction 中,自動先 ROLLBACK 再放回 pool。Context cancellation:client 取消請求時,PGKeeper 立刻回空結果給 caller,背景再走 pg_cancel_backend,跑一條 SELECT 1 把 kill signal flush 掉,最後這條連線才回到 pool。連 cancel 用的連線都另外開一個獨立 pool,避免 cancel 把主 pool 占用掉。

01 · POOL WARMING
先把連線預熱
production 流量還沒進來之前,連線池就先建好——把熱機那段高延遲移到使用者看不見的地方。
02 · TOKEN BUCKET
建立 / 拆除都節流
Token bucket 把連線建立速度限住,避免 churn 後恢復本身把 Postgres 再壓一次。13.22 升級才發現拆除也得節流。
03 · CHURN AVOIDANCE
連線回收三件套
Bounded exhaust 排乾未讀完的 row、auto rollback 清掉未提交事務、context cancellation 用獨立 pool 處理 client 中斷。
▸ TRY THIS
「連線是要保護的資產」這個 mental model 很 transferable。對應到設計工作流,你的 component instance 也是資產——用一次就脫鉤的 detached instance,跟 connection churn 一樣會慢慢累積技術負債。下一次重構時把 detached 數字當健康指標看。
04/05 · SECTION
Admission control:流量塞爆時誰先過
優先級 semaphore + CoDel + adaptive LIFO + 多維 fair sharing
Figma Postgres 流量:穩態日週期 vs 突發 spike
週均看起來很穩定(peak 約 20K QPS),放大到分鐘級別就會看到 45K QPS 等級的突發 spike——這是 admission control 真正要解的問題。

超過容量時,請求一定要丟掉一些。問題是丟哪一筆。

V1 的設計是 priority semaphore + CoDel + adaptive LIFO + debt 機制。每個請求都帶著 priority,高優先級對應 critical 的使用者操作(開檔、存檔),低優先級對應背景任務。當高優先級拿不到 ticket,系統會給所有低優先級 tier 累積「debt」——債要還掉,低優先級才能再進來。Debt 會隨時間衰減,高優先級恢復順暢時還得更快。Queue 健康時走 FIFO,當 queue 持續累積、變得不健康,自動切成 LIFO 並縮短可等時間,把卡死的舊請求先丟掉、保住新請求。

V1 上 production 之後團隊發現一個漏洞——同一個 priority tier 內部還是會有單一工作載量把容量吃光。一個爆紅功能、一場 DDoS,都可能把高優先級預算耗盡,連累同 tier 其他無關工作。

V2 加了一層 weighted min-max fair sharing。Traffic 的維度被組成樹——第一層按認證狀態分流(有登入的權重高很多),第二層再按 endpoint 路由。容量從根節點往下遞迴分配,每個節點按權重切給子節點。沒用滿的份額會回到 pool 給需要更多的人——沒人會餓死、也沒人能獨吞。一筆請求要先過 priority admission control、再過 fair sharing,兩道都點頭才放行。

1

進入 admission control

優先級 semaphore 先看高優先級有沒有空缺;沒空缺就讓低 tier 累積 debt。

2

切到 LIFO + CoDel

Queue 持續滿載 → 切 LIFO + 縮短等待 → 老的請求先丟、新請求先過。

3

過 fair sharing 樹

按認證狀態 × 路由分維度切容量;同 tier 不同 endpoint 也不會互相搶死。

4

Client 反向退讓

PGKeeper 回特定 error code,client 端的 AdaptiveThrottle 接到後做 exponential backoff,server 跟 client 自然收斂到穩定速率。

▸ TRY THIS
「scarcity 來臨時要服務誰」這個問題在 UX 寫了一年也答不完。當功能多到塞滿 nav、當 onboarding 要塞五個提示、當系統錯誤訊息要顯示在使用者最忙的那一瞬——你都在做 admission control。下次面對「都很重要」的設計清單,先問:誰是我的 high priority traffic?
05/05 · SECTION
安全 rollout:怎麼把流量無痛切過去
3× peak 壓測、合成事故重播、低風險先行、自動偵測回退

把流量從一個 pooler 切到另一個 pooler 而不出事,本身就是一場工程任務。團隊分四步走。

第一步是 Disaster Readiness Testing。Load test 把 PGKeeper 推到 3 倍 production peak QPS——測它撐不撐得住,也量 gRPC 多塞的延遲是多少。結果是 sub-millisecond 的開銷,換來 load management 跟 observability 的能力,划算。Synthetic load 是另一條——團隊把過去害過自己的 incident 流量重新生成出來,一場場放給 PGKeeper 試,調參數調到每一場都過得去。

第二步是分級 rollout,把 blast radius 鎖小。每組資料庫先按關鍵性分級,從風險最低的開始。先在每個 cluster 上一台 replica(DBProxy 會 hedging,掛了還有 PgBouncer 接),確認沒事再擴到全部 replica,然後是非關鍵的 primary,最後才動到關鍵 primary。

第三步是自動偵測。團隊在 DBProxy 上做了 sliding window error detector,連續幾個 window 錯誤率破門檻,DBProxy 會自動把流量切回 PgBouncer。這套機制在 rollout 初期觸發過幾次——每次都把 incident 控制在「短暫不可用」這個範圍內,比一個壞掉的 PGKeeper 繼續服務流量好得多。

第四步沒有寫在標題裡,但藏在每一步背後——可以隨時切回去的能力。Rollback path 不是事後想辦法,是 day 0 就設計進來。

▸ TRY THIS
把同樣的 rollout 思維搬到設計改版。新一版 design system 上線時——分級換、跑壓測(找一個邊角案例最多的產品線當白老鼠)、留 rollback path(feature flag 或 component variant 並存)。改版掛掉的成本不是設計失敗,是無法回頭。
06 · 整體觀察
工程決策也是設計決策

讀這篇文章最有意思的地方,是 storage 團隊在每個分岔點都揭露了 tradeoff——擴展性換複雜度、靈活性換 fork 維護、CoDel 換 LIFO 切換邏輯、fair sharing 換實作成本。每一刀切下去都不是免費的。

這跟設計師的日常完全相通。Component variant 數量多寡、design token 顆粒度大小、Auto Layout 巢狀深度、prototype interaction 的複雜度——每個選擇背後都壓著「現在好用 vs 三年後好維護」「彈性 vs 一致性」「酷炫 vs 跨團隊溝通成本」。PGKeeper 的故事把這些 tradeoff 攤在工程語言裡講一遍,但底層思考方式跟你在 Figma 做 design system 決策時是同一套。

讓 Figma 在你電腦上順順跑的那層基礎設施,每天承受的決策密度跟你的設計檔一樣高——只是你看不到。

"
The question is not whether to shed traffic, but how.
— Lihao He, Tim Goh, Mehant Baid · Figma Storage Team
▸ 重點整理 · TL;DR
01 Figma 把 PgBouncer 換成自己用 Go 寫的 PGKeeper——擴展性、load management、連線管理、可延伸性四個方向同時撞牆,原本的方案頂不住了。
02 PGCat fork 路被排除——理由是 observability、feature flag、admission control 要深改 core 的維護成本太高。
03 連線管理三件套:pool warming、token bucket 節流建立 / 拆除、bounded exhaust + auto rollback + context cancellation 把連線乾淨還回 pool。
04 Admission control 兩層:priority semaphore + CoDel + adaptive LIFO + debt 顧優先級;weighted min-max fair sharing 樹顧同 tier 內部公平。
05 Rollout 分四步:3× peak 壓測 + 合成事故重播 + 分級從低風險到高、自動 sliding window error detector 一旦出事自動 flip 回 PgBouncer。Q4 2025 已擋下 20+ 起原本會出事的事件。
CONTINUE LEARNING
RAR · 設計攻略
Figma 全攻略|從 0 到 design system 完整實戰
PGKeeper 的故事提醒:每個 tradeoff 在 design system 裡都有對應版本——variant 數量、token 顆粒度、Auto Layout 巢狀深度。把 Figma 的底層邏輯摸熟,你做的每個元件設計才會是有意識的選擇,不是隨手堆。
查看課程 →
RIVEN · 訂閱
AI 設計覺醒|每天一篇,跟上 AI × 設計
Figma MCP、Make、Weave、FigJam — 工具更新得太快,與其每天滑社群,不如每天收一篇譯介加觀點。免費訂閱。
免費訂閱 →