SlideShare a Scribd company logo
1 of 36
Download to read offline
Copyright © 2015 NTT DATA Corporation
2015年1月31日
株式会社NTT DATA
NTT DATA と PostgreSQL が挑んだ総力戦 裏話
~ セミナでは話せなかったこと ~
2Copyright © 2015 NTT DATA Corporation
はじめに
本日のお話は、2014/12/5 の PostgreSQL カンファレンスの基調講演で
話せなかった技術的(特にトラブルにまつわる)な話のトピックを裏話と称して
お伝えするものです。
前半は先日の基調講演のダイジェスト、後半は特に手ごわかった(でも技術的には
興味深い)2つのトラブルについて、解決に至る過程と併せて紹介します。
PostgreSQLや周辺ツールのコアに関わる未知のバグに遭遇したケース、
そしてその解析などに関して、Hackers ML を除き、あまり世の中には
情報がないと思います。
有益な情報ではないかもしれませんが、何かのヒントになれば幸いです。
3Copyright © 2015 NTT DATA Corporation
PostgreSQLカンファレンス 2014 の内容を振り返り。
以下の資料と同内容です
www.slideshare.net/hadoopxnttdata/ntt-data-postgresql
4Copyright © 2015 NTT DATA Corporation
今日の本題
様々なトラブルがありました。
特に手を焼いた 2つ のトラブルを紹介。
・ SEGVでPostgreSQLが落ちた
・ SELECT結果が間違っている。
共通しているのは、いずれもPostgreSQL、周辺ツールの
バグであったこと。いずれも解決済みです。
5Copyright © 2015 NTT DATA Corporation
SEGVでPostgreSQLが落ちた
1. ある日、本番環境での試験中にPostgreSQLがダウンした
2. とあるSQLが実行された際に、Segmentation Fault が発生していた
 PostgreSQLは、あるバックエンドプロセスがSEGVを出すと、インスタンス全体が落ち
ますね
3. 復旧後、同じSQLを発行したら、再度 PostgreSQL がダウン
4. とりあえず、そのSQLの発行を停止して様子見(その後はダウンせず)
SQLは特に変哲もないもの。今までは特に問題なかった。
では、直前に何かしたか?
・・・・ そういえばパーティションのローテートをした。
あと残されたのは core ファイル。
まずはここから解析が始まった。
6Copyright © 2015 NTT DATA Corporation
coreの解析
実行計画作成中に統計情報を見ている箇所でクラッシュしている
(gdb) bt
#0 pg_detoast_datum (datum=0x1d7f0f2c28ee3) at fmgr.c:2240
#1 0x000000000069c7b4 in numeric_lt (fcinfo=0x7fffaee58560) at numeric.c:1408
#2 0x000000000071a8c0 in FunctionCall2Coll (flinfo=<value optimized out>, collation=<value optimized out>,
arg1=<value optimized out>, arg2=<value optimized out>)
at fmgr.c:1326
#3 0x00000000006c320f in ineq_histogram_selectivity (root=0x107ab28, vardata=0x7fffaee58a80,
opproc=0x7fffaee589f0, isgt=0 '¥000', constval=17280952, consttype=1700)
at selfuncs.c:834
#4 0x00000000006c3a8c in scalarineqsel (root=0x107ab28, operator=<value optimized out>, isgt=0 '¥000',
vardata=0x7fffaee58a80, constval=17280952,
consttype=<value optimized out>) at selfuncs.c:558
#5 0x00000000006c470c in scalarltsel (fcinfo=<value optimized out>) at selfuncs.c:1021
#6 0x000000000071cdb7 in OidFunctionCall4Coll (functionId=<value optimized out>, collation=0, arg1=17279784,
arg2=1754, arg3=17280664, arg4=0) at fmgr.c:1682
#7 0x00000000005fc0e5 in restriction_selectivity (root=0x107ab28, operatorid=1754, args=0x107ae98, inputcollid=0,
varRelid=0) at plancat.c:1021
#8 0x00000000005d079e in clause_selectivity (root=0x107ab28, clause=0x107b040, varRelid=0, jointype=JOIN_INNER,
sjinfo=0x0) at clausesel.c:668
#9 0x00000000005d0274 in clauselist_selectivity (root=0x107ab28, clauses=<value optimized out>, varRelid=0,
jointype=JOIN_INNER, sjinfo=0x0) at clausesel.c:108
#10 0x00000000005d1dad in set_baserel_size_estimates (root=0x107ab28, rel=0x107b120) at costsize.c:3411
#11 0x00000000005cec4a in set_plain_rel_size (root=0x107ab28, rel=0x107b120, rti=1, rte=0x1044090) at
allpaths.c:361
7Copyright © 2015 NTT DATA Corporation
coreの解析
ちょっと追うと、何故か異なるテーブルの統計情報を見ていた・・・
(gdb) frame 3
#3 0x00000000006c320f in ineq_histogram_selectivity (root=0x107ab28, vardata=0x7fffaee58a80,
opproc=0x7fffaee589f0, isgt=0 '¥000', constval=17280952, consttype=1700)
at selfuncs.c:834
(gdb) p *vardata
$15 = {var = 0x107ae28, rel = 0x107b120, statsTuple = 0x7f2bd2ee1da0, freefunc = 0x7f2bd5921830
<FreeHeapTuple>, vartype = 1700, atttype = 1700, atttypmod = -1,
isunique = 0 '¥000'}
(gdb) p *(Form_pg_statistic) ((char *) vardata->statsTuple->t_data + vardata->statsTuple->t_data->t_hoff)
$14 = {starelid = 299892885, staattnum = 1, stainherit = 0 '¥000', stanullfrac = 0, stawidth = 8, stadistinct
= -1, stakind1 = 2, stakind2 = 3, stakind3 = 0,
stakind4 = 0, stakind5 = 0, staop1 = 2062, staop2 = 2062, staop3 = 0, staop4 = 0, staop5 = 0}
問題のSQLで参照しているテーブルのOID は 299106453。numeric型
(oid=1700)の列に対し、WHERE句で col < xxxx の条件を使っていた。
しかし、上記のcoreからは
OID = 299892885 のテーブルのtimestamp型(oid = 2062)の列の統計情報を
参照し、可変長データのアクセスロジックに行ってしまい、クラッシュしていた。
8Copyright © 2015 NTT DATA Corporation
pg_dbms_statsで問題がある?
1. 統計情報固定化ツール(pg_dbms_stats)を使っている
 まずはこいつを怪しむ
2. 固定化した統計情報の中身がまずいのか?
 中身を確認するも、問題なし
3. 別の試験環境に当該の統計情報を持ってきてSQLを
発行しても大丈夫
当該環境でしか発生しない様子・・
ここまでの情報とコード解析を行った結果、特定の条件において、
pg_dbms_statsが誤った統計情報を返していることが分かった。
9Copyright © 2015 NTT DATA Corporation
その前に、pg_dbms_statsの内部について
relid attnum stats
固定化した統計情報 本来の統計情報
pg_dbms_stats
Hash
relid attnum stats
Planning Phase Execute Phase
バックエンドプロセス
のローカルメモリ
10Copyright © 2015 NTT DATA Corporation
その前に、pg_dbms_statsの内部について
relid attnum stats
固定化した統計情報 本来の統計情報
pg_dbms_stats
Hash
Hook
relid attnum stats
Planning Phase
統計情報取得
Execute Phase
① 統計情報の取得時、pg_dbms_statsが有
効になっていると、Hook経由で
pg_dbms_statsのロジックに入る
バックエンドプロセス
のローカルメモリ
11Copyright © 2015 NTT DATA Corporation
その前に、pg_dbms_statsの内部について
relid attnum stats
固定化した統計情報 本来の統計情報
pg_dbms_stats
Hash
Hook
relid attnum stats
Planning Phase
統計情報取得
Execute Phase
②固定化された統計情報と本来の統計情報
(pg_statistics)をマージし、固定化されたもの
があれば、それを使い、なければ本来の統計
情報を使う
バックエンドプロセス
のローカルメモリ
12Copyright © 2015 NTT DATA Corporation
その前に、pg_dbms_statsの内部について
relid attnum stats
固定化した統計情報 本来の統計情報
pg_dbms_stats
Hash
Hook
relid attnum stats
Planning Phase
統計情報取得
Execute Phase
バックエンドプロセス
のローカルメモリ
③次回以降の参照のため、取得した
統計情報はローカルメモリのハッシュ
へ格納しておく
13Copyright © 2015 NTT DATA Corporation
その前に、pg_dbms_statsの内部について
relid attnum stats
固定化した統計情報 本来の統計情報
pg_dbms_stats
Hash
Hook
relid attnum stats
Planning Phase
統計情報取得
Execute Phase
バックエンドプロセス
のローカルメモリ
④統計情報を取得し、Hookのポ
イントへ返る
14Copyright © 2015 NTT DATA Corporation
その前に、pg_dbms_statsの内部について
relid attnum stats
固定化した統計情報 本来の統計情報
pg_dbms_stats
Hash
Hook
relid attnum stats
Planning Phase
統計情報取得
Execute Phase
バックエンドプロセス
のローカルメモリ
①’ 二回目以降、Hashに統計情
報があれば、それを使う
relid, attnum, stats
15Copyright © 2015 NTT DATA Corporation
Hashの仕組み
Hash
Hashは ハッシュキーと値の形でデータを管理。ロック情報管理などで使われている。
これを外部モジュール(contribやbgworker)等でも使えるよう、汎用の
ユーティティリティとしてPostgreSQLのコアが提供している。
・ Hash化、マッチングは組み込み or ユーザ定義を利用
・ Hashからの検索はキーサーチ、あるいはSeq_search
・ ローカル or 共有バッファのどちらにもおける
・ 共有バッファの場合は排他制御用のロック構造体を指定
(パーティション化もサポート)
・ 上記はHashの作成時に指定
hash_fn(ユーザ定義 or
組み込み(oid, string など)) bucket
bucket
bucket
hash_serach()など
match_fn(ユーザ定義 or
組み込み(memcmpなど))
key
+
body key
+
body
key
該当のbucketの中から、
規定の方法で
キーマッチングを実施
16Copyright © 2015 NTT DATA Corporation
今回のバグは・・
仕組みとしてはrelationのOID(relid)をキーとしていたが・・Hash_createの
引数フラグにHASH_FUNCTIONを指定していなかった・・
MemSet(&ctl, 0, sizeof(ctl));
ctl.keysize = sizeof(Oid);
ctl.entrysize = sizeof(StatsRelationEntry);
ctl.hash = oid_hash;
ctl.hcxt = CacheMemoryContext;
hash = hash_create("dbms_stats relation statistics cache",
MAX_REL_CACHE,
&ctl, HASH_ELEM | HASH_CONTEXT | HASH_FUNCTION );
rel_stats = hash;
この部分が無かった
pg_dbms_stats.c より
17Copyright © 2015 NTT DATA Corporation
今回のバグは・・
そのため、ハッシュ化の関数として想定していたoid_hash は使われず、OIDを
文字列としてハッシュ化する string_hashが設定されていた。
/* Initialize the hash header, plus a copy of the table name */
hashp = (HTAB *) DynaHashAlloc(sizeof(HTAB) + strlen(tabname) +1);
MemSet(hashp, 0, sizeof(HTAB));
hashp->tabname = (char *) (hashp + 1);
strcpy(hashp->tabname, tabname);
if (flags & HASH_FUNCTION)
hashp->hash = info->hash;
else
hashp->hash = string_hash; /* default hash function */
hash化の関数にstring用の
関数が選択される
src/backend/utils/hash/dynahash.c より
18Copyright © 2015 NTT DATA Corporation
今回のバグは・・
そしてHashサーチ時のマッチング手段も文字列として設定されていた
/*
* If you don't specify a match function, it defaults to string_compare if
* you used string_hash (either explicitly or by default) and to memcmp
* otherwise. (Prior to PostgreSQL 7.4, memcmp was always used.)
*/
if (flags & HASH_COMPARE)
hashp->match = info->match;
else if (hashp->hash == string_hash)
hashp->match = (HashCompareFunc) string_compare;
else
hashp->match = memcmp;
src/backend/utils/hash/dynahash.c より
19Copyright © 2015 NTT DATA Corporation
今回のバグは・・
・ relid = 299106453 ==> 文字列で見てしまうと ==> 0x11d40095
と
・ relid = 299892885 ==> 文字列で見てしまうと ==> 0x11e00095
となる。ハッシュ化や文字列マッチング(比較)では0x00を終端として扱い、
上記2つは「95」として同じものとなってしまう・・・つまり・・
Hash
hash_string
bucket
bucket
bucket
hash_serach()
string_cmp(0x95, 0x95)
relid = 299892885
stats = XXXXX
stats =
XXXXX
key = relid =
299106453
0x95で
hash化
0x95で
hash化
relid = 299106453ではなく
relid = 299892885の統計情報を返す
20Copyright © 2015 NTT DATA Corporation
解決
というわけで、突然SEGVが発生した理由がわかった。
・ OIDに0x00を含み、かつその下位バイトが同じになるテーブルを
同じクライアントが読む時にだけ発生
・ ハッシュは各クライアントのローカルメモリにあるから
・ 突然発生したのは、パーティションローテで偶然にも上記に
該当するテーブルを読む状況が揃ってしまったため
・ 別環境で再現しなかったのは、pg_dbms_statsの統計情報の
export/importは、OIDは引き連れていかないから
修正は、hash_create()のフラグに正しく HASH_FUNCTIONを立てるのみ。
coreが無かったら解析は難航していた。最悪、迷宮入りになった可能性
もある・・
21Copyright © 2015 NTT DATA Corporation
SELECT結果が間違っている !?
1. 試験環境にて、AP側でSELECT結果がおかしい事象が出た。
2. AP側のログ解析から
「SELECT FOR UPDATEで未処理ステータスの行を取得し、ロックした
行のステータスを処理済みにUPDATEしている。この処理において、
なぜか処理済みのステータスの行をSELECT FOR UPDATEで取得し
てしまう」
という事象に見えた。
3. たまに出ていた。1日1回出るか出ないかの頻度。
SQLは特に変哲もないもの。
PostgreSQLに原因があるというが、前例がない・・
APのバグでは?いや、JDBC?JVM?・・
容疑者も当時の状況では絞り切れていない
22Copyright © 2015 NTT DATA Corporation
切り分けしよう
• 幸い、試験環境でも出ていたので、発生時と同様の業務APも使えるし、
テストデータもある
• まず、PostgreSQL <-> JDBC <-> AP で切り分けをする
• tcpdumpやWiresharkでNW上のデータをキャプチャする案もあったが、時間の都合
と環境の問題で、とりあえず見送り
• 当該の事象が発生した業務処理を高頻度で行い加速試験
PostgreSQL JDBC 業務AP
PostgreSQL側でプローブを
仕込む。ここで検知できれ
ばPostgreSQLがアウト
(詳細は次項)
JDBCのResultSetにプロー
ブを仕込む。ここで検知で
きたらJDBCがアウト
(結果的にこれは未実施)
APのResultSetにプローブ
を仕込む。ここで検知でき
たらJDBCがアウト
23Copyright © 2015 NTT DATA Corporation
PostgreSQLを容疑者と仮定してのプローブ
1. 発生するとAPが停止する可能性もあるので、SELECT条件と矛盾するデー
タは弾きつつ、検知もしたい
2. フィルタ関数で当該SQLを囲ってしまおう
3. Limit & OFFSETを使っているので、負荷も低い
SELECT …
FROM …
WHERE … LIMIT ..
FOR UPDATE;
SELECT * FROM
(
SELECT …
FROM …
WHERE … LIMIT ..
) s1
WHERE check_filter(s1.xxx)
FOR UPDATE
check_filter()は、引数をチェックして、サブクエリの
条件と矛盾する行が来たら falseを出しつつ、
内部でRAISE LOG ‘inavalid status xxxx’ を出力
Before
After
24Copyright © 2015 NTT DATA Corporation
出た
異常検知メッセージがPostgreSQLのフィルタ関数で出てしまった。
つまりPostgreSQLが黒だった。
急いで手元環境で再現できるよう、再現パターンを絞り込みつつ、
APの該当処理をJavaで作成。結果的に、手元でも再現した。
さらにミニマムなテストセットにより、psql 上でも確認できた・・・・
絞り込めた条件は、
1. パーティショニングされており、
2. 部分インデックスを使ったインデックススキャンを経由した
3. UPDATEとFOR UPDATE が競合する
こと。
ここからはコード解析も併せて問題の特定へ。
25Copyright © 2015 NTT DATA Corporation
ヒントだったもの
実行計画を見ると、IndexScanの条件(IndexCond/Filter)がおかしい
(事項から)
ソースコードとREADME(src/backend/executor/README など)から、
部分インデックスの際の特別な最適化のコードがあることが分かる。
これらをヒントに、仮説と試験をして、原因が解明できた。
26Copyright © 2015 NTT DATA Corporation
今回のバグは・・・
PostgreSQLのオプティマイザの問題だった
部分インデックス経由のインデックスアクセスをする際、部分インデックスの定
義と同様のWHERE条件は、実行時に省略する最適化がされる
-> 部分インデックスのエントリには、その条件に合致するものしかないため
postgres=# EXPLAIN SELECT * FROM tbl WHERE c1 < 100;
QUERY PLAN
-----------------------------------------------------------
Index Scan using tbl_idx2 on tbl (cost=0.00..208.79 rows=100 width=10)
Index Cond: (c1 < 100)
(2 rows)
postgres=# EXPLAIN SELECT * FROM tbl WHERE (c3 = '0' OR c3 = '1' OR c3 = '2');
QUERY PLAN
-----------------------------------------------------------
Index Scan using tbl_idx on tbl (cost=0.00..114.34 rows=305 width=10)
(1 row)
Indexのスキャン
条件が無い!
27Copyright © 2015 NTT DATA Corporation
今回のバグは・・・
PostgreSQLのオプティマイザの問題だった
PostgreSQLでは、FOR UPDATE やUPDATEなどの行ロックを必要とする場合、
SELECT結果についてロック取得時に改めて再確認(EvalPlanQual)する
-> ロック取得までにSELECT対象外のデータへ更新されている可能性があるので
postgres=# EXPLAIN SELECT * FROM tbl WHERE (c3 = '0' OR c3 = '1' OR c3 = '2') FOR
UPDATE;
QUERY PLAN
-----------------------------------------------------------------
----
LockRows (cost=0.00..117.39 rows=305 width=16)
-> Index Scan using tbl_idx on tbl (cost=0.00..114.34 rows=305 width=16)
Filter: ((c3 = '0'::bpchar) OR (c3 = '1'::bpchar) OR (c3 = '2'::bpchar))
(3 rows)
そのため、部分インデックスを使っていたとしても、SELECT FOR UPDATE時
にはWHERE句条件の省略最適化はしないようにしている・・・はずだった。
Indexのスキャン
条件がある!
28Copyright © 2015 NTT DATA Corporation
今回のバグは・・・
ところが・・
postgres=# EXPLAIN SELECT * FROM tbl WHERE (c3 = '0' OR c3 = '1' OR c3 = '2') FOR UPDATE;
QUERY PLAN
----------------------------------------------------------------------
LockRows (cost=0.00..198.78 rows=337 width=23)
-> Result (cost=0.00..195.41 rows=337 width=23)
-> Append (cost=0.00..195.41 rows=337 width=23)
-> Index Scan using tbl_idx on tbl (cost=0.00..114.34 rows=305 width=20)
-> Index Scan using tbl_c1_c3_c2_idx on tbl_c1 tbl (cost=0.00..40.54 rows=16 width=54)
-> Index Scan using tbl_c2_c3_c2_idx on tbl_c2 tbl (cost=0.00..40.54 rows=16 width=54)
Indexのスキャン
条件が無い!
29Copyright © 2015 NTT DATA Corporation
今回のバグは・・・
パーティションの子テーブル部分については、ただしく SELECT FOR UPDATEさ
れているかどうかの判定がされていなかった・・
本来ならPlannerInfoに紐づくPlanRowMarks(どのテーブルがFOR UPDATE等
の対象かを示す情報)を元にFOR UPDATEの有無を判定をすべきだったのが
PlannerInfo->Query を元に判定していた・・・
Tom Lane 曰く、”thinko (うっかり勘違い)” のミス。
【本バグの修正コミット】
http://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=cd63c5
7e5cbfc16239aa6837f8b7043a721cdd28
30Copyright © 2015 NTT DATA Corporation
今回のバグは・・・
つまり子テーブルに関しては FOR UPDATE対象外と判定され、部分インデック
スの最適化対象となり・・
最終的に、子テーブルへの部分インデックス経由のアクセスの際、直前に更新
された結果でも誤って返却していた。
id status other
1 0 ZZ
2 0 ZZ
3 0 ZZ
1 9 ZZ
① ある処理が
UPDATE tbl SET
status = 9
WHERE status = 0
ORDER BY id LIMIT 1;
を実施
② 別の処理が
SELECT * FROM tbl
WHERE status = 0
ORDER BY id LIMIT 1;
を実施
①の更新が終わるまで
待つ
31Copyright © 2015 NTT DATA Corporation
今回のバグは・・・
つまり子テーブルに関しては FOR UPDATE対象外と判定され、部分インデック
スの最適化対象となり・・
最終的に、子テーブルへの部分インデックス経由のアクセスの際、直前に更新
された結果でも誤って返却していた。
id status other
1 0 ZZ
2 0 ZZ
3 0 ZZ
1 9 ZZ
③ COMMITする。 ④ SELECT * FROM tbl
WHERE status = 0
ORDER BY id LIMIT 1;
が走る
更新結果を参照する。
32Copyright © 2015 NTT DATA Corporation
今回のバグは・・・
つまり子テーブルに関しては FOR UPDATE対象外と判定され、部分インデック
スの最適化対象となり・・
最終的に、子テーブルへの部分インデックス経由のアクセスの際、直前に更新
された結果でも誤って返却していた。
id status other
1 0 ZZ
2 0 ZZ
3 0 ZZ
1 9 ZZ
⑤ SELECT * FROM tbl
WHERE status = 0
ORDER BY id LIMIT 1;
の結果
status = 9 のものが
返ってくる
本来であれば WHERE句 条件の
status = 0 の再確認をすべきだが
それをしないために、status = 9
であっても返してしまう
33Copyright © 2015 NTT DATA Corporation
解決
というわけで、SELECT結果が間違っていた理由がわかった。
・ 再現頻度の低さは、当該のSQLは FOR UPDATE NOWAIT をしており、
よりシビアな条件だったから
一応、歯止めのフィルタ関数もあり、かつ発生条件に該当するSQLはそ
こだけだった。
(パーティションかつ部分インデックスを使っているSQLなので、絞り込み
やすいことが幸いだった・・)
34Copyright © 2015 NTT DATA Corporation
振り返って
エンタープライズ、に限りませんが、
深刻な問題は根本原因を特定すること
= ホワイトボックスとして明確にすることが大切です。
今回の事象も、根本原因が分かったため、
・なぜ今まで発生頻度が低かったか?
・不具合の発生影響が他にないのか?
・検討した対処が本当に正しいのか?
を見極めることができました。
PostgreSQLのコードには、親切なREADMEやコメントが多数あります。
これらの情報やcoreの情報、問題発生状況を元に、地道に検査していけば
多くの場合は何とかなります・・多分。
bugs ML などを通じてコミュニティに報告するのもアリです。ただし、必ず
再現資材(self-contained test case)を添えましょう。
35Copyright © 2015 NTT DATA Corporation
ご清聴ありがとうございました!
Copyright © 2011 NTT DATA Corporation
Copyright © 2015 NTT DATA Corporation 本資料には、当社の秘密情報が含まれております。当社の許可なく第三者へ開示することはご遠慮ください。

More Related Content

Viewers also liked

[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata
[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata
[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata
Insight Technology, Inc.
 

Viewers also liked (20)

PostgreSQLアーキテクチャ入門(PostgreSQL Conference 2012)
PostgreSQLアーキテクチャ入門(PostgreSQL Conference 2012)PostgreSQLアーキテクチャ入門(PostgreSQL Conference 2012)
PostgreSQLアーキテクチャ入門(PostgreSQL Conference 2012)
 
10大ニュースで振り返るpg con2013
10大ニュースで振り返るpg con201310大ニュースで振り返るpg con2013
10大ニュースで振り返るpg con2013
 
10大ニュースで振り返るPGCon2015
10大ニュースで振り返るPGCon201510大ニュースで振り返るPGCon2015
10大ニュースで振り返るPGCon2015
 
perfを使ったPostgreSQLの解析(後編)
perfを使ったPostgreSQLの解析(後編)perfを使ったPostgreSQLの解析(後編)
perfを使ったPostgreSQLの解析(後編)
 
pg_trgmと全文検索
pg_trgmと全文検索pg_trgmと全文検索
pg_trgmと全文検索
 
pg_bigmを用いた全文検索のしくみ(前編)
pg_bigmを用いた全文検索のしくみ(前編)pg_bigmを用いた全文検索のしくみ(前編)
pg_bigmを用いた全文検索のしくみ(前編)
 
perfを使ったPostgreSQLの解析(前編)
perfを使ったPostgreSQLの解析(前編)perfを使ったPostgreSQLの解析(前編)
perfを使ったPostgreSQLの解析(前編)
 
PostreSQL監査
PostreSQL監査PostreSQL監査
PostreSQL監査
 
使ってみませんか?pg_hint_plan
使ってみませんか?pg_hint_plan使ってみませんか?pg_hint_plan
使ってみませんか?pg_hint_plan
 
PostgreSQL: XID周回問題に潜む別の問題
PostgreSQL: XID周回問題に潜む別の問題PostgreSQL: XID周回問題に潜む別の問題
PostgreSQL: XID周回問題に潜む別の問題
 
PostgreSQL9.3新機能紹介
PostgreSQL9.3新機能紹介PostgreSQL9.3新機能紹介
PostgreSQL9.3新機能紹介
 
PostgreSQL replication
PostgreSQL replicationPostgreSQL replication
PostgreSQL replication
 
JSONBはPostgreSQL9.5でいかに改善されたのか
JSONBはPostgreSQL9.5でいかに改善されたのかJSONBはPostgreSQL9.5でいかに改善されたのか
JSONBはPostgreSQL9.5でいかに改善されたのか
 
まずやっとくPostgreSQLチューニング
まずやっとくPostgreSQLチューニングまずやっとくPostgreSQLチューニング
まずやっとくPostgreSQLチューニング
 
[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata
[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata
[D31] PostgreSQLでスケールアウト構成を構築しよう by Yugo Nagata
 
PostgreSQLクエリ実行の基礎知識 ~Explainを読み解こう~
PostgreSQLクエリ実行の基礎知識 ~Explainを読み解こう~PostgreSQLクエリ実行の基礎知識 ~Explainを読み解こう~
PostgreSQLクエリ実行の基礎知識 ~Explainを読み解こう~
 
バックアップと障害復旧から考えるOracle Database, MySQL, PostgreSQLの違い - Database Lounge Tokyo #2
バックアップと障害復旧から考えるOracle Database, MySQL, PostgreSQLの違い - Database Lounge Tokyo #2バックアップと障害復旧から考えるOracle Database, MySQL, PostgreSQLの違い - Database Lounge Tokyo #2
バックアップと障害復旧から考えるOracle Database, MySQL, PostgreSQLの違い - Database Lounge Tokyo #2
 
PostgreSQLの冗長化について
PostgreSQLの冗長化についてPostgreSQLの冗長化について
PostgreSQLの冗長化について
 
PostgreSQLアーキテクチャ入門(INSIGHT OUT 2011)
PostgreSQLアーキテクチャ入門(INSIGHT OUT 2011)PostgreSQLアーキテクチャ入門(INSIGHT OUT 2011)
PostgreSQLアーキテクチャ入門(INSIGHT OUT 2011)
 
Migr8.rb チュートリアル
Migr8.rb チュートリアルMigr8.rb チュートリアル
Migr8.rb チュートリアル
 

More from NTT DATA OSS Professional Services

More from NTT DATA OSS Professional Services (20)

Global Top 5 を目指す NTT DATA の確かで意外な技術力
Global Top 5 を目指す NTT DATA の確かで意外な技術力Global Top 5 を目指す NTT DATA の確かで意外な技術力
Global Top 5 を目指す NTT DATA の確かで意外な技術力
 
Spark SQL - The internal -
Spark SQL - The internal -Spark SQL - The internal -
Spark SQL - The internal -
 
Apache Kafkaって本当に大丈夫?~故障検証のオーバービューと興味深い挙動の紹介~
Apache Kafkaって本当に大丈夫?~故障検証のオーバービューと興味深い挙動の紹介~Apache Kafkaって本当に大丈夫?~故障検証のオーバービューと興味深い挙動の紹介~
Apache Kafkaって本当に大丈夫?~故障検証のオーバービューと興味深い挙動の紹介~
 
Hadoopエコシステムのデータストア振り返り
Hadoopエコシステムのデータストア振り返りHadoopエコシステムのデータストア振り返り
Hadoopエコシステムのデータストア振り返り
 
HDFS Router-based federation
HDFS Router-based federationHDFS Router-based federation
HDFS Router-based federation
 
PostgreSQL10を導入!大規模データ分析事例からみるDWHとしてのPostgreSQL活用のポイント
PostgreSQL10を導入!大規模データ分析事例からみるDWHとしてのPostgreSQL活用のポイントPostgreSQL10を導入!大規模データ分析事例からみるDWHとしてのPostgreSQL活用のポイント
PostgreSQL10を導入!大規模データ分析事例からみるDWHとしてのPostgreSQL活用のポイント
 
Apache Hadoopの新機能Ozoneの現状
Apache Hadoopの新機能Ozoneの現状Apache Hadoopの新機能Ozoneの現状
Apache Hadoopの新機能Ozoneの現状
 
Distributed data stores in Hadoop ecosystem
Distributed data stores in Hadoop ecosystemDistributed data stores in Hadoop ecosystem
Distributed data stores in Hadoop ecosystem
 
Structured Streaming - The Internal -
Structured Streaming - The Internal -Structured Streaming - The Internal -
Structured Streaming - The Internal -
 
Apache Hadoopの未来 3系になって何が変わるのか?
Apache Hadoopの未来 3系になって何が変わるのか?Apache Hadoopの未来 3系になって何が変わるのか?
Apache Hadoopの未来 3系になって何が変わるのか?
 
Apache Hadoop and YARN, current development status
Apache Hadoop and YARN, current development statusApache Hadoop and YARN, current development status
Apache Hadoop and YARN, current development status
 
HDFS basics from API perspective
HDFS basics from API perspectiveHDFS basics from API perspective
HDFS basics from API perspective
 
SIerとオープンソースの美味しい関係 ~コミュニティの力を活かして世界を目指そう~
SIerとオープンソースの美味しい関係 ~コミュニティの力を活かして世界を目指そう~SIerとオープンソースの美味しい関係 ~コミュニティの力を活かして世界を目指そう~
SIerとオープンソースの美味しい関係 ~コミュニティの力を活かして世界を目指そう~
 
20170303 java9 hadoop
20170303 java9 hadoop20170303 java9 hadoop
20170303 java9 hadoop
 
ブロックチェーンの仕組みと動向(入門編)
ブロックチェーンの仕組みと動向(入門編)ブロックチェーンの仕組みと動向(入門編)
ブロックチェーンの仕組みと動向(入門編)
 
Application of postgre sql to large social infrastructure jp
Application of postgre sql to large social infrastructure jpApplication of postgre sql to large social infrastructure jp
Application of postgre sql to large social infrastructure jp
 
Application of postgre sql to large social infrastructure
Application of postgre sql to large social infrastructureApplication of postgre sql to large social infrastructure
Application of postgre sql to large social infrastructure
 
Apache Hadoop 2.8.0 の新機能 (抜粋)
Apache Hadoop 2.8.0 の新機能 (抜粋)Apache Hadoop 2.8.0 の新機能 (抜粋)
Apache Hadoop 2.8.0 の新機能 (抜粋)
 
データ活用をもっともっと円滑に! ~データ処理・分析基盤編を少しだけ~
データ活用をもっともっと円滑に!~データ処理・分析基盤編を少しだけ~データ活用をもっともっと円滑に!~データ処理・分析基盤編を少しだけ~
データ活用をもっともっと円滑に! ~データ処理・分析基盤編を少しだけ~
 
商用ミドルウェアのPuppet化で気を付けたい5つのこと
商用ミドルウェアのPuppet化で気を付けたい5つのこと商用ミドルウェアのPuppet化で気を付けたい5つのこと
商用ミドルウェアのPuppet化で気を付けたい5つのこと
 

NTT DATA と PostgreSQL が挑んだ総力戦 裏話

  • 1. Copyright © 2015 NTT DATA Corporation 2015年1月31日 株式会社NTT DATA NTT DATA と PostgreSQL が挑んだ総力戦 裏話 ~ セミナでは話せなかったこと ~
  • 2. 2Copyright © 2015 NTT DATA Corporation はじめに 本日のお話は、2014/12/5 の PostgreSQL カンファレンスの基調講演で 話せなかった技術的(特にトラブルにまつわる)な話のトピックを裏話と称して お伝えするものです。 前半は先日の基調講演のダイジェスト、後半は特に手ごわかった(でも技術的には 興味深い)2つのトラブルについて、解決に至る過程と併せて紹介します。 PostgreSQLや周辺ツールのコアに関わる未知のバグに遭遇したケース、 そしてその解析などに関して、Hackers ML を除き、あまり世の中には 情報がないと思います。 有益な情報ではないかもしれませんが、何かのヒントになれば幸いです。
  • 3. 3Copyright © 2015 NTT DATA Corporation PostgreSQLカンファレンス 2014 の内容を振り返り。 以下の資料と同内容です www.slideshare.net/hadoopxnttdata/ntt-data-postgresql
  • 4. 4Copyright © 2015 NTT DATA Corporation 今日の本題 様々なトラブルがありました。 特に手を焼いた 2つ のトラブルを紹介。 ・ SEGVでPostgreSQLが落ちた ・ SELECT結果が間違っている。 共通しているのは、いずれもPostgreSQL、周辺ツールの バグであったこと。いずれも解決済みです。
  • 5. 5Copyright © 2015 NTT DATA Corporation SEGVでPostgreSQLが落ちた 1. ある日、本番環境での試験中にPostgreSQLがダウンした 2. とあるSQLが実行された際に、Segmentation Fault が発生していた  PostgreSQLは、あるバックエンドプロセスがSEGVを出すと、インスタンス全体が落ち ますね 3. 復旧後、同じSQLを発行したら、再度 PostgreSQL がダウン 4. とりあえず、そのSQLの発行を停止して様子見(その後はダウンせず) SQLは特に変哲もないもの。今までは特に問題なかった。 では、直前に何かしたか? ・・・・ そういえばパーティションのローテートをした。 あと残されたのは core ファイル。 まずはここから解析が始まった。
  • 6. 6Copyright © 2015 NTT DATA Corporation coreの解析 実行計画作成中に統計情報を見ている箇所でクラッシュしている (gdb) bt #0 pg_detoast_datum (datum=0x1d7f0f2c28ee3) at fmgr.c:2240 #1 0x000000000069c7b4 in numeric_lt (fcinfo=0x7fffaee58560) at numeric.c:1408 #2 0x000000000071a8c0 in FunctionCall2Coll (flinfo=<value optimized out>, collation=<value optimized out>, arg1=<value optimized out>, arg2=<value optimized out>) at fmgr.c:1326 #3 0x00000000006c320f in ineq_histogram_selectivity (root=0x107ab28, vardata=0x7fffaee58a80, opproc=0x7fffaee589f0, isgt=0 '¥000', constval=17280952, consttype=1700) at selfuncs.c:834 #4 0x00000000006c3a8c in scalarineqsel (root=0x107ab28, operator=<value optimized out>, isgt=0 '¥000', vardata=0x7fffaee58a80, constval=17280952, consttype=<value optimized out>) at selfuncs.c:558 #5 0x00000000006c470c in scalarltsel (fcinfo=<value optimized out>) at selfuncs.c:1021 #6 0x000000000071cdb7 in OidFunctionCall4Coll (functionId=<value optimized out>, collation=0, arg1=17279784, arg2=1754, arg3=17280664, arg4=0) at fmgr.c:1682 #7 0x00000000005fc0e5 in restriction_selectivity (root=0x107ab28, operatorid=1754, args=0x107ae98, inputcollid=0, varRelid=0) at plancat.c:1021 #8 0x00000000005d079e in clause_selectivity (root=0x107ab28, clause=0x107b040, varRelid=0, jointype=JOIN_INNER, sjinfo=0x0) at clausesel.c:668 #9 0x00000000005d0274 in clauselist_selectivity (root=0x107ab28, clauses=<value optimized out>, varRelid=0, jointype=JOIN_INNER, sjinfo=0x0) at clausesel.c:108 #10 0x00000000005d1dad in set_baserel_size_estimates (root=0x107ab28, rel=0x107b120) at costsize.c:3411 #11 0x00000000005cec4a in set_plain_rel_size (root=0x107ab28, rel=0x107b120, rti=1, rte=0x1044090) at allpaths.c:361
  • 7. 7Copyright © 2015 NTT DATA Corporation coreの解析 ちょっと追うと、何故か異なるテーブルの統計情報を見ていた・・・ (gdb) frame 3 #3 0x00000000006c320f in ineq_histogram_selectivity (root=0x107ab28, vardata=0x7fffaee58a80, opproc=0x7fffaee589f0, isgt=0 '¥000', constval=17280952, consttype=1700) at selfuncs.c:834 (gdb) p *vardata $15 = {var = 0x107ae28, rel = 0x107b120, statsTuple = 0x7f2bd2ee1da0, freefunc = 0x7f2bd5921830 <FreeHeapTuple>, vartype = 1700, atttype = 1700, atttypmod = -1, isunique = 0 '¥000'} (gdb) p *(Form_pg_statistic) ((char *) vardata->statsTuple->t_data + vardata->statsTuple->t_data->t_hoff) $14 = {starelid = 299892885, staattnum = 1, stainherit = 0 '¥000', stanullfrac = 0, stawidth = 8, stadistinct = -1, stakind1 = 2, stakind2 = 3, stakind3 = 0, stakind4 = 0, stakind5 = 0, staop1 = 2062, staop2 = 2062, staop3 = 0, staop4 = 0, staop5 = 0} 問題のSQLで参照しているテーブルのOID は 299106453。numeric型 (oid=1700)の列に対し、WHERE句で col < xxxx の条件を使っていた。 しかし、上記のcoreからは OID = 299892885 のテーブルのtimestamp型(oid = 2062)の列の統計情報を 参照し、可変長データのアクセスロジックに行ってしまい、クラッシュしていた。
  • 8. 8Copyright © 2015 NTT DATA Corporation pg_dbms_statsで問題がある? 1. 統計情報固定化ツール(pg_dbms_stats)を使っている  まずはこいつを怪しむ 2. 固定化した統計情報の中身がまずいのか?  中身を確認するも、問題なし 3. 別の試験環境に当該の統計情報を持ってきてSQLを 発行しても大丈夫 当該環境でしか発生しない様子・・ ここまでの情報とコード解析を行った結果、特定の条件において、 pg_dbms_statsが誤った統計情報を返していることが分かった。
  • 9. 9Copyright © 2015 NTT DATA Corporation その前に、pg_dbms_statsの内部について relid attnum stats 固定化した統計情報 本来の統計情報 pg_dbms_stats Hash relid attnum stats Planning Phase Execute Phase バックエンドプロセス のローカルメモリ
  • 10. 10Copyright © 2015 NTT DATA Corporation その前に、pg_dbms_statsの内部について relid attnum stats 固定化した統計情報 本来の統計情報 pg_dbms_stats Hash Hook relid attnum stats Planning Phase 統計情報取得 Execute Phase ① 統計情報の取得時、pg_dbms_statsが有 効になっていると、Hook経由で pg_dbms_statsのロジックに入る バックエンドプロセス のローカルメモリ
  • 11. 11Copyright © 2015 NTT DATA Corporation その前に、pg_dbms_statsの内部について relid attnum stats 固定化した統計情報 本来の統計情報 pg_dbms_stats Hash Hook relid attnum stats Planning Phase 統計情報取得 Execute Phase ②固定化された統計情報と本来の統計情報 (pg_statistics)をマージし、固定化されたもの があれば、それを使い、なければ本来の統計 情報を使う バックエンドプロセス のローカルメモリ
  • 12. 12Copyright © 2015 NTT DATA Corporation その前に、pg_dbms_statsの内部について relid attnum stats 固定化した統計情報 本来の統計情報 pg_dbms_stats Hash Hook relid attnum stats Planning Phase 統計情報取得 Execute Phase バックエンドプロセス のローカルメモリ ③次回以降の参照のため、取得した 統計情報はローカルメモリのハッシュ へ格納しておく
  • 13. 13Copyright © 2015 NTT DATA Corporation その前に、pg_dbms_statsの内部について relid attnum stats 固定化した統計情報 本来の統計情報 pg_dbms_stats Hash Hook relid attnum stats Planning Phase 統計情報取得 Execute Phase バックエンドプロセス のローカルメモリ ④統計情報を取得し、Hookのポ イントへ返る
  • 14. 14Copyright © 2015 NTT DATA Corporation その前に、pg_dbms_statsの内部について relid attnum stats 固定化した統計情報 本来の統計情報 pg_dbms_stats Hash Hook relid attnum stats Planning Phase 統計情報取得 Execute Phase バックエンドプロセス のローカルメモリ ①’ 二回目以降、Hashに統計情 報があれば、それを使う relid, attnum, stats
  • 15. 15Copyright © 2015 NTT DATA Corporation Hashの仕組み Hash Hashは ハッシュキーと値の形でデータを管理。ロック情報管理などで使われている。 これを外部モジュール(contribやbgworker)等でも使えるよう、汎用の ユーティティリティとしてPostgreSQLのコアが提供している。 ・ Hash化、マッチングは組み込み or ユーザ定義を利用 ・ Hashからの検索はキーサーチ、あるいはSeq_search ・ ローカル or 共有バッファのどちらにもおける ・ 共有バッファの場合は排他制御用のロック構造体を指定 (パーティション化もサポート) ・ 上記はHashの作成時に指定 hash_fn(ユーザ定義 or 組み込み(oid, string など)) bucket bucket bucket hash_serach()など match_fn(ユーザ定義 or 組み込み(memcmpなど)) key + body key + body key 該当のbucketの中から、 規定の方法で キーマッチングを実施
  • 16. 16Copyright © 2015 NTT DATA Corporation 今回のバグは・・ 仕組みとしてはrelationのOID(relid)をキーとしていたが・・Hash_createの 引数フラグにHASH_FUNCTIONを指定していなかった・・ MemSet(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(Oid); ctl.entrysize = sizeof(StatsRelationEntry); ctl.hash = oid_hash; ctl.hcxt = CacheMemoryContext; hash = hash_create("dbms_stats relation statistics cache", MAX_REL_CACHE, &ctl, HASH_ELEM | HASH_CONTEXT | HASH_FUNCTION ); rel_stats = hash; この部分が無かった pg_dbms_stats.c より
  • 17. 17Copyright © 2015 NTT DATA Corporation 今回のバグは・・ そのため、ハッシュ化の関数として想定していたoid_hash は使われず、OIDを 文字列としてハッシュ化する string_hashが設定されていた。 /* Initialize the hash header, plus a copy of the table name */ hashp = (HTAB *) DynaHashAlloc(sizeof(HTAB) + strlen(tabname) +1); MemSet(hashp, 0, sizeof(HTAB)); hashp->tabname = (char *) (hashp + 1); strcpy(hashp->tabname, tabname); if (flags & HASH_FUNCTION) hashp->hash = info->hash; else hashp->hash = string_hash; /* default hash function */ hash化の関数にstring用の 関数が選択される src/backend/utils/hash/dynahash.c より
  • 18. 18Copyright © 2015 NTT DATA Corporation 今回のバグは・・ そしてHashサーチ時のマッチング手段も文字列として設定されていた /* * If you don't specify a match function, it defaults to string_compare if * you used string_hash (either explicitly or by default) and to memcmp * otherwise. (Prior to PostgreSQL 7.4, memcmp was always used.) */ if (flags & HASH_COMPARE) hashp->match = info->match; else if (hashp->hash == string_hash) hashp->match = (HashCompareFunc) string_compare; else hashp->match = memcmp; src/backend/utils/hash/dynahash.c より
  • 19. 19Copyright © 2015 NTT DATA Corporation 今回のバグは・・ ・ relid = 299106453 ==> 文字列で見てしまうと ==> 0x11d40095 と ・ relid = 299892885 ==> 文字列で見てしまうと ==> 0x11e00095 となる。ハッシュ化や文字列マッチング(比較)では0x00を終端として扱い、 上記2つは「95」として同じものとなってしまう・・・つまり・・ Hash hash_string bucket bucket bucket hash_serach() string_cmp(0x95, 0x95) relid = 299892885 stats = XXXXX stats = XXXXX key = relid = 299106453 0x95で hash化 0x95で hash化 relid = 299106453ではなく relid = 299892885の統計情報を返す
  • 20. 20Copyright © 2015 NTT DATA Corporation 解決 というわけで、突然SEGVが発生した理由がわかった。 ・ OIDに0x00を含み、かつその下位バイトが同じになるテーブルを 同じクライアントが読む時にだけ発生 ・ ハッシュは各クライアントのローカルメモリにあるから ・ 突然発生したのは、パーティションローテで偶然にも上記に 該当するテーブルを読む状況が揃ってしまったため ・ 別環境で再現しなかったのは、pg_dbms_statsの統計情報の export/importは、OIDは引き連れていかないから 修正は、hash_create()のフラグに正しく HASH_FUNCTIONを立てるのみ。 coreが無かったら解析は難航していた。最悪、迷宮入りになった可能性 もある・・
  • 21. 21Copyright © 2015 NTT DATA Corporation SELECT結果が間違っている !? 1. 試験環境にて、AP側でSELECT結果がおかしい事象が出た。 2. AP側のログ解析から 「SELECT FOR UPDATEで未処理ステータスの行を取得し、ロックした 行のステータスを処理済みにUPDATEしている。この処理において、 なぜか処理済みのステータスの行をSELECT FOR UPDATEで取得し てしまう」 という事象に見えた。 3. たまに出ていた。1日1回出るか出ないかの頻度。 SQLは特に変哲もないもの。 PostgreSQLに原因があるというが、前例がない・・ APのバグでは?いや、JDBC?JVM?・・ 容疑者も当時の状況では絞り切れていない
  • 22. 22Copyright © 2015 NTT DATA Corporation 切り分けしよう • 幸い、試験環境でも出ていたので、発生時と同様の業務APも使えるし、 テストデータもある • まず、PostgreSQL <-> JDBC <-> AP で切り分けをする • tcpdumpやWiresharkでNW上のデータをキャプチャする案もあったが、時間の都合 と環境の問題で、とりあえず見送り • 当該の事象が発生した業務処理を高頻度で行い加速試験 PostgreSQL JDBC 業務AP PostgreSQL側でプローブを 仕込む。ここで検知できれ ばPostgreSQLがアウト (詳細は次項) JDBCのResultSetにプロー ブを仕込む。ここで検知で きたらJDBCがアウト (結果的にこれは未実施) APのResultSetにプローブ を仕込む。ここで検知でき たらJDBCがアウト
  • 23. 23Copyright © 2015 NTT DATA Corporation PostgreSQLを容疑者と仮定してのプローブ 1. 発生するとAPが停止する可能性もあるので、SELECT条件と矛盾するデー タは弾きつつ、検知もしたい 2. フィルタ関数で当該SQLを囲ってしまおう 3. Limit & OFFSETを使っているので、負荷も低い SELECT … FROM … WHERE … LIMIT .. FOR UPDATE; SELECT * FROM ( SELECT … FROM … WHERE … LIMIT .. ) s1 WHERE check_filter(s1.xxx) FOR UPDATE check_filter()は、引数をチェックして、サブクエリの 条件と矛盾する行が来たら falseを出しつつ、 内部でRAISE LOG ‘inavalid status xxxx’ を出力 Before After
  • 24. 24Copyright © 2015 NTT DATA Corporation 出た 異常検知メッセージがPostgreSQLのフィルタ関数で出てしまった。 つまりPostgreSQLが黒だった。 急いで手元環境で再現できるよう、再現パターンを絞り込みつつ、 APの該当処理をJavaで作成。結果的に、手元でも再現した。 さらにミニマムなテストセットにより、psql 上でも確認できた・・・・ 絞り込めた条件は、 1. パーティショニングされており、 2. 部分インデックスを使ったインデックススキャンを経由した 3. UPDATEとFOR UPDATE が競合する こと。 ここからはコード解析も併せて問題の特定へ。
  • 25. 25Copyright © 2015 NTT DATA Corporation ヒントだったもの 実行計画を見ると、IndexScanの条件(IndexCond/Filter)がおかしい (事項から) ソースコードとREADME(src/backend/executor/README など)から、 部分インデックスの際の特別な最適化のコードがあることが分かる。 これらをヒントに、仮説と試験をして、原因が解明できた。
  • 26. 26Copyright © 2015 NTT DATA Corporation 今回のバグは・・・ PostgreSQLのオプティマイザの問題だった 部分インデックス経由のインデックスアクセスをする際、部分インデックスの定 義と同様のWHERE条件は、実行時に省略する最適化がされる -> 部分インデックスのエントリには、その条件に合致するものしかないため postgres=# EXPLAIN SELECT * FROM tbl WHERE c1 < 100; QUERY PLAN ----------------------------------------------------------- Index Scan using tbl_idx2 on tbl (cost=0.00..208.79 rows=100 width=10) Index Cond: (c1 < 100) (2 rows) postgres=# EXPLAIN SELECT * FROM tbl WHERE (c3 = '0' OR c3 = '1' OR c3 = '2'); QUERY PLAN ----------------------------------------------------------- Index Scan using tbl_idx on tbl (cost=0.00..114.34 rows=305 width=10) (1 row) Indexのスキャン 条件が無い!
  • 27. 27Copyright © 2015 NTT DATA Corporation 今回のバグは・・・ PostgreSQLのオプティマイザの問題だった PostgreSQLでは、FOR UPDATE やUPDATEなどの行ロックを必要とする場合、 SELECT結果についてロック取得時に改めて再確認(EvalPlanQual)する -> ロック取得までにSELECT対象外のデータへ更新されている可能性があるので postgres=# EXPLAIN SELECT * FROM tbl WHERE (c3 = '0' OR c3 = '1' OR c3 = '2') FOR UPDATE; QUERY PLAN ----------------------------------------------------------------- ---- LockRows (cost=0.00..117.39 rows=305 width=16) -> Index Scan using tbl_idx on tbl (cost=0.00..114.34 rows=305 width=16) Filter: ((c3 = '0'::bpchar) OR (c3 = '1'::bpchar) OR (c3 = '2'::bpchar)) (3 rows) そのため、部分インデックスを使っていたとしても、SELECT FOR UPDATE時 にはWHERE句条件の省略最適化はしないようにしている・・・はずだった。 Indexのスキャン 条件がある!
  • 28. 28Copyright © 2015 NTT DATA Corporation 今回のバグは・・・ ところが・・ postgres=# EXPLAIN SELECT * FROM tbl WHERE (c3 = '0' OR c3 = '1' OR c3 = '2') FOR UPDATE; QUERY PLAN ---------------------------------------------------------------------- LockRows (cost=0.00..198.78 rows=337 width=23) -> Result (cost=0.00..195.41 rows=337 width=23) -> Append (cost=0.00..195.41 rows=337 width=23) -> Index Scan using tbl_idx on tbl (cost=0.00..114.34 rows=305 width=20) -> Index Scan using tbl_c1_c3_c2_idx on tbl_c1 tbl (cost=0.00..40.54 rows=16 width=54) -> Index Scan using tbl_c2_c3_c2_idx on tbl_c2 tbl (cost=0.00..40.54 rows=16 width=54) Indexのスキャン 条件が無い!
  • 29. 29Copyright © 2015 NTT DATA Corporation 今回のバグは・・・ パーティションの子テーブル部分については、ただしく SELECT FOR UPDATEさ れているかどうかの判定がされていなかった・・ 本来ならPlannerInfoに紐づくPlanRowMarks(どのテーブルがFOR UPDATE等 の対象かを示す情報)を元にFOR UPDATEの有無を判定をすべきだったのが PlannerInfo->Query を元に判定していた・・・ Tom Lane 曰く、”thinko (うっかり勘違い)” のミス。 【本バグの修正コミット】 http://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=cd63c5 7e5cbfc16239aa6837f8b7043a721cdd28
  • 30. 30Copyright © 2015 NTT DATA Corporation 今回のバグは・・・ つまり子テーブルに関しては FOR UPDATE対象外と判定され、部分インデック スの最適化対象となり・・ 最終的に、子テーブルへの部分インデックス経由のアクセスの際、直前に更新 された結果でも誤って返却していた。 id status other 1 0 ZZ 2 0 ZZ 3 0 ZZ 1 9 ZZ ① ある処理が UPDATE tbl SET status = 9 WHERE status = 0 ORDER BY id LIMIT 1; を実施 ② 別の処理が SELECT * FROM tbl WHERE status = 0 ORDER BY id LIMIT 1; を実施 ①の更新が終わるまで 待つ
  • 31. 31Copyright © 2015 NTT DATA Corporation 今回のバグは・・・ つまり子テーブルに関しては FOR UPDATE対象外と判定され、部分インデック スの最適化対象となり・・ 最終的に、子テーブルへの部分インデックス経由のアクセスの際、直前に更新 された結果でも誤って返却していた。 id status other 1 0 ZZ 2 0 ZZ 3 0 ZZ 1 9 ZZ ③ COMMITする。 ④ SELECT * FROM tbl WHERE status = 0 ORDER BY id LIMIT 1; が走る 更新結果を参照する。
  • 32. 32Copyright © 2015 NTT DATA Corporation 今回のバグは・・・ つまり子テーブルに関しては FOR UPDATE対象外と判定され、部分インデック スの最適化対象となり・・ 最終的に、子テーブルへの部分インデックス経由のアクセスの際、直前に更新 された結果でも誤って返却していた。 id status other 1 0 ZZ 2 0 ZZ 3 0 ZZ 1 9 ZZ ⑤ SELECT * FROM tbl WHERE status = 0 ORDER BY id LIMIT 1; の結果 status = 9 のものが 返ってくる 本来であれば WHERE句 条件の status = 0 の再確認をすべきだが それをしないために、status = 9 であっても返してしまう
  • 33. 33Copyright © 2015 NTT DATA Corporation 解決 というわけで、SELECT結果が間違っていた理由がわかった。 ・ 再現頻度の低さは、当該のSQLは FOR UPDATE NOWAIT をしており、 よりシビアな条件だったから 一応、歯止めのフィルタ関数もあり、かつ発生条件に該当するSQLはそ こだけだった。 (パーティションかつ部分インデックスを使っているSQLなので、絞り込み やすいことが幸いだった・・)
  • 34. 34Copyright © 2015 NTT DATA Corporation 振り返って エンタープライズ、に限りませんが、 深刻な問題は根本原因を特定すること = ホワイトボックスとして明確にすることが大切です。 今回の事象も、根本原因が分かったため、 ・なぜ今まで発生頻度が低かったか? ・不具合の発生影響が他にないのか? ・検討した対処が本当に正しいのか? を見極めることができました。 PostgreSQLのコードには、親切なREADMEやコメントが多数あります。 これらの情報やcoreの情報、問題発生状況を元に、地道に検査していけば 多くの場合は何とかなります・・多分。 bugs ML などを通じてコミュニティに報告するのもアリです。ただし、必ず 再現資材(self-contained test case)を添えましょう。
  • 35. 35Copyright © 2015 NTT DATA Corporation ご清聴ありがとうございました!
  • 36. Copyright © 2011 NTT DATA Corporation Copyright © 2015 NTT DATA Corporation 本資料には、当社の秘密情報が含まれております。当社の許可なく第三者へ開示することはご遠慮ください。