PostgreSQL その59 shared_buffersの算定 その1のつづき

インデックス付きテーブルを検索する場合のキャッシュ使用量調査

インデックスを貼ったカラムの値をキーに検索する場合、カラムの値がユニークなときとユニークでないときでキャッシュ使用量に違いがあるか調べる。

※カラムの値がユニークでなくてもインデックスを作成することはできる

1.インデックスを貼ったカラムの値がユニークな場合

【検証0】

2500万行(テーブルサイズ1.5GB)のテーブルのへのselect結果が100行ヒットする場合

・where句で絞り込むカラムにインデックスが張られていないとき

→初回検索後、kernelキャッシュ使用量が1.5GB増大した

・where句で絞り込むカラムにインデックスが張られてたとき

→初回検索後、kernelキャッシュ使用量が数百KB増大した

【結果0】

・インデックスが張られてない場合、ほぼ2500万回ディスクアクセスされるためテーブル全体がキャッシュに乗ると考えらる。

・インデックスが張られてた場合、平均log 2500万 ×100回しかディスクアクセスされないために、キャッシュに乗る量も少なくなる。

【検証1】

【検証0】の追検証。

別のテーブル(PostgreSQL その44 性能評価とチューニング検証用データベース作成 その3で作成した「table0i」・・・インデックス有り、1000万行、テーブルサイズ=781MB、インデックスサイズ=429MB)で検索結果が約100万行になるクエリを実行して【検証0】を追検証する

testdb=# \d table0i
                                        Table "punisuke.table0i"
    Column       |            Typ e                      | Collation | Nullable |               Default
-------------+-------------------------+--------+-------+-------------------------------------
 uriage_no        | integer                               |              |             | nextval('table0i_no_seq'::regclass)
 uriage_date     | timestamp without time zone |              |             |
 kokyaku_name | text                                   |              |             |
 shohin_name   | text                                   |              |             |
 price             | integer                               |              |             |
 number          | integer                               |              |             |
 comment       | text                                    |              |             |
Indexes:
    "table0i_comment_idx" btree (comment)
    "table0i_uriage_no_idx" btree (uriage_no)

【懸念点1】

・1000万行のテーブルのうち10%に当たる100万行がヒットする検索の場合、where句で指定するカラムにユニークなインデックスを貼ってても、実行計画で総当たりスキャンが選択されて、結果的にインデックス無しの「table0」と同じく、キャッシュにテーブル全行書き込みが為されてしまわないだろうか?

・職場の有識者からの話だと、PostgreSQLは共有メモリに乗せるデータが満タンになったときに優先して残すデータを取捨択一するアルゴリズムになっているらしい。

例えば、 joinしたデータ>インデックス>テーブル のような感じになってるらしい(うろおぼえ)

・レスポンス時間をbashから実行してbashに返ってきた時間をtimeコマンドで相対比較しているが、厳密に測るにはpg_stat_statusを使った方がよい(あくまで相対比較だし・・・)

 

【検証1-1】 

一度に全テーブルの10%(100万行)がヒットする検索の場合

 

せ70> systemctl restart postgresql-10

せ70> echo 3 > /proc/sys/vm/drop_caches
せ70> free -h

                  total        used        free      shared   buff/cache   available
Mem:           2.8G        537M        2.0G         51M        222M        2.0G
Swap:          2.0G          0B        2.0G

ちなみに、インデックスを張ってない「shohin_name」カラムで検索すると、「table0」と同じようにsequential scan(非インデックススキャン)になり、下記のようにレスポンスに14秒かかった。

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select count(*) from punisuke.table0i where shohin_name='商品8'"
 count
--------
 999745
(1 row)

real    0m14.089s
user    0m0.008s
sys     0m0.026s

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        539M        1.3G         53M        993M        2.0G     ←kernel cacheに一気に771MB乗った
Swap:          2.0G          0B          2.0G

・kernel cacheを一回フラッシュ

せ70> echo 3 > /proc/sys/vm/drop_caches

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        537M        2.0G         53M        226M        2.0G
Swap:          2.0G          0B          2.0G

・100万行がヒットする検索をインデックス行(uriage_no)に対して実行

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select count(*) from punisuke.table0i where uriage_no >= 6000000 and uriage
_no <= 6999999;"

  count
---------
 1000000
(1 row)

real    0m2.172s     ←約2.2秒
user    0m0.002s
sys     0m0.023s

せ70> free -h
                  total        used        free         shared  buff/cache   available
Mem:           2.8G        539M        1.8G        149M        446M        1.9G
Swap:          2.0G          0B          2.0G

→一発でkernel cacheに223MB乗り、共有メモリに96MB乗った

・もう一度同じ検索範囲で実行

→レスポンス時間は0m0.163s(約0.2秒)に改善。kernel cache使用量(buff/cache)と共有メモリ使用量(shared)は変化なし

・今度は検索範囲を変えて実行

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select count(*) from punisuke.table0i where uriage_no >= 8000000 and uriage
_no <= 8999999;"

  count
---------
 1000000
(1 row)

real    0m2.554s
user    0m0.006s
sys     0m0.005s

せ70> free -h
                  total        used        free         shared  buff/cache   available
Mem:           2.8G        539M        1.6G        245M        650M        1.8G
Swap:          2.0G          0B          2.0G

→一発でkernel cacheに224MB乗り、共有メモリに96MB乗った

cacheに乗った量が前回とほぼ同じ!

・もう一度同じ検索範囲で実行

→レスポンス時間は0m0.163s(約0.2秒)に改善。kernel cache使用量(buff/cache)と共有メモリ使用量(shared)は変化なし

改善されたレスポンス時間とそれ以降cache使用量に増減が起こらないことも完全に一致!

・もう一度同じ検索範囲を残りのuriage_no=1~999999、1000000~1999999、2000000~2999999、3000000~3999999、4000000~4999999、5000000~5999999、7000000~7999999、9000000~9999999で実行

せ70> free -h
                  total        used         free        shared  buff/cache   available
Mem:           2.8G        538M        128M        1.0G        2.1G        1.1G
Swap:          2.0G          0B          2.0G

→kernel cacheに1450MB乗り、共有メモリが1GB(shared_buffers値)に達した

計算では、kernel cacheに8×224MB=1792MB乗ると思われたが、メモリ管理してるkernelの判断でfree領域に128MBが確保されてpostgresqlデータはすべてkernel cacheに乗り切らなかったと思われる。

 

【検証1-2】

一度に全テーブルの0.01%未満(1000行未満)がヒットする検索の場合

 

下記のように、「table0i」に張られているもう一つのインデックス列「comment」には、「11111a1」のようなランダムな数字と文字列が入っている。

testdb=# \d table0i
                                        Table "punisuke.table0i"
    Column       |            Typ e                      | Collation | Nullable |               Default
-------------+-------------------------+--------+-------+-------------------------------------
 uriage_no        | integer                               |              |             | nextval('table0i_no_seq'::regclass)
 uriage_date     | timestamp without time zone |              |             |
 kokyaku_name | text                                   |              |             |
 shohin_name   | text                                   |              |             |
 price             | integer                               |              |             |
 number          | integer                               |              |             |
 comment       | text                                    |              |             |
Indexes:
    "table0i_comment_idx" btree (comment)
    "table0i_uriage_no_idx" btree (uriage_no)

testdb=# select * from punisuke.table0i where comment='12345a1';
 uriage_no |     uriage_date          | kokyaku_name | shohin_name | price | number | comment
---------+------------------+------------+-----------+-----+------+---------
      6462  | 1991-01-29 00:00:00 | 顧客1           | 商品8          |   800 |       71 | 12345a1
     15247 | 1991-02-07 00:00:00 | 顧客10          | 商品1          |   100 |       88 | 12345a1
    133244 | 1991-06-05 00:00:00 | 顧客1           | 商品6          |   600 |       23 | 12345a1

---(略)---

   9708850 | 2017-08-22 00:00:00 | 顧客4          | 商品3          |   300 |        55 | 12345a1
   9936919 | 2018-04-07 00:00:00 | 顧客8          | 商品5          |   500 |        88 | 12345a1
(62 rows)

・postgresqlの共有メモリとkernel cacheをフラッシュする

せ70> systemctl restart postgresql-10
せ70> echo 3 > /proc/sys/vm/drop_caches

・クエリ実行前の共有メモリとkernel cache使用量

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        532M        2.0G         51M        224M        2.0G
Swap:          2.0G          0B          2.0G

・一度に全テーブルの0.00062%(62行)がヒットする検索を実行

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select count(*) from punisuke.table0i where comment='12345a1';"
 count
-------
    62
(1 row)

real    0m1.125s                          ←レスポンスは約1.1秒
user    0m0.009s
sys     0m0.030s

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        532M        2.0G         53M        242M        2.0G
Swap:          2.0G          0B          2.0G

→一発でkernel cacheに18MB乗り、共有メモリに2MB乗った

・もう一度同じ検索をする

→レスポンス時間は0m0.015sに改善。kernel cache使用量(buff/cache)は2MB増大し、共有メモリ使用量(shared)は変化なし

※kernel cacheの増大は値が小さいのでもしかしたら別の要因の可能性あり

【わかったこと】

・インデックス(テーブルも?)がキャッシュに乗った場合と乗ってない場合で応答時間が約100倍違った。

・インデックススキャンの方がシーケンシャルスキャンよりキャッシュの使用量が少ないが、それでも長期的には少なくともインデックスがすべてキャッシュに乗り切ってパフォーマンスが最大限発揮されると思われる。

 

2.インデックスを貼ったカラムの値がユニークじゃない場合

【準備】

[テーブル作成]

インデックスが張られたユニークなカラム「id1」とユニークでないカラム「id2」をもつテーブル「table1」を作成する。

testdb=> create table table1 (id1 serial,id2 int,comment text);
CREATE TABLE

testdb=> create index table1_id1_idx on table1 (id1);
CREATE INDEX
testdb=> create index table1_id2_idx on table1 (id2);
CREATE INDEX
testdb=> \d table1
                            Table "punisuke.table1"
 Column  |  Type    | Collation | Nullable |               Default
--------+-------+--------+-------+-------------------------------------
 id1         | integer |              | not null  | nextval('table1_id1_seq'::regclass)
 id2         | integer |              |             |
 comment | text     |             |             |
Indexes:
    "table1_id1_idx" btree (id1)
    "table1_id2_idx" btree (id2)

 

[データ挿入]

id1カラムはユニークで、id2カラムは例えば下記のようにユニークでないようにデータを挿入する

testdb=> insert into table1(id2,comment) values
(1,'hoge'),
(1,'fuge'),
(1,'piyo'),
(1,'puni'),
(1,'pyororin'),
(1,'oppekepe');

INSERT 0 6

testdb=> insert into table1(id2,comment) values
(2,'hoge'),
(2,'fuge'),
(2,'piyo'),
(2,'puni'),
(2,'pyororin'),
(2,'oppekepe');

INSERT 0 6
testdb=> select * from table1;
 id1 | id2 | comment
---+---+----------
   1 |   1 | hoge
   2 |   1 | fuge
   3 |   1 | piyo
   4 |   1 | puni
   5 |   1 | pyororin
   6 |   1 | oppekepe
   7 |   2 | hoge
   8 |   2 | fuge
   9 |   2 | piyo
  10 |   2 | puni
  11 |   2 | pyororin
  12 |   2 | oppekepe
(12 rows)

 

・スクリプトでこれに600万行挿入する (4時間かかる?→3時間45分でした)

せ70> cat /tmp/20180812/insert.sh
#!/bin/bash
for ((i=1;i<=1000000;i++))
do
PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c \
"insert into punisuke.table1 (id2,comment) values
(${i},'hoge'),
(${i},'fuge'),
(${i},'piyo'),
(${i},'puni'),
(${i},'pyororin'),
(${i},'oppekepe');" > /dev/null
done

 

【検証2】

下記の①、②の検索をした場合のキャッシュ使用量とレスポンス時間を比較する

①ユニークでないカラム「id2」をキーにして「comment」を検索

select comment from table1 where id2=n;

→6レコードヒットする

②ユニークなカラム「id1」をキーにして「comment」を検索

ただし、必ず6レコードヒットするように下記のように検索する

select comment from table1 where id1=6m+1 or id1=6m+2 or id1=6m+3 or id1=6m+4 or id1=6m+6;

→6レコードヒットする

 

【結果2】

せ70> systemctl restart postgresql-10

せ70> echo 3 > /proc/sys/vm/drop_caches
せ70> free -h

                  total        used        free        shared  buff/cache   available
Mem:           2.8G        527M        2.0G         51M        226M        2.0G
Swap:          2.0G          0B          2.0G

・ユニークでないインデックスで検索

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select * from punisuke.table1 where id2=500000;"
   id1   |  id2   | comment
---------+--------+----------
 3000000 | 500000 | oppekepe
 2999999 | 500000 | pyororin
 2999998 | 500000 | puni
 2999997 | 500000 | piyo
 2999996 | 500000 | fuge
 2999995 | 500000 | hoge
(6 rows)

real    0m1.206s                        ←レスポンスは約1.2秒
user    0m0.001s
sys     0m0.046s

せ70> free -h
                total        used        free        shared  buff/cache   available
Mem:           2.8G        526M        2.0G         52M        240M        2.0G          ←kernel cacheに約14MB乗った
Swap:          2.0G          0B          2.0G

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select * from punisuke.table1 where id2=500000;"
---(略)---
real    0m0.042s                      ←レスポンスは約0.04秒
user    0m0.004s
sys     0m0.006s

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        526M        2.0G         52M        241M        2.0G
Swap:          2.0G          0B          2.0G

・ユニークなインデックスで検索

せ70> systemctl restart postgresql-10
せ70> echo 3 > /proc/sys/vm/drop_caches
せ70> free -h

                  total        used        free        shared  buff/cache   available
Mem:           2.8G        526M        2.0G         51M        224M        2.0G
Swap:          2.0G          0B          2.0G

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select * from punisuke.table1 where id1=2999995 or id1=2999996 or id1=2999997 or id1=2999998 or id1=2999999 or id1=3000000;"
   id1   |  id2   | comment
---------+--------+----------
 2999995 | 500000 | hoge
 2999996 | 500000 | fuge
 2999997 | 500000 | piyo
 2999998 | 500000 | puni
 2999999 | 500000 | pyororin
 3000000 | 500000 | oppekepe
(6 rows)

real    0m0.610s                      ←レスポンスは約0.6秒
user    0m0.004s
sys     0m0.021s

せ70> free -h
                 total        used        free        shared  buff/cache   available
Mem:           2.8G        527M        2.0G         52M        244M        2.0G          ←kernel cacheに約20MB乗った
Swap:          2.0G          0B          2.0G

せ70> time PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select * from punisuke.table1 where id1=2999995 or id1=2999996 or id1=2999997 or id1=2999998 or id1=2999999 or id1=3000000;"
---(略)---
real    0m0.017s                    ←レスポンスは約0.02秒
user    0m0.001s
sys     0m0.010s

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        527M        2.0G         52M        244M        2.0G
Swap:          2.0G          0B          2.0G

ユニークでないインデックスで検索を1万回実行

せ70> for((i=10000;i<=20000;i++));do  PGPASSWORD=postgres /opt/PostgreSQL/10/bin/psql -U postgres testdb -c "select * from punisuke.table1 where id2=1${i};";done

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        526M        2.0G         56M        262M        2.0G          ←kernel cacheに約18MB、共有メモリに4MB乗った
Swap:          2.0G          0B        2.0G

【わかったこと】

ユニークなカラムをキーにして検索した場合とユニークでないカラムをキーにして検索した場合では、

・キャッシュ使用量はユニークな方がちょっとだけ使用量が大きかった。

・レスポンス時間はユニークな方がちょっとだけ速かった

・初回検索でいきなり10~20MBキャッシュに乗るが、2回目以降はキャッシュの乗り方が少なくなる(1万回検索で20MBとか)

※試行回数一回だけだし、あいまいな表現だけど極端な違いが出なかったのでこれ以上深追いしないんご

 

【検証3】

insert文で挿入されたデータはinsertしただけでPostgreSQLのshared_buffersに乗るのか?

それともselect などでreadされるまでshared_buffersに乗らないのか?

・20万レコード挿入した時点の共有メモリ使用量

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        562M        1.6G         77M        673M        2.0G
Swap:          2.0G          0B          2.0G

・40万レコード挿入した時点の共有メモリ使用量

せ70> free -h
                  total        used        free        shared  buff/cache   available
Mem:           2.8G        568M        1.5G         88M        727M        1.9G
Swap:          2.0G          0B          2.0G

・100万レコード挿入した時点の共有メモリ使用量

せ70> free -h
                  total        used         free       shared  buff/cache   available
Mem:           2.8G        564M        1.4G        142M        858M        1.9G
Swap:          2.0G          0B          2.0G

・420万レコード挿入した時点の共有メモリ使用量

せ70> free -h
                  total        used         free         shared  buff/cache   available
Mem:           2.8G        592M        621M        424M        1.6G        1.6G   ←shared_buffersの512MBには一応達してないけど
Swap:          2.0G          0B          2.0G

「table1」テーブルの場合、20万レコードinsertするごとに共有メモリに約18MB乗った。

【分かったこと】

insertしただけでデータがPostgreSQLのshared_buffersに乗る

 

■shared_buffers算定

よく見かける資料では、PostgreSQLのshared_buffersの値は、おおむねシステムメモリの1/4と書かれているが、これはおそらくすべてのケースには当てはまらない。

例えば、下記のような場合。

 

「PostgreSQLデータ以外のファイルのread/write量が多いため、kernel cacheからPostgreSQLデータがすぐに追い出されてしまうために、kernel cacheのヒット率が当てにならないケース。この場合はPostgreSQLのキャッシュ場所はshared_buffersだけになる。

ホストにPostgreSQLだけインストールしているような場合にはこのようなケースに陥ることはない」

 

たとえば、PostgreSQLデータ(テーブル+インデックス)が1GB程度なら、shared_buffers値を同程度の1GBにしておけば、PostgreSQLデータはすべてshared_buffersに乗り切る。

 

【課題】 

■お題

テーブルにログのようなデータも持たせていて、これが大量に挿入されるために、ログ以外の高速レスポンスが要求されるデータがキャッシュから追い出されるケース。

kernel cacheは当てにならない場合、PostgreSQLの共有メモリからもデータが追い出されてキャッシュのヒット率が低減する可能性がある。

上記の【検証3】で調べた通りログデータはinsert文で挿入されただけでPostgreSQLの共有メモリに乗る。

■検討項目

・大量insertされるログデータがinsertされることによって実際にどのくらい他のデータをキャッシュから追い出すかシミュレーションして確認する

・ログデータがテーブル内に大量に登録された状態でヒット数の多いクエリを実行した場合、どのくらいキャッシュに乗って、他のデータをキャッシュから追い出すかシミュレーションして確認する

・クエリの応答速度が求められるデータ(テーブル、インデックス)を優先的にキャッシュに乗せたり、追い出されないようにする方法はないか?

・クエリの応答速度が求められるデータとそうでないデータを別のクラスタにすれば共有メモリも別々になって影響が及ばなくなる