RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

デッドロックの原因になりうるPostgreSQLのパーティショニング

f:id:tech-rakus:20200928194206p:plain

こんにちは、takaramです。

私が担当しているサービスでは、RDBMSPostgreSQLを利用しています。今回は業務で行ったデッドロックの調査で知った、PostgreSQLの仕様に関して書いていきます。

ここでは デッドロックパーティショニング といった用語が登場しますが、今回これらの説明は割愛します。
パーティショニングについてご存じでない方は、まずはこちらの記事をお読みください。

tech-blog.rakus.co.jp qiita.com

なお、この記事の内容はPostgreSQL 12.3で確認したものです。

テーブルのロック

PostgreSQLのロックについては、以下のドキュメントに詳しく書かれています。

www.postgresql.jp

これによると、テーブルレベルのロックだけで8種類が存在します。LOCK TABLE文でロックが獲得されるのは言うまでもありませんが、INSERT文やUPDATE文、SELECT文ですら対象のテーブルのロックを獲得します。

さらに、実は以下のような操作でもテーブルのロックが獲得されます。

子テーブルの作成

PostgreSQLの継承機能を利用し子テーブルを作成すると、親テーブルのロックが獲得されます。例として、psqlコマンドで以下のようなSQLを実行してみます。

partition_test=# CREATE TABLE parent ();
partition_test=# BEGIN;
BEGIN
partition_test=# CREATE TABLE child () INHERITS (parent);
CREATE TABLE

ここで別のコンソールからロックの状態を確認してみましょう。PostgreSQLの場合、pg_locksを参照することでロック状態が確認できます。結果を見てみると、parentテーブルのSHARE UPDATE EXCLUSIVEロックが獲得されているのがわかります。

partition_test=# SELECT d.datname, l.locktype, l.relation::regclass, l.mode
FROM pg_locks l LEFT JOIN pg_database d ON l.database = d.oid WHERE l.pid != pg_backend_pid();
    datname     |   locktype    | relation |           mode
----------------+---------------+----------+--------------------------
                | virtualxid    |          | ExclusiveLock
 partition_test | object        |          | AccessShareLock
 partition_test | relation      | 16428    | AccessExclusiveLock
                | transactionid |          | ExclusiveLock
 partition_test | relation      | parent   | ShareUpdateExclusiveLock
(5 行)

パーティションの追加

PostgreSQL 10以降では宣言的パーティショニングがサポートされていますが、パーティションテーブルにパーティションを追加する際に、パーティションテーブルのロックが獲得されます。これもpsqlコマンドで確認してみます。

partition_test=# CREATE TABLE partition (id integer) PARTITION BY LIST (id);
CREATE TABLE
partition_test=# BEGIN;
BEGIN
partition_test=# CREATE TABLE partition_1 PARTITION OF partition FOR VALUES IN (1);
CREATE TABLE

別コンソールのpsqlで確認すると、パーティションテーブルのACCESS EXCLUSIVEロックが獲得されているのが確認できます。

partition_test=# SELECT d.datname, l.locktype, l.relation::regclass, l.mode
FROM pg_locks l LEFT JOIN pg_database d ON l.database = d.oid WHERE l.pid != pg_backend_pid();
    datname     |   locktype    | relation  |        mode
----------------+---------------+-----------+---------------------
                | virtualxid    |           | ExclusiveLock
 partition_test | object        |           | AccessShareLock
 partition_test | relation      | 16434     | AccessExclusiveLock
 partition_test | relation      | partition | AccessExclusiveLock
                | transactionid |           | ExclusiveLock
(5 行)

デッドロックが起こりそうな例

上記のようにPostgreSQLではパーティションの追加などの操作でもロックが発生します。こうした挙動を把握、意識していないと、うっかりデッドロックを引き起こしてしまう場合があります。

前提

あるアプリケーションにユーザとグループが存在し、一人のユーザは複数のグループに所属することができるとします。

これをデータベース上で表現しようとすると、ユーザとグループは多対多の関係なので、ユーザテーブル、グループテーブルと、この2つを結ぶ中間テーブルを作成することになると思います。

f:id:takaram:20200921234402p:plain
ユーザとグループのER図

この中間テーブルはレコード数がとても多くなりそうなので、グループごとにパーティショニングすることにしました。グループテーブルにレコードを挿入するたびに、トリガーで新しいパーティションを追加します。

以上をSQLで表現すると、以下のようになります。

CREATE TABLE users (
  id serial PRIMARY KEY,
  name text
);

CREATE TABLE groups (
  id serial PRIMARY KEY,
  name text
);

CREATE TABLE user_group (
  user_id integer,
  group_id integer
) PARTITION BY LIST (group_id);

CREATE FUNCTION groups_insert_trigger_func()
  RETURNS trigger AS $$
  BEGIN
    EXECUTE 'CREATE TABLE user_group_g' || NEW.id
        || ' PARTITION OF user_group FOR VALUES IN (' || NEW.id || ')';
    RETURN NEW;
  END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER groups_insert_trigger AFTER INSERT ON groups
  FOR EACH ROW EXECUTE FUNCTION groups_insert_trigger_func();

デッドロックの発生

上記のようなテーブル、トリガーを作成した状態で、2つのトランザクションA, Bで以下のような順序で操作を行うとデッドロックが発生します。

トランザクション SQL
A LOCK TABLE user_group;
B INSERT INTO groups (name) VALUES ('group 1');
A LOCK TABLE groups;

トランザクションBでINSERT文を実行すると、

  1. groupsテーブルをロックし行挿入
  2. トリガーでuser_groupのパーティションを作成
  3. user_groupのロック待ち

となり、その後でトランザクションAがgroupsのロックを獲得しようとするとデッドロック状態になってしまいます。

トランザクションBでは見た目上はgroupsテーブルに挿入しているだけなので、これでデッドロックが発生するとは気づきにくそうです。

ここでは例として宣言的パーティショニングを使ったケースを挙げていますが、子テーブルの作成でもロックが発生するため、継承によるパーティショニングでも同様にデッドロックが起こり得ます。ただし宣言的パーティショニングの場合、トランザクションBが獲得しようとするuser_groupのロックはもっとも厳しいACCESS EXCLUSIVEロックなので、LOCK TABLE user_group;SELECT * FROM user_group;などに置き換えても同じくデッドロックしてしまいます。

対策

このようなデッドロックを回避する方法の一つは、テーブルのロック順序に気をつけることです。

上の例でいうと、トランザクションAでuser_groupよりも先にgroupsをロックすると、デッドロックが発生しなくなります。

まとめ

  • 子テーブルの作成(CREATE TABLE ... INHERITS ...)の際、親テーブルがSHARE UPDATE EXCLUSIVEロックされる
  • パーティションの作成(CREATE TABLE ... PARTITION OF ...)の際、大元のパーティションテーブルがACCESS EXCLUSIVEロックされる
Copyright © RAKUS Co., Ltd. All rights reserved.