ディスクのファイル使用量 (df) と実際のファイル使用量 (du) が一致しなくなったら lsof +L1 で削除済未解放のファイルを調べよう

## TL;DR - 記事を読んでられない人向けの要約

`df` と `du` / `ls` 両者でディスクの使用量が一致しない場合、テンポラリファイル（プロセスが掴んでいる削除済ファイル）のサイズが `du` 側でカウントされていないので、  **`lsof +L1` (リンク数0のファイルを羅列) コマンドを使うと削除済未解放のファイルの一覧が出る。**

```console
$ lsof +L1
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NLINK    NODE NAME
python3 1731 root    3w   REG    8,3        0     0 7344873 /root/foobar.txt (deleted)

# → /root/foobar.txt というファイルは削除済みだが解放されていない
```

## 本文

みなさんが日々Linuxを運用している中で、Linuxの監視等から「ディスクが足りないよ〜」とアラートが出る場合、まずは事象確認のために `df` コマンドを使うと思います。（下記）

```console
$ df -h
Filesystem     Used      Available  Use% Mounted on
/dev/sda1      124G              0  100% /
```

うわあ！　ディスクが100%埋まっていますね。大事なサーバなら超冷や汗ものです。

（ログやローカルバックアップ、Dockerのゴミとかが詰まったのか？）

普通の管理者ならちゃんとディスクが埋まった原因を把握するために `du` コマンドを使うと思います。

```console
# ルート以下のディレクトリごとの使用量を表示
$ du -h --max-depth 1 /
...
3.0G    /
```

**普通なら**、ここで原因を特定できるのですが、上記の様子だとディスク全体の　`124GB` のうち `3GB` しか使われていないようです。 **これはおかしい。**

下記の両者で使用量の値が大きく異なってしまっています。

- ファイルシステム上で合算した場合: `du`
- ディスク側で統計とった場合: `df`

こんなことがあっていいのか？ Linuxのファイルシステムにもダークマターはあるのか？


## 原因はプロセスが掴んでいる `unlink` (削除)済みのテンポラリファイル

前述のような現象になる原因は、 **プロセスが削除済みのファイルをそのままオープンしたまま保持しているのが原因** （かもしれない）です。

こういったファイルは下記のようなコードで簡単に作成できます。

```python:create_missing_file.py
import os

# ファイルを作成＆中身を書く
print("Create my_temp.txt")
f = open("my_temp.txt", "w")
f.write("Hello from FILE!")
f.flush()
print("Waiting for input...")
input()

# 該当ファイルを削除
print("Delete my_temp.txt")
os.remove("my_temp.txt")
print("Waiting for input...")
input()

# ファイルを閉じる
print("Close my_temp.txt")
f.close()
print("Waiting for input...")
input()
```

上記を動かして動作を見てみましょう。

```console
$ python3 create_missing_file.py
Create my_temp.txt
Waiting for input...
```

上記を動かしているシェルとは別のシェル上の `ls` でファイルを見てみます。

```console
$ ls -al | grep my_temp.txt
-rw-r--r--  1 root root    16 Dec  5 09:24 my_temp.txt
```

作成されたファイルがいますね。Python側で処理を進めて削除させてみましょう。

```console
Delete my_temp.txt
Waiting for input...
```

```console
$ ls -al | grep my_temp.txt
(何も表示されない)
```

`ls` に何も表示されなくなりました。プログラム側でファイルを削除したから当然なのですが。

**ただ、 `ls` から辿れなくなっただけで、ディスク上のファイルはまだ消えていません。** このようなファイルは `lsof +L1` コマンドで出てきます。

```console
$ lsof +L1
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NLINK    NODE NAME
python3 1978 root    3w   REG    8,3       16     0 7344872 /root/my_temp.txt (deleted)
```

該当のPID (1978) の procfs を見てもファイルが実在していることがわかります。(`/fd/3`)

```console
$ ls -al /proc/1978/fd/
lrwx------ 1 root root 64 Dec  5 09:27 0 -> /dev/pts/0
lrwx------ 1 root root 64 Dec  5 09:27 1 -> /dev/pts/0
lrwx------ 1 root root 64 Dec  5 09:27 2 -> /dev/pts/0
l-wx------ 1 root root 64 Dec  5 09:27 3 -> '/root/my_temp.txt (deleted)'
```

ではプログラム側の処理を更に進めてファイルをクローズしてみましょう。

```console
Close my_temp.txt
Waiting for input...
```

この状態で先程表示された削除済ファイルを確認すると無事ちゃんと消えています。この状態になってようやく `df` = `du` となります。

```console
$ lsof +L1
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NLINK    NODE NAME
python3 1978 root    3w   REG    8,3       16     0 7344872 /root/my_temp.txt (deleted)

$ ls -al /proc/1978/fd/
lrwx------ 1 root root 64 Dec  5 09:27 0 -> /dev/pts/0
lrwx------ 1 root root 64 Dec  5 09:27 1 -> /dev/pts/0
lrwx------ 1 root root 64 Dec  5 09:27 2 -> /dev/pts/0
```

## どういう場合にこれが起こるのか？

プロセスが削除済みのファイルを使う真っ当な理由はただ一つ、プロセスが終了した時に自動解放されるファイル ＝ **テンポラリファイルを作成したい場合**です。

たとえば、データベース（MySQLなど）の一時表領域を利用すると発生するのを確認しています。要するに、クソデカ重いSELECT文を発行するとこういったファイルができます。そしてそのクエリが大量に詰まると巨大テンポラリファイルでファイルシステムが詰まります。（そして障害発生します）

あとは長時間走るプログラムでファイルをクローズし忘れても発生するかもしれません。（遭遇したことはないですが）

## `lsof +L1` の `+L1` とはどういう意味か？

```console
$ man lsof
NAME
       lsof - list open files
...
       +|-L [l] enables  (`+')  or disables (`-') the listing of file link counts, where they are available - e.g., they aren't available for
                sockets, or most FIFOs and pipes.

                When +L is specified without a following number, all link counts will be listed.  When -L is specified (the default), no link
                counts will be listed.

                When  +L is followed by a number, only files having a link count less than that number will be listed.  (No number may follow
                -L.)  A specification of the form ``+L1'' will select open files that have  been  unlinked.   A  specification  of  the  form
                ``+aL1 <file_system>'' will select unlinked open files on the specified file system.

                For other link count comparisons, use field output (-F) and a post-processing script or program.
```

> When  +L is followed by a number, only files having a link count less than that number will be listed.
> A specification of the form ``+L1'' will select open files that have  been  unlinked.

`+L1` だとリンク数1未満(＝0)のファイル、要するに削除済みのファイルが表示されるという意味だそうです。


## まとめ

- ディスクが何故か埋まっている時は、削除済だがプロセスが掴んだままのファイルを `lsof +L1` でチェックしよう
- プロセスが巨大テンポラリファイルを作成する原因がないか考えよう