More Related Content
More from NTT DATA OSS Professional Services (20)
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)を添えましょう。
- 36. Copyright © 2011 NTT DATA Corporation
Copyright © 2015 NTT DATA Corporation 本資料には、当社の秘密情報が含まれております。当社の許可なく第三者へ開示することはご遠慮ください。