デバイス、ファイルシステム、マウントの三層
一言でいうと
ブロックデバイスはバイトの配列で、ファイルシステムはその配列の上に載せたデータ構造であり、マウントはそのデータ構造をディレクトリツリーに取り付ける行為です。この3つは、完全に別のものです。
なぜ必要なのか
「ディスクがいっぱいです」という1文には、少なくとも4つの異なる問題が混ざっています。
- ブロックが本当にいっぱいになっている(
df -hが100%) - inodeが先に尽きた(
df -iが100%で、ブロックには余裕がある) - 削除したファイルを誰かが開いているため、容量が返却されない(
dfとduが一致しない) - そもそもそのパスが想定したファイルシステムではない(マウントされていない)
4つは原因も対処もまったく違います。ところが、症状はどれも同じNo space left on deviceです。3つの層を区別して初めて、この4つが分かれます。
どう動くのか
層1: ブロックデバイス
/dev/sda、/dev/nvme0n1p1、ループデバイスの/dev/loop0のようなものです。カーネルにとっては、ただの読み書きできるバイトの配列です。
ここで重要な事実が1つあります。ファイルシステムは、ブロックデバイスではなく、ふつうのファイルの上にも作れます。mkfs.ext4は、対象がデバイスなのかファイルなのかを気にしません。その対象の先頭から、決まった構造を書き込むだけです。この性質のおかげで、特権がなくてもファイルシステムの内部を学べます。
truncate -s 256M /root/disk/lab.img # 희소 파일 — 실제 점유는 0
mkfs.ext4 -F -L LABHUB /root/disk/lab.img
blkid /root/disk/lab.img
dumpe2fs -h /root/disk/lab.img
debugfs -R 'ls -l /' /root/disk/lab.img
truncateで作ったファイルは、スパースファイル(sparse)です。論理サイズは256MBなのに、実際のディスク占有はほぼ0です。du --apparent-sizeとduの値が違うことで確認できます。
層2: ファイルシステム
mkfsが作るのは、スーパーブロック + inodeテーブル + ブロックグループです。dumpe2fs -hでスーパーブロックを読むと、次のような値が出ます。
| 項目 | 意味 | なぜ重要か |
|---|---|---|
| Block size | ブロック1つのサイズ(通常4096) | 小さなファイルが多いと内部断片化 |
| Inode count | フォーマット時点で固定されたinodeの数 | ext4は、あとから増やせない |
| Reserved block count | root専用の予約ブロック | 既定は5%で、大容量のデータディスクでは無駄 |
| Filesystem UUID | このファイルシステム固有のID | fstabで、デバイス名の代わりに使う |
inodeは、フォーマットするときに数が決まり、あとから増やせません。そのため、セッションファイルやメールキューが小さなファイルを数百万個作ると、ブロックが残っていてもinodeが先に尽きます。これが、容量アラートをdf -hとdf -iの両方にかけなければならない理由です。
予約ブロックも、実務上のポイントです。既定の5%は、ルートファイルシステムでは合理的ですが(rootがログを書く余裕を残します)、8TBのデータディスクでは400GBを遊ばせることになります。tune2fs -m 1で下げます。
層3: マウント
マウントは、ファイルシステムをディレクトリツリーの1点に取り付けます。ここで、2つを区別する必要があります。
- デバイス名(
/dev/sdb1)は不安定です。ディスクを追加したり、起動順序が変わったりすると、sdbがsdcになります。そのため、fstabにはUUIDかLABELを書きます。 - マウントポイントのディレクトリにもともとあったファイルは、隠れるだけで消えません。アンマウントすると、また見えます。「データが消えた」という報告が、実は「新しいファイルシステムがその上に覆いかぶさった」という場合があります。
ディスクを取り付けたあとに必ずすること
新しいディスクを取り付けてマウントするまでの手順は短いですが、抜かすと再起動で表面化する項目があります。
名前ではなくUUIDで書きます。/dev/sdbは、デバイスの順序によって変わります。ディスクをもう1つ取り付けたり、順序が変わったりすると、/etc/fstabのその行が別のディスクを指すことになり、最悪の場合、起動が止まります。
blkid /dev/sdb1
# UUID="..." TYPE="ext4"
fstabにnofailを入れるかを決めます。ない場合、そのディスクが取り付けられなかったときに、起動がエマージェンシーシェルに落ちます。データディスクなら、nofailを入れてシステムは起動させ、アラートで知らせるほうがよく、そのディスクがなければサービスが意味をなさない場合は、外すのが正しいです。
直す前に、マウントできるかを試します。再起動して確認するのは、最もコストが高い方法です。
mount -a # fstab 대로 전부 붙여 본다. 오류가 나면 여기서 난다
findmnt --verify # 문법과 대상까지 검사한다
このコードブロックの2つの韓国語コメントは、順に、fstabのとおりにすべてをマウントしてみること(エラーがあればここで出ます)と、文法と対象まで検査することを述べています。
アライメントと予約ブロックを見ます。ext4は、既定で5%をrootに予約しますが、1TBのデータディスクでは50GBを遊ばせることになります。データ専用なら、減らしてもかまいません。
tune2fs -m 1 /dev/sdb1
増やすのは2段階です。クラウドでボリュームを大きくしても、ファイルシステムはそのままです。パーティションを増やし(growpart)、そのあとファイルシステムを増やします(resize2fsまたはxfs_growfs)。マウントしたまま増やせますが、縮小はたいていできません。XFSは縮小をまったくサポートしていません。そのため、最初に大きく確保するより、小さく確保して増やすほうが安全です。
LVMを使うかを、今決めます。あとで変えるには、データを移す必要があります。ディスクを追加する予定があったり、スナップショットが必要だったりするなら、最初からLVMの上に作ります。
現場での姿
マウントの失敗でルートがいっぱいになる事故です。/var/lib/postgresqlに別のディスクをマウントするよう設計したのに、fstabのエントリが間違っていてマウントされませんでした。データベースは何の文句も言わず、ルートファイルシステムにデータを書き始めます。数日後にルートが100%になり、システム全体が止まります。そのため、マウントポイントのディレクトリに.not-mountedのような目印のファイルを置き、アプリケーションにそれを確認させるチームもあります。
次のラボですること
ファイルの上に本物のext4を作ってUUIDを取り出し、スーパーブロックを読み、tune2fsで予約の割合を変え、debugfsで中をのぞきます。最後に、そのイメージをマウントするfstabの行を、UUIDで作成します。