systemctlコマンドで "No space left on device" が表示されるけどdfでは余裕があるナゾ

Red Hatの森若です。

systemctlコマンドでサービスを起動すると、予期しないエラーが出力されます。しかし操作は成功しているし、df等でファイルシステムを見ても余裕があります。 今回はこの状況で何が起きていたのか見てみます。

# systemctl start httpd.service
Error: No space left on device

inotifyとは?

linuxにはinotifyという機能があり、ファイルやディレクトリ等への操作をイベントとして取得することができます。 inotifyではアプリケーションがファイルとして「inotify instance」を用意し、inotify instanceにイベントに対応する「inotify watch」を複数登録します。 inotify watchがイベントを検出するごとに、inotify instanceのキューにイベントを追記していきます。 アプリケーションがinotify instanceから読み込みを行うと、イベントの種類やファイル名などを取得できる仕組みです。

inotify概要図

inotifyはlinux特有の仕組み(現在のUNIX系OSはそれぞれ同様の目的の仕組みを持っていますが標準化はされていません)ですが、systemctlを含む多数のソフトウェアでごく一般的に利用されています。

今回の現象の原因

一部のアプリケーションは非常に多くのinotify watchを利用するため、システム全体の利用上限に触れる場合があります。今回問題が発生したのはこのケースでした。 inotify はkernel内部でファイルシステムの枠組みを利用しているため、エラー時には "No space left on device"(ENOSPC) のようなファイルシステムに関連するエラーを返す場合があります。

linux/fs/notify/inotify/inotify_user.c より

static int inotify_new_watch(struct fsnotify_group *group,
                             struct inode *inode,
                             u32 arg)
{
(中略)
        ret = inotify_add_to_idr(idr, idr_lock, tmp_i_mark);   # 新しいinotify watchを追加する
        if (ret)
                goto out_err;

        /* increment the number of watches the user has */
        if (!inc_inotify_watches(group->inotify_data.ucounts)) {  # inotify watchのカウンタを増やす
                inotify_remove_from_idr(group, tmp_i_mark);
                ret = -ENOSPC;                                 # 失敗するとENOSPC
                goto out_err;
        }

(以下略)

inotifyの上限設定

「inotify instance」と「watch」のどちらにもシステム全体とユーザ毎の上限があり、sysctlで設定・読み出しできます。inotify watch の上限数はシステムの搭載メモリから自動的に計算されます。

例:

$ sudo sysctl -a |grep inotify
fs.inotify.max_queued_events = 16384
fs.inotify.max_user_instances = 128
fs.inotify.max_user_watches = 28587
user.max_inotify_instances = 128
user.max_inotify_watches = 28587

それぞれは以下のような意味です。

項目 意味
fs.inotify.max_queued_events 1つのinotify instanceで保持するイベント数の上限
fs.inotify.max_user_instances (システム全体での) inotify instance数の上限
fs.inotify.max_user_watches (システム全体での) inotify watch数の上限
user.max_inotify_instances (1 UIDあたりの) inotify instance数の上限
user.max_inotify_watches (1 UIDあたりの) inotify watch数の上限

inotifyの利用状況

inotify を利用しているプログラムには対応するファイルディスクリプタがあり、lsofで見つけられます。

例: inotify instanceに対応するファイルディスクリプタをみつける

# lsof -p 1|grep inotify
systemd   1 root    6r  a_inode               0,14        0      10890 inotify
systemd   1 root   11r  a_inode               0,14        0      10890 inotify
systemd   1 root   13r  a_inode               0,14        0      10890 inotify
systemd   1 root   20r  a_inode               0,14        0      10890 inotify
systemd   1 root   21r  a_inode               0,14        0      10890 inotify
systemd   1 root   22r  a_inode               0,14        0      10890 inotify

inotify watchの利用状況を見るには、 /proc/PID/fdinfo/FD 以下を見ます。 inotify instanceに対応するファイルディスクリプタの/proc/PID/fdinfo/FD には、"inotify"で初まる行が含まれていて、inotify watchの情報が含まれています。

例: inotify instanceに対応するfdinfoには対応するinotify watchの情報が含まれる

# cat /proc/1/fdinfo/6
pos:    0
flags:  02004000
mnt_id: 15
ino:    10890
inotify wd:38c ino:850 sdev:1a mask:2 ignored_mask:0 fhandle-bytes:8 fhandle-type:fe f_handle:5008000000000000
inotify wd:38b ino:838 sdev:1a mask:2 ignored_mask:0 fhandle-bytes:8 fhandle-type:fe f_handle:3808000000000000
inotify wd:38a ino:bd1 sdev:1a mask:2 ignored_mask:0 fhandle-bytes:8 fhandle-type:fe f_handle:d10b000000000000
inotify wd:389 ino:bb9 sdev:1a mask:2 ignored_mask:0 fhandle-bytes:8 fhandle-type:fe f_handle:b90b000000000000
inotify wd:388 ino:d9f sdev:1a mask:2 ignored_mask:0 fhandle-bytes:8 fhandle-type:fe f_handle:9f0d000000000000
inotify wd:387 ino:d87 sdev:1a mask:2 ignored_mask:0 fhandle-bytes:8 fhandle-type:fe f_handle:870d000000000000
(以下略)

全プロセスから参照されているinotify watchを確認するには cat /proc/*/fdinfo/*|grep inotify|sort|uniq|wc -l のようにします。 実行中にもファイルのopen/closeやプロセスの作成・終了などが行われていますので多少エラーがでますがおおまかな 現在の利用数を取得できます。

# cat /proc/*/fdinfo/*|grep inotify|sort|uniq|wc -l
cat: /proc/39155/fdinfo/255: No such file or directory
(中略)
cat: /proc/thread-self/fdinfo/3: No such file or directory
369

この例は最小限+GUI環境のRHEL 9環境で取得したので369件と少ないですが、 問題が発生するような場合は先に fs.inotify.max_user_watches で確認した数に近い利用数を確認できます。

利用件数が多い場合、以下のようなコマンドでinotify watchの利用数が多いプロセスを搾り込めます。

# grep inotify /proc/*/fdinfo/*| cut -d/ -f3|uniq -c|sort -n
(中略)
    104 4880
    138 3972
   1888 216722
   3875 988006
   8362 4804
  25398 4294       # PID 4294が25398件(重複込み)のinotify watchを利用

対策

inotify watchが不足する場合、単純に fs.inotify.max_user_watches の値を大きくすることで対応します。 設定変更自体でメモリ消費等が増えることはなく、実際にinotify watchが利用された場合にだけ消費されます。

関連するナレッジ

access.redhat.com

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。