Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

PostgreSQLのパーティショニングを「カナリア方式」で成功させる(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: 炭鉱のカナリア - ウィクショナリー日本語版

PostgreSQLのパーティショニングを「カナリア方式」で成功させる(翻訳)

はじめに

私たちはPostgreSQLデータベースのパーティショニングで、認めたくないほど多くの時間を費やしてきたにもかかわらず、テストもされていなければ、ロジスティックス上の理由によって機能を凍結することもできなくなっていたことがありました。
しかしその後で突破口を見つけたのです。決め手となったのは文字通り「カナリア」、具体的にはいわゆるカナリアテストでした。貴重な体験を得られたので、同じような困難に遭遇した人たちのために、パーティショニングで起きたことを記事にすることに決めました。

長年運用されている成熟したプロジェクト(あるいは初期段階のスタートアップ)に取り組んでいると、いつの間にかデータベースのパフォーマンスが低下してしまうことがあります。そうなると、データベースの再設計が必要になります。この作業を適切に(つまりデータを一切損なわずに)実行したいのであれば、本記事が役に立ちます。

その前に、いくつか押さえておくべき事柄があります。
まず、データベースのパーティショニング(database partitioning)は、大規模データベースを複数のパーティションに分割するための手法です。個別のパーティションは、異なる物理ストレージのデバイスやサーバーに保存できるので、パフォーマンスや管理のしやすさの向上が見込めます。

カナリアテスト(canary test)は、新しい変更をユーザー全体に展開する前に、少数のユーザー(すなわち「カナリア」)に新しい変更を公開する手法です。これにより、問題や予想外の振る舞いの監視を強化し、制御された環境内で結果を収集できるようになります。

本記事では、世の中であまり目にする機会のない手法(カナリアベースのパーティショニング)について扱います。

これは、果てしなく流れ込んでくるクエリを、眠気をこらえながら手動で修正するという退屈極まるつらい作業を経験したすべての人々への賛辞であり、古今東西あらゆる場所に存在する本物のカナリアたちへの賛辞でもあります。

🔗 どんなプロジェクトに取り組んでいたか

プロジェクトの背景を説明します。
そのアプリは、パーティショニングを行ったときの時点で80万人を超えるユーザーにサービスを提供していました。また、パーティショニング対象となるテーブルには6千万件近いレコードが存在し、テーブルのサイズは140GBでした。

ユーザー数が急増していたうえに、それまで開発してきた機能が複雑だったために、データベースが何度も肥大化し、データベースクエリ数も急増していました。

プラットフォームチームは、パーティショニングの重要性をはっきりと理解していました。しかし、パーティショニングの準備を終えた途端にユーザー数とデータがまたしても膨れ上がってしまいました。このままでは、何を変更するにも不安が募る一方でした。

🔗 状況を詳しく評価する

このときは、巨大なデータベースをパーティショニングするという課題に加えて(なお、Evil Martiansは既にこの作業をPostgreSQLで行える状態になっていました)、大量の不適切なクエリを修正することも重要な課題でした。さもないと、データベースが不安定になって、メモリエラーで頻繁にクラッシュしやすくなっていたでしょう。

SQLクエリの修正は、パーティショニング作業の中でも最もリソースを食ううえに、単調で退屈な作業となるのが普通です。クエリの発生元はさまざまなので、クエリを(生SQLで)明示的に書ける場合もあれば、Active RecordやArelで実行される場合もあります。また、プロジェクトの内部サービスで生成されるクエリもあれば、サードパーティのライブラリで生成されるクエリもあります。

この種の作業にありがちですが、このときも納期が迫っていたために事態はさらに緊迫していました。データベースが現在の構成を処理できる状態ではなかったため、ただちにクエリを修正しなくてはなりませんでした。さらに厄介だったのは、数100件ものファイルのどのファイルが犯人なのかが謎のままだったことです。

SQLクエリの修正で最もつらいのは、途中で寝てしまわないよう、しょっちゅう気張らなければならなくなることです。

🔗 「問題を見つけるのが難しい」問題

それにしても、「パーティショニング」を短時間で効率よく実施しなければならないようなクエリを発見するのにこれほど手こずった理由は何だったのでしょうか?以下に述べる3つが基本的な要因だったのでした。

要因1: クエリ統計がなく、統計取得用のツールすらなかった。
最初にテストを実施することはよくても、すべてのコードを平等に(楽観的に)チェックすると、誤った印象を得てしまう可能性があります。その一方で、ボトルネックの原因となるクエリは早急に修正する必要がありました(最終的にはすべてのクエリを修正しなければなりませんでしたが)。

より悲観的に言えば、テストのカバレッジが100%に達していなければ、問題を引き起こしたテーブルに疑いの眼差しを向けるまでに大変な時間がかかる可能性もあるということです。

同じことはstaging環境にも当てはまります(staging環境の方が現実に近いと言えますが)。

要因2:機能を凍結できない(または凍結したくない)事情があった。
プロジェクトの開発作業が活発だと、その分パーティショニングの難易度も増し、作業の見積もりも難しくなります(しかも統計情報すらない状態で...後はお察しください)。当然ですが、こんな状況では機能の凍結は不可能です。

要因3: プロジェクトが複雑だった。
プロジェクトの複雑さが増せば増すほど、特にレガシーコードでメイン開発者たちと密に連携する必要性が高まります。作業やコラボレーションを効率よく進められない状況では(または時間が足りなくなれば)、さらに大きなプレッシャーがかかります。

こんな状況に置かれたら、たいていの人は船乗りや鉱夫のように天を仰いで呪いの言葉を発し、プロジェクトへの悪影響を最小限に抑えつつ、必要な統計情報を収集して、既存の修正を現実のユーザーに対してテストする方法を模索することになるでしょう。

そういうわけで、私たちにはカナリアが必要でした。この美しい鳥たちは、私たちが学ぶ必要のある「悪い」クエリをすべて知らせてくれるからです。そして(よくあることですが)、何か問題が起きれば一部のカナリアは死んでしまいます。しかしこれは、最終的にはより大きな目的のための尊い犠牲となるのです。

免責事項: 本記事における「カナリア」はあくまで比喩であり、本記事を書くために本物のカナリアたちに危害を加えることはしていません(ただし、危害こそ加えていませんが、リスクを理解したうえで進んでミッションに同意してくれたカナリアだけを対象としています)。

🔗「誰をカナリアに指名するか」問題

この顧客プロジェクトの場合は、アセットが組織に属していたので、プラットフォームのアセット(画像やドキュメントなど)をパーティショニングするときに、この「組織」をカナリアとして使いました。

ただし通常のプロジェクトならそこまで複雑にはならないので、組織の代わりにユーザーをカナリアにする手もあります。本記事ではこの点を明確にするため、user_idを利用することにします。

それでは、どのユーザーをカナリアに指名すればよいでしょうか?私たちはこのプロジェクトの開始時点で、全体として大きすぎず重要すぎないアクティブな組織(またはユーザー)を厳選しました。

重要: 最初のうちは、カナリアグループに含めるカナリアユーザーを増やしすぎないこと。カナリアが多すぎると、クエリに問題が見つかると同時に重大なパフォーマンス問題が生じる可能性があります。

もうひとつ注意: リスクを冒すべきではありません。
主要なユーザーや最もアクティブなユーザーをカナリアにすれば、必要な情報をすぐ知らせてくれますが、その代わりに時間と評判も犠牲になります。ですから、アクティブなユーザーの中から「アクティブすぎず」なおかつ「最適化が終わるまで待つつもりのある」ユーザーをカナリアとして慎重に選ぶようにしましょう。

その状態から、様子を見ながら少しずつカナリアを増やしていき、最終的には全員がカナリアになっても幸せでいられる状態を達成します。

なお、私たちのパーティショニングプロジェクトの開始時点では、単にカナリアをABC順に上からいくつか選んで追加しました。

🔗 linterをセットアップする

まずは、クエリに改善が必要かどうかを決定するツールの選定から始めましょう。言い換えれば、以下のような作業が必要になります。

  1. クエリを1件受け取る。
  2. クエリの全ステートメントを分析する。
  3. パーティショニングされたテーブルに関連するステートメントが存在するかどうかを判定する。
  4. ステップ2で見つかったステートメントをチェックして、パーティションのキーにするのが適切な条件がそこに含まれているかどうかを確認する。
  5. 適切な条件が存在しない場合は、そのクエリとメソッドを報告する。できれば、ファイル名とそのクエリを実行したときの行番号も報告されるのが望ましい。

このツールを作成するために、ActiveSupport notificationspg_queryライブラリを使いました。

pganalyze/pg_query - GitHub

手作りlinterの基本的な枠組みは以下のとおりです。

class PartitionKeyReporter
  attr_reader :table_name

  def initialize(table_name, partition_key)
    @table_name = table_name
    @partition_key = partition_key
  end

  def sampling_percentage
    @sampling_percentage ||= 10
  end

  def check_query?
    # production以外の環境では、可能なものをすべてチェック
    return true unless Rails.env.production?

    # production環境では、クエリを指定された割合の件数だけチェックする
    rand(100) < sampling_percentage
  end

  def call(*_args, payload)
    return unless check_query?

    return if payload[:sql].blank?

    # この'Resouce'はモデル名(Railsはこういうときにめちゃくちゃ有用ですね)
    if payload[:name] == 'Resource Create'
      report(payload[:sql]) unless payload[:sql].include?(partition_key)
    else
      parsed = PgQuery.parse(payload[:sql])
      return unless parsed.tables.include?(table_name)

      # クエリチェックのコア部分をここに書く
    end
  end
end

必要なイベントは、以下のようにサブスクライブできます。

ActiveSupport::Notifications.subscribe 'sql.active_record', PartitionKeyReporter.new('resources', 'user_id')

このコードのresourcesは、クエリを分析するテーブル名であり、RailsのResouceモデルに対応します。
一方、user_idはパーティションで使うキーであり、ユーザーIDに対応します。

私たちが悪魔と取引して手に入れたlinterの完全版はGistで参照できます。私たちが欲しかった機能はライブラリの既存機能になかったので地獄を見ました。そこで、メソッドの1つをインスピレーションの源泉として取り入れ、最初は多少なりともシンプルなクエリを対象とするシンプルなlintを構築しました。

その後、プロジェクトには複数レベルのJOINを含む巨大なクエリがあることが判明したので、linterを何度も手直しする必要がありました。たとえば、あるバージョンでは必要なクエリをすべて検出できたものの、欲しくない結果(つまり誤検知)まで検出されてしまったこともありました。

最終的なlinterのコードは100行程度になりました。上出来とはいかなかったものの、役目は十分果たしてくれました。そして当然と言うべきか、私たちには結果をテストでカバーする時間すらありませんでした。

linterを実行するとメモリも時間も消費するので、このlinterで分析・探索できるテストは、staging環境とproduction環境におけるクエリのごく一部に限られます。

ただし、選択するユーザーグループの人数が少なければ、そのグループで分析できるクエリをその分増やせます。さらに、その少人数グループだけがパーティショニングのメリットを得られる形でパーティションを実装し、それ以外のユーザーには私たちがデータベースで行っている作業を一切気づかれないようにできます(完全なコードスニペットはこのGistを参照)。

この最初のユーザーグループが、私たちが選んだカナリアユーザーたちになります。

🔗 データベースのセットアップ

ご興味がおありでしたら、以下の過去記事でパーティショニング作業に待ち構えている困難や注意すべき点について解説しています。本記事では、カナリアを作成するのに必要な情報だけを取り上げます。

参考: A slice of life: table partitioning in PostgreSQL databases

  1. パーティショニングされたresources_partitionedテーブルを作成する。
  2. resoucesテーブルに操作用トリガー( 変更をresources_partitionedにコピーする)を追加する。
  3. resourcesテーブルのデータをresources_partitionedテーブルにコピーする。
    システムのダウンタイム(ほとんどの場合読み取り専用のダウンタイム)に作業できれば理想だが、それが叶わない場合は、データをチャンクでコピーする。
  4. resources_partitionedテーブルに操作用トリガーを追加する。
    この時点のresources_partitionedテーブルへのすべての変更は、元のresourcesテーブルの操作用トリガーによってのみ適用される。

🔗 インデックスを追加すべきタイミング

インデックスの追加は、resources_partitionedテーブルにデータを入れ終わってから行うのが望ましいのは明らかです。しかし巨大なデータを入れた後でインデックスを追加すると、トリガー実行中にアクティビティが高くなったときにパフォーマンスが悪化する可能性があります。ここで重要なのは、テーブルの負荷とパーティションのサイズなので(どちらも単一テーブルよりずっと小さくなりますが)、以下の3つを心がけておきましょう。

1: パーティションを大きくしすぎないこと

2: production環境にインデックスを追加するときのコストを事前に見積もってから作業すること(productionと同程度のデータ量を持つstaging環境を使うなど)。

3: インデックスの追加やマージで時間がかかりすぎる場合は、負荷を考慮するために以下の操作を検討すること

  • トリガーをオフにする
  • インデックスを追加する
  • トリガーを再びオンにしてデータを同期(または同期を完了)する。

トリガーを再びオンにする前に、作業中にどのレコードが変更または削除されたかを特定しておく必要があります(Rails-wayに沿っていれば、updated_atタイムスタンプで実行可能です)。特定したデータを同期するときにも、トリガーをオフにしたりテーブルをトラバースする前の事前準備が必要になる可能性がありますが、この方法もありです。

🔗 外部キーはどのタイミングで追加すべきか

データの整合性に気配りしている責任感の強い開発者なら、外部キーのことを忘れてはいけません。
私たちの場合はNOT VALIDキーを作成できたおかげで作業はずっと簡単に済みましたが、それでも注意すべき点がいくつかあります。

  • パーティショニングされたテーブルの主キーは複合主キーになり、それに伴って外部キーも複合主キーになります。必要なテーブルには、適切なフィールドを事前に追加しておくこと。

  • 制約をNOT VALIDとして作成できるとは限りません(PostgreSQL 16ドキュメント ADD table_constraint [ NOT VALID ])。
    ただし幸いなことに、パーティションごとにNOT VALID外部キーを作成しておき、それらをバリデーションしてから、有効な外部テーブルを親テーブル内に作成するという方法が使えます(これでパーティション内の既存の制約が使われるようになります)。

🔗 インフラストラクチャをセットアップする

以上で、完全に同期した2つのテーブルと、尊い犠牲となってくれるカナリアたちを準備できたので、後必要なのは「炭鉱」だけです。つまり、カナリアユーザーたちによる負荷を処理するための新しいアプリケーションプロセスをセットアップします。

この作業はコードレベルでは楽勝です(少なくともActive Recordでは)。

class Resource < ApplicationRecord
  if ENV['CANARY']
    self.table_name = :resources_partitioned
    self.primary_key = :id # Railsの新しい複合主キーの機能を使っていない場合はこう書く
  else
    self.table_name = :resources
  end
end

インフラストラクチャのレベルではもう少し手間をかける必要があります。データベースへのクエリを生成する全プロセスを複製してから、CANARY環境変数を有効にする必要がありますし、カナリアユーザーがそれらのプロセスに背負わせる負荷(ネットワークリクエストやバックグラウンドジョブなど)を適切にルーティングする方法も考えておかなければなりません。

これを実行する手順は現状のインフラストラクチャによって異なりますが、それでも回避可能な「よくある落とし穴」がいくつかあります。

🔗 カナリアアプリを作成する

カナリアアプリケーションのプロセスを作成するときの一般的な経験則は、カナリアで新しいテーブルが使われている点を除けば、元のプロセスと完全に同じに振る舞うようにすることです。これによって、カナリアユーザーでのみ発生するバグの可能性を回避できます。

この作業では、多くの場合以下の2つの作業が必要になります。

  • メインアプリとカナリアアプリの環境変数を同期させる。

  • カナリアアプリがメインアプリと同様にビルド・デプロイされるようCIを構成する。

ただしこの作業では、メインアプリでのみ必要な副作用(データベースマイグレーションの実行、CIビルド中のサードパーティAPI呼び出し、cronジョブなど)を誤ってカナリアアプリに持ち込まないよう、特に注意が必要です。

また、新しいプロセスをサポートするのに十分なデータベースコネクション数を確保できているかどうか(さもなければPgBouncerなどのコネクションプールを使っているかどうか)も必ず確認しておきましょう。

🔗 ネットワークトラフィックをルーティングする

トラフィックをメインアプリとカナリアアプリに振り分ける最も素直な方法は、アプリの手前にプロキシを設置することです。このプロキシは、クエリのヘッダーにある値(ここにパーティショニングのキーが含まれます)に基づいてルーティングを処理します。

クライアントは、既にあらゆるクエリでそうしたヘッダーを送信している可能性が高いので(例: JWTトークン内のユーザーID)、残る作業はルーティングロジックを実装するだけとなります。

もちろん、ここでどんな方法にするかは自分の想像力次第ですが、最初はシンプルな方法にしておくのがベストでしょう。
つまり、数人の「アーリーバード(early bird)」を選び出して、彼らのIDをプロキシのコードに直接ハードコードし(または環境変数で指定)、そのIDを持つユーザーのクエリをすべてカナリアアプリにルーティングするというものです。

カナリアアプリが十分安定してきたと感じられる状態になったら、ルーティングロジックを緩める(例: ハードコードする個別のユーザーIDをIDのレンジに変更する)ことで、より多くのユーザーをある程度まとめてカナリアに変換する作業を始められます。

ここで注意しておきたい主な点は、作業中に整合性を損なわないようにすることです。同一のユーザーをメインアプリとカナリアアプリの両方にルーティングしてしまうとデッドロックが発生するリスクが生じます。

使うプロキシは、自分に合ったものなら何でも構いません。ただし、プロキシで作業するときは、クラウドのプロバイダがエッジコンピューティング機能を提供しているかどうかを確認してからにしましょう。私たちの見解では、エッジ機能をリバースプロキシとしてデプロイする方法が、目的を達成するうえで最も柔軟で面倒を避けられる構成だと思います。

🔗 バックグラウンドジョブをルーティングする

バックグラウンドジョブの扱いはもう少し複雑です。ネットワークリクエストと異なり、バックグラウンドジョブは必ずしもシングルユーザーのコンテキストで実行されるとは限りません。たとえシングルユーザーのコンテキストで実行されるとしても、引数リストを見ただけではシングルユーザーかどうかを判別するのが難しいこともあります。つまり、バックグラウンドのルーティングを適切に実装しようとするとかなり時間がかかってしまう可能性があるということです。

とは言うものの、バックグラウンドジョブ内で実行されるSQLクエリにパーティショニングキーが脱落することがあったとしても、そのリスクは、同じミスが通常のネットワークリクエストで実行されるコードで発生したときよりはずっと低くなります。バックグラウンドジョブは、ネットワークリクエストに比べて時間に縛られておらず、堅牢なリトライメカニズムを備えていることが多いためです。

つまり、潜在的なデッドロックを回避するには、カナリアアプリケーションのプロセスが単一ユーザーの負荷全体を処理するのが理想ではありますが、バックグラウンドジョブをメインワーカーとカナリアワーカーに振り分けるロジックをもっとシンプルにしたぐらいで直ちに世界が破滅すると決まったわけではありません。

たとえば、バックグラウンドジョブを種別ごとにルーティングする形で、パーティショニングされたテーブルへの移行を少しずつ段階的に進めてみてもよいでしょう。この方法は本格的なルーティングを実装するのに比べて少々リスクはあるものの、その分手間を省けますし、ジョブのどの種別を移行するかを慎重に選んでおけばリスクを最小化できる、と思います。

それはともかく、どの方法を選ぶにしても、バックグラウンドジョブを処理するSidekiqやSolid Queueなどのジョブプロセッサ向けに「リバースプロキシ」をセットアップして、「既存のキュー」と「カナリアのバックグラウンドワーカーで処理されるキュー」のどちらかにジョブを動的にルーティングする必要がある点は変わりません。

Sidekiqの場合は、ジョブの種別や引数リストに基づいて、ジョブをオリジナルのキューとカナリア用のキュー(例: defaultdefault_partitioned)にルーティングするカスタムのクライアントミドルウェアを使うことになるでしょう。

ジョブキューを動的に振り分ける機能も備えているミドルウェアが他にもあるかどうか、ぜひチェックしておきましょう。そうしたミドルウェアが使えるのであれば、処理のチェインの末尾に追加しておきましょう。

SQLクエリの問題をすべて修正し終わったと確信できるようになったら、いよいよ以下の作業が行えるようになります。

  • メインワーカーでパーティションをデフォルトで有効にする
  • ルーティングロジックを削除する
  • *_partitionedキューが完全に空になるまで待つ
  • 最後にカナリアワーカーを撤去する

🔗 カナリア方式のメリットとデメリット

私たちがやってきた方式のメリットとデメリットを、まずはメリットから見ていくことにしましょう。

  • メリット1: 修正前のSQLクエリに関するライブ統計を得られる。
    プロジェクトの実運用プロセスから本物のクエリデータを得て統計を得られます(もちろんデータの難読化も考慮しておくこと)。この統計が手に入れば、どのコードを優先的に修正すべきか、どのコードは利用頻度が低いので修正を先延ばしできるかを見極めるうえで役立つので、時間を大きく節約できます。

  • メリット2: パーティショニング作業で使うコードを段階的に準備できる。
    収集した統計情報を分析し、カナリアに割り当てたユーザーのグループを段階的に大きくしていくことで、負荷が急増したときに最適化がどの程度進んでいるかを把握しつつ、ユーザーへの悪影響を最小限に抑えながら作業を継続できるようになります。最終的に、すべてのユーザーがカナリアになるところまで見届けるのが理想的です。

  • メリット3: 作業中のパーティション(または作業の終わったパーティション)を任意のユーザーグループでいつでもテストできるので、柔軟性が高まる。

  • メリット4: パーティショニングを全ユーザー(または一部のユーザー)についてロールバックできる。
    通常のテーブルとパーティショニングされたテーブルが同期しているので、問題発生時にデータを喪失せずに済みます。たとえパーティショニングを中断する場合でも、膨大なクエリを調整する必要がありません。

次はデメリットについてです。

  • デメリット1: データ量が増大する。
    データベースにサイズ制限がある状況でテーブルをパーティショニングすると多くのディスク容量が専有される場合は、まずデータベースのサイズ制限を増やす必要があります。

  • デメリット2: データベースの負荷が増大する。
    データベースのあらゆる操作が重複するので、対象となる(メインおよびパーティショニング)テーブルの読み出しや書き込みは少なくとも2倍になります。したがって、パーティショニング作業を開始する前に、通常のデータベース最適化を済ませておくことをおすすめします(データベース最適化は常に有用ですが、それなりに時間がかかる可能性もあります)。

  • デメリット3: デッドロックのリスクがある。
    リソースに対する操作が重複するので、パーティショニングされたテーブルと通常のテーブルにある同一のリソースにも同時に更新をかけることになります。
    たとえば、2つのワーカーで作業している状況を考えます。ワーカー1は「パーティショニング済み」(つまりパーティショニング済みテーブルで動いている)で、ワーカー2はまだそうなっていないと、デッドロックにつながる可能性があります。デッドロックの可能性を最小限に抑えるために、コードをどのような順序で変更するかを慎重に検討しましょう。

🔗 推奨事項と最終的な見解

カナリア方式は、パーティショニングのリリースを準備して、作業のロールバックを可能にする方法の1つなので、問題が発生することが予想される以下のような状況で使うのが合理的です。

  • 修正が必要なクエリが大量にある場合
  • クエリ修正に必要な時間の見積もりが難しい場合
  • コードが複雑でわかりにくく、見通しがきかない場合

カナリア方式では、さまざまな問題が解消されます。
たとえば、パーティショニング作業中のブランチに最新の変更をプルして、クエリが追加されるたびにチェックを繰り返す必要がなくなります(既に作業中のパーティショニングは、一部のデータやユーザーに対してのみ適用している状態です)。
これによって、一部のリスクも軽減されます。パーティショニングされるテーブルと同じデータセットを持ち、完全に動作する元のテーブルも維持されるので、カナリアの人数を減らして作業することも、パーティショニング作業全体を中止することも可能です。

最後に、作業中に考慮すべき点をいくつか示しておきます。

まず、クエリが適切にパーティショニングされていることを確かめるためのテストは遠慮なく追加すること。テストを追加しておけば、編集結果が他の開発者に渡されたときに多数の問題が発生するのを防止できます。

RSpec::Matchers.define :use_partitioning do |table_name, partition_key|
  supports_block_expectations

  match do |actual|
    success = true

    callback = lambda do |event|
      query = event.payload[:sql]

      next unless query.match?(/\s+"?#{table_name}"?\s+/)
      next if query.match?(/"?#{table_name}"?\."?#{partition_key}"?\s+(=|BETWEEN|IN)/)

      success = false
    end

    expect(callback).to receive(:call).at_least(:once).and_call_original
    ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &actual)

    success
  end
end

次に、監視を怠らないこと
たとえば、データベースのサイズは重要です(特に、データベースサイズに制限がある状態でテーブルサイズが急速に増大している場合)。
インデックスも忘れずチェックしておくこと。使われていないインデックスや「みんながそうしているから」という理由で作成されたインデックスは不要です。そんなインデックスでリソースを無駄にする必要などありません。

最後に、他の作業者への周知や連携を徹底すること
「カナリアユーザー」を設定したことはもちろん、同じデータを持つテーブルがデータベース内に2つあることも、関係者全員に必ず周知しておかなければなりません。一方のテーブルにフィールドを追加するときは、他方のテーブルにも同じフィールドを追加する必要がありますし、インデックスも両方に設定する必要があります。他の開発者がインデックスを追加する場合に備えて、パーティショニングテーブルでインデックスを適切に追加する方法を知らせておく必要もあります。

🔗 ボーナス!

私たちが実装を開始した頃は、複合主キーと複合外部キーを指定できるRails 7.1.0はまだベータ版でした。私たちはこの機能を長期間テストしており、全般的にこの機能は実に便利だと考えているので、安心してお使いいただけます。
ただし潜在的な問題があることをお忘れなく(#496711

ふぅ〜!カナリア方式によるパーティショニングについて一通り理解できたので、皆さんも鳥かごから解き放たれた気分になったのでは?ぜひ空高く舞い上がって、私たちにも知らせてください。

関連記事

Rails: Evil Martiansが使って選び抜いた夢のgem -- 2024年度版(翻訳)

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)


  1. 訳注: 少なくとも関連付けのquery_constraintsオプションが今後変更されることになっています(#51571)。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。