OpenAIは2026年1月22日、エンジニアリングブログ「Scaling PostgreSQL to power 800 million ChatGPT users」(日本語版の題は「PostgreSQL を拡張して8億人の ChatGPT ユーザーに対応」、著者はMember of the Technical StaffのBohan Zhang氏)で、ChatGPTとAPIを支えるPostgreSQLの構成を公開しました。書き込みを受けるプライマリは1台だけで、シャーディングはしていません。本記事では公開された数値と9つの対策を整理し、そのうち設定で再現できるものをPostgreSQL 18.6で動かした結果と、Azureの公開ドキュメント上の上限との差を示します。
まとめ:OpenAIのPostgreSQL構成の要点
- 構成はAzure Database for PostgreSQL フレキシブルサーバーのプライマリ1台と、複数リージョンに置いた約50台のリードレプリカ。毎秒数百万クエリを処理し、クライアント側p99レイテンシは2桁ミリ秒の低い水準(原文 low double-digit millisecond)、可用性はファイブナイン
- シャーディングしない理由は、既存アプリの数百のエンドポイントを改修する必要があり数か月から数年かかるため。読み取り中心の負荷なので、現構成で伸びしろがあると判断している
- 書き込みの多いワークロードはAzure Cosmos DBなどのシャード型システムへ移し、PostgreSQLへの新規テーブル追加は禁止している。「全部PostgreSQLで済ませた」事例ではない。2026年9月の続報では、その受け皿の社内基盤Habitatが毎秒7,000万件超のリクエストを処理していると公表された
- 対策はプライマリの負荷削減、ORM生成クエリの見直し、PgBouncer、キャッシュロック、多層のレート制限、5秒のスキーマ変更タイムアウトなど。設定で再現できる部分はPostgreSQL 18.6で効き方を確認できる
- 約50台のレプリカは、Azureの公開ドキュメントにある上限(プライマリ直下5台、カスケード込みで30台)を超える。同じ構成をそのまま組めるとは考えないほうがよい
以下、公開された数値、対策の中身、再現手順、採用判断の順に説明します。
OpenAIが公開したPostgreSQL構成の数値
以下は2026年1月22日公開のブログに記載された数値です。「直近1年」「直近12か月」は同ブログの公開時点を基準とし、p99はクライアント側で測定された値です。
| 項目 | 公開値 |
|---|---|
| 書き込みを受けるプライマリ | 1台(Azure PostgreSQL フレキシブルサーバー) |
| リードレプリカ | 約50台(複数リージョンに分散) |
| 処理量 | 毎秒数百万クエリ |
| 負荷の伸び | 直近1年で10倍超 |
| クライアント側p99レイテンシ | 2桁ミリ秒の低い水準 |
| 可用性 | ファイブナイン(99.999%) |
| 直近12か月のSEV-0 | 1件 |
| レプリケーション遅延 | ほぼゼロ |
| 1インスタンスの最大接続数 | 5,000(Azure PostgreSQLの上限) |
| PgBouncer導入後の平均接続時間 | 50ミリ秒から5ミリ秒 |
| スキーマ変更のタイムアウト | 5秒 |
唯一のSEV-0は、ChatGPTの画像生成(ImageGen)の公開時に起きました。1週間で1億人超が新規登録し、書き込みが10倍超に急増したためです。読み取りを広げる仕組みは整っていても、書き込みの急増には単一プライマリが弱いことを、OpenAI自身の障害記録が示しています。
シャーディングしない理由と書き込みの逃がし先
OpenAIはPostgreSQLを分割せず、すべての書き込みを1台のプライマリで受けています。理由として挙げているのは、既存のアプリケーションをシャーディングに対応させるには数百のエンドポイントを変える必要があり、数か月から数年かかることです。負荷の大半は読み取りで、最適化を重ねた結果、現構成でも今後の伸びに耐える余力があると判断しています。将来のシャーディングは否定していませんが、近い優先事項ではないとしています。
そのかわり、書き込みは積極的にPostgreSQLの外へ出しています。水平分割できて書き込みが多いワークロードはAzure Cosmos DBなどのシャード型システムへ移行済みで、分割しにくい書き込みワークロードも移行を続けています。さらに2026年1月のブログ時点では、PostgreSQLに新しいテーブルを追加できず、新機能が使うテーブルは最初からシャード型システムに置く決まりでした。単一プライマリの構成は、この「書き込みを増やさない」運用とセットで成り立っています。シャーディングの設計そのものはシャーディングの仕組みとシャードキー設計で扱っています。
PostgreSQLが書き込みに弱い理由:MVCCの行コピー
ブログは書き込みが課題になる原因を、PostgreSQLのMVCC(多版型同時実行制御)の実装に求めています。1つの列だけを更新しても行全体がコピーされて新しい版が作られるため、書き込みが増えるほど書き込み量が膨らみます。読み取り側も、古い版(デッドタプル)を読み飛ばして最新版を探すぶん負荷が増えます。テーブルやインデックスの肥大化、インデックス保守の負荷、autovacuumの調整の難しさも同じ根から来ています。著者はカーネギーメロン大学のAndy Pavlo教授と、この問題を扱った記事「The Part of PostgreSQL We Hate the Most」(2023年4月)を書いています。版の回収の仕組みはMVCCの仕組みと過去版の回収で詳しく説明しています。
書き込みの受け皿Habitat:2026年9月の続報
OpenAIは2026年9月11日、書き込みの逃がし先側を説明する記事「Rapidly scaling online storage to serve over 1 billion ChatGPT users」(2回連載の1回目)を公開しました。Habitatは背後にAzure Cosmos DBを持つ社内のオンラインストレージ基盤で、毎秒7,000万件超のリクエスト、週10億人超が使う製品、約40の地域、500PB超のデータを扱っています。
同記事によれば、Habitatへ移る前はオンラインデータの大半がPostgreSQLにありました。チームと製品が増えるとクエリとスキーマ変更のレビューが追いつかなくなり、ホットパスに入った高コストな新クエリ1本がデータベースを落とす障害が頻発したといいます。そこでHabitatは任意のSQLを書かせず、TAOに倣ったオブジェクトとエッジのNoSQL APIだけを公開し、複雑な問い合わせはCDCで流したRocksetの別インスタンスで処理させています。1月の記事が挙げた「多表結合を避ける」「新規テーブルを足さない」を、API設計の段階で強制した形です。なお続報はPostgreSQL側のレプリカ台数などに触れておらず、1月時点の構成から変わったかは公表されていません。
過負荷障害の典型パターン:上流の異常からリトライの悪循環へ
OpenAIが経験したPostgreSQL過負荷によるSEVは、多くが同じ経過をたどっています。上流で何かが起きてデータベースの負荷が急に跳ね、使用率が上がってクエリが遅くなり、リクエストがタイムアウトする。そこへリトライが重なって負荷がさらに増え、ChatGPTとAPI全体が劣化する、という流れです。
引き金として挙げられているのは3つです。キャッシュ層の障害による大量のキャッシュミス、CPUを使い切る多表結合クエリの急増、新機能の公開に伴う書き込みの嵐。次の章で扱う対策は、どれもこの3つの引き金を潰すか、引き金が引かれても悪循環に入らないようにするものです。
プライマリを守る対策:書き込み・クエリ・スキーマ変更
プライマリの負荷削減:読み取りの移送と書き込み量の抑制
書き込みの急増に耐えられるよう、プライマリの負荷は読み書きとも最小限に抑えています。読み取りは可能な限りレプリカへ回し、書き込みトランザクションの一部としてプライマリに残す読み取りは遅いクエリにならないよう見張ります。書き込みについては、重複書き込みを起こしていたアプリのバグを直し、適する箇所には遅延書き込み(lazy writes)を入れて急増をならしました。テーブルの列を後から埋めるバックフィルには厳しいレート制限をかけ、1週間以上かかることがあっても本番への影響を避けています。
クエリ最適化:12テーブル結合とORMが生成するSQL
過去の重大SEVの原因の1つは、12個のテーブルを結合するクエリの急増でした。OpenAIの結論は、多表結合はできる限り避け、必要なら分解して結合ロジックをアプリケーション層へ移すことです。こうしたクエリの多くはORMが生成していたため、ORMが出すSQLを確認する工程を入れています。もう1つ挙げられているのが、トランザクションを開いたまま放置される接続です。idle_in_transaction_session_timeout を設定してautovacuumを妨げないようにすることが不可欠だとしています。この設定の効き方は後の実測の章で示します。
スキーマ変更の制限:5秒タイムアウトと全表書き換えの禁止
列の型変更のような小さな変更でも、テーブル全体の書き換えが起きることがあります。そのためOpenAIが許可しているのは、全表書き換えを起こさない一部の列の追加・削除といった軽い変更だけで、スキーマ変更には5秒の厳格なタイムアウトをかけています。インデックスの作成と削除は CONCURRENTLY 付きなら許可。変更できるのは既存テーブルに限られ、新しいテーブルが要る機能はCosmos DBなどのシャード型システムに置きます。
読み取りと接続を捌く対策:レプリカ・接続プール・キャッシュ
リードレプリカの拡張:WAL送出の負荷とカスケードレプリケーション
プライマリはすべてのリードレプリカへWAL(先行書き込みログ)を送ります。レプリカが増えるほど送り先が増え、ネットワーク帯域とCPUの負荷が上がり、レプリカ遅延が大きく不安定になります。現在は非常に大きなインスタンスと広い帯域で約50台を支えていますが、際限なく増やせるわけではありません。
そこでOpenAIはAzureのPostgreSQLチームと、中間のレプリカが下流のレプリカへWALを中継するカスケードレプリケーションに取り組んでいます。実現すれば100台を超えるレプリカも視野に入るとする一方、フェイルオーバーの管理が複雑になるため、ブログ公開時点ではテスト中で、安全に切り替えられることを確かめてから本番に入れるとしています。WALの仕組みはWALの仕組みとfsync設計、レプリカの基本はリードレプリカの作成手順と読み書き分離の実装を参照してください。
接続プール:PgBouncerによる平均接続時間の短縮
ブログによればAzure PostgreSQLの1インスタンスの最大接続数は5,000で、OpenAIは接続の嵐で全接続を使い切る障害を経験しています。対策としてPgBouncerをプロキシ層に置き、statementプーリングかtransactionプーリングのモードで接続を使い回しています。ベンチマークでは平均接続時間が50ミリ秒から5ミリ秒に縮みました。
配置にも工夫があります。リージョンをまたぐ通信は高くつくため、プロキシ、クライアント、レプリカを同じリージョンに置いています。リードレプリカごとに、複数のPgBouncer Podを動かすKubernetesのDeploymentを1つ用意し、複数のDeploymentを同じKubernetes Serviceの背後に置いてPod間で負荷分散しています。アイドルタイムアウトなどの設定を誤ると接続枯渇を招くとも書かれています。
キャッシュロック:同一キーへのDB読み取りの集約
読み取りの大半はキャッシュ層が返しています。問題はヒット率が急に落ちたときで、ミスした要求がまとめてPostgreSQLへ流れ込みCPUを使い切ります。OpenAIはキャッシュロック(とリース)の仕組みを入れ、同じキーでミスした要求のうちロックを取った1件だけがPostgreSQLから読み、残りはキャッシュが埋まるのを待つようにしました。動作の最小例は後の章に載せています。
障害を広げない対策:高可用性・ワークロード分離・レート制限
単一障害点の緩和:読み取りの分離とホットスタンバイ付きHA構成
レプリカは1台落ちても他に回せますが、書き込みを受ける1台が落ちればサービス全体に響きます。OpenAIはまず、重要なリクエストの多くが読み取りだけで完結する点に注目し、それらをプライマリからレプリカへ移しました。プライマリが落ちても書き込みが失敗するだけで読み取りは続くため、SEV-0にはならなくなります。
プライマリ自体はホットスタンバイ付きの高可用性モードで動かし、障害時や保守時にはスタンバイを昇格させます。高負荷下でもフェイルオーバーを安全に行えるよう、Azure PostgreSQLチームが大きく作業したと書かれています。レプリカ側は各リージョンに複数台を余裕を持って置き、1台の障害が地域全体の停止にならないようにしています。
ワークロード分離:優先度別の専用インスタンスへの振り分け
新機能の公開時に非効率なクエリがCPUを食い、他の重要機能まで遅くなる「うるさい隣人」の問題に対しては、リクエストを高優先度と低優先度に分け、別々のインスタンスへ振り分けています。低優先度側が重くなっても高優先度側は影響を受けません。同じ考え方を製品やサービスの単位にも適用し、ある製品の負荷が別の製品の信頼性を下げないようにしています。
レート制限:アプリ・接続プーラー・プロキシ・クエリの4層
特定エンドポイントへの急な集中、高コストなクエリの急増、リトライの嵐は、CPU、I/O、接続を一気に食い尽くします。OpenAIはアプリケーション、接続プーラー、プロキシ、クエリの4層でレート制限をかけています。リトライ間隔を短くしすぎるとそれ自体がリトライの嵐を生むため、避けるべきだとしています。ORM層にもレート制限の機能を足し、必要なら特定のクエリダイジェスト(正規化したクエリの型)を丸ごと遮断できるようにしました。高コストなクエリが急増したときは、これで狙った負荷だけを落として素早く回復させます。
PostgreSQL 18で確かめる:OpenAIが挙げた設定の効き方
ブログの対策のうち、PostgreSQLの設定とアプリのロジックで再現できる4つを、手元のPostgreSQL 18.6(Homebrew版)とPython 3.9.6で動かしました。テーブルは20万行の conv を使っています。
全表書き換えになるALTER TABLEの見分け方
テーブルの実体ファイル番号(filenode)は、全表書き換えが起きると新しい値に変わります。各ALTER TABLEの直後に SELECT pg_relation_filenode('conv'); を実行し、変更前の値と比較します。以下のコメントにある番号は検証環境での例で、同じ番号になるとは限りません。
CREATE TABLE conv (id bigserial PRIMARY KEY, user_id int NOT NULL, title text);
INSERT INTO conv (user_id, title)
SELECT g % 1000, 'title ' || g FROM generate_series(1, 200000) g;
SELECT pg_relation_filenode('conv'); -- 16385
ALTER TABLE conv ADD COLUMN archived boolean DEFAULT false; -- 16385(書き換えなし)
ALTER TABLE conv ADD COLUMN created_at timestamptz
DEFAULT clock_timestamp(); -- 16397(書き換え)
ALTER TABLE conv ALTER COLUMN user_id TYPE bigint; -- 16404(書き換え)
ALTER TABLE conv DROP COLUMN archived; -- 変化なし
定数の既定値を持つ列の追加と列の削除はファイルが変わらず、VOLATILE関数を使う既定値(clock_timestamp())の列追加と、int から bigint への型変更は書き換えになりました。OpenAIが「一部の列の追加・削除だけを許す」と書いている線引きは、この差に当たります。
lock_timeoutで5秒打ち切りにするスキーマ変更
この例のALTER TABLE ADD COLUMNは、テーブルに最も強いACCESS EXCLUSIVEロックを要求します。長いトランザクションが読み取り中だと、ALTERは待ち続け、その後ろに並んだ通常のクエリまで止まります。打ち切り時間を決めておけば、この連鎖を5秒で断てます。別セッションで conv を8秒間読み続けている間に、次を実行しました。
SET lock_timeout = '5s';
ALTER TABLE conv ADD COLUMN note text;
-- ERROR: canceling statement due to lock timeout(開始から5秒で失敗)
ブログは5秒のタイムアウトの実装方法までは書いていません。上は lock_timeout での再現例で、ロック獲得後の実行時間も制限したいなら statement_timeout を併用します。失敗したALTERは、間隔を空けて再試行する前提で運用します。
idle_in_transaction_session_timeoutが守るautovacuum
トランザクションを開いたまま放置された接続は、不要行の回収を止めることがあります。別セッションで BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT count(*) FROM conv; を実行し、コミットせずに待機する状態(pg_stat_activity の state が idle in transaction)を作り、その間に5万行を更新してVACUUMの出力を見ました。
UPDATE conv SET title = title || 'y' WHERE id <= 50000;
VACUUM (VERBOSE) conv;
-- 放置側が REPEATABLE READ のとき
-- tuples: 0 removed, ..., 50000 are dead but not yet removable
-- 放置側が READ COMMITTED で読み取りだけのとき
-- tuples: 50000 removed, ..., 0 are dead but not yet removable
回収を止めたのは、スナップショットを握ったまま放置されたREPEATABLE READのトランザクションでした。READ COMMITTEDで読み取りしかしていない場合は文ごとにスナップショットを手放すので、回収は進みます。書き込みを済ませたまま放置した場合はトランザクションIDが残るため、分離レベルにかかわらず回収の基準を押しとどめます。どのケースかをアプリ側で管理しきるのは難しいので、放置そのものを切る設定が効きます。
放置された接続を自動で切るのが idle_in_transaction_session_timeout です。2秒に設定して BEGIN のまま3秒待つと、サーバー側から切断されます。
SET idle_in_transaction_session_timeout = '2s';
BEGIN;
SELECT 1;
-- 3秒待つ
SELECT 2;
-- FATAL: terminating connection due to idle-in-transaction timeout
既定値は0(無効)です。ORMやコネクションプールの使い方次第で放置トランザクションは簡単に生まれるので、OpenAIの規模でなくても設定しておく価値があります。
キャッシュロックの模擬実装:同一キーへの並行要求の集約
キャッシュロックの効果は、同じキーへの同時ミスを並べると分かります。DBの応答に50ミリ秒かかる想定で、100スレッドを順に開始して同じキーへアクセスさせる模擬です。
import threading, time
db_queries = 0
db_lock = threading.Lock()
def query_db(key):
global db_queries
with db_lock:
db_queries += 1
time.sleep(0.05)
return f"value-of-{key}"
cache = {}
inflight = {}
inflight_lock = threading.Lock()
def get_naive(key):
if key in cache:
return cache[key]
v = query_db(key)
cache[key] = v
return v
def get_with_lease(key):
if key in cache:
return cache[key]
with inflight_lock:
if key in cache:
return cache[key]
ev = inflight.get(key)
leader = ev is None
if leader:
ev = inflight[key] = threading.Event()
if leader:
cache[key] = query_db(key)
ev.set()
with inflight_lock:
del inflight[key]
else:
ev.wait(timeout=1.0)
return cache[key] if key in cache else query_db(key)
for name, fn in [("naive", get_naive), ("lease", get_with_lease)]:
cache.clear(); db_queries = 0
ts = [threading.Thread(target=fn, args=("user:42",)) for _ in range(100)]
for t in ts: t.start()
for t in ts: t.join()
print(f"{name}: 100 requests -> DB queries = {db_queries}")
naive: 100 requests -> DB queries = 100
lease: 100 requests -> DB queries = 1
これは1プロセス内の模擬です。実際のサービスでは複数サーバーが同じキャッシュを共有するので、リースはRedisなどキャッシュ側に置き、取得役が落ちたときのためにリースに有効期限を付けます。この模擬コードの1秒待機はリースの有効期限ではありません。DBが遅いと待機者が一斉にDBへ流れ、取得役の例外時にはinflightも残ります。本番では取得役の後始末と、期限切れ後の取得権の再調停が必要です。
Azureで同じ構成を組むときの上限:公開ドキュメントとの差
OpenAIの構成は公開サービスの上で動いていますが、台数は一般向けの上限を超えています。Microsoft Learnの読み取りレプリカの解説と制限の一覧(いずれも2026年7月更新)と並べると次のとおりです。
| 項目 | Azure公開ドキュメント | OpenAI |
|---|---|---|
| プライマリ直下のレプリカ | 最大5台 | 約50台(構成の内訳は非公開) |
| カスケードを含む合計 | 最大30台(2階層・各5台) | 100台超を目標にテスト中 |
| 最大接続数 | 5,000(大型インスタンスの既定値) | 5,000 |
カスケードの読み取りレプリカには、中間レプリカがPostgreSQL 14以上であること、仮想エンドポイントが使えないこと、カスケード先を持つ中間レプリカはプライマリへ昇格できないこと、といった制約もあります。ブログの「100台超」はAzureチームとの共同作業の話で、一般向けの機能として提供される台数ではありません。最大接続数5,000は大きいインスタンスの max_connections 既定値で、予約分15を引いた4,985がユーザー接続に使えます。既定値より上げることもできますが、Microsoftは推奨していません。Azure側の構成とHA・料金はAzure Database for PostgreSQLのフレキシブルサーバーの構成と料金にまとめています。
単一プライマリ戦略を見直すべき条件
この事例は「Just use Postgres」の裏付けとして紹介されがちですが、OpenAIがやっているのは「読み取り中心の既存データはPostgreSQLに残し、書き込みが多いものは外へ出す」という使い分けです。次の条件がある場合は、ピーク時の書き込み負荷と必要な整合性を検証し、単一プライマリで要件を満たせない場合に分散や別システムへの移行を選びます。
- 書き込みが負荷の中心にある(イベントログ、計測値、チャットのメッセージ本体のように、増え続ける追記型データ)。OpenAIでも書き込みの急増が唯一のSEV-0になった
- 新しいテーブルを頻繁に足す必要がある。OpenAIは新規テーブルを禁止して成り立たせている
- 書いた直後の読み取りに最新値が必須で、レプリカの非同期遅延を許せない処理が多い
- キャッシュ層、接続プール、多層のレート制限を運用する体制がない。単一プライマリが持つのは、これらが負荷を吸収しているから
逆に、読み取りが大半で、書き込みの急増をアプリ側で平準化でき、上の運用を回せるなら、シャーディングの前にここまで伸ばせるという実例になります。分割を始める前に、どの書き込みを外へ出せるかを洗い出すのが先です。レプリカの同期・非同期の選び方はデータベースレプリケーションの同期・非同期とレプリカ遅延の設計で扱っています。
よくある質問
OpenAIはPostgreSQLをシャーディングしていますか?
2026年1月のブログ時点ではしていません。書き込みはすべて1台のプライマリが受けています。ただし書き込みの多いワークロードはAzure Cosmos DBなどのシャード型システムへ移しており、将来のPostgreSQLのシャーディングも否定はしていません。
OpenAIのPostgreSQLは何台で動いていますか?
プライマリ1台(ホットスタンバイ付き)と、複数リージョンに分散した約50台のリードレプリカです。これで毎秒数百万クエリを処理しています。
PgBouncerだけで読み取りと書き込みを振り分けられますか?
できません。PgBouncerは接続を使い回すプーラーで、SQLを見て読み書きを振り分ける機能は持ちません。OpenAIの構成も、レプリカごとにPgBouncerのPod群を置く形です。振り分けはアプリケーション側か、振り分け機能を持つ別のプロキシで行います。
OpenAIの構成で起きた最大の障害は何ですか?
2026年1月22日のブログが振り返った過去12か月で唯一のPostgreSQLのSEV-0は、ChatGPTの画像生成の公開時に1週間で1億人超が登録し、書き込みが10倍超に急増したときのものです。
OpenAIのブログに日本語版はありますか?
あります。題は「PostgreSQL を拡張して8億人の ChatGPT ユーザーに対応」で、英語版と同じURLの日本語ロケール版(/ja-JP/)として公開されています。