MySQL 5.1.41リリース

出ました。今回は機能の追加・変更が4件、バグ修正が62件あります。
MySQL 5.1.38から同梱されるようになったInnoDB Pluginですが、MySQL 5.1.41ではバージョンが1.0.5に上がり、ついにRC(リリース候補版)となりました。再掲になりますがInnoDB PluginはビルトインのInnoDBに比べて以下のような機能強化が施されており、非常に有用性の高いものです。そろそろ利用を検討しても良い時期に入ってきたのではないかと思います。

  • 高速なインデックス作成。従来InnoDBのCREATE INDEXはテーブルの再作成を伴っていました
  • テーブルとインデックスの圧縮 (検証結果その1その2)
  • INFORMATION_SCHEMAによるロック競合の検出 (検証結果)
  • CPUスケーラビリティの向上 (1.0.3から)
  • バックグラウンドI/Oスレッドの増加 (1.0.4から。検証結果)
  • グループコミットの修正 (1.0.4から。Sun松信氏による検証結果)

InnoDB Plugin 1.0.5ではバッファプールの管理アルゴリズムが強化されました。今回はこの点について、他の製品と比較しながら理解していきたいと思います。

InnoDBのバッファプール管理

ほとんどのRDBMSは、ディスク上に保存されたテーブルのデータをメモリ上にキャッシュする仕組みを備えています。テーブルのデータをメモリ上にキャッシュする目的は、頻繁にアクセスされるデータについてアクセスのたびにディスクI/Oが発生することを防ぎ、全体の性能を向上させることです。InnoDBではこの領域のことをバッファプールと呼び、通常1GBから、多い場合は8GB程度を割り当てて利用しています。
すべてのデータがメモリ上に収まってしまえば悩むことはないのですが、多くの場合メモリのサイズはデータベースサイズよりも小さいため、すべてのデータを収めるわけにはいきません。そのため、よく使われるデータというものをより分けてメモリに蓄える必要があります。これにはLRUアルゴリズムが用いられます。バッファプールのそれぞれのページをリストとして連結し、ページがアクセスされるたびに対象のページをリストの先頭(MRU側、Most Recently Used)に移動させます。しばらくアクセスされなかったページはリストの末尾(LRU側、Least Recently Used)に寄せられていき、バッファプールの容量を超えたものはメモリから破棄されるというアルゴリズムです。
LRUリストは仕組みが明快で有効性も高いのですが、弱点があります。それは、1回しかアクセスされない巨大なデータが来た場合に、本来蓄えておくべきアクセス頻度の高いデータが巨大なデータによって押し流されてしまうという点です。そのため各RDBMS製品はLRUリストに工夫を加えることでこれに対処しています。

これはInnoDBのバッファプールをLRUの観点から図示したものです。見てすぐ分かるようにLRUリストが2本あります。1本はYoung LRUリスト、もう1本はOld LRUリストと呼ばれているものです。

  • ディスクから読み込まれたデータは、まずOld LRUリストのMRU側に配置されます。
  • Old LRUリスト上のデータがもう一度アクセスされると、Young LRUリストの先頭に移動されます。
  • Young LRUリストからあふれたデータはOld LRUリストのMRU側に戻されます。
  • Old LRUリストからあふれたデータはメモリ上から破棄されます。

ディスクから読み込んだデータがLRUリスト全体に対するMRU側ではなく中間地点に挿入されることから、このアルゴリズムのことをミッドポイント挿入戦略と呼びます。この戦略によって、1回しかアクセスされないデータが他の重要なデータを押し流してしまうことを防ごうとしています。
しかし、これまでこのミッドポイント挿入戦略はうまく機能していませんでした。InnoDBのページは16KBあって1ページ内に複数のレコードが格納されているのですが、テーブルのフルスキャンを行う際に例えば1ページに2レコードが格納されていると、それだけでアクセス回数が2回と数えられてしまうためです。そのため結局のところ1回のフルスキャンによって他のデータが押し流されてしまうということが発生してしまっていました。
InnoDB Plugin 1.0.5ではinnodb_old_blocks_timeという新規パラメータによってこの問題に対処できるようになりました。このパラメータを設定すると、Old LRUリストに格納されたページに対して再びアクセスが発生しても、指定した時間が経過するまではYoung LRUリストに移動されないようになります。innodb_old_blocks_timeはミリ秒単位で指定し、デフォルトは0(無効)となっています。

MyISAMのキーキャッシュ管理

他の製品ではどうなっているのか、まず近いところでMyISAMアルゴリズムを見てみましょう。MyISAMの特徴としてテーブルのデータはキャッシュせず、インデックスのデータのみをキーキャッシュという領域に格納するという点があります。テーブルのデータはOSのキャッシュ機構にまかせてしまう、というのがMyISAMの設計思想です。

MyISAMのキーキャッシュも、InnoDBと同様に2本のLRUリストで管理されています。それぞれHot LRUリスト、Warm LRUリストという名前がついていて、役割はInnoDBのYoung LRUリスト、Old LRUリストとまったく同じです。
InnoDBはYoung LRUリストへの昇格を時間で管理していましたが、MyISAMはHot LRUリストへの昇格を回数で管理しています。Warm LRUリストにあるブロックは、Warm LRUリストから脱落するまでに3回アクセスされるとHot LRUリストに移動されるという仕組みになっています。
また、Hot LRUリストから脱落したブロックは、Warm LRUリストのMRU側ではなくLRU側に配置されます。Hot LRUリストから脱落した時点で、それはもう不要なデータだと判断されていることになります。

Oracleのデータベースバッファキャッシュ管理

続いてOracle Databaseのバッファキャッシュ管理アルゴリズムを確認していきましょう。

OracleのLRUリストは内部的にホットバッファとコールドバッファという二つの領域に分けられています。これはInnoDBにおけるYoung LRUリスト、Old LRUリストとほぼ同じものですが、二つの領域のサイズ比をOracleが自動で調節するようになっているところが異なります。また、OracleはLRUリストを用途別に複数持っています。
Oracleのバッファキャッシュ管理におけるポイントは、データへのアクセスパターンによってLRUリストの挿入位置を変化させているところにあります。
まず、インデックスアクセスによりディスクのランダムリードを行った場合は、InnoDBMyISAMと同様に読み込んだデータブロックをホットバッファとコールドバッファの境目にあるコールドポインタという位置に挿入します。またLRUリスト内のデータブロックはそれぞれがアクセスカウンタを持っており、一定回数アクセスされたデータブロックはコールドバッファからホットバッファに移されます。このあたりの考え方はMyISAMと同じです。
一方、テーブルフルスキャンによりディスクのシーケンシャルリードを行った場合は、読み込んだデータブロックはLRUリストのLRU側に挿入されます。そのためフルスキャンによって読み込んだデータは、次の瞬間には破棄されてしまうということになります。これはつまりdb_block_size×db_file_multiblock_read_count分のメモリを本当にただのバッファとして利用しているだけで、データを再利用できるように保持するつもりがまったくないということです。この仕組みによって、アクセス頻度の高いデータがバッファキャッシュから押し流されてしまうことを防いでいます。
ただし、いくつかの条件の下ではテーブルフルスキャンを行ったデータでもコールドポインタに挿入することがあります。

  • インスタンス起動直後でバッファキャッシュに未使用領域がある場合
  • アクセス対象のテーブルがバッファキャッシュのサイズに比べて十分小さい場合
  • SQLにCACHEヒント句をつけた場合(SELECT /*+ CACHE(emp) */ ename FROM emp;)

こうした仕組みによって、Oracleではどのデータをメモリ上に保持しておくのかがきめ細かく制御されています。
さらに、特定のテーブルがメモリ上から押し出されないようにするために、Oracleはバッファキャッシュを分割して管理する機能を備えています。

このようにOracleではデフォルトのバッファプールに加えてKEEPバッファプール、RECYCLEバッファプールという2種類の領域を定義し、それぞれにメモリを割り当てることができるようになっています。KEEPバッファプールにはメモリ上に保持しておきたいデータ、RECYCLEバッファプールにはメモリ上に保持する必要性の薄いデータを配置します。この配置はテーブル単位で行われ、図のようにALTER TABLE文を用いて事前に配置先を指定しておく必要があります。何も指定していないテーブルはデフォルトのバッファプールに配置されることになります。全体として、限られたメモリを有効活用するために手厚いサポートがされていることが分かると思います。
2本目のLRUリストにはLRUWリストという名前がついていますが、これはYoung LRUリスト、Old LRUリストと対比するための一つの例として記載したもので、実際にはOracleのLRUリストはさまざまな用途に応じたものが合計10種類あります。
LRUWリストはメモリ上で更新されたデータブロックが格納されるもので、このLRUWリスト上に存在するデータブロックはDBWRプロセスによってそのうちディスクに書き戻されることが決まっています。このように更新されたデータブロックを通常のLRUリストから分離しておくと、チェックポイント時にDBWRがスキャンしなければならないメモリ量を減らすことができるため、性能が良くなるというわけです。この考え方はLinuxカーネル 2.6.28で導入されたSplit LRUと似ていると思います。
性能向上の観点からもう一つ、OracleのLRUリストはこの10種類を1セットとして複数セットが用意されており、バッファキャッシュを内部的に分割して管理しています。なぜ分割されているかというと、LRUリストはその構造上リストを更新するために排他ロックを取らなければならず、リストが一組しかないとCPU数の増加に応じた性能を発揮しにくいためです。このセット数はCPU数やメモリサイズに応じてOracleが内部的に決めるもので、私の手元のPCでも32セットというかなり大量のLRUリストが用意されています。これがすべてではありませんが、Oracleはこうした作りによって高いCPUスケーラビリティを確保しています。

Oracleに近づくInnoDB

InnoDB Pluginはバージョンアップごとにかなりアグレッシブな機能強化が行われていて、徐々にではありますがアーキテクチャOracleに近づいてきているように思います。このエントリだけ読むとまだOracleとの機能差はかなりあると感じるかもしれませんが、実際の利用シーンでは微々たる差になりつつあります。Oracle一筋というエンジニアの方も、そろそろMySQLを触ってみてもよい頃合いかもしれませんね。