狠狠撸

狠狠撸Share a Scribd company logo
PostgreSQL 9.3 の新しいロック
モード
- 共有ロックを振り返る -
2013.2.16
笠原 辰仁
去る 2013.1.24ごろ
● PostgreSQLのgitに
「Improve concurrency of foreign key locking」
なるパッチが入る
● 何となく覗くと凄い大きい (106 files changed)
– 実に2年以上かけてようやっと入った改良
– 9.3で一番複雑なパッチではなかろうか?
● マテビューとかBgWorkerとかpostgresql_fdwなどは注目
していたけど、これは何だ?
●
というのがこの話をしようと思ったきっかけです
改良の内容
●
簡潔に言うと
1. 新しい行ロックモードが導入された
? 今までは FOR SHARE と FOR UPDATE の2つ
? 今回新たに FOR KEY SHARE と
FOR NO KEY UPDATE が加わった
2. 外部キーでこの新しいロックモードを使うことで、競合
が減って性能向上となる
? 外部キーの参照テーブルへのINSERT時は、被参照テーブルの該
当キー保持列にFOR KEY SHAREが実施される
?
ユーザが被参照テーブルに対し、キーを変更しない更新処理を
行う場合に自動でFOR NO KEY UPDATEが使われる
? FOR KEY SHARE と FOR NO KEY UPDATEは競合しない!
新しいロックモード
FOR UPDATE FOR NO KEY
UPDATE
FOR SHARE FOR KEY SHARE
FOR UPDATE
× × × ×
FOR NO KEY
UPDATE × × ×
FOR SHARE
× ×
FOR KEY
SHARE ×
FOR NO KEY UPDATE は キー(外部キーとしての被参照)列の更新
を行わない更新時に使われる。
FOR KEY SHARE は、弱いロックで、対象列のキー列が変更され
ないことだけを保証したい場合に使われる。
分かりづらいので、昔を振り返りつつ
●
簡単な仕組みのおさらい
– PostgreSQL は外部キーをトリガで実現している
– 参照テーブル、被参照テーブルについて、
TRIGGER EACH ROWSを仕掛けておき、矛盾す
る更新処理は弾くようになっている
ORDER_ID ITEM_ID DELIVER
1 1 2012-01-01
2 2 2012-01-02
ITEM_ID NAME STOCK
1 ペン 60
2 紙 80
3 寒天 1000
INSERT INTO ORDER
VALUES (1, 4, now());
ITEM_ID の 4は無い!
RI_FKey_check_in
s
制約違反!
ORDERテーブル ITEMテーブル
寄り道
● PostgreSQLで外部キーを設定すると自動的にトリ
ガが付きますが、どんなトリガなのかはシステムカ
タログやsrc/backend/utils/adt/ri_triggers.cを見ると
分かります。
=# SELECT proname, prosrc
   FROM pg_proc p, pg_trigger t
   WHERE p.oid = t.tgfoid AND t.tgrelid = '被参照テーブル'::regclass;
proname | prosrc
----------------------+----------------------
RI_FKey_noaction_del | RI_FKey_noaction_del
RI_FKey_noaction_upd | RI_FKey_noaction_upd
(2 rows)
=# SELECT proname , prosrc
FROM pg_proc p, pg_trigger t
WHERE p.oid = t.tgfoid AND t.tgrelid = '参照テーブル'::regclass;
proname | prosrc
-------------------+-------------------
RI_FKey_check_ins | RI_FKey_check_ins
RI_FKey_check_upd | RI_FKey_check_upd
(2 rows)
PG8.0までの問題
● 参照テーブルへINSERTすると、被参照テーブル
の当該キーのレコードへFOR UPDATEでロック
をかけていた
– 被参照テーブルで、参照テーブルが必要とするレコー
ドが無くなる(キー更新やDELETE)と困るから
ORDER_ID ITEM_ID DELIVER
1 1 2012-01-01
2 2 2012-01-02
ITEM_ID NAME STOCK
1 ペン 60
2 紙 80
3 寒天 1000
INSERT INTO ORDER
VALUES (1, 3, now());
ITEM_ID の 3の変更阻止
RI_FKey_check_ins
DELETE FROM ITEM
WHERE ITEM_ID = 3;
ブロック!
ORDERテーブル ITEMテーブル
PG8.0までの問題
●
そのため、デッドロックの温床になりやすかった
ORDER_ID ITEM_ID DELIVER
1 1 2012-01-01
2 2 2012-01-02
ITEM_ID NAME STOCK
1 ペン 60
2 紙 80
3 寒天 1000
INSERT INTO ORDER
VALUES (1, 1, now());
INSERT INTO ORDER
VALUES (1, 2, now())
INSERT INTO ORDER
VALUES (1, 2, now());
INSERT INTO ORDER
VALUES (1, 1, now())
背後でデッドロック??
そこで、PG8.1から共有ロック導入
● 参照テーブルへのINSERT時には、被参照テーブルへ排他
ではなく共有ロックを取れるように改良
– FOR SHARE ロックモードが導入され、被参照テーブルへの
ロックはこれを使うようになった
– FOR SHARE 同士は競合しない!
ORDER_ID ITEM_ID DELIVER
1 1 2012-01-01
2 2 2012-01-02
ITEM_ID NAME STOCK
1 ペン 60
2 紙 80
3 寒天 1000
INSERT INTO ORDER
VALUES (1, 1, now());
INSERT INTO ORDER
VALUES (1, 2, now())
INSERT INTO ORDER
VALUES (1, 2, now());
INSERT INTO ORDER
VALUES (1, 1, now())
共有ロック同士は競合しない!
更新処理やFOR UPDATEとは
競合
共有ロックの簡単?な仕組み
●
共有ロックは、レコードのヘッダに誰が共有ロックをしてい
るかを記録しておく形で実現
– 各プロセスが保持しているロック情報をメモリに置くと大変な量に
なってしまうため
– そのため、マルチトランザクションとそれに属する仲間たち(メン
バー)みたいな形で実装されている
XMIN XMAX その他 データ部
分
12345 12346 ABC
XMIN XMAX その他 データ部
分
12345 -- ABC
XMIN XMAX その他 データ部
分
12345 2 ABC
XID=12345が
挿入
XID=12346が
FOR SHARE取得
XID=12347も
FOR SHARE取得
これがMultiXID。このメンバーには
XID=12346と12347が含まれる。
共有ロックの簡単?な仕組み
●
XMAXには、マルチトランザクションIDが記録される
– 実態は、pg_multixact/offsets ディレクトリ配下のどのファイルのどのエントリを見るべきかの情報
(MultiXIDから、ファイルとエントリが一意に決まる)
– そこからさらにpg_multixact/members ディレクトリ配下のファイルとエントリへ繋がる
– この中にマルチトランザクションIDが含む実XIDのメンバーが記録されており、これと共有バッファ上
(PGPROCとか)の情報を元に、ロック管理をしている
XMIN XMAX その他 データ部
分
12345 2 ABC
$PGDATA/pg_multixact/offsets/0000 など
$PGDATA/pg_multixact/members/0000 など
MultiXID=2を元にoffset配下の
ファイルとエントリを覗く
offset配下のファイルの該当エン
トリの情報を元に、membersを
見に行く
members配下のファイルに、MultiXID=2が含む
XIDのメンバーが記録されている。
PG9.2までの問題?
●
とはいえ、被参照テーブルには相変わらず更
新を妨げるロックとなっている
– キーを更新しない場合でも競合してしまう???
ORDER_ID ITEM_ID DELIVER
1 1 2012-01-01
2 2 2012-01-02
ITEM_ID NAME STOCK
1 ペン 60
2 紙 80
3 寒天 1000
INSERT INTO ORDER
VALUES (1, 3, now());
ITEM_ID の 3の変更阻止
RI_FKey_check_ins
UPDATE ITEM SET
STOCK = STOCK – 100
WHERE ITEM_ID=3;
ブロック!
ORDERテーブル ITEMテーブル
キーは更新しないんで
勘弁してくれー
PG9.2までの問題?
● そこで新たに FOR KEY SHARE と FOR NO
KEY UPDATE モードが加わった
ORDER_ID ITEM_ID DELIVER
1 1 2012-01-01
2 2 2012-01-02
ITEM_ID NAME STOCK
1 ペン 60
2 紙 80
3 寒天 1000
3 寒天 900
INSERT INTO ORDER
VALUES (1, 3, now());
ITEM_ID の 3の変更阻止
RI_FKey_check_ins
UPDATE ITEM SET
STOCK = STOCK – 100
WHERE ITEM_ID=3;
キー変更しないならOK
ORDERテーブル ITEMテーブル
このときの更新処理では
裏でNO KEY UPDATE モード
となっている
仕組み
●
ロックの管理については、基本的な仕組みは変
わっていない
– 共有トランザクションの仕組みを使う
– ただし、以前までは単純に共有ロックを確保している
メンバー情報だけだったが、加えてロックモード等の
フラグ情報もmembers配下の情報に入っている
– pg_multixact 配下のファイルのクリーンナップ
は、VACUUM FREEZEのタイミング
=# SELECT * FROM
pg_get_multixact_members('6'::xid);
xid | mode
------+-------
1006 | keysh
1010 | keysh
仕組み
●
基本的な仕組みは変わっていない
– 更新時、キー列の更新の有無を確認するルーチン
が追加されており、自動でロックモードを調整し
ている
heap_update()
(snip)
HeapSatisfiesHOTandKeyUpdate(relation, hot_attrs, key_attrs,
&satisfies_hot, &satisfies_key,
&oldtup, newtup);
if (satisfies_key)
{
*lockmode = LockTupleNoKeyExclusive;
mxact_status = MultiXactStatusNoKeyUpdate;
key_intact = true;
MultiXactIdSetOldestMember();
}
else
{
*lockmode = LockTupleExclusive;
mxact_status = MultiXactStatusUpdate;
key_intact = false;
}
(snip)
おわりに
●
地味な新機能ですが、裏ではとても小難しい処理がなされて
います
● 決して派手では無いですが、DBMSの重要部分であるロック
が改良されており、広く恩恵を受ける改善ではないでしょう
か?
● こういう機能改善がある点もPostgreSQLの魅力ですね
● 開発者に感謝しつつ、PostgreSQLを存分に活用していきた
いです
● 本機能のメイン開発者 Alvaro氏のCOMMIT時のコメント↓
● http://www.postgresql.org/message-id/20130123183312.GG4
249@alvh.no-ip.org
I seriously hope that no patch of mine ever becomes this
monstruous again.
小ネタ
- Hookで遊ぼう -
2013.2.16
笠原 辰仁
Hook
● PostgreSQLの内部にはHookがいくつか仕掛けら
れており、ユーザが外部から処理の制御を奪える
ようになっています
● pg_stat_statementsやauto_explainなどは、Hook
の仕組みを活用し、Executorの処理の前後で独
自処理を挟み込んでいます。
– pg_stat_statementsは、SQL情報をピックアップした
り、処理にかかった時間を独自に計っています
● もしHookを使って遊んでみたい場合は
– 簡単なものならauto_explain
– 応用編ならpg_stat_statements
– をそれぞれ参考にすると良いです
折角なので簡単に遊ぶ
● PostgreSQL9.2からは emit_log_hook が入り
ました
– これは、サーバログへの出力前に、仕掛けられて
おりログメッセージの構造体をユーザ側で自由に
改変!することも可能です
– あるいは、独自のファイルへ書き出し、サーバロ
グには書かないなんてことも出来ます
– あまり自由度はありませんが、本気で取り組むと
色々有用なHookかもしれません
– 今回は、これでちょっと遊んでみましょう
– ちなみにHookはたくさんあります。探してみてく
ださい。
ところでエラーメッセージの構造体っ
て?
● src/include/utils/elog.h
typedef struct ErrorData
{
int elevel; /* error level */
bool output_to_server; /* will report to server log? */
bool output_to_client; /* will report to client? */
bool show_funcname; /* true to force funcname inclusion */
bool hide_stmt; /* true to prevent STATEMENT: inclusion */
const char *filename; /* __FILE__ of ereport() call */
int lineno; /* __LINE__ of ereport() call */
const char *funcname; /* __func__ of ereport() call */
const char *domain; /* message domain */
const char *context_domain; /* message domain for context message */
int sqlerrcode; /* encoded ERRSTATE */
char *message; /* primary error message */
char *detail; /* detail error message */
char *detail_log; /* detail error message for server log only */
char *hint; /* hint message */
char *context; /* context message */
char *schema_name; /* name of schema */
char *table_name; /* name of table */
char *column_name; /* name of column */
char *datatype_name; /* name of datatype */
char *constraint_name; /* name of constraint */
int cursorpos; /* cursor index into query string */
int internalpos; /* cursor index into internalquery */
char *internalquery; /* text of internally-generated query */
int saved_errno; /* errno at entry */
} ErrorData;
ついでにエラーコード
● src/backend/utils/errcodes.h
/* Class 00 - Successful Completion */
#define ERRCODE_SUCCESSFUL_COMPLETION MAKE_SQLSTATE('0','0','0','0','0')
/* Class 01 - Warning */
#define ERRCODE_WARNING MAKE_SQLSTATE('0','1','0','0','0')
#define ERRCODE_WARNING_DYNAMIC_RESULT_SETS_RETURNED MAKE_SQLSTATE('0','1','0','0','C')
#define ERRCODE_WARNING_IMPLICIT_ZERO_BIT_PADDING MAKE_SQLSTATE('0','1','0','0','8')
#define ERRCODE_WARNING_NULL_VALUE_ELIMINATED_IN_SET_FUNCTION
MAKE_SQLSTATE('0','1','0','0','3')
#define ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED MAKE_SQLSTATE('0','1','0','0','7')
#define ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED MAKE_SQLSTATE('0','1','0','0','6')
#define ERRCODE_WARNING_STRING_DATA_RIGHT_TRUNCATION MAKE_SQLSTATE('0','1','0','0','4')
#define ERRCODE_WARNING_DEPRECATED_FEATURE MAKE_SQLSTATE('0','1','P','0','1')
(以下略)
本当に簡単に使ってみる
● auto_explainを元に改変してみた
– ユニーク制約エラー(23505)を99999にしてみよう
– edata->sqlerrcode == ERRCODE_UNIQUE_VIOLATION
だったら、
– edata->sqlerrcode = MAKE_SQLSTATE('9','9','9','9','9')
にするだけ???
本当に簡単に使ってみる
● auto_explainを元に改変してみた
– ユニーク制約エラー(23505)を99999にしてみよう
– edata->sqlerrcode == ERRCODE_UNIQUE_VIOLATION
だったら、
– edata->sqlerrcode = MAKE_SQLSTATE('9','9','9','9','9')
にするだけ???
コード本体(log_hook_play)
#include "postgres.h"
#include "utils/elog.h"
PG_MODULE_MAGIC;
static emit_log_hook_type prev_emit_log_hook = NULL;
void _PG_init(void);
void _PG_fini(void);
static void log_hook_play(ErrorData *edata);
/*
* Module load callback
*/
void
_PG_init(void)
{
EmitWarningsOnPlaceholders("log_hook_play");
prev_emit_log_hook = emit_log_hook;
emit_log_hook = log_hook_play;
}
/*
* Module unload callback
*/
void
_PG_fini(void)
{
/* Uninstall hooks. */
emit_log_hook = prev_emit_log_hook;
}
必要なヘッダを読む
PG_MODULE_MAGICをつける
先にいるHookの待避用
独自処理プロト
独自処理をロードした際の初期化処理。
基本的にhookを仕掛けるのみ。
共有バッファなどを間借りする
場合はここでその処理をする
独自処理をアンロードした際の
クリーンナップ処理。
コード本体(log_hook_play)
/*
* log_hook_play: Change SQL error code.
*/
#define MY_ERRCODE_1 MAKE_SQLSTATE('9','9','9','9','9')
static void
log_hook_play(ErrorData *edata)
{
/* Check errcode and replace */
if (edata->sqlerrcode == ERRCODE_UNIQUE_VIOLATION)
edata->sqlerrcode = MY_ERRCODE_1;
}
}
エラーメッセージの構造体の中身を
見て、差し替える
Makefile
MODULE_big = log_hook_play
OBJS = log_hook_play.o
ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
else
subdir = contrib/log_hook_play
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
include $(top_srcdir)/contrib/contrib-global.mk
endif
ContribモジュールのMakefileを
参考に??
EXTENSION対応はそれなりに
難しいかもしれません。
使う
● Make && make install したら、postgresql.confの
設定をする
– Log関連
● logging_collector = on
● log_line_prefix = '[%t][%p][%d][%u][%e]
– 大事なところ
● shared_preload_libraries = 'log_hook_play'
– こうすると、PostgreSQLへの接続時に自動で共有ライブラリがロー
ドされる
● もしくは、接続後に LOAD 'log_hook_play'; でもOK
試す
[2013-02-16 04:08:11 JST][72457][postgres][postgres][00000] LOG: 00000: statement: INSERT into test values(1);
[2013-02-16 04:08:11 JST][72457][postgres][postgres][00000] LOCATION: exec_simple_query, postgres.c:890
[2013-02-16 04:08:11 JST][72457][postgres][postgres][23505] ERROR: 23505: duplicate key value violates unique constraint "uqi"
[2013-02-16 04:08:11 JST][72457][postgres][postgres][23505] DETAIL: Key (c1)=(1) already exists.
[2013-02-16 04:08:11 JST][72457][postgres][postgres][23505] LOCATION: _bt_check_unique, nbtinsert.c:398
[2013-02-16 04:08:11 JST][72457][postgres][postgres][23505] STATEMENT: INSERT into test values(1);
[2013-02-16 04:08:28 JST][72457][postgres][postgres][00000] LOG: 00000: statement: LOAD 'log_hook_play';
[2013-02-16 04:08:28 JST][72457][postgres][postgres][00000] LOCATION: exec_simple_query, postgres.c:890
[2013-02-16 04:08:31 JST][72457][postgres][postgres][00000] LOG: 00000: statement: INSERT into test values(1);
[2013-02-16 04:08:31 JST][72457][postgres][postgres][00000] LOCATION: exec_simple_query, postgres.c:890
[2013-02-16 04:08:31 JST][72457][postgres][postgres][99999] ERROR: 99999: duplicate key value violates unique constraint "uqi"
[2013-02-16 04:08:31 JST][72457][postgres][postgres][99999] DETAIL: Key (c1)=(1) already exists.
[2013-02-16 04:08:31 JST][72457][postgres][postgres][99999] LOCATION: _bt_check_unique, nbtinsert.c:398
[2013-02-16 04:08:31 JST][72457][postgres][postgres][99999] STATEMENT: INSERT into test values(1);
エラーコード変わった
おわりに
●
お作法などは癖がありますが、分かってしま
えば後は簡単?
● ただし、Hook処理は容易にPostgreSQLをク
ラッシュさせることもできるので、注意は必
要です
●
内部の有意義な情報が簡単に扱えますので、
トライしてみてはいかがでしょうか?

More Related Content

Postgre sql9.3 newlockmode_and_etc