スパースファイルにする
2011年7月8日公開
ディスクが足りない
仮想OSをいっぱい入れると、たちどころにディスク領域が不足する。OS1つ分のディスクイメージが十数GBぐらい使うものだから、なけなしの給料の大半が健康保険料と住民税で消えて無くなるのと同じように、数百GBのディスクの空き領域が気がつくと無くなっていたりする。
やはり世の中一般でもそう思う人が多いのだろう。仮想化ソフトは可変サイズのディスクイメージというものをサポートしている。これは、ディスクイメージのファイルを、必要な分だけ動的に割り当てるという便利な機能である。
例えば、下記のように64bit版Windows XPのCドライブに、50GBのディスク領域を割り当てたものと仮定する。

しかしながら、ホストOSからみた実際のディスクイメージのファイルは、約35GBしかない。

この15GBの差は、使っていないために割り当てられていない部分である。これにより、ホストOSの物理ディスクの使用量を最小限に抑えることができる。
だが、よく見てほしい。
仮想OSとして起動している64bit版Windows XPは、ディスクを19GBしか使用していない。それなのにホストOSから認識されるディスクイメージのサイズは35GBである。確かにディスク全体のサイズである50GBよりは小さいが、実使用量よりもは大きい。何かだまされている気がする。パッと見は安いように思えても、その実高い通販の洗剤を買ってしまったがごとくである。
本当であればもっと小さくてもいいのではないか。
これは、仮想ディスクイメージは増大する一方であり縮小することがないという仕様によるものである。つまり、仮想化ソフトは仮想OSがディスクを使用する都度、ディスクイメージのファイルを大きくすることで仮想ディスク領域を確保するのだが、逆に仮想OSがファイルを削除してディスク使用量を削減したとしても、一度確保した仮想ディスク領域を縮小することはないのである。
日本経済とは逆で、仮想ディスクイメージは単調増大なのである。
なぜ縮まないのか
一度拡張された仮想ディスクイメージは、太った腹と同じく二度と元のサイズに戻ることはない。なぜなのか。
おそらく理由は2つある。1つは、仮想化ソフトからすると、一度使用されたディスク領域が未使用になったと言うことが理解できないためだと思われる。
ファイルシステムは、ファイルを削除するときにはファイルを削除したというマークをつけるだけで、ファイルのデータはそのまま放置しておくことが多い。でもって、他のファイルのために領域が必要になったら、削除済みのファイルが使っていたディスク領域を再割り当てする。すなわち、ファイルの削除とはディスク領域を他の目的に使えるように開放すると言うことに他ならない。そしてそれは、仮想OSが実行するファイルシステムの、内部的な管理上の問題に過ぎない。結果として、仮想化ソフトは当該の領域が不要になったのか否かが判断できないと言うことになる。
例えば、下記のようにディスクにファイルA・ファイルB・ファイルCの3つのファイルが格納されていたものと仮定する。

ここで、ファイルCを削除したとする。

ファイルCは削除され、エクスプローラーの画面からも見えなくなった。しかし、元々ファイルCが使用していた記憶領域には、実はファイルCのデータはそのまま手つかずで残されている。上記の絵では灰色で示されている部分である。
灰色の部分は、他のファイルを作成するときに使われるかもしれない領域だと言うに過ぎず、ファイルCのデータが無くなったわけではないのだ。
つまり、今後こうなる可能性があると言うことを意味しているに過ぎない。

そして問題なのは、上記のような記憶領域の使用状況を管理しているのは、仮想OSが実行するファイルシステムであり、仮想化ソフトではないと言うことだ。だから、仮想化ソフトとしては「未だかつて一度も使われたことがないディスク領域は確かに未使用であるといえる」という判断を下すのが精一杯であり、「かつて使われていたが今は使われていない領域が、本当に使われていないのかどうか」は判断できないのである。
結果として、一度確保した領域は二度と手放すわけにはいかないのである。
2つ目の理由としては、おそらく、性能上の問題と考えられる。
例えば仮想ディスクイメージの真ん中に空き領域があると判断できたと仮定しよう。

ここで、空き領域をつめてディスクイメージのサイズを縮める処理を行うものと考える。

その場合、ファイルの後半部分にある使用中の領域を、前半に向かって移動させなければならない。そうでなければ、ファイルのサイズが小さくならない。
しかし、場合によっては何十GBにもなるかもしれないデータを移動させるのに、一体どれぐらいの時間がかかるのだろうか? おそらく、仮想化ソフトとしては受け入れがたい負荷になるものと考えられる。
上記のような理由から、一度大きくなった可変サイズのディスクイメージは、二度と小さくなることはないのである。
でも小さくしたいのだが
サーバ用で、高負荷の環境で24時間365日動かしているとか、そんなことは一切無く、たまに気が向いたときに起動する程度だという場合、多少処理時間がかかってもいいから、ディスク使用量を削減したいと思えてくる。
ましてや、なけなしの生活費をはたいてディスクを4つ買ってきてRAID0+1にしてたりするのだから、数十GB単位で無駄に領域を喰っているファイルがあると思うだけで腹立たしく思えてくる。毎月親に10万円ずつ仕送りをするのと同じぐらい無駄である。
そういうことで、多少なりともサイズを小さくするような方法について考えてみた。
スパースファイルにすることを考える
ファイルを小さくする方法としては、圧縮するという手もある。しかし、どう考えても性能が劣化するとしか思えず気が進まない。だから、仮想ディスクイメージのファイルをスパースファイルにしてしまって、使っていない部分のディスク領域を開放してやることにしてみた。
スパースファイルというのは、ファイル中に空き領域があるファイルのことである。
通常であれば、ファイルが使用しているディスク領域は、アプリケーションが使っていようがいなかろうが、所定のディスク領域がきっちりと割り当てられる。

上図は、10GBのディスクに5GBのファイルが1つだけ格納されていることを示している。通常であれば、5GBのファイルが格納されている以上、ディスクの記憶領域も5GBは使われていることになり、当然、残りの容量も5GBということになる。
しかしながら、スパースファイルになっていると、使っていない空き領域の部分(具体的には全部0で埋められている領域)にはディスク領域を割り当てないでおいておくと言うことが可能になる。

上図は、同じく10GBのディスクに5GBのファイルが1つだけ可能されていることを示している。しかしながら、このファイルの中程には3GB分の未使用領域が存在する。スパースファイルにしてやれば、この未使用の領域にはディスク領域をあり当てないままにすることができる。結果として、5GBのファイルが存在しているにもかかわらず、ディスク使用量は2GBで、残り容量は8GBということになる。
こういうことをやってやれば、ディスクの利用効率を向上することが可能になる。
どうやってスパースファイルにするのか
基本的な戦術は簡単である。
プログラムで仮想ディスクイメージのファイルを頭から読み込んで、使っていない部分を見つけて、そこをスパースとしてファイルを書き出してやればいいだけである。

そうすれば、使っている部分だけからなるスパースファイルを作成してやることができる。
だがここで1つ問題がある。どうやって「使っていない部分」を見つけるかだ。
上にも書いたとおり、ファイルシステムはファイルを削除したときに「このファイルは削除された」とマークをつけるだけであり、ファイルのデータそのものはディスク領域に残したままにする。当然、何らかのデータで削除されたデータを塗りつぶすとか、そんなことはやらないのである。結果として、プログラムで仮想ディスクイメージを読み込んだとしても、使っていない部分を見つける手がかりがないと言うことになる。
もっとも、仮想ディスクイメージのファイルフォーマットと、ファイルシステムのオンディスクレイアウトが理解できるのであれば、削除されたファイルが使用している領域を特定することが可能である。なぜならば、ファイルシステムにはディスクの空き領域と使用領域がどこなのかがちゃんと理解できているのだから。
だが、それができるのなら、もとより苦労はしない。ファイルシステムのディスクレイアウトを理解するようなプログラムを作るのが辛いから、こんな回りくどいことを考えているのである。
ではどうするのか。
答えは簡単である。一旦、仮想OS上でディスクフルを引き起こすまで、ディスクを0で塗りつぶしてやればいいのである。

スパースファイルのスパースの部分は、データの0が格納されているものと見なされることから、逆に、物理的に0が並んでいる部分はスパースファイルにしてしまっていいと言うことになる。
これで全ての理屈は揃ったから、ここからは手を動かすことにする。
0で塗りつぶす
まずは、仮想OS上でディスク領域を0で塗りつぶすことから始める。
プログラムとしては、下記のようなものを使う。
char buf[4096] = { 0 };
DWORD WroteSize = 0;
// ファイルを作成する
HANDLE h = CreateFile(
ファイル名, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL
);
if ( h == INVALID_HANDLE_VALUE )
return 0;
// エラーが起きるまで0を出力しつづける
while ( ::WriteFile( h, buf, sizeof( buf ), &WroteSize, NULL ) );
CloseHandle( h );
// ファイルを削除する
DeleteFile( ファイル名 );
return 0;
|
ファイルを作って、ただひたすら0(あるいはNULL)を、エラーが起きるまで出力し続けるだけである。
コマンドラインで実行するようにしてもいいのだが、それだとちょっと使いにくいから、GUIがあるツールに仕立て上げてみた。
ヘルプも説明もない。その代わりソースがつけてある。気になることがあれば、まぁ自分で調べることだ。
とりあえず、起動するとこんな画面が表示される。

0での塗りつぶしを行いたいディスク上に、適当なファイル名を指定して「実行」ボタンを押下する。
するとものすごい勢いでファイルを出力し始める。

当然のことながら、いつかはディスクの空き領域を使い切ってしまう。まさに、嫁という名の粗大ゴミに有り金全てを吸い取られるかのごとくだ。

だが現実の嫁とは異なり、このツールは処理が終了したら作ったファイルを削除するから安心して欲しい。空き領域はすぐ元通りに復活する。
スパースファイルにする
これでようやく、悪鬼を打ち倒す準備が整った。
スパースファイルにするには、下記のようなプログラムを実行してやればいい。
char buf[4096];
DWORD ReadSize;
DWORD WroteSize;
int i = 0;
LARGE_INTEGER DistMove;
long long TotalReadSize = 0;
HANDLE hR = INVALID_HANDLE_VALUE;
HANDLE hW = INVALID_HANDLE_VALUE;
// 入出力ファイルを開く
hR = CreateFile(
入力ファイル名, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
);
hW = CreateFile(
出力ファイル名, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL
);
// 出力ファイルをスパースファイルとして設定する
DeviceIoControl( hW, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &ReadSize, NULL );
// 最初の1ブロックを読み込む
ReadFile( hR, buf, sizeof( buf ), &ReadSize, NULL );
TotalReadSize += ReadSize;
// 入力ファイルから読めなくなるまで繰り返す
while ( ReadSize > 0 ) {
// 入力したデータが全部0か否か調べる
for ( i = 0; i < ReadSize && buf[i] == 0; i++ );
if ( i >= ReadSize ) {
// 入力したデータが全部0ならば、その部分はスパースにしてしまう
DistMove.QuadPart = TotalReadSize;
SetFilePointerEx( hW, DistMove, &DistMove, FILE_BEGIN );
}
else {
// 通常通り出力する
WriteFile( hW, buf, ReadSize, &WroteSize, NULL );
}
// 次のブロックを読み込む
ReadFile( hR, buf, sizeof( buf ), &ReadSize, NULL );
TotalReadSize += ReadSize;
}
// 出力ファイルの最終的なサイズを設定する
SetFileValidData( hW, TotalReadSize );
SetEndOfFile( hW );
// ファイルを閉じる
CloseHandle( hR );
CloseHandle( hW );
|
注意するべきは太字にしてある部分だけである。それ以外は、単純に入力ファイルから4096バイトずつ読み込んで、それをそのまま出力ファイルに書き出しているだけである。
その途中で、入力したデータが全部0の部分があれば、SetFilePointerEx関数でファイルの末尾よりも先にファイルポインタを移動することにより、スパースの部分を作り出しているのである。

上図は、途中まで途切れなくデータを出力し続けた様子を表している。当然のことながら、ファイルポインタはファイルの末尾に存在する。その状態で、ファイルポインタをファイルの末尾よりも後、すなわちファイルとして実在しない部分に、SetFilePointerEx関数で移動してしまう。

すると、ファイルとしての領域は存在しなくても、とりあえずファイルポインタは指定された位置に移動される。そこから続きのデータを出力してやると、飛ばされた部分はスパースになる。

なお、ここでSetFilePointer関数ではなくSetFilePointerEx関数を使うと言っているのは、ファイルのサイズが4GBを超える可能性が高いためであり、スパースファイルであることとは直接の関係はない。
さて、0で塗りつぶすときと同様、上記の処理を行うツールを用意してみた。
ヘルプは用意してみた。だが大したことは書いていない。一応ソースもつけておく。
起動すると、こんな画面が表示される。

ウインドウにファイルをドロップするか、あるいは「追加」ボタンを押下してファイル名を指定してやれば、「変換するファイル」と書かれたリストボックスにファイルを登録することができる。

上図ではファイルを1つしか登録していないが、複数個いっぺんに指定することもできる。
この状態で「実行」ボタンを押下すると変換処理が開始される。

変換処理が終わると、一応正しく変換できたかチェックする処理が行われる。チェック処理では、変換前のファイルと変換後のファイルを読み直して、完全に一致しているかどうか比較する処理を行う。そのため、それ相当に処理時間がかかる。

処理内容と進捗状況は、リストの「メッセージ」と書かれた欄に表示されている。
全ての処理が終わると、ステータスが「正常終了」に変わる。

正常に変換されると、既存のファイルは末尾に「bk_日付_通番」が設定されバックアップが作成される。

上記の例では、「Windows 98.vdi」というファイルを変換対象と指定し、その処理結果が「Windows 98.vdi」として保存されている。また「Windows 98.vdi_bk_20110707_0」が、変換前のファイルのバックアップである。
上のエクスプローラーの画面を見る限りでは、変換前後でファイルのサイズは同じように見える。しかし、ファイルのプロパティで「ディスク上のサイズ」の値を見ると、確かにディスク領域が節約できていることが確認できる。

元は「サイズ」の値と「ディスク上のサイズ」の値は2.81GBで同じであったが、これが変換処理行うことにより2.64GBに縮小されている。もっとも、元が2.8GBしかないため、期待したほどには小さくなってはいないようだ。この程度の差なら、スパースファイルにはしないでおいたほうがいいかもしれない。
だが、一番上で示した64bit版WindowsXPのディスクイメージでは下記のようになる。

35.2GBが20.5GBになっている。約15GBのディスク領域の節約に成功している。
これぐらい差があると、やって良かったという気がしてくる。後は、バックアップのファイルを削除するか、別のメディアに移動させて完了である。
|