■ 1. 背景と概要
- PostgreSQLはChatGPTやOpenAIのAPIなどの中核製品を支える最も重要な基盤データシステムの一つである
- ユーザーベースが急速に拡大するにつれてデータベースへの要求も指数関数的に増加している
- この1年でPostgreSQLの負荷は10倍以上増加しており現在も急速に増え続けている
- PostgreSQLはこれまで考えられていたよりもはるかに大きな読み取り中心のワークロードを確実にサポートできるように拡張できる
- 単一のプライマリAzure PostgreSQLフレキシブルサーバーインスタンスと世界中の複数のリージョンに分散された約50のリードレプリカを使用して大規模なグローバルトラフィックをサポートしている
- 厳密な最適化と確固たるエンジニアリングを通じて8億人のユーザー向けに毎秒数百万件のクエリをサポートするに至った
■ 2. 当初の設計の欠陥と課題
- ChatGPTの公開後トラフィックはこれまでにない速度で増加した
- アプリケーション層とPostgreSQLデータベース層の両方で広範な最適化を迅速に実装した
- インスタンスサイズを増やしてスケールアップしさらにリードレプリカを追加してスケールアウトした
- 単一のプライマリアーキテクチャがOpenAIの規模の要求を満たすことができるというのは驚くべきことである
- 実際にこれを実現するのは簡単ではない
- Postgresの過負荷によるSEVを複数確認しておりその多くは同じパターンに従っている:
- 上流の問題がデータベース負荷の急激な増加を引き起こす
- キャッシュ層の障害による広範なキャッシュミス
- CPUを飽和させる高コストなマルチウェイ結合の急増
- 新機能リリースに伴う書き込み集中
- リソース使用率が上昇するとクエリの遅延が増加しリクエストがタイムアウトし始める
- 再試行によって負荷がさらに増大し悪循環を引き起こす
- ChatGPTとAPIサービス全体の性能を低下させる可能性がある
- PostgreSQLは読み込みが多いワークロードに対してはスケーラビリティが高い
- 書き込みトラフィックが多い時期には依然として課題がある:
- PostgreSQLのマルチバージョン同時実行制御(MVCC)の実装によるもので書き込みが多いワークロードに対しては効率が低下する
- クエリがタプルや単一のフィールドを更新する際には新しいバージョンを作成するために行全体がコピーされる
- 書き込み負荷が高い状況下では深刻な書き込み増幅を引き起こす
- クエリが最新のデータを取得するために複数のタプルバージョンをスキャンしなければならないため読み取り増幅も増大する
- MVCCはさらにテーブルやインデックスの肥大化インデックスメンテナンスのオーバーヘッド増加複雑なautovacuumのチューニングといった課題をもたらす
■ 3. 全体的な戦略
- 書き込み負荷を軽減するためシャード化可能な書き込み集約型ワークロードをAzure Cosmos DBなどのシャード化システムへ移行し続けている
- アプリケーションロジックを最適化し不要な書き込みを最小限に抑えている
- 現行のPostgreSQLデプロイメントへの新規テーブル追加は許可していない
- 新しいワークロードはデフォルトでシャードシステムに設定される
- インフラが進化してきたにもかかわらずPostgreSQLはシャーディングされず単一のプライマリインスタンスがすべての書き込みを処理している
- 主な理由:
- 既存のアプリケーションのワークロードをシャーディングすることは非常に複雑で時間がかかる
- 数百のアプリケーションエンドポイントの変更が必要となる
- 数か月場合によっては数年かかる可能性がある
- ワークロードは主に読み取り中心であり広範な最適化を実装している
- 現在のアーキテクチャでも継続的なトラフィック増加を支えるのに十分な余力がある
- 将来的にPostgreSQLのシャーディングを行う可能性を排除しているわけではない
- 現在および今後の成長に対して十分な余裕があるため短期的な優先事項ではない
■ 4. プライマリの負荷軽減
- 課題:
- ライターが1つのみのため単一プライマリ構成では書き込みを拡張できない
- 書き込みの急増が発生するとプライマリノードが瞬時に過負荷状態となる
- ChatGPTやAPIなどのサービスに影響を及ぼす
- 解決策:
- 書き込みの急増に対応できる十分な容量を確保するためプライマリノードへの負荷を可能な限り最小化する
- 読み取りトラフィックは可能な限りレプリカにオフロードする
- 一部の読み取りクエリは書き込みトランザクションの一部であるためプライマリに残す必要がある
- それらについては効率性を確保し低速のクエリを回避することに注力する
- 書き込みトラフィックに関してはシャード化可能で書き込み負荷の高いワークロードをAzure CosmosDBなどのシャード化されたシステムに移行した
- シャーディングが難しいのに書き込み量が多いワークロードは移行に時間がかかりそのプロセスは現在も進行中である
- 書き込み負荷を減らすためにアプリケーションを積極的に最適化した
- 冗長な書き込みを引き起こしていたアプリケーションのバグを修正した
- 適切な場合には遅延書き込みを導入してトラフィックの急増を緩和した
- テーブルフィールドをバックフィルする際には過剰な書き込み負荷を防ぐために厳格なレート制限を設けている
■ 5. クエリの最適化
- 課題:
- PostgreSQLでコストのかかるクエリをいくつか特定した
- 過去にはこれらのクエリの処理量が急増すると大量のCPUリソースを消費した
- ChatGPTとAPIリクエストの両方の速度が低下していた
- 解決策:
- 多くのテーブルを結合するような高コストなクエリが少数存在するだけでサービス全体のパフォーマンスが著しく低下したり最悪の場合は停止することがある
- PostgreSQLクエリを継続的に最適化し効率を確保して一般的なオンライントランザクション処理(OLTP)のアンチパターンを回避する必要がある
- 12個のテーブルを結合する非常にコストのかかるクエリを特定した
- このクエリの急増が過去に重大度の高いSEVの原因となっていた
- 可能な限り複雑なマルチテーブル結合は避けるべきである
- 結合が必要な場合はクエリを分解することを検討し複雑な結合ロジックは代わりにアプリケーション層へ移すようにした
- これらの問題のあるクエリの多くはオブジェクトリレーショナルマッピング(ORM)フレームワークによって生成される
- 生成されたSQLを注意深く確認し期待通りに動作することを確認することが重要である
- PostgreSQLでは長時間アイドル状態のクエリが見つかることも一般的である
- idle_in_transaction_session_timeoutのようなタイムアウトを設定することはautovacuumのブロックを防ぐために不可欠である
■ 6. 単一障害点の軽減
- 課題:
- 読み取りレプリカがダウンした場合でもトラフィックは他のレプリカにルーティングされる
- 単一のライターに依存するということは単一障害点が存在することを意味する
- そのライターがダウンするとサービス全体が影響を受ける
- 解決策:
- 最も重要なリクエストには読み取りクエリのみが関係する
- プライマリにおける単一障害点を軽減するためにこれらの読み取りをライターからレプリカにオフロードした
- プライマリがダウンした場合でもこれらのリクエストが引き続き処理されるようにした
- 書き込み操作は引き続き失敗するが影響は軽減されている
- 読み取りは利用可能なままのためSEV0ではなくなる
- プライマリの障害を軽減するためにプライマリは高可用性(HA)モードでホットスタンバイと共に運用している
- ホットスタンバイは常に同期されているレプリカでいつでもトラフィックの処理を引き継ぐ準備ができている
- プライマリがダウンしたりメンテナンスのためにオフラインにする必要がある場合はスタンバイをすぐに昇格させてダウンタイムを最小限に抑えることができる
- Azure PostgreSQLチームは非常に高い負荷がかかってもこれらのフェイルオーバーの安全性と信頼性を確保するために多大な努力を払ってきた
- リードレプリカの障害に対処するため各リージョンに十分な余裕を持たせて複数のレプリカを配置している
- 単一のレプリカ障害がリージョン全体の障害につながらないようにしている
■ 7. ワークロードの分離
- 課題:
- PostgreSQLインスタンスにおいて特定のリクエストがリソースを過剰に消費する状況が頻繁に発生する
- 同じインスタンス上で実行されている他のワークロードのパフォーマンスが低下する可能性がある
- 新しい機能の導入によりPostgreSQLのCPUを多く消費する非効率なクエリが発生し他の重要な機能のリクエストが遅くなることがある
- 解決策:
- ノイジーネイバー問題を緩和するためワークロードを専有インスタンスに分離する
- リソース集約的なリクエストの急増が他のトラフィックに影響しないようにする
- リクエストを低優先度と高優先度の層に分けそれぞれを別のインスタンスに振り分ける
- 優先度の低いワークロードがリソースを大量に消費する状態になっても優先度の高いリクエストのパフォーマンスが低下することはない
- 異なる製品やサービス間でも同様の戦略を適用する
- ある製品での活動が別の製品のパフォーマンスや信頼性に影響を与えないようにしている
■ 8. 接続プール
- 課題:
- 各インスタンスには最大接続数制限(Azure PostgreSQLでは5000)がある
- 接続が不足したりアイドル状態の接続が過剰に蓄積したりしやすい状況である
- 過去には接続ストームにより利用可能な接続が全て枯渇する事象が発生した
- 解決策:
- データベース接続をプールするためにプロキシレイヤーとしてPgBouncerを導入した
- ステートメントまたはトランザクションプールモードで実行することで接続を効率的に再利用できアクティブなクライアント接続数を大幅に削減できる
- 接続設定の遅延も短縮されベンチマークでは平均接続時間が50ミリ秒から5ミリ秒に低下した
- リージョン間の接続やリクエストはコストがかかるためプロキシクライアントレプリカを同一リージョンに配置している
- ネットワークオーバーヘッドと接続使用時間を最小限に抑えている
- PgBouncerは慎重に設定する必要がある
- アイドルタイムアウトのような設定は接続の枯渇を防ぐために非常に重要である
- 各リードレプリカは複数のPgBouncerポッドを実行する独自のKubernetesデプロイメントを保持している
- 同じKubernetesサービスの背後で複数のKubernetesデプロイメントを実行しポッド間でトラフィックの負荷を分散する
■ 9. キャッシュ
- 課題:
- キャッシュミスの急増によりPostgreSQLデータベースの読み取りが急増しCPUが飽和状態になりユーザーリクエストが遅くなる可能性がある
- 解決策:
- PostgreSQLへの読み込み負荷を軽減するためにキャッシュ層を使用して大部分の読み込みトラフィックを処理する
- キャッシュヒット率が予期せず低下した場合キャッシュミスが急増することで大量のリクエストがPostgreSQLに直接押し寄せることがある
- データベースの読み取りが急増するとかなりのリソースを消費しサービスの速度が低下する
- キャッシュミス集中時の過負荷を防ぐため特定のキーでミスした読み取りプロセスがPostgreSQLからデータを取得するのは1つだけとなるようキャッシュロック(およびリース)機構を実装している
- 複数のリクエストが同一キャッシュキーでミスした場合1つのリクエストのみがロックを取得しデータ取得とキャッシュ再構築を実行する
- 他のすべてのリクエストはPostgreSQLに一斉にアクセスするのではなくキャッシュが更新されるのを待つ
- これにより冗長なデータベースの読み取りが大幅に減少し連鎖的な負荷の急増からシステムを保護する
■ 10. リードレプリカの拡張
- 課題:
- プライマリはすべてのリードレプリカにログ先行書き込み(WAL)データを書き込む
- レプリカ数が増加するにつれてプライマリはより多くのインスタンスにWALを送信する必要がある
- ネットワーク帯域幅とCPUの両方に負荷がかかる
- これによりレプリカの遅延がより高く不安定になりシステムを信頼性高く拡張することが難しくなる
- 解決策:
- 遅延を最小限に抑えるため複数の地理的リージョンにわたり約50のリードレプリカを運用している
- 現在のアーキテクチャではプライマリはすべてのレプリカにWALをストリーミングする必要がある
- 現時点では非常に大規模なインスタンスタイプと高ネットワーク帯域幅で適切に拡張できる
- プライマリに過負荷をかけずにレプリカを無期限に追加し続けることはできない
- この問題を解決するためにAzure PostgreSQLチームと協力して中間レプリカがWALを下流レプリカに中継するカスケードレプリケーションを導入している
- このアプローチによりプライマリに負担をかけずに潜在的に100以上のレプリカに拡張できる
- しかしこれにより運用上の複雑さが増し特にフェイルオーバー管理において顕著になる
- この機能はまだテスト中である
- 本番環境に展開する前に堅牢性と安全なフェイルオーバーが可能であることを確認予定である
■ 11. レート制限
- 課題:
- 特定のエンドポイントでの突然のトラフィック急増高コストなクエリの急増またはリトライストームにより重要なリソースが急速に枯渇する
- CPU、I/O、接続などのリソースが枯渇し広範囲にわたるサービスの劣化を引き起こすことがある
- 解決策:
- 突然のトラフィックの急増によってデータベースインスタンスが過負荷になり連鎖的な障害が発生するのを防ぐためアプリケーションコネクションプーラープロキシクエリの複数のレイヤーにわたってレート制限を実装した
- リトライ間隔が短すぎるとリトライストームを引き起こす可能性があるためこれを避けることも極めて重要である
- レート制限をサポートし必要に応じて特定のクエリダイジェストを完全にブロックできるようORMレイヤーを強化した
- この的を絞った負荷遮断により高コストなクエリの急増から迅速に復旧することができる
■ 12. スキーマ管理
- 課題:
- 列の型変更といった小さなスキーマ変更でもテーブル全体の書き換えを引き起こす可能性がある
- スキーマ変更は慎重に適用し軽量な操作に限定するとともにテーブル全体を書き換える変更は避ける必要がある
- 解決策:
- 特定の列の追加や削除などテーブル全体の書き換えを引き起こさない軽量なスキーマ変更のみを許可する
- スキーマ変更には厳格に5秒のタイムアウトを設けている
- インデックスの作成と削除は同時に行うことができる
- スキーマの変更は既存のテーブルに限定されている
- 新機能に追加のテーブルが必要な場合それらはPostgreSQLではなくAzure CosmosDBなどの代替シャーディングシステムに配置する必要がある
- テーブルフィールドをバックフィルする際には書き込みの急増を防ぐために厳格なレート制限を適用する
- このプロセスは時には1週間以上かかることがあるが安定性を確保し本番環境への影響を避けることができる
■ 13. 結果と今後の道筋
- 達成した成果:
- 適切な設計と最適化によりAzure PostgreSQLが最大規模の本番ワークロードに対応できるよう拡張できることを実証した
- PostgreSQLは読み込み中心のワークロードで数百万QPSを処理している
- ChatGPTやAPIプラットフォームなどOpenAIの最も重要な製品を支えている
- レプリケーション遅延をほぼゼロに保ちながら約50のリードレプリカを追加した
- 地理的に分散したリージョン間で低遅延の読み取りを維持している
- 将来の成長を支える十分なキャパシティの余裕を確保した
- 遅延を最小限に抑えながら信頼性を向上させる形で機能している
- 本番環境ではクライアント側のレイテンシの99パーセンタイルを常に2桁ミリ秒台前半で維持している
- 99.999%の可用性を実現している
- 過去12か月間で発生したPostgreSQLの重大障害(SEV-0)は1件のみであった(ChatGPT ImageGenのバイラルローンチ時に発生し1週間で1億人以上の新規ユーザーが登録し書き込みトラフィックが10倍以上に急増した)
- 今後の取り組み:
- PostgreSQLがこれまで当社を支えてくれたことには満足している
- 将来の成長に向けて十分な運営余力を確保できるよう引き続きその限界に挑戦していく
- 既にシャード可能な書き込み集約型ワークロードはCosmosDBなどのシャード化されたシステムへ移行済みである
- 残っている書き込み負荷の高いワークロードはシャーディングがより難しいためPostgreSQLのプライマリからの書き込みをさらに軽減すべくそれらについても積極的に移行を進めている
- Azureと協力してカスケードレプリケーションを有効化しより多くのリードレプリカを安全に拡張できるようにしている
- インフラストラクチャの需要が拡大し続けるにつれてシャード化されたPostgreSQLや代替の分散システムなどさらなる拡張のための追加のアプローチを模索し続けていく