ファイルディスクリプタ数の上限変更とlimits.confの罠


こんにちは、mikkoです。

今回は、1プロセスが同時オープン可能なファイルディスクリプタ数の上限を変更する方法と、その際の落とし穴についてです。

Linuxでは、同時にオープンできるファイルディスクリプタ数が制限されています。
OS全体での設定は、/proc/sys/fs/file-max等で確認でき、大規模なアクセスがあるサーバでは/etc/sysctl.confに設定を追加して上限を増やしているケースも多いでしょう。

これとは別に、1プロセスが同時オープン可能なファイルディスクリプタ数は、標準で1024となっています。
apacheのpreforkモデル等の子プロセスが多数稼動するアプリケーションだと、1プロセスあたりのファイルオープン数はそこまで増加しないので、普段はあまり意識することは少ないかもしれません。
しかし、apacheのworkerモデル等のスレッドを使用するアプリケーションでは、この制限が致命的になるケースが発生します。

では、このファイルディスクリプタ数上限を上げるにはどうすればよいのでしょうか。

1プロセスあたりのファイルディスクリプタ数上限を上げるには3通りあり、

  1. カーネルヘッダファイル中のINR_OPENを書き換えてrebuild
  2. ulimit -n で一時的に更新する
  3. /etc/security/limits.conf に設定を記述する

の何れかを選択することになりますが、カーネルのrebuildは手間なので除外すると、選択肢は2つになります。

※プログラム内からsetrlimit()システムコールを呼ぶ方法もありますが、今回は既存のプログラムを扱うことを前提としているので除外します

ulimitとは何なのか?

シェルのbuilt-inコマンドで、プロセスに割り当てる各種リソースの制限を行うためのものです(csh系はlimitコマンド)。
ulimit -n を実行すると、現在のファイルディスクリプタ数上限値(SoftLimitおよびHardLimit)が確認できます。
SoftLimitはHardLimit以下の範囲内で一般ユーザが変更可能ですが、HardLimitはrootでしか変更できません。
ulimitを実行したシェルおよび、そのシェルから起動される子プロセスに対して一時的に設定を変えることが出来ます。

/etc/security/limits.conf とは何なのか?

/etc/security/limits.confは、PAM認証モジュールの1つであるpam_limitsの設定ファイルです。

<domain> <type> <item> <value>

というフォーマットで記述を行い、

root soft nofile 2048
root hard nofile 2048

と書けば、rootで実行されるプロセス単位の最大ファイルオープン数が2048に設定されます。

pam_limitsモジュールは、PAMの設定ファイル/etc/pam.d/system-auth内においてsession型で定義されており、/etc/pam.d/loginや/etc/pam.d/sshd、/etc/pam.d/sudo等多くの設定ファイルがsystem-authをインクルードする形で使用しています。
従って、先の設定はログイン時やsudo時など、PAM認証が発生する(且つsessionでpam_limitsモジュールがrequireされる)タイミングで有効化されます。

つまり、「PAM認証を介さないようなdaemon系プログラムの制限には/etc/security/limits.confは使えない」ことになるのです。

ところが、googleで「apache limits.conf」等を検索すると、limits.confを書き換えればapacheの上限ファイルディスクリプタ数を増加可能という内容の記事が散見されます。 これはいったいどういうことなのでしょうか?

罠1:該当ユーザでのulimit -nによる確認

limits.confに

* soft nofile 2048
* hard nofile 2048

を記述し、su等でrootになり、ulimit -nを実行して変更確認を行う方法です。

しかし、ログイン時やsu時においてPAM認証が行われた時点でlimits.confの設定が適用されるため、rcスクリプトやdaemontoolsから起動されるプロセスと環境が異なり、確認の意味を成していません。

罠2:rcスクリプトによる再起動での確認

実際にdaemonの再起動を行い、’Too many open files’のエラーが出なくなったことを確認する方法です。

実は、rcスクリプトやapachectl等で再起動を行うと、実際に設定が反映されています。
これは非常にわかりにくく、以下のような状況が発生しています。

例:sudo /etc/init.d/httpd restart

  1. sudoでrootユーザになる際にPAM認証が行われ、limits.confの設定が適用される(ulimit -n 2048と同義)
  2. rootユーザで/etc/init.d/httpdを実行する際、上限値が子プロセス(/etc/init.d/httpdはスクリプトなので、bashになる)に引き継がれる
  3. スクリプト内でhttpdを停止・開始する際、上限値が子プロセス(開始されるhttpd)に引き継がれる
  4. 上限値が引き継がれたhttpdが開始される

実際に以下の手順で検証してみました。

  1. 1100個のファイルを同時オープンする簡単なphpスクリプト(index.php)を用意
  2. <?php
    $fp = array();
    for ($i = 0; $i < 1100; $i++) {
    $fp[] = fopen('index.php', 'r');
    }
    echo '<pre>';
    var_dump($fp);
    echo '</pre>';
    
  3. apache経由でアクセスし、1000個付近でファイルが開けなくなることを確認
  4. ...snip...
    [999]=>
    resource(1002) of type (stream)
    [1000]=>
    resource(1003) of type (stream)
    [1001]=>
    bool(false)
    [1002]=>
    bool(false)
    ...snip...
    

    ※ apacheのerror_logにも、’Too many open files’が表示されています。

    ※ 上限の1024に満たないのは、httpdプロセス自身がいくつか消費しているためです。lsofコマンド等で確認が可能です。

  5. limits.conf を書き換える
  6. * soft nofile 2048
    * hard nofile 2048
    

    を追加する。

  7. sudo /etc/init.d/httpd restartでapacheを再起動する
  8. 再度apache経由でアクセスし、1000個以上ファイルが開けることを確認
  9. ※ ここまでで、上限値が反映されたと誤解してしまいます!

  10. マシンを再起動する
  11. 三度apache経由でアクセスし、1000個付近でファイルが開けなくなることを確認
  12. ※ 上限値が1024に戻ってしまいます!

つまり、手動で再起動した場合は一時的にlimits.confの設定内容が有効になるだけなのです。
当然、マシン自体が再起動した場合はinitが各daemonを起動し、PAM認証が入らないため、OS規定の上限値である1024に戻ってしまいます。

結局、リミット値を上げるには、ulimit -n の記述を

  • daemontoolsであれば、/service/<サービス名>/runファイルに追加
  • rcスクリプトであれば、/etc/init.d/<サービス名>ファイルに追加

しないといけません。

ただしapacheの場合、apachectl経由での制御を行うとrcスクリプトを介さないため、apachectlにも追加しておかないといけないことに注意してください。

  • apachectl restart
  • 既存のhttpdプロセスにシグナルを送り、既存httpdから新規httpdがforkされるため、既存httpdのリミット値が引き継がれます。

  • apachectl start
  • 新規httpdが起動され、上記rcスクリプトの実行の流れと同じ現象が起こるため、ulimitの追加が必要です。

結論

daemon系プロセスのファイルディスクリプタ数上限を設定する際、/etc/security/limits.conf は使えません。状況によっては一見設定されたように見えますが、大きな落とし穴にはまることになります。
面倒ですが、必要なプロセス毎にulimitを用いて適切に設定しましょう。

3 thoughts on “ファイルディスクリプタ数の上限変更とlimits.confの罠

  1. PAM認証を通らないと、プロセスオーナーに紐付いたlimits.confのFD上限値が適用されないのね。不勉強でした。