こんにちは、hachi8833です。
今回は、PostgreSQLのスケーリングソリューションで知られるCitus Data社のブログ記事の翻訳をお送りいたします。
概要
- 元記事: Postgres tips for Rails developers(Apr 28, 2017)
- 著者: Lukas Fittl
- ブログ: Citus Data Blog
原著者の許諾をいただいて翻訳・公開します。
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
ステートメントを発行すると、organizations
のid= 123
で即座に行レベルロックが発生します。そしてこのロックは、COMMIT
が実行されるまで継続します。
同じorganization
に異なるユーザーから別のリクエストが発生し、これと同じようなトランザクションが発生したとしましょう。通常、別のリクエストは最初のトランザクションがコミットするまで待機しないと自分のトランザクションを進められないので、レスポンスタイムが低下します。
この問題を修正するには、トランザクションの見直しが有用です。UPDATE
を最後の方に移動し、さらにtimestampフィールドの変更処理をトランザクションの外に出して、メインの処理が完了してから行われるようにします。
こうした問題を検出するには、PostgreSQLでlog_lock_waits = on
を設定するのが便利です。
データベース接続を管理する
Railsのデータベース接続は、デフォルトでコネクションプールに保持されます。新規リクエストを受け付けると、Railsはコネクションプールから接続を1つ取り出してアプリに渡します。Railsアプリがスケールすると数百ものデータベース接続がオープンされますが、実際に動作しているのはほんの一握りです。
動作しているデータベース接続数を削減するには、pgBouncerなどのコネクションプール管理ソフトウェアが便利です。こうしたソフトウェアは、トランザクションが有効なときにコネクションをオープンし、処理の終わったアイドリング状態のコネクションにはトランザクションを渡さないようにします。
もっと知りたい方へ
RailsでPostgreSQLを管理するうえでお役に立てば幸いです。他にも便利な情報をお持ちの方は、@citusdataまでお知らせください。
こうした記事にご興味がおありの方は、元記事下部のフォームにメールアドレスを登録いただければ毎月ニュースレターをお届けいたします。内容には自信がありますので、どうかスパムとお間違えにならないよう。
関連記事
- [Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)
- [Rails 5] マイグレーション時にデータベースのカラムにコメントを追加する
- 【ゆるふわDocker部】任意バージョンのPostgreSQLコマンドを実行して外部DBに接続する
- RailsでPostgresを使おうとしてはまった