Linuxで開ける最大のfile descriptorの数について調べたメモ

1サーバで大量の常時接続コネクションを維持するにはlinuxのfile descriptorの上限がボトルネックになるんじゃないかと思って調べた。
最初に注意として、以下に解説する設定を下手に変更してしまうとシステムが動かなくなる場合があるので変更する場合は慎重に。当たり前ですが責任は負えません。

----

file descriptorの上限には以下の2つがある。

- 1プロセスが開ける上限
- システム全体で開ける上限

# 1プロセスが開ける上限
1プロセスが開ける上限にはsoft limit, hard limitと呼ばれる２種類ある。またこれらはプロセス毎に設定される。
自分のsoft limitは0〜hard limitの範囲で変更できる。
自分のhard limitは小さくする分には自由に変更できるが、大きくするには CAP_SYS_RESOURCE という専用の権限がいる。自分以外のプロセスの設定するにも同様の権限がいる。

コマンドラインからはbashのbuilt-inコマンドであるulimitコマンドがよく使われる。

- `ulimit -n` あるいは `ulimit -Sn` でsoft limitを参照。引数つけたら変更。
- `ulimit -Hn` でhard limitを参照。引数つけたら変更。

このulimitコマンドはgetrlimit/setrlimitシステムコールを使って実装されている。多分。
で、 `ulimit -n` コマンドではgetrlimit/setrlimitで`RLIMIT_NOFILE`という項目を取得/変更している。 
この`RLIMIT_NOFILE`のデフォルトはsoft limitは1024, hard limitは4096。

* https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/fs.h#L29-L30
* https://github.com/torvalds/linux/blob/v4.20/include/asm-generic/resource.h#L20
* http://man7.org/linux/man-pages/man2/getrlimit.2.html

file descriptorについてググってると、`INR_OPEN`を変更してlinux kernelをrebuildすれば上限を変更できる、という記事が見つかるが、これは古いlinux kernelのRLIMIT_NOFILEのsoft limit, hard limitの初期値を変更する、という話の模様。今はINR_OPENという変数はない。

* INR_OPENについて書いてる記事
    * http://www.asahi-net.or.jp/~ty4s-tkhs/memo/kernelparm.html
    * https://qiita.com/tdrk/items/2d232f19861cca117ad5
* 変更のコミット
    * https://github.com/torvalds/linux/commit/0ac1ee0bfec2a4ad118f907ce586d0dfd8db7641

で、このRLIMIT_NOFILEは前述の通りulimitコマンドなどで変更できるが、これはkernelパラメータのfs.nr_openが上限になる。nr_openのデフォルトは1048576(=1024*1024)。
これも変更できるが、上限が次の式で表されていた。普通の64bit環境だと2147483584(=(2^31-1)&-64)になると思う多分

```c
unsigned int sysctl_nr_open_max =
	__const_min(INT_MAX, ~(size_t)0/sizeof(void *)) & -BITS_PER_LONG;
```

* https://github.com/torvalds/linux/blob/v4.20/Documentation/sysctl/fs.txt#L116
* https://github.com/torvalds/linux/blob/v4.20/fs/file.c#L26
* https://github.com/torvalds/linux/blob/v4.20/kernel/sysctl.c#L1711-L1719

# システム全体の上限
システム全体の上限はkernelパラメータのfs.file-max。

* https://github.com/torvalds/linux/blob/v4.20/Documentation/sysctl/fs.txt#L95
* https://github.com/torvalds/linux/blob/v4.20/kernel/sysctl.c#L1704-L1710

当然nr_open <= file-maxになるものかと思ったけど、priviledged userはfile-max以上のファイルを開けるっぽい。びっくり。

* https://github.com/torvalds/linux/blob/v4.20/fs/file_table.c#L139
* https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/fs.h#L17-L25

初期値はここで決まるっぽい。正確には理解してないけど、メモリ(kb)の10%弱になるみたい。

* https://github.com/torvalds/linux/blob/v4.20/fs/file_table.c#L376-L389

最大値はunsigned longの上限までいける。

* https://github.com/torvalds/linux/blob/v4.20/kernel/sysctl.c#L1707
* https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/fs.h#L92

# まとめ
設定可能な１プロセスあたりの開ける最大のfile descriptorの数は以下の式で表され、一般的な64bit環境だと2147483584。

* https://github.com/torvalds/linux/blob/v4.20/fs/file.c#L26

設定可能なシステム全体で開ける最大のfile descriptorの数はULONG_MAXで、一般的な64bi環境だと18446744073709551615。
調べ始めた動機の１サーバで捌けるコネクション数という意味だと、設定すれば1プロセスで20億超捌けるのでこれがボトルネックになるということはなさそう。

linuxのコードベースに詳しくないのでソースコード引用したところ間違ってたらすいません。

あと最初に変更する場合は注意と書いたけど、特にfs.file-maxは注意しないといけない。これを小さすぎる値にすると新しいfile descriptorが作れなくなって、rootユーザでなければbashからの実行ファイルの実行が全て失敗する模様[^1]。つまりfs.file-maxの値を戻すsysctlコマンドもrebootも失敗する。擬似端末作ることができないから新規にログインも出来ない。またunsigned longの上限を超える値を設定するとオーバーフローするらしくULONG_MAXを少し超えた値を設定しようとしても同様のことが起こる。実際にやってみるとこんな感じになる。

```
vagrant@ubuntu-xenial:~$ sudo sysctl fs.file-max=18446744073709551617
fs.file-max = 18446744073709551617
vagrant@ubuntu-xenial:~$ ls
-bash: start_pipeline: pgrp pipe: Too many open files in system
-bash: /bin/ls: Too many open files in system
```

VM等、外から再起動が可能ならともかくデータセンターの物理サーバとかだと多分現場で物理電源ボタンを押すか電源供給を止めて終了してから起動し直すなどするしかない。
RLIMIT_NOFILEはプロセスごとなのでshellならログインし直したらリセットされるし、特定のプロセスを別プロセスから変更することも可能。nr_openはminimumが設定されていて64bitマシンだと最小64までしか設定できない。file-maxと比べると間違った操作してもまだまし。

[^1]: 実行ファイルを実行するたびにpipeを作ってるみたい。