pg_rlimit で PostgreSQL のセッションごとに ulimit設定をする

この記事は [PostgreSQL Advent Calendar 2018](https://qiita.com/advent-calendar/2018/postgresql) のための予備記事として書きましたが、無事埋まりましたので出番なしでした。

# PostgreSQLでメモリ制限をしたい

皆さん、PostgreSQL のメモリ管理をどうしていますか？

PostgreSQLでは巨大なテーブルを対象に分析目的の複雑な SQL を実行すると大きくメモリが使われることがあります。そういった問い合わせは実行時間も長いですので、そのセッションがメモリを多く、長く使うことでサーバ全体がメモリ不足になり、OOMKiller が走ったり、サーバ全体が応答不能状態に陥ってしまったりすることがあります。

work_mem という大きくメモリを使いそうな場面におけるメモリ確保量の上限を設定するパラメータがありますが、これはサブクエリがあるときサブクエリ毎にこの上限までのメモリ確保を許してしまいます。また、セッション内での work_mem で制限されないメモリ利用もあります（プランナ動作のメモリ利用、多数のプリペアドステートメント、etc）。

セッション単位のメモリ制限機能は昔から何度か議論されていますが、今のところ PostgreSQL には備わっていません。下記はずいぶん昔のスレッドですが（当時 Red Hat社にいた Tom Lane が Windows を Disっているのが面白くて）私が記憶していたやりとりの一例です。

https://www.postgresql.org/message-id/19958.1254412063%40sss.pgh.pa.us
Subject: 	Re: Limit allocated memory per session
Date: 	2009-10-01 15:47:43
> Euler Taveira de Oliveira <euler(at)timbira(dot)com> writes:
>> Tom Lane escreveu:
>>> daveg <daveg(at)sonic(dot)net> writes:
>>>> I'd like to propose adding a new GUC to limit the amount of memory a backend
>>>> can allocate for its own use.
>>>
>>> Use ulimit.
>>
>> What about plataforms (Windows) that don't have ulimit?
>
> Get a real operating system ;-)
>
> {中略}
>			regards, tom lane

ulimit使え。現代なら cgroup（docker）使え、もあるでしょうか。しかし、これらにも欠点があります。これらは PostgreSQLサービス全体に制限が適用されてしまいます。以下のような使い方ができません。

* 大きくメモリを使うかもしれない分析クエリを実行するときだけ、そのセッションに限って（他セッションに迷惑をかけないように）メモリ制限をする
* 参照用に他システムからのアクセスを許しているロールからの接続に限って、（メモリ大量使用による攻撃から保護するために）メモリ制限をかける。


# pg_rlimit

そこで [pg_rlimit](https://github.com/harukat/pg_rlimit) という PostgreSQL の拡張を書きました。

これは ulimit で使われてる Linux のシステムコール set_rlimit / get_rlimit を PostgreSQL から呼び出すインタフェースを提供します。

````
    $ vi $PGDATA/postgresql.conf     
        shared_preload_libraries = 'pg_rlimit'
        pg_rlimit.v = '1GB'
````

と設定してあげると、ulimit の -v オプションに相当する設定が各セッションにデフォルトで起用されます。

````
    $ pg_ctl restart
    $ psql -U postgres -d db -c 'CREATE EXTENSION pg_rlimit'
    $ psql -U dbuser -d db
     
    db=> SHOW pg_rlimit.v;
     pg_rlimit.v
    -------------
     1GB
    (1 row)
          
    db=> SET pg_rlimit.v TO '512MB';
    SET
    db=> SHOW pg_rlimit.v;
     pg_rlimit.v
    -------------
     512MB
    (1 row)
````

こんな具合に GUCパラメータとして設定、参照が可能です。ということは、ALTER ROLE ... SET pg_rlimit.v TO '1GB' とすればロールのデフォルト設定にもできるわけです。

関数でも設定できます。関数を使う場合は shared_preload_libraries 設定は不要です。

````
    db=> SELECT * FROM pg_setrlimit('v', 257000000);
     pg_setrlimit
    --------------
        257000000
    (1 row)
````

制限を小さい値にして、SQLを実行すると（psql の \d はシステムテーブルをいくつか結合する SELECTを実行します）、以下のように out of memory エラーになります。エラーで済むのがポイントです。

````  
    db=> SELECT * FROM pg_setrlimit('v', 117000000);
     pg_setrlimit
    --------------
        117000000
    (1 row)
     
    db=> \d
    ERROR:  out of memory
    DETAIL:  Failed on request of size 24.
````

## pg_rlimit の注意点

[pg_rlimit](https://github.com/harukat/pg_rlimit) はプロセスのバーチャルメモリサイズに対して上限を設定します。ということは、共有バッファ（shared_buffers）を使っている分も含まれます。したがって、ＳＱＬで使うことを許すメモリの最大サイズを指定できるわけではありません。この点は ulimit コマンドを使ってメモリ制限を与えている場合も同様です。

使い方一例としては、物理メモリ: 8GB、shared_buffers = '2GB'、pg_rlimit.v = '3GB' といった設定が考えられます。

また、[pg_rlimit](https://github.com/harukat/pg_rlimit) は、今のところ pg_rlimit.v （メモリ制限）にしか対応していません。ニーズが生じたら足しますので、Patch、pull request、コメントをお待ちしています。

それともう一つ。Windows では動作しません。
Get a real operation system ;)