高負荷を捌くDBチューニングノウハウを公開!PHPとMySQLの TCP TIME-WAIT チューニング(後編)

こんにちは。(株)インフィニットループの波多野です。

カーネルパラメータとコンフィグ調整による TCP チューニングの実例

後編では、CentOS 7 を前提に、当社 (株)インフィニットループ で採用している手法を使ってチューニングを行ってみます。
仮想的に Web サーバーが 100台あり、DB サーバーはとても高速に処理されているケースをイメージして、必要なコネクション数の設定を行っていきます。

前述のパーシステント接続の場合のテストで Web サーバー1台 に対し Apache Bench で 935 リクエスト/秒 を達成しました。これをベースにWeb サーバー 100台と仮定、MySQL は1台、Web サーバーは1台あたり 900 リクエスト/秒以上の性能が出せるようにTCP ポート数やオープン可能ファイル数などのパラメータを調整します。

MySQL の max_connections と open_files_limit

MySQL で設定しているコネクションの最大数は max_connections でデフォルトは 151 です。接続してくる Web サーバーは 100 台あり、それぞれデフォルトで MaxClient が 256 の Apache Prefork になります。最大で同時に 255 * 100 = 25500 の接続があり得ますので max_connections も /etc/my.cnf にて変更します。

/etc/my.cnf の変更点

# max_connections=151 
max_connections=25500

Unix システムはディスクファイルだけではなくて TCP のソケットもファイルとして取り扱います。各プロセスにはオープン可能な最大ファイル数の数値をもっており、一般ユーザーで利用可能な soft limit と root の場合に利用可能な hard limit の2種持っています。

オープン可能ファイル数上限は ulimit -n でそのユーザーのシェル上での "Max open files" が確認出来ます。しかしながらシェルの ulimit は mysqld に関与しませんので本記事では変更しません。

またシステム全体に対するファイルシステムでのオープンファイルの上限が fs.file-max カーネルパラメータで設定されていますので確認します。

[knowl@db ~]$ sysctl fs.file-max 
fs.file-max = 97634

DB サーバーでのファイルシステム上の上限はデフォルトの 97634 で足りていますのでこのまま使います。

現在の mysqld の実際の "Max open files" を /proc/[pid]/limits ファイルで調べると 5000 であることがわかります。

[knowl@db ~]$ grep "open files" /proc/`pidof mysqld`/limits 
Max open files 5000 5000 files

mysqld では /proc/[pid]/limits で直接見る他に open_files_limit システム変数によってこの設定値を確認することが出来ます。

open_files_limit のデフォルトは 5000 ですがそのうち 151 がコネクション用でしたので、5000 - 151 = 4849 がテーブルやログなどでつかわれている MySQL のためのファイルオープン数と見なせます。新しくコネクション数を増やしますのでこれと合わせて、4849 + 25500 = 30349 に open_files_limit を設定します。

CentOS 7 の場合はこれを行うには my.cnf ではなく systemd の起動ファイル /usr/lib/systemd/system/mysqld.service を

systemctl edit --full mysqld.service

コマンドで変更します。

/usr/lib/systemd/system/mysqld.service の変更点

# Sets open_files_limit 
#LimitNOFILE = 5000 
LimitNOFILE = 30349

ファイルを変更した後は mysqld を restart します。

[knowl@db ~]$ sudo systemctl restart mysqld

mysql 上で確認します。

mysql> show global variables like 'open_files_limit'; 
+------------------+-------+ 
| Variable_name    | Value | 
+------------------+-------+ 
| open_files_limit | 30349 | 
+------------------+-------+ 
1 row in set (0.00 sec) 

mysql> show global variables like 'max_connections'; 
+-----------------+-------+ 
| Variable_name   | Value | 
+-----------------+-------+ 
| max_connections | 25500 | 
+-----------------+-------+ 
1 row in set (0.00 sec)

Apache と OS の TCP チューニング

MySQL では ESTAB な接続をイメージしてその最大値についてのコネクションとファイルオープン数について配慮しました。Apache の場合には同時接続数に加えて TIME-WAIT による60秒残留してしまうソケットによる消費分も加えて設定します。

Apache でデフォルトの KeepAlive on を使っている場合の接続のイメージ図

実際に通信しているコネクション数は httpd 数を元に算出します。MySQL とクライアントと両方向であること、KeepAlive が成立しなかった場合に新規コネクションとなる分を加味して、httpd 数の4倍、256  * 4 = 1024 程度あれば足りる数です。

一方 TIME-WAIT の方は 秒間リクエスト数 * 60秒 の数のソケットを必要とします。今目標を 900リクエスト/秒にしていますので、TIME-WAIT だけで 54000 ソケットを消費します。

合わせて最大 55024 のオープン数に耐えられるように調整していきます。

ip_local_port_range

宛先の IP アドレスとポートを指定してソケットをオープンする際、自分側の NIC と IP は宛先に到達可能な適切なものがカーネルによって選ばれます。そして送信元のポート番号はエフェメラルポートから自動で選ばれます。このエフェメラルポートの範囲を定義したものが ip_local_port_range です。CentOS 7 のデフォルトの 32768 - 60999 = 28231 個では今回想定の 55024 より小さく枯渇してしまいます。 55024 個が使える様に 10511 - 65535 に広げます。

パケットフィルタ

ローカルポートに指定した番号は送信側のポートとして使われます。ステートレスのフィルタなのでこのポートに向かって戻りの通信のパケットがやって来ることを考慮して同じ範囲を開ける必要があります。パケットフィルタの定義でも同様に 10511 - 65535 に設定します。

fs.file-max

db サーバーと同じくデフォルトの 97364 で足りていますので、変更不要です。

tcp_max_tw_buckets

55024 個に拡張したソケットの殆どは TIME-WAIT に使う予定ですが、この TIME-WAIT のソケットをいくつカーネル上で保持出来るかは tcp_max_tw_buckets で決められています。デフォルトは 4096 個です。

tcp_max_tw_buckets に達していた場合 TIME-WAIT ステートにはならず即 CLOSE となります。この buckets 数を意図的に小さく持っていれば TIME-WAIT 状態そのものを無くしてしまうことが出来ますが、これは TCP のハンドシェークのしくみを違反しているのと同じ効果があるのでそうならないように気をつけます。

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt より

tcp_max_tw_buckets - INTEGER 
        Maximal number of timewait sockets held by system simultaneously. 
        If this number is exceeded time-wait socket is immediately destroyed 
        and warning is printed. This limit exists only to prevent 
        simple DoS attacks, you _must_ not lower the limit artificially, 
        but rather increase it (probably, after increasing installed memory), 
        if network conditions require more than default value.

デフォルト値は 4096 ですので、このまま使っていると意図して下げているのと同じ状態になってしまいます。ポート数と同数の 65536 にセットします。

tcp_tw_reuse

TCP のシーケンス番号とタイムスタンプによる新旧の情報から、カーネルによって再利用可能と判定された TIME-WAIT ソケットを再利用出来るようになります。強制的に切断してしまう tcp_max_tw_buckets 超過とは違って TCP の仕様に沿いつつ行われるため比較的安全で、またソケットの不足時には効果を発揮しますので有効にします。

ここまでのカーネルパラメータの変更をまとめるとこちらになります。

# default & my config value 
#net.ipv4.ip_local_port_range=32768 60999 
net.ipv4.ip_local_port_range=10511 65535 
#net.ipv4.tcp_max_tw_buckets=4096 
net.ipv4.tcp_max_tw_buckets=65536 
#net.ipv4.tcp_tw_reuse=0 
net.ipv4.tcp_tw_reuse=1

httpd の max open files

1個の httpd プロセスで全ソケットを賄うことはありませんが、どのように配分されるか予測が付きませんので、それぞれのプロセス全てで 55024 個のファイルオープンが可能なように設定します。

現在の値は以下のように確認すると soft limit が 1024 で hard limit が 4096 だとわかります。

[knowl@app ~]$ for i in `pidof httpd`;do grep "open files" /proc/$i/limits;done 
Max open files 1024 4096 files 
Max open files 1024 4096 files
  : 省略

変更は mysqld と同じく

systemctl edit --full httpd.service

コマンドにて /usr/lib/systemd/system/httpd.service の [Service] セクションに記述を追加し、httpd を restart をします

省略 
[Service] 
LimitNOFILE=55024 
省略

10万リクエストを実行して TIME-WAIT の数を測定します

db サーバー側から Apache Bench で1台の Web サーバーに対して 16 並行、10万リクエストを実行しました。エラー無しで 1555 リクエスト/秒、通算64秒で実行されました。

Concurrency Level: 16 
Time taken for tests: 64.308 seconds 
Complete requests: 100000 
Failed requests: 0 
Write errors: 0 
Total transferred: 20500000 bytes 
HTML transferred: 500000 bytes 
Requests per second: 1555.02 [#/sec] (mean) 
Time per request: 10.289 [ms] (mean) 
Time per request: 0.643 [ms] (mean, across all concurrent requests) 
Transfer rate: 311.31 [Kbytes/sec] received

実行完了直後に ポート 3306 が宛先のソケット数は 43836 個ありました。

[knowl@app ~]$ ss -nrt state all '( dport = 3306 )'|wc -l 
43836

ここまでで行ったパラメータのチューニングによってエラーなく処理することが可能になりました。

一方結果をよく見ると、最初に想定していたリクエスト数を大幅に超過した 1555 リクエスト/秒 を処理してしたことがわかります。tcp_tw_reuse によって再利用されたことで運よくエラーにはなっていませんが、必要なソケット数は9万個を超えています。

MySQL にもアクセスする PHP のスクリプトの応答時間が 0.5 ms というケースは高速すぎて現実的ではないですが、仮にこのような高速な領域になってきた場合には利用可能なポート数をさらに増やすか、既に上限いっぱいの場合は MySQL へのアクセスをある程度まとめてアクセス回数を減らすようにアプリケーションを変更したり、Web サーバーの台数を変更して1台あたりの処理数を減らすなどの考慮が必要になります。

また今回活躍した tcp_tw_reuse ですが、あくまで条件がそろっているときに再利用しているに過ぎず、残念ながらこれだけで TIME-WAIT なソケットを 100% 気にしなくてよくなる、ということはありません。実際にエフェメラルポートの数を sysctl で1000個に制限して同じテストを行ってみるとエラーが多数発生することがわかります。

Concurrency Level: 16 
Time taken for tests: 56.175 seconds 
Complete requests: 100000 
Failed requests: 71193 
(Connect: 0, Receive: 0, Length: 71193, Exceptions: 0) 
Write errors: 0 
Non-2xx responses: 71193 
Total transferred: 26053054 bytes 
HTML transferred: 4629194 bytes 
Requests per second: 1780.17 [#/sec] (mean) 
Time per request: 8.988 [ms] (mean) 
Time per request: 0.562 [ms] (mean, across all concurrent requests) 
Transfer rate: 452.92 [Kbytes/sec] received

まとめ

最後に本連載の内容についてまとめました。

  • PHP のパーシステント接続はトランザクションを使う MySQL では事実上採用出来ない
  • PHP の非パーシステント接続は TCP のアクティブクローズ処理になり TIME-WAIT ソケットを残留させる
  • TIME-WAIT ソケットのタイムアウト時間は 60秒固定。変更にはカーネルのビルドが必要
  • tcp_fin_timeout は FIN-WAIT-2 のチューニングには使えるが TIME-WAIT は変わらない
  • fs.file-max はデフォルト値でもだいたい足りる(ケースによって変更必要)
  • MySQL の open_files_limit は /usr/lib/systemd/system/mysqld.sevice の LimitNOFILE で大きくする
  • MySQL の max_connections は /etc/my.cnf で大きくする
  • Apache 側の ip_local_port_range はデフォルトだと 28231 個しかないので必要なサイズに変更する
  • Apache 側のステートレス・パケットフィルタのアウトバウンド通信の戻り側パケットの範囲は ip_local_port_range に合わせる
  • Apache 側の tcp_max_tw_buckets も 4096 では少なすぎるので 65536 に変更する
  • Apache 側の tcp_tw_reuse を有効化してソケットの枯渇を緩和する
  • Apache の max open files も /usr/lib/systemd/system/httpd.service の LimitNOFILE で大きくする
  • 秒間リクエスト数が見積もりを超えている場合は tcp_tw_reuse によって運よくエラーになっていない可能性がある。ポート数の見積もり、Web サーバーの台数、MySQL へのアクセス数などの見直しが必要

今回の記事が日々のチューニングに邁進されているエンジニアのみなさんのお手伝いになれば幸いです。