Amazon EC2 はじめました

というのも、作成中の VM のデモ目的で。自宅の PC は何度か試したが Wake on LAN では正常に立ち上がらない。家族に立ち上げてもらってもディスクにパスワードかかってる。そもそもリモートからちゃんと弄れる環境を作るのは少し厳しいし、この PC で仮想化をするのは CPU 的には問題ないもののメモリ的に少し厳しい。というわけで Amazon EC2 でデモ用マシンを立ちあげられるか試してみた。

インスタンスの立ち上げとセットアップ

最近は EC2 の余剰リソースを安く利用出来る Spot Instances があるため、立ち上げのコストは低め。VM の計算量と解析パスを同時に実行することを考えて m2.4xlarge*1 (mem 68.4GB, 26 ECU [3.25ECU * 8core]) の Windows Server*2 インスタンスを 1 時間立ち上げても 150 円程度。下手に他の手段を取るよりも安い。
立ち上げたインスタンスの CPU は Xeon X5550 と、今使ってるものと同世代の Nehalem マイクロアーキテクチャ。CPU とメモリでベンチマークを取ってみたが、まず自宅 PC に負けることはない。OS は Windows Server 2008 Datacenter*3
最初 Windows Server には 2GB のスワップが入っていたが、このメモリ量*4からして必要無さそうなのと EBS アクセスで余計に料金を取られそうなのとでスワップは切った。*5インスタンスは最初英語で立ち上がるのでそれを日本語にし*6、さらに .NET Framework 4 *7 などをインストールして AMI を作成。

ストレージの特性を知る前に。

AMI を作成する前後で不思議に思ったのは、ローカルのストレージはどこにあるのか? ということだ。至ってどこにも見当たらない。色々調べると、AmazonWindows Server AMI は EBS がデフォルトになっており、またローカルのストレージがデフォルト無効化されているとのことだ。
最初は Amazon EC2 のドキュメントに書いてあった ec2-register を使う方法を試したが AMI すら正しく作れない。色々試行錯誤したりググってみると、インスタンス立ち上げにコマンドラインを使い、そのとき -b オプションで明示的にマウントするディスクを指定すれば良いようだ。私の場合 Spot Instance を立ち上げるので、

ec2-request-spot-instances ami-******** -t m2.4xlarge -p *** -k ********** -b "xvdf=ephemeral0" -b "xvdg=ephemeral1"

とすれば、ローカルストレージを有効にした状態の Spot Instance を正しく要求できた。(この ephemeral0, ephemeral1 というのがローカルストレージ。インスタンスの種類によってこの ephemeral ディスクの数は異なる。)

ストレージの特性を知るために…

最近知ったことだが、ストレージには 2 種類、ローカルインスタンスストレージと EBS があるらしい。Amazon EC2 が用意している Windows Server AMI は幾つかあるが、すべて EBS ベースらしい。しかも EBS は容量だけでなくアクセスでも課金されるらしい。課金されるぐらいだから、EBS って良いのか? 某所では遅いと書いてあったし別の場所では速いと書いてあったけど。というわけでベンチマークをしてみた。
ベンチマークソフトは CrystalDiskMark 3.0。某所のようなサーバ用ベンチマークではない。まぁこの辺は単に私がサボったというのもあるし、ここで動かす予定のソフトの開発者だからアクセスの種類はある程度予測できているというのもある。いずれも 1000MB、5回平均。

X25-M 160GB (手元の PC)
Read (MB/s) Write (MB/s)
Seq 217.0 100.3
512k 113.5 52.17
4k (QD=1) 10.67 24.16
4k (QD=32) 20.88 28.04

まぁ SSD だな〜、って結果だ。ただしこれは純粋なベンチマークではない。TrueCrypt 7.0 が入った状態で調べているため、実際のパフォーマンスが暗号化処理でオーバーヘッドを受けている可能性がある。

Amazon EC2 - ローカルインスタンスストア (m2.4xlarge)
Read (MB/s) Write (MB/s)
Seq 111.0 38.88
512k 46.61 25.92
4k (QD=1) 0.712 0.628
4k (QD=32) 1.877 1.430

この結果を見るに、EC2 のストレージは明らかに HDD だ。(いや、840GB って容量で気づけよ。) 4k アクセスで低いスコアになっているのは比較的厳しい。あと Sequential Write の結果がやけに低い*8のが気になる。

Amazon EC2 - EBS (m2.4xlarge)
Read (MB/s) Write (MB/s)
Seq 104.0 56.53
512k 94.04 57.28
4k (QD=1) 13.45 5.953
4k (QD=32) 90.56 8.785

スコアの特性からするに、おそらくこれもバックエンドのストレージは HDD だろう。しかし…驚愕だ。明らかに性能が違う。Sequential Write が底上げされているのは当然として、512k、4k のランダムアクセスが明らかに HDD の1,2台では有り得ない速度を叩き出している。特に Random Read に関しては SSD 以上の性能を出している。
こういうスコア特性になるのは何だろうか? ディスクキャッシュだけでは説明の難しい結果だし、(HDD+SSD ハイブリッド and/or HDD 並列かつキャッシュメモリも載った) 鯖用のストレージシステムに高速接続しているかのだと予想する。

まとめない。

色々やってみたが、とりあえずデモ環境はうまく立ち上げられたからヨシとする。どうせ当面はトータルでも 10 時間ぐらいしか立ち上げない環境だろうし、EC2 でかかる費用もある程度予測できてはいる。まぁ…デモできる機会があるかどうかが最大の問題なんだけどね

*1:今のところリソースに余裕はあるようなので、場合によってはいっこ下の m2.2xlarge でも十分なようだ。

*2:わざわざ高い Windows Server にしたのは VM 実行と解析のデモを同時にやるためというのもあるが、最大の理由は Linux 向けの UI がしばらく完成する見込みすら無いということ。

*3:無駄にこの Datacenter というエディション名の響きで興奮しない?

*4:しかもデフォルト状態ではキャッシュを含めた全使用メモリが 2GB 未満だった。

*5:本当はスワップを切ると BSOD 時のダンプ等が正常にできなくなるのだが、そのような開発段階のバグは自宅 PC で潰してしまうため問題無さそう。

*6:Windows Server 2003 時には色々面倒だったようだが、2008 では単純に言語設定を変更するだけ (幾つかの言語がデフォルトで入っている。)

*7:VM も解析側も UI は .NET Framework で構成している。VM は 2.0、解析側は 4.0 Client Profile。

*8:この値が低いと高負荷時の VM トレースにおいて最大のオーバーヘッドになる。

Bochs を使って命令 / メモリトレースを取得する

Bochs の組み込みデバッガはなかなかよくできています。メモリアクセス、実行命令など、必要なほとんどの情報 (ハードウェアからの実際の入力を除く!) を取得することができるのです。私は Bochs for Windows に付属の bochsdbg.exe を用いていますが、必要なら自分でビルドすることができます。(この辺は簡単な上に面白くもないので省略。)

仮想マシンの構成

仮想マシンの構成はほぼ普段通りに行えばそれだけで OK ですが、CPU については

cpu: count=1, ips=1000000

のように、1 秒間に実行する命令数をあらかじめ設定しておくことを強く推奨します。(そうしないと、Bochs の都合で IPS が著しく上下したり、あるいはどんな構成のトレースを行っているのかが分からなくなってしまう。) ここでは 1 MIPS のマシンをエミュレートすることを明示的に指定しています。

スタートアップ

ここで便利なのは、Bochs デバッガには予めデバッグコマンド列をスタート時に仕込んでおくことが可能だという点です。これには -rc コマンドラインオプションを用います。私が用いている debugrc として、次のものがあります。

trace on
trace-mem on

s 2000000000
s 2000000000
s 2000000000
s 2000000000
s 2000000000

q

ここではメモリトレース (trace-mem) とトレース (trace) をともにオンとして、s コマンドを使用して合計 100 億命令のトレースを実行し、最後には終了 (q) します。ここでは 100 億命令固定となっていますが、テキスト形式のトレースは復元が容易であるため、途中で強制終了しても使えます。

実行

実行時の注意として先程の -rc オプションを指定することはもちろん、トレースやすべての詳細出力はファイルへのリダイレクトとすることを推奨します。これは Windows 固有かもしれませんが、Windows においてはコマンドラインへの表示に関する処理の負荷がかなり大きく、コンソールそのものの出力を最小限に抑えないことには実用になりません。(その中で 1 日半かけて Linux の起動シーケンスをトレースしたのですが。)
具体的には次のようになります。

  • コンソールをそのまま表示 : 300〜400 命令
  • コンソールを最小化 : 500〜1000 命令
  • コンソール管理用のプロセスである conhost.exe を kill する : 20000〜30000 命令
  • 最初からリダイレクトする : 25000〜40000命令

解析

トレースのフォーマットは、テキストフォーマットであるため解析が容易です。そのため、落とし穴だけ解説。
これは実際に Bochs を使ってトレースを作成した一例です。

(0).[8760124] [0x000f315a] f000:315a (unk. ctxt): cmp di, 0xf800            ; 81ff00f8
(0).[8760125] [0x000f315e] f000:315e (unk. ctxt): jbe .+7 (0x000f3167)      ; 7607
(0).[8760126] [0x000f3167] f000:3167 (unk. ctxt): mov es, ax                ; 8ec0
(0).[8760127] [0x000f3169] f000:3169 (unk. ctxt): mov dx, word ptr ss:[bp+12] ; 8b560c
[CPU0 RD]: LIN 0x000000000000ff5c PHY 0x0000ff5c (len=2, pl=0): 0x01F0
(0).[8760128] [0x000f316c] f000:316c (unk. ctxt): mov ah, byte ptr ss:[bp+3] ; 8a6603
[CPU0 RD]: LIN 0x000000000000ff53 PHY 0x0000ff53 (len=1, pl=0): 0x01
(0).[8760129] [0x000f316f] f000:316f (unk. ctxt): cmp ah, 0x01              ; 80fc01
(0).[8760130] [0x000f3172] f000:3172 (unk. ctxt): jz .+4 (0x000f3178)       ; 7404
(0).[8760131] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c00 PHY 0x00007c00 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x006CEBFA
(0).[8760132] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c04 PHY 0x00007c04 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x494C0000
(0).[8760133] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c08 PHY 0x00007c08 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x00014F4C
(0).[8760134] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c0c PHY 0x00007c0c (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x005A0014
(0).[8760135] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c10 PHY 0x00007c10 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x00000000
(0).[8760136] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c14 PHY 0x00007c14 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x3B05F7A1
(0).[8760137] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c18 PHY 0x00007c18 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x00809909
(0).[8760138] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c1c PHY 0x00007c1c (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x80990A01
(0).[8760139] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c20 PHY 0x00007c20 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x99080100

rep insd 命令の実行形式に着目してみてください。Bochs 開発者のひとりによれば、CPU0 RD によるメモリ読み取り (仕様上発生しない) は Bochs の仕様によるダミー操作のようですが、CPU0 WR のメモリアドレスに注目してください。――そう、完全に壊れているのです。
これは見たところ Bochs のバグのようです。この rep insd 操作に限らず、同じメモリアドレスに対して読み / 書きを連続で行うもの (例 : add [esp], edx) 全般に関して書き込み対象のアドレスが壊れているので、値は書き込まれるもの、アドレスは先行する読み取り操作のものを参照するようにしてください。

Bochs のバグ (?)

Bochs を使って命令トレースとメモリトレースを取得し、そこから最小情報トレースで必要とされるトレース量を計算、比較する試み――なのだが、思ったより時間がかかっている。最大の理由はその遅さ。メモリトレースを無効にして命令トレースだけにしても 1000 IPS (命令/秒) いくかいかないかだし、メモリトレースも有効にすると 700 IPS が平均値といったところだ。
それ以外はぶっちゃけ私のオペレーションミスのせいなのだが、そのおかげでトレースを詳しく見回す時間ができた。そこでバグらしきモノを発見した。
詳細は解説しない。 (下のログだけで皆さん理解できると思うから) 細かいことを言えば、おかしく見える点は 2 つある。(命令仕様との[厳密な見方をしたときの]不一致と、それ以前の問題)

(0).[8760124] [0x000f315a] f000:315a (unk. ctxt): cmp di, 0xf800            ; 81ff00f8
(0).[8760125] [0x000f315e] f000:315e (unk. ctxt): jbe .+7 (0x000f3167)      ; 7607
(0).[8760126] [0x000f3167] f000:3167 (unk. ctxt): mov es, ax                ; 8ec0
(0).[8760127] [0x000f3169] f000:3169 (unk. ctxt): mov dx, word ptr ss:[bp+12] ; 8b560c
[CPU0 RD]: LIN 0x000000000000ff5c PHY 0x0000ff5c (len=2, pl=0): 0x01F0
(0).[8760128] [0x000f316c] f000:316c (unk. ctxt): mov ah, byte ptr ss:[bp+3] ; 8a6603
[CPU0 RD]: LIN 0x000000000000ff53 PHY 0x0000ff53 (len=1, pl=0): 0x01
(0).[8760129] [0x000f316f] f000:316f (unk. ctxt): cmp ah, 0x01              ; 80fc01
(0).[8760130] [0x000f3172] f000:3172 (unk. ctxt): jz .+4 (0x000f3178)       ; 7404
(0).[8760131] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c00 PHY 0x00007c00 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x006CEBFA
(0).[8760132] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c04 PHY 0x00007c04 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x494C0000
(0).[8760133] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c08 PHY 0x00007c08 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x00014F4C
(0).[8760134] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c0c PHY 0x00007c0c (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x005A0014
(0).[8760135] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c10 PHY 0x00007c10 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x00000000
(0).[8760136] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c14 PHY 0x00007c14 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x3B05F7A1
(0).[8760137] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c18 PHY 0x00007c18 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x00809909
(0).[8760138] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c1c PHY 0x00007c1c (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x80990A01
(0).[8760139] [0x000f3178] f000:3178 (unk. ctxt): rep insd dword ptr es:[di], dx ; f3666d
[CPU0 RD]: LIN 0x0000000000007c20 PHY 0x00007c20 (len=4, pl=0): 0x00000000
[CPU0 WR]: PHY 0x00007c00 (len=4): 0x99080100

リバースエンジニアリングのための Record and Replay (レイテンシ削減編)

当初、最小情報トレース (= Record and Replay) において、VM で当該命令を実行してからその部分についてある程度の解析が終了するまでのレイテンシは 5〜20 分と見積もっていた。それは、Replay に相当する部分における解析に比較的時間がかかると見込んでいたからだ。

当初のモデル:

  • これらを並列実行する:
    • VM0 : (Virtual Machine 0) Record を行い、ストリームとして他のコンピュータに転送する (これをやらないと VM1 の負荷で VM0 に悪影響が出てしまう)
    • VM1 : VM0 で取得した Record から、比較的詳細な情報を解析、抽出しつつ Replay を行う
    • PR1 : (Processor 1) VM1 が出力した情報から詳細な情報を解析する
    • PR2 : PR1 と同様
    • PR3 : 以下省略

しかしこれは単なる先入観だ。Record and Replay 法において、Record に対応する Replay はひとつでなくともよい。つまり、複数の情報抽出用 VM を用いて次のように構成することで、少なくとも時間のそれほどかからない解析 (例えばシステムコールのトレース) のためのレイテンシを大幅に抑えることができる。

レイテンシ少なめのモデル:

  • これらを並列実行する:
    • VM0 : Record を行い、ストリームとして他のコンピュータに転送する
    • VM+PR1 : VM0 で取得した Record から、必要な情報を抽出する
    • VM+PR2 : (同文)
    • VM+PR3 : (同文)
    • VM+PR4 : (同文)

これを階層化することもでき、さらなる効率の向上を見込むことができる。(ただし、バッファオーバーフローの確実な検出のように、ほぼ完全な依存関係を必要とする場合はそれほど向上しない…のか? うまくポインタ追跡を行うコードを書けば、かなり効率化できそうな気もする。)

現実にありそうな一例 (Record 部分は省略):

  • Fast Analysis : Record データから比較的時間のかからない情報の抽出を行う。複数同時稼動可能。場合によっては、複数 VM で (解析および抽出なしの) Replay をしながら、時間区切りで Fast Analysis ジョブを分割して走らせることで、さらにレイテンシを抑えることが可能だと思う。
  • Indexing : Record データから、一定時間毎のスナップショットを生成する。もちろんこれも複数同時稼動可能。
    • Slow Analysis : 比較的時間のかかる情報の抽出と解析は、各スナップショットに対する VM を並列稼働させてレイテンシを削減する。
    • Combinator : 必要に応じて、各スナップショットで取得した Slow Analysis のデータを直列化する。
    • Verbose Analysis : Combinator から取得したデータを元に精細な解析を行う。

ここまでやって、基本的な解析 (HIPS に必要とされる部分) においては数秒単位のレイテンシを実現できると考えている。
加えて、Record 以外の VM (つまり Replay 系の VM) に関してはかなりの情報を共有できる。これを用いればレイテンシ削減の他に効率向上という利点も得られる。この点に関して最適化を施した VM というのは存在せず、実装できればかなりお得そうだ。