Hatena::ブログ(Diary)

SH2の日記 RSSフィード

2009-07-05 The Art of Work:MySQL InnoDB Pluginのデータ圧縮機能 性能編 このエントリーを含むブックマーク

MySQL InnoDB Pluginのデータ圧縮機能の続きです。前回はInnoDB Pluginの独自機能であるデータ圧縮の仕組みを解説し、Wikipedia日本語版のデータが約半分にまで圧縮されることを確認しました。今回はデータ圧縮によって性能がどのように変化するかを、実際にベンチマーク試験を行って見ていきます。

試験の方針

データ圧縮による性能への影響は、以下の二点が考えられます。

  • メリット:データサイズが小さくなるため、ディスクI/Oが減る
  • デメリット:圧縮・展開の処理が行われるため、CPU負荷が高くなる

そこで、これらの特徴がよく分かるように試験パターンを工夫します。Wikipedia日本語版のデータはInnoDB上でおよそ5GBありますが、まず狭い範囲に絞って読み取り処理を行うことでディスクI/Oがあまり発生しないようにします。これでCPU負荷の傾向を確認することができます。次に読み取り範囲を徐々に広げていってディスクI/Oを発生させるようにし、ディスクI/Oの傾向を確認していきます。

試験を行いやすくするため、テーブルを以下のように作り直します。

CREATE TABLE `text_r` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `old_id` int(10) unsigned NOT NULL,
  `old_text` mediumtext COLLATE utf8_bin NOT NULL,
  `old_flags` tinyblob NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `old_id` (`old_id`)
) ENGINE=InnoDB

元のテーブルではold_idが主キーなのですが、抜け番があってレコードを選択しづらいため新たにidを主キーとして連番を振り直します。元のold_idには、今回の試験では使いませんが念のため一意キーを付与します。また、本文が格納されているold_textはMEDIUMBLOBだったのですが、バイナリ型は本来あるべき姿ではないと考えてMEDUIMTEXTに変更しています。

データロード時には、ORDER BY rand()を用いてレコードをバラバラに並べ替えます。

INSERT INTO text_r (old_id, old_text, old_flags)
SELECT old_id, cast(old_text as char), old_flags
FROM text
ORDER BY rand()

これは、textテーブルのデータを分析したところold_idの小さなレコードには比較的短い記事が、old_idの大きなレコードには比較的長い記事が格納されていたためです。Wikipediaでは記事を更新するとold_idを採番し直すので、よく更新される記事、つまり長い記事ほど後ろのほうに寄せられるようです。このままでは読み取り範囲と実際にアクセスするデータサイズの相関がつかみづらいため、ランダムに並べ替えることで読み取り範囲あたりのデータサイズを平準化しています。

試験では以下のSQLを実行します。

SELECT t.old_id, left(t.old_text, 100), t.old_flags
FROM text_r t
WHERE t.id = ?

これは記事を一件取り出すだけの単純なSQLです。idに与える乱数の範囲を変更することで、読み取り範囲を制御します。最初は1から25,000までの乱数、次は50,000まで、その次は75,000まで…と広げていき、全件の1,167,411件まで繰り返し試験を行います。

old_text列にleft()関数をかけていますが、これはold_textをそのまま取得してしまうと100Base-TXのネットワーク帯域が足りなくなってしまったためです。old_textの平均カラム長は約3,000バイトになっていますが、left()関数によって最初の100文字のみをクライアントに返すようにしています。

その他の試験条件を以下に示します。

my.cnfのうちポイントとなるところだけ説明します。

innodb_buffer_pool_size = 512M
innodb_flush_method = O_DIRECT
# query_cache_type = 1
# query_cache_size = 16M

innodb_buffer_pool_sizeは512MBと少なめにしておき、Wikipedia日本語版のデータがたとえ圧縮しても収まらないようにしています。またinnodb_flush_methodをO_DIRECTにすることで、OSのページキャッシュによって性能が上がってしまうことを防ぎます。クエリキャッシュコメントアウトして無効にしておきます。

試験結果

前置きが長くなってしまいました。それでは結果を確認していきましょう。最初は性能のグラフです。読み取り範囲(search range)を横軸に、1秒あたりのクエリ実行数を縦軸にプロットしています。横軸の数値は1万件単位です。

f:id:sh2:20090704095053p:image

いかがでしょう、みなさんの予想は当たりましたか?私を含めどこまで性能が落ちるのだろうと思っていた方は、いい意味で期待を裏切られたかもしれませんね。今回のワークロードではデータ圧縮を行っても性能が大幅に劣化するポイントはなく、場合によっては圧縮した方が性能が上がるという興味深い結果がでました。

このあとの解説のために、試験結果を三つのゾーンに分けておきます。

f:id:sh2:20090704123357p:image

  • ゾーンA:読み取り範囲が狭いところです。テーブルを圧縮した場合、性能がおよそ12%低下しています。
  • ゾーンB:読み取り範囲がやや広いところです。テーブルを圧縮した方が性能が高くなり、最大で約3.5倍のスループットが出ています。
  • ゾーンC:読み取り範囲が広いところです。グラフではよく見えませんが、テーブルを圧縮した方が10%ほど性能が良くなっています。ただいずれにせよ、スループットはピーク時の50分の1程度まで落ち込んでしまっています。

InnoDBバッファプールのヒット率

次に示すグラフは、InnoDBバッファプールのヒット率です。Oracle Databaseのバッファキャッシュヒット率と同じで、必要なデータがどれだけの確率でメモリ上にすでに存在していたかを示す指標値です。

f:id:sh2:20090704095054p:image

  • ゾーンAでは、圧縮、非圧縮どちらもほぼ100%のヒット率になっています。非常に効率よく動いている状態です。
  • ゾーンBでは、非圧縮の方がヒット率100%を維持できなくなっています。性能のグラフとあわせて見ると、ヒット率が下がり始めたとたんに性能が大幅に劣化してしまっていることが分かります。一方、圧縮した方はヒット率100%をおおむね維持していますが、ヒット率100%を維持しているにも関わらず徐々に性能が低下していきます。ここポイントです。
  • ゾーンCでは、どちらもヒット率は80%程度まで低下しています。ただし圧縮した方は非圧縮に比べ常により高いヒット率を維持しています。80%というとそれほど悪くないように見えますが、性能は地を這ってしまいます。Oracle Databaseにおいても昔はバッファヒット率80%がチューニングにおける一つの目安だったのですが、CPUストレージの性能差が大きくなってしまった現在は99%を目指すのが常識になりつつあります。

CPU使用率

次はCPU使用率のグラフです。最初は非圧縮時におけるグラフです。

f:id:sh2:20090704095056p:image

最初に断っておくと、全体的にsysが異常に高くなっていますが、これはVMware Server上で試験を行っているためです。VMware Player/Workstation/Serverはネットワーク通信の負荷がとても高く、これによってsysが高騰してしまいます。ネイティブの環境では通常usr:sys=4:1ぐらいになります。自宅にはネイティブのLinux環境がないので…すいません。

  • ゾーンAでは、usrとsysでほぼ100%を占めています。無駄なくCPUを使い切っている状態です。
  • ゾーンB・Cでは、waitが支配的になります。ディスクI/Oによって待たされてしまうため、CPUが有効に使われていません。

次は圧縮した場合のグラフです。

f:id:sh2:20090704095057p:image

明らかに違う傾向です。ちょっと仕組みが見えてきましたね!

  • ゾーンAではusrが約40%となっており、非圧縮時に比べてやや高くなっています。
  • ゾーンBではゾーンAよりもusrが高くなっています。非圧縮のものと比べると差は歴然で、読み取り範囲30万件においては実にCPU負荷(usr)が16倍になっています。

クエリあたりのCPUコスト

性能とCPUのデータから、1回のクエリにどれだけCPUを使ったのかを計算することができます。次のグラフは、データ圧縮を行うことによってクエリあたりのCPUコスト(usr)が何倍になるかをプロットしたものです。

f:id:sh2:20090704095058p:image

あまり値が安定していませんが、傾向の違いは見てとれると思います。

  • ゾーンA:およそ1.4倍のCPUコストがかかっています
  • ゾーンB・C:2.0倍から4.7倍のCPUコストがかかっています。

つまりInnoDB Pluginはただ言われるがままにデータを圧縮・展開しているのではなくて、何か工夫をしているということです。

InnoDB Pluginにおけるメモリ管理アーキテクチャ

というわけで解説編です。InnoDB Pluginは、バッファプールを「圧縮ページ」と「展開済ページ」の二つに分けて管理しています。図にすると以下のようになります。

f:id:sh2:20090705012333p:image

ディスクから読み込まれたデータはまずバッファプール内に圧縮ページとして格納され、それからzlibライブラリによる展開が行われて展開済ページが作成されます。このときいくつかのルールがあります。

  1. 圧縮ページと展開済ページは、それぞれ別のLRUリストによって管理されています。
  2. 圧縮ページが正、展開済ページが副となります。バッファプール上に展開済ページのみが存在することはできず、「両方ある」「圧縮ページだけがある」「どちらもない」の三パターンのみとれるようになっています。
  3. バッファプールに余裕がある場合、InnoDB Pluginは展開済ページを積極的に残すように振る舞います。ただし先のルールにより、最大でも圧縮ページと展開済ページの割合は1:1となります。
  4. バッファプールに余裕がない場合、InnoDB Pluginは展開済ページを優先的に破棄します。ただし全ページに対し最低でも10%は展開済ページが残るようにしています。

試験結果と照らし合わせると、ゾーンAにおける性能劣化の少なさはルール3によるものです。実はゾーンAにおいてzlibによる展開処理なんて行われていなかった!というわけです。またゾーンBにおいてヒット率100%にもかかわらず徐々に性能が低下していくのは、ルール4によるものです。バッファプールに余裕がなくなって展開済ページの割合が減っていくにつれて、クエリのたびに圧縮ページを展開しては捨て、展開しては捨て、という処理が増えていくことになります。

試験結果の再確認

InnoDB Pluginのアーキテクチャを理解したところで、試験結果を再度確認していきましょう。バッファプールにおける圧縮ページと展開済ページの数は、show engine innodb statusコマンドで確認することができます。

f:id:sh2:20090704095100p:image

今回はinnodb_buffer_pool_sizeを512MBにしているので、標準の16KBページの場合32,768個のページをバッファプール内に格納することができます。ゾーンAではバッファプールに余裕があるため展開済ページを必要なだけ確保することができ、読み取り範囲が広がるに応じて展開済ページの量も増えていっていることが分かります。そして読み取り範囲15万件において圧縮ページと展開済ページの割合がほぼ1:1となり、ここが展開済ページを全量確保できる限界点となります。

読み取り範囲15万件におけるバッファプールの状態を図示すると、以下のようになります。

f:id:sh2:20090705012334p:image

圧縮ページと展開済ページが1:1(ページサイズが異なるので容量比では1:2)となり、クエリは圧縮ページにアクセスすることなく処理されます。

ゾーンBに入って展開済ページを全量確保できなくなると、InnoDB Pluginはルール4により展開済ページを優先的に破棄してその分圧縮ページにより多くのメモリを割り当てるようになります。読み取り範囲27万5千件において圧縮ページと展開済ページの割合が9:1となり、ここがバッファプール内で最大限ページ数を確保した地点となります。このときバッファプール内における合計ページ数は32,768を大きく超え、54,215まで増加しています。

読み取り範囲27万5千件におけるバッファプールの状態を図示すると、以下のようになります。

f:id:sh2:20090705012335p:image

バッファプール内の展開済ページの数が不足しているため、大半のクエリは圧縮ページを展開しながら処理されることになります。私を含め多くの方が予想していたデータ圧縮機能によるCPU負荷の増加率は、おそらくこの地点のものではないかと思います。クエリあたりのCPUコストでおよそ4倍となっています。

またshow engine innodb statusコマンドでは、圧縮ページの展開が何回行われたのかも確認することができます。

f:id:sh2:20090704095059p:image

一目瞭然ですね。ゾーンAでは圧縮ページの展開処理がまったく発生していないこと、ゾーンBでは展開処理が大量に発生していることがよく分かると思います。

まとめ

今回はInnoDB Pluginのデータ圧縮機能について、実際にベンチマーク試験を行ってその性能を確認しました。その結果、以下のことが分かりました。

  • 非圧縮状態でもメモリに載るデータ量の場合、CPUコストはおよそ1.4倍となり、性能は0.9倍に低下する
  • 非圧縮状態でメモリに載らないデータ量になると、できるだけ多くのデータを圧縮状態でメモリに載せようとする
  • その場合CPUコストはおよそ4倍となるが、ディスクI/Oが抑制できる場合は性能が最大で3.5倍に向上する
  • 圧縮状態でもメモリに載らないデータ量になると、性能向上は1.1倍にとどまる

数値は環境依存なので、あくまで参考程度としてください。

それにしてもInnoDB Pluginのデータ圧縮機能はよく考えて作られていますね。特に圧縮機能の開発コストとそれによって得られる効果のバランスが、他のRDBMSに比べて非常に優れていると感じます。かつてGoogleMark Callaghan氏がInnoDBのことを評して「The Art of Work」と言っていましたが、私も本当にそう思います。