読者です 読者をやめる 読者になる 読者になる

Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

resize2fsコマンドはどのようにして1秒未満での容量拡張を実現しているのか

テクノロジー

この記事はLinux Advent Calendar 2014 の23日目の記事です。

背景

アカツキではAWS EC2をテストサーバ、ステージングサーバ、本番サーバとして利用しています。先日1周年を迎えた千メモは、リリース時よりも大分デプロイ時に容量を使うようになってきました。ステージングサーバのストレージ容量が当初想定していたものより大分カツカツになってきたため、Amazon社の出しているマニュアルに従って、ストレージ容量を拡張しました。ストレージ容量の拡張そのものは無事うまくいったのですが、どうしてこんな1秒もかからずに拡張出来るのだろうか(resize2fsコマンドが終わるのだろうか)、という疑問がわいてきました。そこで、Linuxでどのようにファイルシステムのサイズを拡張しているか調査するためのとっかかりとして、resize2fsコマンドを調査しました。

調査対象

調査対象は以下の通りです。

AWSで採用しているバージョンと多少処理が異なる箇所があるかもしれないので、ご注意ください。

resize2fsの処理内容

ストレージ容量を拡張する際にresize2fsコマンドを使用しますが、resize2fsというコマンド単体が配布されているわけではありません。E2fsprogsのgitレポジトリを見ていただくとおわかりいただけるかと思いますが、fsckコマンドなどを含めたパッケージの一部として提供されています。resize2fsの中身はresizeディレクトリ内にあります。

main()

まずはmain関数を読むためにresize/main.cというファイルをまず見ましょう。今回はオンラインの容量拡張の方法を調べていきたいと思います。それらしい関数を調べてみると・・・ありました、online_resize_fs()という関数を使っていますね。

471     if (mount_flags & EXT2_MF_MOUNTED) {
472         bigalloc_check(fs, force);
473         retval = online_resize_fs(fs, mtpt, &new_size, flags);
474     } else {
475         bigalloc_check(fs, force);
476         printf(_("Resizing the filesystem on "
477              "%s to %llu (%dk) blocks.\n"),
478                device_name, new_size, blocksize / 1024);
479         retval = resize_fs(fs, &new_size, flags,
480                    ((flags & RESIZE_PERCENT_COMPLETE) ?
481                     resize_progress_func : 0));
482     }

EXT2_MF_MOUNTEDはその名の通り、対象のファイルシステムがマウントしているかどうかを表すフラグなので、マウントしていたらonline_resize_fs()、マウントしていなかったらresize_fs()を呼びます。

bigalloc_check()

いきなり本筋とは脱線しますが、その前のbigalloc_check()について説明します。ext4ファイルシステムではブロック単位(1KB/2KB/4KB/8KB)でデータを管理しています。しかしもっと大きな単位で管理したほうが、データを管理するためのデータ(メタデータ)が少なくて済み、大きなデータに対しては飛び飛びにブロックをアクセスしなくて済むようになります。そこで、ext4ではブロックを複数集めたクラスタ単位(例えば1MB単位)でデータを管理出来るようにした機能であるbigallocをサポートしています。しかし、bigalloc_check()の中には以下のように書かれています。

155       fprintf(stderr, "%s", _("\nResizing bigalloc file systems has "
156                     "not been fully tested.  Proceed at\n"
157                     "your own risk!  Use the force option "
158                     "if you want to go ahead anyway.\n\n"));

つまり、bigallocのリサイズはちゃんとテストされていないので自己責任でよろしく!とのことです。まあ、普通に使っていたらページサイズと同じ4KB単位になっている(=bigallocはオフになっている)ので、そこまでbigallocについて気にしなくて良いでしょう。ちなみにもしbigallocを適用したファイルシステムをリサイズする場合は-fオプションを付けて実行してください。

online_resize_fs()

さて、次にonline_resize_fs()について見て行きましょう。online_resize_fs()の始めに気になるチェックを見つけました。

73         if (kvers < VERSION_CODE(3, 7, 0))
74             no_meta_bg_resize = 1;
75         if (kvers < VERSION_CODE(3, 3, 0))
76             no_resize_ioctl = 1;

何やらカーネルバージョンをチェックしています。3.7.0前だとno_meta_bg_resizeフラグが、3.3.0前だとno_resize_ioctlフラグが立ちます。AWS EC2サーバのカーネルバージョンは3.4.Xなのでno_meta_bg_resizeフラグとは何かを理解しておいた方が良さそうです。

meta_bg

meta_bgとはMeta Block Groupsの略で、ファイルシステムの最大ファイルシステムサイズを増やすために取り込まれた機能です。meta_bgを理解するためには、ext4ファイルシステムがどのようにデータを管理しているかをまず理解する必要があります。

ext4ファイルシステムではブロックグループという単位で複数のブロックをグループ化してデータを管理しています。ブロックグループは最大32,768ブロック(1ブロック4kBとすると128MB分)を1グループとします。ブロックグループの中にはGroup Descriptor Tables(GDT)というブロック領域があり、ここに全ブロックグループを管理するための情報であるグループディスクリプタ(ext4_group_desc構造体)を格納しています。ext4_group_desc構造体は64byteなので、1ブロックあたり4096byte/64bybte=64個のブロックグループを管理出来ます。つまり、全体のブロックグループ数が多ければ多いほど、GDT領域が大きくなっていくことはお分かりいただけると思います。

ext4_bg

ここまで読んでピンと来た人がいるかもしれませんが、全体のブロックグループを管理する情報が各ブロックグループに入っている必要は実はありません。例えば、仮にブロックグループ全てをグループディスクリプタで埋めた場合、128MB/64byte=2^21個のブロックグループを管理することになります。しかしこれでは2^21*128MB=256TBがファイルシステムサイズの上限となってしまいます。もちろんこれは実際のデータや他のメタデータ(データを管理しているデータ。GDTもメタデータの一種)を完全無視して算出した結果なので、実際に扱えるファイルシステムサイズはこれよりも少なくなります。

そこで考えだされたのが、meta_bgです。まず64ブロックグループを1meta groupという単位とします。なぜ64かというと、既に書いたようにGDT領域は1ブロックで64ブロックグループ管理可能で、自meta group内の情報のみGDTに記憶しておくようにするためです。meta groupへのアクセスには、スーパーブロック(ファイルシステム全体を管理しているブロック)に格納されているs_first_meta_bg(最初のブロックグループ)とs_blocks_per_group(1グループディスクリプタ辺り何ブロックあるか)を使用します。これらは32bitの値のため、結果的に2^32 * 128MB = 512PBまでファイルシステムサイズの上限が増えます。(さらに、meta_bgではGDT領域の削減も行っています。具体的には、1meta group内の最初(0番目)、1番目、63番目のブロックグループ内にのみGDT領域を用意します。これにより、61ブロック分の領域を削減出来ます。)

ext4_meta_bg

このようにmeta_bgを適用すると、GDTの役割が、全ブロックグループの管理から自meta groupの管理に変わります。これにより、今回のようにファイルシステムサイズを大きくする際に注意が必要となります。

online_resize_fs()の続き

次に気になるのは、このコードです。

 99     new_desc_blocks = ext2fs_div_ceil(
100         ext2fs_div64_ceil(*new_size -
101                   fs->super->s_first_data_block,
102                   EXT2_BLOCKS_PER_GROUP(fs->super)),
103         EXT2_DESC_PER_BLOCK(fs->super));
104     printf("old_desc_blocks = %lu, new_desc_blocks = %lu\n",
105            fs->desc_blocks, new_desc_blocks);

ここでは拡張後のGDTのブロック数を計算して、出力しています。(GDTについてはmeta_bgの説明の最初の方に書きました) これをわざわざ出力するぐらいなので、 ファイルシステムのサイズの拡張のポイントは、GDT領域を更新することなんだなあ、ということがわかります。(多くのユーザはこれを意味することがわからないと思うのですが・・・)

では既存のGDT領域より大きなGDTが欲しいときはどうするのでしょうか。こういうときのためにext4ファイルシステムでは予めGDTの予約領域(RGDT)をGDTのすぐ後のブロックに用意しています。もちろん予め用意したRGDTで管理可能なサイズより大きなサイズをオンラインで拡張することは出来ません。(このすぐ後に出てきます) RGDTはmkfsコマンドでファイルシステム作成時に決められます。(ただし、meta_bgが有効のときはこの予約領域は存在しません。meta_bgを使っていてGDTより大きな領域が欲しい場合は、meta groupを追加します。)

さあ、online_resize_fs()をどんどん読み進めていきましょう。

107     /*
108      * Do error checking to make sure the resize will be successful.
109      */
110    if ((access("/sys/fs/ext4/features/meta_bg_resize", R_OK) != 0) ||
111         no_meta_bg_resize) {
112         if (!EXT2_HAS_COMPAT_FEATURE(fs->super,
113                     EXT2_FEATURE_COMPAT_RESIZE_INODE) &&
114             (new_desc_blocks != fs->desc_blocks)) {
115             com_err(program_name, 0,
116                 _("Filesystem does not support online resizing"));
117             exit(1);
118         }
119
120         if (EXT2_HAS_COMPAT_FEATURE(fs->super,
121                     EXT2_FEATURE_COMPAT_RESIZE_INODE) &&
122             new_desc_blocks > (fs->desc_blocks +
123                        fs->super->s_reserved_gdt_blocks)) {
124             com_err(program_name, 0,
125                 _("Not enough reserved gdt blocks for resizing"));
126             exit(1);
127         }
128
129         if ((ext2fs_blocks_count(sb) > MAX_32_NUM) ||
130             (*new_size > MAX_32_NUM)) {
131             com_err(program_name, 0,
132                 _("Kernel does not support resizing a file system this large"));
133             exit(1);
134         }
135     }

1つ目のチェックではオンラインリサイズ機能をそもそも備えているかをチェックしています。EXT2_FEATURE_COMPAT_RESIZE_INODEは、resize_inode機能があるかどうかをチェックするためのフラグです。ファイルシステムサイズ拡張用のinode(7番が予約されている)が用意されていれば、このフラグは立ちます。

2つ目のチェックでは先ほど少し書きましたが、今から拡張しようとしているファイルシステムを管理するためのGDTが既存のGDTとRGDTで収まりきれるかを確認します。

3つ目のチェックでは2^32個以上のブロック数を増加させようとしていないかチェックしています。

そしてやっと・・・

144     if (no_resize_ioctl) {
145         printf(_("Old resize interface requested.\n"));
146     } else if (ioctl(fd, EXT4_IOC_RESIZE_FS, new_size)) {
...
171     } else {
172         close(fd);
173         return 0;
174     }

EXT4_IOC_RESIZE_FSリクエストのioctl(2)をしています。ここで何をしているかはカーネルの内部を見て行く必要があります。また、EC2では関係ありませんが、カーネルのバージョンが古い場合はioctl(2)を利用せずにリサイズすることがわかります。

まとめ

resize2fsコマンドは以下のように動作することがわかりました。

  1. bigalloc機能を使っていないかチェック
  2. カーネルバージョンをチェックしてmeta_bgが有効かどうか、  ioctl(2)出来るかをチェック
  3. これから確保しようとしているGroup Descriptor Tablesのサイズを調べ、  確保出来るかチェック
  4. EXT4_IOC_RESIZE_FSリクエストのioctl(2)を発行してカーネルへリサイズを命令

"どうしてこんな1秒もかからずに拡張出来るのだろうか”という疑問は、本質的にメタデータのGDTを更新するだけだから、ということがおおよそわかります。

このようにLinuxコマンドを調査することで、カーネル内部の動作をある程度予想することが出来ます。実際にカーネルのソースを読んでみなければ正確なことはわかりませんが、ある程度あたりを付けておくとカーネルソースコードが読みやすくなります。カーネル内の挙動を詳細に追う前に、是非コマンドのソースを読んで、どんなシステムコールを使っているのか、どんなチェックをユーザ空間で行っているかを調べることをおすすめします。

今回はresize2fsコマンドのソースを読みましたが、ioctl(2)が発行された後にカーネルが実際にどのような処理をしているのか、Linuxカーネルのソースを次回読んでいきます。

参考文献