Rails開発者のためのPostgreSQLの便利技(翻訳)

こんにちは、hachi8833です。
今回は、PostgreSQLのスケーリングソリューションで知られるCitus Data社のブログ記事の翻訳をお送りいたします。

概要

原著者の許諾をいただいて翻訳・公開します。

Rails開発者のためのPostgreSQLの便利技(翻訳)

今週のRailsConfで、私たちはRailsでPostgreSQLを使ううえでの多くの知見を共有しました。有用な情報なので、その多くを広く公開したいと思います。Railsアプリでのデバッグやデータベースのパフォーマンス改善にお役立ていただければと思います。

また、フェニックスで開催されたRailsConfに惜しくも出席できなかったRailsコミュニティメンバーの皆さまにも、私たちがRailsConfで得た有用な情報を提供したいと思います。元記事末尾のフォームにてニュースレターをお申込みいただければ、Citusのこの他のノウハウをメール配信いたしますので、ぜひご応募ください。

それでは本編にまいりましょう。

実行時間の長いクエリをstatement_timeoutで管理する

クエリ実行に時間がかかると、データベースでさまざまな問題を引き起こす可能性があります。数時間単位のクエリはもちろん、秒単位のクエリですらデータベースのロックやログ先行書き込み(WAL)でのキュー待ちが発生したり、そこまでいかなくてもシステムリソースを大量に消費したりすることがあります。

PostgreSQLでは、デフォルトのstatement timeout値を変更することでこの問題をもう少し安全に扱えるようになります。この方法のよい点は、たとえばデフォルト値を5秒に設定すると、それより時間のかかるクエリがすべて無効になることです。

production:
   url: <%= DATABASE_URL %>
   variables:
     statement_timeout: 5000

データベースセッション内で実行時間をもっと長く取りたい場合は、次のように現在の接続でのみ有効なstatement timeout値を別途指定することもできます。

class MyAnalyticsJob < ActiveJob::Base
  queue_as :analytics
  def perform
    ActiveRecord::Base.connection.execute "SET statement_timeout = 600000" # 10分
    # ...
  ensure
    ActiveRecord::Base.connection.execute "SET statement_timeout = 5000"   # 5秒
  end
end

不正なクエリを検出する

Railsでは、データベースとのやりとりの多くが抽象化されています。抽象化にはメリットもありますが、デメリットもあります。PostgreSQLは実行に時間のかかるクエリを表示する機能がありますが、Railsアプリが成長して複雑になるにつれ、PostgreSQLの通常ログ出力だけでは問題解決に必要な情報が足りないことがあります。

クエリの発生元を突き止めるmarginaliaという小さな専用gemを使うと、クエリの正確な発生元をログ出力できます。クエリに問題がある場合や異様に遅い場合は、この情報を元に問題をピンポイントで修正できます。

Account Load (0.3ms)  SELECT `accounts`.* FROM `accounts` 
WHERE `accounts`.`queenbee_id` = 1234567890 
LIMIT 1 
/*application:BCX,controller:project_imports,action:show*/

データベースクエリの概要を詳しく表示する

データベースで起こっていることをもう少し詳しく知りたいと思うことはよくあります。pg_stat_statementsは、Citus Cloudなどのクラウド環境でPostgreSQLにプレインストールされる拡張機能であり、統計情報を最後にリセットしてから実行されたクエリや、クエリの実行状況を詳しく表示できます。

たとえば、実行時間の長い上位10件のクエリをリストアップして平均実行時間を表示するには、以下のようにします。

SELECT query, total_time / calls AS avg_time
 FROM pg_stat_statements
ORDER BY total_time DESC 
LIMIT 10;

PostgreSQLデータベースでtrack_io_timingをオンにしておけば、パフォーマンスのボトルネックがCPUやI/Oで生じているかどうかも確認できるようになります。詳しくはpg_stat_statementsについての記事をご覧ください。

PostgreSQLの高度な機能を利用する

Railsでは、データベーススキーマのコピーをデフォルトでschema.rbというファイルに保存します。このスキーマファイルは、テストの実行前などにデータベースを初期化するのによく使われます。

残念なことに、関数インデックス部分インデックス、複合プライマリキーといったPostgreSQLの高度な機能は、スキーマファイルのDSLでは記述できません。

そういう場合は、Railsでdb/structure.sqlというファイルをgenerateし、config/application.rbで以下のように指定するとstructure.sqlをスキーマファイルにできます。

    # データベース作成時にActive Recordのスキーマダンプの代わりにSQLを使う
    # スキーマダンプからの出力が完全ではない場合に必要
    # (例: データベースの制約や固有のカラム型を使う場合)
    config.active_record.schema_format = :sql

この場合、内部的にはPostgreSQLのpg_dumpフォーマットが使われます。このフォーマットはやや記述が増える代わりに、データベース構造を完全にリストアできるようになります。このフォーマットにするとstructure.sqlが更新されるたびに大量のdiffが発生するので、これがつらい場合はactiverecord-clean-db-structure gemをご検討ください。

相互ロックする複雑なトランザクションを監視する

Railsではすべてをトランザクションに入れることが好まれています。特に、before_saveフックや、モデル間の多段リレーションシップにおいて顕著です。トランザクションを監視するうえで1つ注意したい点があります。この問題は、スケールアウトしたときに発生する可能性があります。

以下のようなトランザクションがあるとします。

BEGIN;
UPDATE organizations SET updated_at = ‘2017-04-27 11:31:03 -0700’ WHERE id = 123;
SELECT * FROM products WHERE store_id = 456;
--- 省略
COMMIT;

最初のUPDATEステートメントを発行すると、organizationsid= 123で即座に行レベルロックが発生します。そしてこのロックは、COMMITが実行されるまで継続します。

同じorganizationに異なるユーザーから別のリクエストが発生し、これと同じようなトランザクションが発生したとしましょう。通常、別のリクエストは最初のトランザクションがコミットするまで待機しないと自分のトランザクションを進められないので、レスポンスタイムが低下します。

この問題を修正するには、トランザクションの見直しが有用です。UPDATEを最後の方に移動し、さらにtimestampフィールドの変更処理をトランザクションの外に出して、メインの処理が完了してから行われるようにします。

こうした問題を検出するには、PostgreSQLでlog_lock_waits = onを設定するのが便利です。

データベース接続を管理する

Railsのデータベース接続は、デフォルトでコネクションプールに保持されます。新規リクエストを受け付けると、Railsはコネクションプールから接続を1つ取り出してアプリに渡します。Railsアプリがスケールすると数百ものデータベース接続がオープンされますが、実際に動作しているのはほんの一握りです。

動作しているデータベース接続数を削減するには、pgBounceなどのコネクションプール管理ソフトウェアが便利です。こうしたソフトウェアは、トランザクションが有効なときにコネクションをオープンし、処理の終わったアイドリング状態のコネクションにはトランザクションを渡さないようにします。

もっと知りたい方へ

RailsでPostgreSQLを管理するうえでお役に立てば幸いです。他にも便利な情報をお持ちの方は、@citusdataまでお知らせください。

こうした記事にご興味がおありの方は、元記事下部のフォームにメールアドレスを登録いただければ毎月ニュースレターをお届けいたします。内容には自信がありますので、どうかスパムとお間違えにならないよう。

関連記事

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833 コボラー、ITコンサル、ローカライズ業界を経てなぜかWeb開発者志願。 これまでにRuby on Rails チュートリアルの大半、Railsガイドのほぼすべてを翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

Rubyスタイルガイドを読む

BigBinary記事より

ActiveSupport探訪シリーズ