Linuxのinotify、変更通知があふれた後に一覧を正しく戻すには

薄紫のトレーに桃色のタイルが三枚並び、一枚は半透明。奥の緑色の板の二枚のタイルへ白い曲面の橋が伸びる抽象的な構図。

フォルダーの変更を見つけたら、画面の一覧や検索用の索引を更新する。Linuxのinotifyを使うと、毎回すべてのファイルを調べる代わりに、変更通知をきっかけに作業できます。しかし、その通知を完全な履歴として扱うと、負荷が高まったときに一覧が実物からずれることがあります。

問題の境目になるのがIN_Q_OVERFLOWです。イベントの待ち行列があふれたことを示すこの通知を受けたら、「何件か処理が遅れた」だけでなく、「手元の状態を作る材料が欠けたかもしれない」と考える必要があります。

簡単にまとめると?

  • inotifyはファイルやディレクトリの変化を通知しますが、完全な操作履歴や正確な発生回数を保証する仕組みではありません。
  • イベントキューがあふれると通知が失われ、IN_Q_OVERFLOWが発生します。どのファイルのどの変化を失ったかまでは、この通知から復元できません。
  • 変更通知で更新するキャッシュには、実ファイルを再走査して整合性を戻す経路が必要です。容量を増やすことと、欠落後に正しく復旧できることを分けて設計します。

通知を使う一覧は、実物の写しを持っている

例として、監視対象フォルダーにあるファイル名をメモリー上の集合に保存する一覧アプリを考えます。起動時にフォルダーを走査し、その後は作成の通知で名前を足し、削除の通知で名前を消します。この集合が、実ファイルの状態を写したキャッシュです。

変更が少ない間は、毎回フォルダー全体を読み直さずに済みます。ところが、この仕組みの正しさは、必要な通知を受け取り、適切に処理できることに依存しています。通知を読み出す処理で重い変換や遠隔通信まで行うと、その間も別のプロセスがファイルを変更し、未処理の通知が積み上がります。

inotifyの一つのインスタンスには、複数の監視対象を登録できます。読み取るイベントには、監視対象を示すwd、変更の種類を示すmask、場合によってファイル名などが入ります。通知の順序は保たれますが、それだけで通知が欠けないことまで保証されるわけではありません。

欠けた通知を後から処理し続けても、元には戻らない

最初の実フォルダーとキャッシュが、どちらも{a}だったとします。その後、次の三つの変化が起きたと考えます。

  1. bを作成し、その通知は一覧アプリへ届いた。
  2. aを削除したが、その通知はキューあふれにより失われた。
  3. cを作成し、キューの空きが戻った後、その通知は届いた。

受け取れた作成通知だけを順に処理すると、キャッシュは{a, b, c}になります。しかし、実フォルダーは{b, c}です。順番どおりに処理していても、欠けた削除の情報は戻りません。以後aに変化がなければ、表示には存在しないaが残り続けます。

実フォルダーはaからb,cへ変化。aの削除通知を失うとキャッシュにはa,b,cが残る。変化が止まった状態で再走査するとb,cへ一致する。
通知が一つ欠けた場合の独自の集合モデル。実際のキュー容量や、Linuxで必ずこの順序の欠落が起きることを示す実測ではありません。

この例で必要なのは、次の通知を待ち続けることではなく、キャッシュをいったん信用できない状態として扱い、実際のフォルダーを確認することです。変化が止まった後に再走査すれば、{b, c}を取得して一致を取り戻せます。ただし、これは現在の状態を回復したのであって、失った削除の時刻や実行者まで取り戻したわけではありません。

あふれた場所を、一つのファイルに決めつけない

IN_Q_OVERFLOWのイベントではwdは−1です。通常の監視対象の番号ではないため、「このファイルだけを再読込みすればよい」と通知自身から決めることはできません。同じインスタンスから通知を受けている範囲について、どこまで再確認が必要かを設計します。

マニュアルは、必要に応じてキャッシュの一部または全体を再構築する方法を挙げています。一つの単純な方法は、inotifyのインスタンスを閉じ、キャッシュを空にして、新しいインスタンスと監視対象を作り直すことです。ただし、対象が多ければ再構築の負担も増えます。

実際のフォルダーが更新され続ける場合には、再走査も一瞬で完了する完全なスナップショットではありません。監視の追加、走査、届いた変更との突合せが重なる場面を考え、走査中にも再度あふれた場合に完了扱いにしないことが必要です。上の静止した集合の例だけでは、この並行処理の正しさまでは検証できません。

三つの上限を、同じ問題として扱わない

inotifyには、キューに保持するイベント数の上限であるmax_queued_events、実ユーザーID当たりのインスタンス数の上限であるmax_user_instances、監視対象数の上限であるmax_user_watchesがあります。

監視対象を追加できない問題と、追加済みの対象から来た通知をため切れない問題では、確認する上限が異なります。イベントキューの上限値はインスタンス作成時に使われるため、設定値を書き換えたことと、すでにあるインスタンスの挙動が変わったことも同一視できません。

キューを大きくすれば、短い集中に耐える余地は増やせます。しかし、生産される通知が読み取り側の処理を長期間上回れば、容量だけで解決するとは限りません。読み取り経路を軽くすること、重い作業を別に渡すこと、重複した再計算をまとめること、欠落後の再照合を用意することを、別々の対策として評価します。

変更検知と監査の役割を分ける

通知の欠落がなくても、未読の連続イベントが同じ内容なら一つにまとめられる場合があります。そのため、通知の個数を「書込み回数」として集計する設計には向きません。また、inotifyは、その操作を行ったユーザーやプロセスの情報を提供しません。

監視対象のディレクトリの下も自動的にすべて監視されるわけではありません。サブディレクトリには別の監視が必要で、監視を追加する前に中身が作られる可能性もあります。対象の範囲が漏れている問題と、キューがあふれた問題も分けて点検します。

画面の一覧や索引を現在の状態へ追いつかせたいなら、inotifyを「再確認が必要になった」という入口として利用できます。一方、誰がいつ何回操作したかを欠落なく残したいなら、別の記録方式が必要です。まず保持したいものが現在の状態なのか、過去の全履歴なのかを決めることが、復旧設計の出発点になります。

出典

  • Linux man-pages 6.19: inotify(7) — IN_Q_OVERFLOW、/procの各上限、イベントの合流、監視範囲とキャッシュの再構築。図とファイル集合の例は、本文に示した欠落条件を使った独自モデルです。