Ruby と FD_SETSIZE 問題

FD_SETSIZE 問題とは、1 つのプロセスで大量のファイルを同時に open するなどしてファイルデスクリプタの値が FD_SETSIZE *1の値を超える(fd >= FD_SETSIZE になる)と標準の fd_set 型で取り扱えなくなり、select(2) が正しく実行できなくなる、という問題。有名でわりと古典的な問題だが、今でもいろんなところに埋まっている。
たとえば、同時に大量のネットワークコネクションを取り扱いたい大規模サーバではこれが問題になる。実際にこれを踏むと、範囲外アクセスを起こして Segmentation fault で落ちることが多い。
なお、select(2) そのものの実装は(現代的な OS のカーネルであれば) FD_SETSIZE には依存しておらず、呼び出し側(ユーザランド)の問題である。
また、一般的な解決方法として、子プロセスを作ってそちらにコネクションを渡してしまい、1プロセスあたりのデスクリプタ数が FD_SETSIZE を超えないように調整するという方法がある。

Ruby 1.8 系の場合

  • Ruby 本体
    • 問題あり。大量のファイルやソケットを open すると Segmentation fault で死ぬ。
    • スレッド構造体 rb_thread に fd_set が埋まっており、これを改造すると ABI が変わるため手が出せない。
    • なお、select(2) は Kernel#select 以外に色々なところで呼び出されているので(Thread の待合わせなど)、「たくさんファイルを open しても Kernel#select を使わなければ大丈夫」という訳ではない。
  • 拡張ライブラリ
    • Ruby/EventMachine を使えば、FD_SETSIZE を超える数のネットワーク接続を同時に取り扱える。これは、Ruby 本体を介さずにデスクリプタを操作しているため。
      • が、多数のソケットを開いている状態でうっかり Kernel#open などで普通にファイルを開くと、IO#fileno >= FD_SETSIZE なオブジェクトが生成されて死ぬ。
      • 実用的とは言いがたい。

Ruby 1.9 系の場合

  • Ruby 本体
    • 問題なし。(予定)
      • 1.9.1 の時点でバグ入りだが、次のリリースで既知のバグは修正される見込み。r23109
    • fd_set 型の代わりに rb_fdset_t という独自の型が導入された。これを使っている限りは FD_SETSIZE 問題は起こらないはず。
  • 拡張ライブラリ
    • rb_thread_select() が API として色々な拡張ライブラリから呼ばれており、これが問題を引き起こしていることが多いように見える。(rb_thread_select() の引き数は fd_set 型へのポインタ)
      • 代わりに rb_thread_fd_select() を使うべし。(こちらの引き数は rb_fd_set 型へのポインタ)
    • 付属の readline.so も rb_thread_select() を使っているため、問題あり。
      • もっとも、そもそも libreadline.so が中で fd_set 型を使っているため、Ruby 側のバインディングを修正しても意味が無い。(どうせ死ぬ)
    • けっきょく、使用する拡張ライブラリのソースを全て確認するしかない。

Python の場合

Ruby じゃないけど。

  • Python 本体
    • poll(2) を使っているため、普通の open() が問題になることはなさそう。
  • 拡張ライブラリ
    • select.select() は FD_SETSIZE に依存している。
    • select.epoll() を代わりに使えば OK。(Python 2.6 以降)
    • その他のライブラリは未調査。

結論めいたもの

Ruby 1.8 系は、この点においてはダメそう。将来も期待できない。
Ruby 1.9 系および Python は、本体のみを使っている限りは大丈夫そう。ただし、拡張ライブラリについては個別にソースを確認する必要がある。

*1:Linux では 1024 であることが多い