巨大な象の肩の上に立って:DiscourseをPostgreSQL 13にアップグレードする

Falco
Apr 2, 2021 • 4 min read

Discourseプロジェクトが始まった2013年当時、チームはDiscourseの基盤となる「スタック」を構成するいくつかのツールを選定する必要がありました。CoffeeScriptからJavascriptへの移行のように、早い段階で最適でないと判明した選択肢もあり、迅速に移行することができました。

一方で、ほとんどの選択は優れたものであることが証明され、データベースにPostgreSQLを選んだことは最も優れた選択でした。どれほど満足しているかを示すために、PostgreSQL最新バージョンのお気に入りの機能についてお話しましょう:**B-Treeデduplication(重複排除)**です。

ホスティングサービスの小さな歴史

Discourseはもちろん100%オープンソースソフトウェアですが、何よりもまず私たちはホスティング会社です。2014年に商用ホスティングサービスを開始して以来、毎月4億ページビュー以上を提供し、毎月400万件以上の新規投稿を保存するまでに成長しました。

このデータはすべてPostgreSQLインスタンスに保存されているため、PostgreSQL 13のリリースノートに「大規模データベースに恩恵をもたらすインデックスおよびルックアップシステムの大幅な改善、インデックスのスペース節約とパフォーマンス向上を含む」という情報が掲載されたとき、私たちが非常に興味を持ったことは想像に難くないでしょう。それは、PostgreSQLの奇数バージョンをスキップして2年ごとにアップグレードするという私たちの慣習を破ることさえ検討させるほどでした。そして情報に基づいた判断を下すために、ベンチマークを実施する必要がありました。

シュリンクレイを起動せよ

新しいB-Tree重複排除機能がDiscourseに何らかの恩恵をもたらすかどうかを評価するために、私たちのホスティングにおけるほとんどのDiscourseインスタンスで最も大きなテーブルであるposts_timingsテーブルに効果があるかどうかを確認することにしました。このテーブルは各投稿における各ユーザーの読書時間を保存しており、以下のように定義されています:

discourse=# \d post_timings
              Table "public.post_timings"
   Column    |  Type   | Collation | Nullable | Default
-------------+---------+-----------+----------+---------
 topic_id    | integer |           | not null |
 post_number | integer |           | not null |
 user_id     | integer |           | not null |
 msecs       | integer |           | not null |
Indexes:
    "index_post_timings_on_user_id" btree (user_id)
    "post_timings_summary" btree (topic_id, post_number)
    "post_timings_unique" UNIQUE, btree (topic_id, post_number, user_id)

post_timings_summaryインデックスを削除できるかどうかも調査しています。これはpost_timings_uniqueインデックスの左端列のサブセットであるため、再利用できる可能性があります。

更新(2021年8月):post_timings_summaryインデックスを削除しました。

私たちがホストしている特定のインスタンスでは、このテーブルが最近10億行を超えたため、テストにはこの行数を使用しました。また、本番システムではMVCCにより「膨張(bloat)」が蓄積し分析に影響を与える可能性があるため、クリーンな環境で比較するために、バージョン12と13の両方のpgの最新リリースを新規インストールして使用しました。各バージョンをロードした後の数値は以下の通りです:

テーブルサイズの合計

PostgreSQL 12: 114 GB
PostgreSQL 13:  85 GB

リレーションサイズが25%削減されましたか?素晴らしいですね!:partying_face:

詳細を掘り下げると:

PostgreSQL 12
  Table: 42 GB
  Index: 72 GB

PostgreSQL 13
  Table: 42 GB
  Index: 43 GB

リリースノートで予告されていた通り、最適化はインデックスにのみ適用されており、ここでもそれを再現できています。テーブルサイズは変わりませんが、インデックスサイズはほぼ半分になっています。

さらに詳しく見ると:

PostgreSQL 12

               relation               |    size
--------------------------------------+------------
 public.post_timings                  |   42 GB
 public.post_timings_unique           |   30 GB
 public.index_post_timings_on_user_id |   21 GB
 public.post_timings_summary          |   21 GB

PostgreSQL 13

               relation               |    size
--------------------------------------+------------
 public.post_timings                  |   42 GB
 public.post_timings_unique           |   30 GB
 public.post_timings_summary          | 6939 MB
 public.index_post_timings_on_user_id | 6766 MB

予想通り、定義上重複が0であるUNIQUEインデックスはサイズに変化がありませんでしたが、繰り返し値を持つインデックスは元のサイズの約3分の1に最適化されました。

インデックスサイズだけでなく、パフォーマンスも変化します。このトピックに関するPostgreSQLのドキュメントによると:

これにより、各値(または列値の各異なる組み合わせ)が平均して数回出現するインデックスのストレージサイズが大幅に削減されます。クエリのレイテンシが大幅に削減される可能性があります。全体的なクエリスループットが大幅に向上する可能性があります。定期的なインデックスバキューミングのオーバーヘッドも大幅に削減される可能性があります。

また、重複のない書き込みが多いワークロードでは、小さな固定のパフォーマンスペナルティが発生するという注意点も追記されています。私たちの場合には該当しませんが、仮に該当したとしても、アプリケーションの完全に非同期なコードパスで書き込まれているという事実によって緩和されます:クライアントではバックグラウンドリクエストであり、Rack Hijackを活用したRailsアプリのノンブロッキングルートです。

予言は真実でした:PostgreSQL 13はDiscourseに大幅な改善をもたらします!

これは大きな意味を持ちます。なぜなら、ここでは単一のデータベース内の1つのテーブルへの影響を確認しましたが、私たちのデータベーススキーマには数十のテーブルがあるからです。そして私たちは数千のDiscourseインスタンスをホストしており、高可用性のために各インスタンスに複数のPostgreSQLインスタンスがあるため、その恩恵は何倍にも増大します。

Discourse :heart: PostgreSQL

Discourse Gives Back 2017で述べたように、Discourseは常に100%オープンソースプロジェクトであり、存続するために他の多くのオープンソースプロジェクトが積み重ねてきた数十年にわたる努力の上に成り立っています。成長するにつれて、私たちが最も依存しているプロジェクトへの資金援助に直接貢献できることを嬉しく思っています。そのため、昨年もPostgreSQL財団へ金銭的な寄付を行い、毎年同様のことを行うことを目指しています。

原文はこちら:


Good Loopでは、Discourseのセルフホスティングを安価で提供しています。開発元であるCDCK社の協力のもと、公式ブログ記事の翻訳・公開など、日本での普及にも努めています。

詳しくはこちら: Discourseの導入・運用支援・コンサルティング – Good Loop