オープンできるファイル数上限を管理する「ファイルディスクリプタ」について( `Too many open files` エラーを理解する )

2020-06-29Bash,CentOS,Linux,Ubuntu

はじめに

プロジェクトで大量のファイルを扱う必要があり、
ファイルディスクリプタの話が出てきたので整理してみました。

検証環境

$ uname -moi
x86_64 x86_64 GNU/Linux

$ cat /etc/os-release
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

ファイルディスクリプタとは

  • OSが以下のようなものにアクセスする場合に用いる識別子(ニックネームのようなもの)を 「ファイルディスクリプタ」 といいます。
    • ファイル
    • 標準入出力など

0から順番に整数の値が割り当てられますが、
通常、以下の3つはOS(シェル)が最初に用意します。

  • 0:標準入力(stdin)
  • 1:標準出力(stdout)
  • 2:標準エラー出力(stderr)

そのため、プログラムがファイルをオープンすると「3」から順番にファイルディスクリプタが割り当てられます。

作成できるファイルディスクリプタの上限数

ファイルディスクリプタは無限に使えるわけではありません。
プロセスごと に使える上限が設定されています。

作成できるファイルディスクリプタの上限数エラーを発生させてみる

作成できるファイルディスクリプタの上限数を確認してみます。
PHP を使った例を挙げます。

$ php -r '
$fd = array();
# 1022ファイルをオープンします
for ($i = 0; $i < 1022; ++$i) {
    $fd[] = fopen("${i}.txt", "w");
    fwrite($fd[$i], "hoge");
}
'

上記スクリプトは以下のような特徴があります。

  1. 大量のファイルをオープンする
  2. オープンしたファイルをクローズしない
  3. オープンしたファイルのファイルディスクリプタを格納する変数は、スコープが広い

3つ目を組み込んだ理由は、スコープの狭い変数の場合、PHPが変数のスコープ終了に伴い自動的にファイルをクローズしてしまうことを回避するためです。

上記のサンプルコードについて言うと、根本原因としてプログラムの作りが悪いのですが、とはいえ並列処理で大量のファイルを処理しないといけないケースでは問題が発生する場合もあります。( Webサーバで大量のデータをさばく、Hadoopのようなビッグデータ分析の分散処理でファイルを処理する、など

実行してみます。

$ php -r '
> $fd = array();
> for ($i = 0; $i < 1022; ++$i) {
>     $fd[] = fopen("${i}.txt", "w");
>     fwrite($fd[$i], "hoge");
> }
> '
PHP Warning:  fopen(1021.txt): failed to open stream: Too many open files in Command line code on line 4
PHP Warning:  fwrite() expects parameter 1 to be resource, boolean given in Command line code on line 5

※注意:後述しますが、エラーが発生するかどうかは環境によって変わります。確認方法も後述します。

Too many open files というエラーが発生しました。(システム開発の経験がある程度あれば、こちらのエラーメッセージに遭遇した経験は少なくないはずです。)

これはファイルディスクリプタの上限数に引っかかってしまったことによりエラーが発生しています。

こちら、ループカウンタの上限値を 1022 から 1021 に変更すると問題は発生しなくなります。

$ php -r '
> $fd = array();
> for ($i = 0; $i < 1021; ++$i) {
>     $fd[] = fopen("${i}.txt", "w");
>     fwrite($fd[$i], "hoge");
> }
> '

作成できるファイルディスクリプタの上限数についてもう少し踏み込んでいきます。

作成できるファイルディスクリプタの上限数の確認

作成できるファイルディスクリプタの上限数は ulimit コマンドで確認できます。
( 以下の設定初期値はOSごと、あるいはDockerといった仮想環境ごとに異なります。 )

# ソフトリミット
$ ulimit -Sn
1024

# ハードリミット
$ ulimit -Hn
4096

「ソフトリミット」「ハードリミット」 とは何でしょうか?

ファイルディスクリプタの上限数に直接影響を与えるのは 「ソフトリミット」 の値になります。

先程 PHP のプログラムから 1022 のファイルをオープンしたタイミングでエラーとなりましたが、それは「ソフトリミット」の上限超えとなったためです。

  • ソフトリミット(1024) < PHPプログラムでオープンしたファイル数(1022) + 標準入力(1) + 標準出力(1) + 標準エラー出力(1)

初めの方で触れましたが、1つのプロセスに対して通常は「標準入力」、「標準出力」、「標準エラー出力」の3つのファイルディスクリプタが予め用意されるため、プログラム内でオープンできるファイルは ソフトリミット - 3 となるわけです。

では 「ソフトリミット」 の設定値はどこまでも上げられるのかというとそういうわけではなく、 「ハードリミット」 の上限を超えることはできません。

作成できるファイルディスクリプタの上限数の設定

先のプログラムがエラーとならないようにファイルディスクリプタの上限数を変更してみましょう。
やはり ulimit コマンドを利用します。

「ソフトリミット」を 1025 まで増やしてみます。

$ ulimit -Sn 1025

これで先程のスクリプトはエラーが出なくなります。

$ php -r '
$fd = array();
for ($i = 0; $i < 1022; ++$i) {
    $fd[] = fopen("${i}.txt", "w");
    fwrite($fd[$i], "hoge");
}
'

「ソフトリミット」の値を「ハードリミット」以上に設定した場合はどうなるでしょう?

# これはエラーが出ない
$ ulimit -Sn 4096

# エラーが発生
$ ulimit -Sn 4097
-bash: ulimit: open files: cannot modify limit: Invalid argument

「ハードリミット」より大きい値をセットしたタイミングでエラーが発生します。

注意: ulimit を設定したプロセスが終了すると設定が戻ってしまう

せっかく設定できた ulimit ですが、設定したプロセス(ログインシェルやプログラム)が終了するともとに戻ってしまいます。

# サブシェルを起動
$ bash

# ここからサブシェル --------------------
# 初期値を確認
$ ulimit -Sn
1024

# 設定
$ ulimit -Sn 2048

$ ulimit -Sn
2048

# サブシェルを抜ける
$ exit
# ここまでサブシェル --------------------

# 設定値が戻ってしまう
$ ulimit -Sn
1024

OS再起動時に自動起動するプロセスでファイルディスクリプタを増やしたいような場合にはちょっとした小細工が必要となりますが、また別の機会に触れます。

ひとこと

今回触れられませんでしたが、 「OS」単位で作成できるファイルディスクリプタの上限数設定 も別であります。
また整理します。

2020-06-29Bash,CentOS,Linux,Ubuntu