OpenAI 的 PostgreSQL 沒有分片,答案要從一個 UPDATE 開始拆
一個 UPDATE 進到 PostgreSQL 裡面,底層到底在做什麼運算?
先問這個,是因為它決定了下面這套架構能不能成立。OpenAI 在 2026 年 1 月 22 日發了篇工程文章,說 ChatGPT 和 API 平台底下的 PostgreSQL 是一台沒分片的 Azure PostgreSQL Flexible Server 主庫,負責全部寫入,後面掛將近 50 台跨區 read replica。原文寫 “a single primary Azure PostgreSQL flexible server instance and nearly 50 read replicas spread over multiple regions globally”,同一段還說過去一年 PostgreSQL 的負載成長超過 10 倍。(作者是 OpenAI 的 Bohan Zhang。原站目前回 403,引用段落核對自 Wayback Machine 存檔。)
轉述這件事的文章大多停在「他們沒分片,猛」。但沒分片是結果不是做法。要看懂它為什麼撐得住,得先回到最上面那個問題。
先釘一個口徑。原文寫 “800 million ChatGPT users”,沒註明是註冊數還是活躍數,也沒給統計窗口。所以下面我不會拿它推算每秒寫入量,推不出來。
改一個欄位,資料庫實際上複製了整列
PostgreSQL 用 MVCC 做併發控制。官方那篇文章對它的描述是逐字這樣寫的:
“when a query updates a tuple or even a single field, the entire row is copied to create a new version. Under heavy write loads, this results in significant write amplification.”
白話版:你 UPDATE 一列裡的某個欄位,資料庫不會跑去那個欄位把值蓋掉。它會把整列複製一份,在新的那份上寫新值,讓舊的那份留在原地。
像一本紙本帳本。要改第 37 行的金額,橡皮擦的做法快,但有人正在讀那一行就會讀到改到一半的東西。MVCC 是另外抄一整行寫在後面,各標一個版本號,誰都不會看到半成品。代價是帳本變厚。
變厚的那部分叫 dead tuple。官方文章接著寫:
“It also increases read amplification, since queries must scan through multiple tuple versions (dead tuples) to retrieve the latest one.”
所以一次寫入的成本不只是「寫一份新資料」。同一段列出三個併發症:table and index bloat、index maintenance overhead,還有複雜的 autovacuum 調校。這三個看起來像平行的清單,其實是一條鏈:舊版本沒被回收,表和索引脹大,查詢要掃更多頁面,autovacuum 得跑更勤,而它自己也吃 CPU 和 IO,跑得越勤,主庫剩給正常查詢的資源越少。
現在把這條鏈的每一步看一遍,問同一句話:這一步能不能丟給後面那 50 台 replica 分擔。
產生新版本要看當前的可見性規則。維護索引要改同一棵 B-tree。回收 dead tuple 要知道還有沒有人在讀舊版本。這三件事全部依賴同一份狀態,而那份狀態只存在於主庫。一步都丟不出去。
MVCC 決定的不是「寫入只能有一台機器」,是「那台機器每次寫入得付多少成本」。這份成本攤不掉,replica 加到 50 台也分不走一頁。
至於寫入為什麼全壓在同一台,那是另一個問題,跟 MVCC 無關。官方文章給了三條理由,等一下會拆。
WAL 只能單向流出去,而它要餵將近 50 張嘴
讀能水平擴充,是因為讀不需要動到那份狀態。主庫把確定的變更寫成 WAL,replica 接收並重放,就得到一份能服務查詢的副本。這條路單向,WAL 從主庫出去,不會回來。
單向也有單向的麻煩。近 50 台 replica 就是 50 條要餵的管線,餵的人只有一個。官方文章說他們正在測 cascading replication,讓中繼的 replica 幫忙轉發,目標是撐到 “potentially over a hundred replicas”。這功能現在的狀態,原文講得很直接:
“The feature is still in testing; we’ll ensure it’s robust and can fail over safely before rolling it out to production.”
還在測,沒上生產。
連線那層也有硬上限,官方文章寫 “Each instance has a maximum connection limit (5,000 in Azure PostgreSQL)”。解法是 PgBouncer,跑 statement 或 transaction pooling,每個 read replica 各有一組 pod 擋在前面。導入後平均連線建立時間從 50ms 降到 5ms,原文標得很清楚是 “in our benchmarks”,benchmark 環境的平均值,不是生產的 p99。
三個真實案例,其中一個的答案不在 CPU 上
官方文章描述故障怎麼擴散的時候,用的詞是 vicious cycle,惡性循環:
“an upstream issue causes a sudden spike in database load, such as widespread cache misses from a caching-layer failure, a surge of expensive multi-way joins saturating CPU, or a write storm from a new feature launch. As resource utilization climbs, query latency rises and requests begin to time out. Retries then further amplify the load, triggering a vicious cycle with the potential to degrade the entire ChatGPT and API services.”
官方文章對這件事只有這一段。有肉的在另一份來源:同一位作者在 PGConf.dev 2025(2025 年 5 月)的技術簡報,三張投影片是三個案例的流程圖。
第一個案例是快取。投影片逐格的文字是 “Something wrong in Redis” → “Redis Cache Misses” → “More Load in Postgres” → “App Slow Requests or Timeout” → “More Requests due to retries”,最後一格畫了條線繞回第三格,旁邊標著 vicious cycle。官方文章在這裡只寫 “a caching layer”,沒指名產品,Redis 這兩個字只出現在簡報上。止血手段掛兩個位置:主庫和 proxy 做 rate limit,應用層過載時直接丟請求。
第二個案例是那個 12 張表的 JOIN。官方文章逐字寫 “we once identified an extremely costly query that joined 12 tables, where spikes in this query were responsible for past high-severity SEVs.”,簡報第 11 頁同樣提到。對策裡有一條要單獨拿出來講:允許針對特定 query digest 封鎖或限流,他們的 rate limit 細到「某一類查詢」這個粒度。
第三個案例是寫入尖峰。三個裡面,這個要慢慢看。
一次寫入尖峰的實際推進(PGConf.dev 2025 簡報第 20–21 頁)
寫入量突然暴增
投影片原文:”A huge spike in writes”。
主庫 CPU 衝到 90%+
“High CPU usage in primary (90%+)”。直覺到這一步都還成立,寫入暴增,CPU 跟著上去。
Read replica 落後超過 10 分鐘
“Increased Read Replica Lags (> 10 mins)”。查詢變慢、replica 上的資料過期、新寫入進不來,ChatGPT 服務受影響。
對寫入限流,CPU 回到正常,lag 繼續漲
“After rate limiting write queries, primary CPU usage returned to normal, but read replica lag continued to increase”。
最後那格才是重點。限流是對的動作,CPU 也真的回去了,但要救的那個指標動都不動。
簡報列出來的三個真因,沒有一個是 CPU:主庫的網路頻寬飽和(”Network bandwidth saturation in primary”)、部分 replica 的磁碟 IOPS 飽和(”Disk IOPS saturation in some replicas”),以及一個 WAL sender 的 bug,在 async_standbys_wait_for_sync_replication 開啟時會讓它瘋狂空轉而不是把 WAL 串流出去(”leads to excessive spinning instead of streaming WAL to replicas”)。處理方式是加大 instance 規格與網路上限、調網路設定,以及修掉那個 bug。
CPU 是最好量的指標,所以查到它就很容易停下來。這個案例裡 CPU 從頭到尾都沒說謊,寫入尖峰時它真的 90%+,限流後它真的回到正常。它只是不在那條卡住的路上。而第三個真因更陰:WAL sender 那時候是有吃 CPU 的,它看起來很忙,但它忙的內容不是「把 WAL 送出去」。
下次遇到 replica lag,先看主庫 CPU 這條路可以直接跳過。CPU 正常不代表 WAL 有流出去,中間還隔著網路頻寬、replica 端的磁碟,以及 WAL sender 有沒有真的在做事。
順著那個參數名字往回挖,挖到 2022 年
async_standbys_wait_for_sync_replication 這個名字,在 PostgreSQL 官方文件的 replication 參數頁(runtime-config-replication.html)上找不到。
它的出處是 2022 年 pgsql-hackers 郵件列表的提案討論串〈Allow async standbys wait for sync replication〉,提案者 Bharath Rupireddy,時間跨 2021 年 12 月到 2022 年 3 月。串上有人貼出測試輸出,show async_standbys_wait_for_sync_replication; 回傳 off,可見它確實存在於某個 build。
Nathan Bossart 在串上提的技術疑慮,逐字是:
“I don’t think it’s a good idea to block sending any WAL like this… I believe this patch will cause the server to avoid sending any WAL until the synchronous LSN advances.”
他擔心的是:可能有一大段 WAL 早就同步複寫完可以送了,卡住的只是最後幾個 byte,但這 patch 會讓 server 在同步 LSN 前進之前一個 byte 都不送。
這串查得到的最後一封信是 2022 年 3 月 17 日,沒找到說它被接受或被拒絕的回覆。合理推論是它沒進核心,但這是推論,不是串裡的明文結論。
還有一點免得被我帶偏。這串查得到的參與者網域是 gmail、amazon、NTT 這些,沒出現 Azure 或 Microsoft 字樣。那個參數在 Azure PostgreSQL Flexible Server 上確實開得起來,但「這是雲商自己帶的 patch」我沒有證據,不寫成結論。能說的只有一件:一個開得起來、又不在官方文件清單裡的參數,讓他們吃了一次事故。
同一件事,兩份來源講得不一樣
官方部落格是 2026 年 1 月的成功故事,PGConf.dev 簡報是 2025 年 5 月講給同業聽的,坦率程度差很多。有幾處要對著看:
| 項目 | 官方部落格(2026-01-22) | PGConf.dev 2025 簡報(2025-05) |
|---|---|---|
| SEV-0 的統計窗口 | “over the past 12 months, we’ve had only one SEV-0 PostgreSQL incident” | “Only one SEV-0 incident involving PostgreSQL in the past 9 months since I joined OpenAI” |
| 主庫掛掉時的嚴重度 | “it’s no longer a SEV0 since reads remain available”,沒說降到哪一級 | 第 12 頁明確寫 SEV2 |
| 快取層是什麼 | 只寫 “a caching layer” | 第 18 頁寫 “Something wrong in Redis” |
| PostgreSQL 的不足 | 沒有這段 | 有完整一段 |
兩個窗口不衝突,但不能混用,起算點不一樣。
那次 SEV-0 官方文章寫得很具體:”it occurred during the viral launch of ChatGPT ImageGen, when write traffic suddenly surged by more than 10x as over 100 million new users signed up within a week.”。
延遲的說法是 “low double-digit millisecond p99 client-side latency”,client-side 這個限定要留著。
什麼時候該抄他們,什麼時候不該
我的立場:如果你的負載是讀多寫少,不要因為「將來會長大」就先去分片。
原文的理由有三條:要改數百個 application endpoint、工期可能數月甚至數年、他們的負載本來就讀多寫少且優化過後餘裕還夠。最貴的是第一條,一次性、不好分批,改完之後每個查詢都要開始想 shard key。
兩種情況下這段對你不成立。寫入本來就有天然的分割鍵,每個租戶各一份、彼此不用 JOIN,那分片成本沒他們說的那麼高,早做早省事。或者寫入量已經逼近單機能產生 WAL 的速度,這時應用層再怎麼優化都沒用。
OpenAI 自己就是照這條界線在走:可分片、寫入密集的負載移去 Azure Cosmos DB,現在也不准在 PostgreSQL 加新表,”New workloads default to the sharded systems.”。原文對未來的措辭是 “not a near-term priority”,近期不是優先項,不是永遠不做。
schema 那層他們也鎖得很緊:只允許不觸發 full table rewrite 的輕量變更、5 秒硬 timeout、索引可以 concurrently 建、只能動既有的表。只要有長查詢壓著那張表,變更就會失敗,所以簡報第 15 頁附了段找兇手的 SQL:
1 | SELECT * FROM pg_stat_activity |
backfill 有時候要跑超過一週。
還沒解完的部分
簡報最後有一整段官方部落格沒有的內容,講 PostgreSQL 哪裡還可以更好。
pg_stat_statement 只給每個 query digest 的平均延遲,拿不到百分位數,原句是 “We cannot get query percentiles (like p95, p99) directly.”。一個每秒跑幾百萬次查詢的系統,query 層級只能看平均值。索引也一樣:PostgreSQL 不支援停用索引,只能直接刪,想先關掉觀察一陣子再決定做不到;刪了要回來就得重建,大索引重建很花時間。
最後那個他們自己也沒答案。有查詢停在 active 狀態長達 2 小時 23 分鐘,wait_event 是 ClientRead,整段時間都在等 client 端的下一個指令。因為狀態是 active 而不是 idle in transaction,idle_in_transaction_session_timeout 殺不掉它。簡報上留的是三個問句:
“Is it a bug in Postgres? Should the state be idle_in_transaction? If not, how to kill it automatically?”
那是簡報第 25 頁,後面三頁沒有再回來回答。
整條線倒過來看,會看到一件跟直覺相反的事:前面那三條不分片的理由,沒有一條是「PostgreSQL 做不到」。三條全部在講划不划算,不在講能不能。
那場簡報最後一頁給開發者的建議只有一句:”If you are a developer, or building a startup, start with Postgres (for read-heavy workloads)”。重點在括號裡。
而讀多寫少是不是你的處境,監控面板不會直接告訴你。把寫入路徑一條一條攤開來問:這次寫入需不需要先看到別的寫入的結果。答「不需要」的那些遲早要搬走,OpenAI 搬去了 Cosmos DB,還順手加了一條規則不准在 PostgreSQL 開新表。答「需要」的那些留下來,它們每一次都會複製一整列、留下一個等人來收的舊版本,而那份成本,你加多少台 replica 都分不掉。
來源
- OpenAI 官方工程文章〈Scaling PostgreSQL〉,2026 年 1 月 22 日,作者 Bohan Zhang:https://openai.com/index/scaling-postgresql/(原站目前回 403,本文引用段落核對自 Wayback Machine 存檔)
- 同作者於 PGConf.dev 2025(2025 年 5 月)的技術簡報,文中標註頁碼者出處為此
- pgsql-hackers 討論串〈Allow async standbys wait for sync replication〉:https://www.postgresql.org/message-id/CALj2ACU0+5aS9NXvm6o_1g8YZ9dq6bYHsUPteZ_YjuwNd4pDOw@mail.gmail.com
- PostgreSQL 官方文件 replication 參數頁:https://www.postgresql.org/docs/current/runtime-config-replication.html










