pgpool-IIの接続性能の改善
SRA OSS, Inc. 日本支社 石井 達夫
はじめに
本記事は2013年のPostgreSQL Advent Calendar の 12/17 の記事です。pgpool-IIに多数のクライアント同時に接続すると、極端にレスポンスが落ちることがあります。ここではその原因と改善方法について考えます。
pgpool-IIはpre-fork型のアーキテクチャ
pgpool-IIは、複数のPostgreSQLを使ったクラスタシステムを構築できるミドルウェアです。pgpool-IIでは、num_init_childrenというパラメータの数だけあらかじめプロセスを起動(pre-fork)しておきます。クライアントからの接続要求があると、そのプロセスの一つがカーネルから選択され、クライアントからの接続を受付けて、処理を開始します。これはApacheなどと同じ方式で、あらかじめプロセスをフォークしておくことから「pre-fork方式」と呼ばれます。この方式は、リクエストがあったときにプロセスを起動する方式と比べると、プロセスを起動する処理がない分だけ高速です。
pgpool-IIの親プロセスは、TCP/IPの接続要求を待つlisten(2)システムコールを発行したままの状態で子プロセスをforkします。空いている子プロセスはlisten()を発行してはクライアントからの接続要求を待ち続けます。接続要求が到着すると、カーネルは子プロセスを1個だけ選択してその子プロセスが接続要求をaccept(2)システムコールで受け取ります。
接続要求を受け付けたpgpool-II子プロセスは、それぞれがクライアントからのセッションに1対1で対応し、1個以上のPostgreSQLデータベースクラスタに接続します。PostgreSQLから見ると、num_init_children以上の接続が来ないので、PostgreSQLへの接続要求が多すぎるときに出る"Sorry, too many clients already"のエラーに悩まされることがなくなります(正確には、num_init_children*[データベースユーザ/データベース名]のペア分以上の接続が来ない、となります。詳細はpgpool-IIのマニュアルをご覧ください)。
num_init_childrenを越えた接続要求への対応
では、num_init_childrenを越えた接続要求が来たらどうなるのでしょう?正解は「クライアントは接続を待たされる」です(つまり、いつもより接続に時間がかかっているように見えます)。待たされているリクエストは、カーネルが管理する「listenキュー」という待ち行列の中に格納されます。この待ち行列の長さは、num_init_children*2で設定されています。
listenキューの溢れがレスポンス低下の原因だった
では、listenキューが溢れるとどうなるのでしょう?次の図は、クライアントがpgpool-IIに接続する際の状況を表したものです。
listenキューが満杯の場合、pgpool-IIは接続を受け付けずにクライアントの接続要求を無視します。無視されたクライアントは、リクエストの再送を試みます。その際、再送間隔は前回の再送間隔の2倍になります。再送回数は通常5回になっています(手元のLinuxでは、/proc/sys/net/ipv4/tcp_syn_retriesに回数が定義されています)。最初の再送間隔は1秒なので、最大5回再送を行うと、1+2+4+8+16=31秒間接続に時間がかかったように見えます。listenキューが溢れると致命的なレスポンスの低下を招くことがわかりました。
listenキューの溢れを再現してみる
では、listenキューの溢れはどのようにしたらチェックできるでしょうか。再現テストを実施して確認してみましょう。
まず、pgpool.confのnum_init_childrenパラメータを適当に小さくします。今回は2にしてみました。そして、pgpool-II起動し、同時に128のクライアントから接続します。ここでは、次のようなスクリプトを使い、pgbenchを128本起動して負荷をかけてみます。ここで注意するのは、接続をTCP/IPで行うことです。pgpool-IIが動いている同じホストでUNIX DOMAINソケットで接続したのでは現象が観測されないのでご注意ください。
#!/bin/bash export LANG=C export PGPORT=11002 export PGHOST=133.137.177.158 cnt=128 while : do (date;pgbench -C -c 1 -n -S test;date)& cnt=`expr $cnt - 1` if [ $cnt -le 0 ];then break; fi done wait
ここで、PGPORTにはpgpool-IIの待ち受けポート番号、PGHOSTにはpgpool-IIが動いているホストのIPアドレスかホスト名をセットしてください。
このスクリプトを実行後、pgpool-IIが動いているサーバで"netstat -s"コマンドを実行すると、以下のように"535 times the listen queue of a socket overflowed"と表示され、listenキューが溢れたことが確認できると思います。
TcpExt:
38 個のSYNクッキーを送信
164 個のSYNクッキーを受信
22 invalid SYN cookies received
ArpFilter: 0
133 TCP sockets finished time wait in fast timer
12 delayed acks sent
Quick ack mode was activated 6 times
535 times the listen queue of a socket overflowed
535 SYNs to LISTEN sockets ignored
更にスクリプト実行中に
Connection to database "test" failed: could not connect to server: Connection timed out
のようなエラーが起きており、クライアントが接続エラーを起こしていることもわかりました。
listenキュー溢れを防ぐには
残念ながら、現在のpgpool-IIではlistenキューの長さ(バックログ)が、「num_init_childrenの2倍」に固定されており、それ以上大きな値を設定できません。num_init_childrenを増やせばバックログを大きくできますが、今度はPostgreSQLへの接続数が増えてしまい、PostgreSQLに負担がかかります。
num_init_chldrenを変えずにバックログを増やすには、ソースコードの修正が必要です。しかし、この修正は非常に簡単なので、listenキュー溢れでお困りの方は、是非チャレンジしてみてください。
本稿執筆時点で最新のpgpool-II 3.3.2を例に、修正方法を示します(pgpool-II 3.0以降であれば同様の方法で修正できます)。
main.cというファイルの1359行目に次のような行があります。
backlog = pool_config->num_init_children * 2; if (backlog > PGPOOLMAXLITSENQUEUELENGTH) backlog = PGPOOLMAXLITSENQUEUELENGTH; status = listen(fd, backlog); <--- この行に注目
この行の前に、
backlog =128;
という行を挿入します。修正後は以下のようになります。
backlog = pool_config->num_init_children * 2;
if (backlog > PGPOOLMAXLITSENQUEUELENGTH)
backlog = PGPOOLMAXLITSENQUEUELENGTH;
backlog = 128;
status = listen(fd, backlog);
なお、"128"という数字は、同時に接続が見込まれるクライアントの数に応じて増やして構いません。
手元の環境では、このように修正したpgpool-IIで同様の検証をしたところ、"the listen queue of a socket overflowed"のエラーが出なくなったばかりでなく、スクリプト全体の実行時間が1/4に減りました。
カーネル設定値にご注意!
システムによっては、上の方法では一定以上にバックログを増やせないことがあります。手元のLinux (kernel 3.4.69) では、この値はSOMAXCONNという変数で上限が決められています。
$ sysctl net.core.somaxconn net.core.somaxconn = 128
手元のLinuxでは128が上限になっていました。pgpool-IIのバックログはすでに説明したように、num_init_childrenの2倍に設定されますから、すでにnum_init_childrenを64よりも大きくしている方は、このカーネル設定値ではバックログが不足している可能性があります(実際に不足しているかどうかは、前述のようにnetstat -sで確認できます)。somaxconnを128より大きくするには、sysctlで上の変数を上書きするか、/etc/sysctl.confに記述してシステムを再起動します。
まとめ
本記事では、pgpool-IIの接続性能がlistenキューのバックログの不足によって低下することを説明し、その改善方法を示しました。
本稿で示した改善方法はソースコードの修正が必要でしたが、次のpgpool-IIのメジャーバージョンアップ時には、pgpool.confで設定できるように改善する予定です。
最後に、本稿を執筆するにあたり、以下のブログ記事を参考にさせていただきました。お礼を申し上げます。