このページの記事一覧

2009年8月27日木曜日

米大統領選でMySQLはどのように使われたのか

 日本の衆議院選挙が間近に迫っていますが、昨年米国で行われた大統領選において、オバマ陣営がIT技術を駆使したという話はよく知られています。MySQLももちろん使われていました。今年4月にサンタクララで開催されたMySQL Conference & Expo 2009というイベントでは、最終日のキーノートにおいて、Obama Tech Teamの方々より、大統領選においてMySQLがいかに使われたかという発表が行われました。
 本当はカンファレンス終了後にすぐちゃんとしたレポートを書いて公開する予定だったのですが、その週に起こった草なぎ剛逮捕とか、そのほかの出来事にすっかり気を取られて放置していました。Blogを始めた契機にTwitterの中で興味を持っている方がいるかどうか聞いたところ、そこそこの方が興味を示したので、ここで簡単にまとめたメモを公開することにします。


●チームメンバー
発表者は以下の5人で、ほか何名かでチームを構成。
Chuck Hagenbuch (Blue State Digital)
Leigh Heyman (Blue State Digital)
Stephen Gunn (Google)
Mikey Dickerson (Google)
Ian Gulliver (Google)
(Google社内のメーリングリストなどでボランティアの公募があって、それに乗る形で参加されたそうです)

●主なミッション
▲Fundraising(資金調達)
・インターネット経由で資金調達できる仕組みの整備
・当初の目標は$290Mの調達
・実際には$500M以上(当時の為替レートで550億円くらい)を調達することができた

▲大量のeメールの送信(購読者に応じて内容を変える)
・当初の目標は500-600万人の購読者に対して計2億5000万通程度のメール送信をすること
・実際には購読者は1300万人に達し、計20億通のメールを送信
(日本だと公職選挙法で選挙期間中のメール配信とかは違法になると思うのですが、米国ではセーフらしいです)

▲Webサイトの整備
・当初の目標は秒間800PVをさばくこと
・実際には最高で秒間4300に達した(2008年10月29日)

●インフラ設計
▲ハードウェア
・Dell 2950 w/ Disk Array (x2)
・Amazon EC2も併用したが、大事なところでは使わなかった。選挙日に2時間止まったりしたらどうしようもないので
(今のEC2のサービスレベルを見ると99.95%保証のようです。大統領選のような「特定の時間帯は絶対に動いている必要がある」というタイプのサービスではまだ厳しいのではないでしょうか)

▲MySQL
・最初はMyISAMのみ。レプリケーション使用、mysqldumpでバックアップ。
・データ量の増加に対してバックアップが追いつかなくなった。
・データ量は5TBを超えた。論理バックアップでは新規レプリカの作成にも数日単位でかかった。
・物理バックアップに移行(ファイルシステムスナップショット。おそらくflush tables with read lock + tar)。
・テーブルロックによって夜間バッチジョブが動かなかった
・結局ロックが深刻になりすぎたので、中心的なトランザクショナルなテーブルをInnoDBにした


●テーブル/データ設計

▲有権者の行動をトレースするための仕組み
・有権者がどこで何をしたかという情報を蓄積して選挙活動に活かす
・キャンペーンのe-mailは、全員に同じ内容が届くわけではない。有権者が過去に何をしたかによってグループ分けをしていて、グループごとに内容が違う。(過去に1回でもオバマ陣営のボランティアをしたか、まったくしていないかとか)
・「AとBをした人にメールを送りたい」という要求があったときに
 行動Aに関するテーブル
 行動Bに関するテーブル
を管理して、それぞれで1300万人の購読者を保持したらレコード数が膨大になってしまう。
・「デフォルト行動」を定義して、デフォルトでない行動をした人の情報だけをテーブルで持つようにした。例えばAをした人よりもしていない人の方が圧倒的に多ければ、Aをしていない人のレコードだけを格納する(Bも同様)。「Aをした人 and Bをした人」は、全体から「Aをしていない人 or Bをしていない人」を引いたものと同じだが、この場合は後者の方が処理効率が良い
・これでレコード件数を減らすことができ、パフォーマンスが上がった

▲有権者情報の検索処理
・MyISAMではロック競合が深刻だったのでInnoDBにした。
・InnoDBにしたことでCOUNT(*)に時間がかかるようになった
 ・AUTO_INCREMENT列に対してMAX関数を使うことで、最大値を取るようにした。最大値=件数という想定
 ・誰か(開発者)がテスト用に巨大な値をセットしたら、値が本来の値(件数)ではなくなってしまった。
 ・AUTO_INCREMENTは再起動かALTER TABLEをしないと戻らないのでmaxは使えなくなった(ダウンタイムを設けることができない状況)
 ・サマリーテーブルに件数情報を書くようにした。ある一定のタイミングで、最新の件数をサマリーテーブルに書き出すようにして、高速に件数を取れるようにした
・MyISAM→InnoDBによってデータ量が増えたので、広範囲にまたがる集計系処理にかなりの時間がかかるようになった
・リアルタイム性の求められない処理だったので、同一テーブルのコピーをMyISAMで作成し、そのMyISAMテーブルに対して集計処理を行うことで高速化した

▲大量のeメールの送信履歴管理
・メールの送受信情報を記録する要求がある
・1時間に200万通を送る必要があり、その履歴も管理
・MyISAMテーブルを使用
・テスト環境では100万件の登録に2分だったが、本番環境では1時間かかった
・テスト環境では空だったのが、本番環境では既存のレコードが多いのが原因
・できるだけ空のテーブルにINSERTさせるように設計変更した
・MyISAMテーブルを複数用意して、日付で分割、空のMyISAMテーブルにINSERTさせる
・これらをMERGEテーブルとして結合
(本番環境でINSERTに長時間を要したのは、インデックスの影響と考えられます)


▲重いバッチ処理によってマスターがスローダウンする
・バッチ処理の中で、重たいSELECTをスレーブで行って、最終結果をマスターに書くようにした


▲Early Vote (期日前投票)推進
・有権者の行動をいち早く分析して、支持者に早期に期日前投票に行くように促す仕組み
・「どの日に、どの週で、どの政党に、どの性別の人が、何秒投票し、平均年齢は何歳で...」といった情報を解析したい
・もともとは各テーブルに行動情報を蓄積して、7テーブルくらいをジョインして結果を返していた
 ・4000秒以上かかっていた
・日ごとに集計するのが分かっていたので、日を切り口にしたサマリーテーブルを導入。秒単位で結果を返せるようになった




●感想
 発表者たちの主な活動期間は、2008年9月中旬頃から11月までだったそうです。極めて限られた期間の中では、事前に練りに練ったMySQLアーキテクチャ構成を元に型にはめていくというアプローチが難しかったようで、かなりの試行錯誤を繰り返しながらそれでもなんとかなった、という感じで生々しい話でした。Early Voteを強力に推進したことが大統領選勝利の大きなポイントだったというニュースは日本にも伝わってきましたが、それを裏方で支えていたのは、必要な情報を正しく迅速に取れるための仕組みであり、そのためのデータモデルであったということを強く感じました。個々のテクニックはよく知られたものばかりですが、それを適切に組み合わせられるのは経験と技術あってのものですし、適切なスキルがあればOSSでもこうした重要なシステムを安定稼動できる(逆に無ければ商用製品でも困難)、ということを示す良いキーノートだったと思います。

2009年8月20日木曜日

正しいベンチマークをするための10のポイント

 世の中ではたくさんの人が独自にベンチマークを行ない、独自に情報発信がされています。そのベンチマークの中には、非常に参考になるものもあれば、現実性に大きく欠けるものもあります。競合他社が、ライバル社の製品にとって不利な条件でベンチマークを行い、それを発信することも日常的に行われています。ベンチマークの結果を鵜呑みにすることは危険で、結果の意味を判断するスキルを持つことが重要です。これはプロジェクトにおいて負荷テストを行う場合にも重要です。負荷テストの条件設定が正しいかどうかを判断できるようになるためです。
 ここでは、私がDBサーバのベンチマーク/負荷テストを行ったり結果を読んだりする上で、心がけているポイントを10個ほど紹介したいと思います。

■ハードウェアに関する4つのポイント

1. ハードウェアのスペックと設定を注視する
 ハードウェア構成によってベンチマーク結果は劇的に変わるので、言わずもがなでしょう。以下のような点、特にDBサーバであればメモリ容量やストレージ構成に注意を払いたいところです。

・CPU型番、クロック、コア数
・メモリ容量
・ストレージ本数、RAID設定、HDDの回転数、SSDの製品名、ライトキャッシュの設定など
・ネットワーク

 RDBMSにおいては、ライトキャッシュの設定(バッテリーバックアップつきライトキャッシュが有効になっているかどうか)が非常に大切だということも付け加えておきます。


2. 大まかなアクセス性能を知っておく
 CPU、メモリ、HDD、ネットワークなどの各ハードウェアに対して、1回アクセスする際にどの程度の時間がかかるのか、あるいは1秒間に何回程度のアクセスができるのか、(概算値を)即答できるでしょうか。何度もアプリケーション運用を経験していると、このあたりの性能指標は嫌でも意識させられることでしょう。
 ざっくりとした感覚では、現在Linux-DB サーバとして主に用いられているハードウェア環境では、単一接続という条件では、CPUへのアクセス時間が10ns、メモリへのアクセス時間が60ns、HDD へのアクセス時間が5ms、Intel SSDが0.2ms、ネットワーク(Gigabit Ethernet)が0.1ms程度といったところではないでしょうか。データ転送量が多ければそれだけ時間はかかりますし、CPUクロック数やキャッシュの存在によっても値は全然違ってくるので、「正確性」という意味では突っ込みどころがあります。しかし、桁の感覚としてはおおむね正しいでしょう。特に、HDDがメモリどころかネットワークに比べても大きく性能が落ちるというのは、パフォーマンスチューニング上で非常に大切なポイントです。HDD へのアクセス時間が5ms ということは、1秒あたり200回くらいしかアクセスできないことを意味しています。
 こういった性能指標を叩き込んでおけば、ベンチマーク結果を見たときに不自然かどうかを見分けられるようになります。


3. 並列性を考慮する
 上で挙げた性能指標は単一接続時のものですが、アプリケーションでは複数の接続が張られるため、並列性についても考慮する必要があります。同時何接続から処理が行われた状態でのベンチマークなのか、ということです。CPUやメモリは桁違いのアクセス性能なので置いておくとしても、HDD/SSD/ネットワークの並列性に関する理解は重要です。ストレージの場合は、RAIDやコマンドキューイングによって並列性を高めることができます。コマンドキューイングと一口で言っても、HDDはSASとSATAによっても違いますし、HDDとSSDでも大きく変わってきます。SAS HDDでは単一接続でのランダムアクセス性能が200IOPSくらいだったのが、大量接続になると500IOPSくらいを得ることができるでしょう。Intel SSDではこれがさらに跳ね上がって、1接続では毎秒のランダムリード性能が5,000くらいだったのが、大量接続では25,000近くを得ることも不可能ではありません。
 ネットワーク(TCP/IP)も並列性が大変優れています。接続数が数十になった場合、ネットワークだけで見ればトータルで秒間20万回クラスのリモートアクセスも可能で、これはSSDをも大きく上回る性能です。
 RDBMSから見ると、同時接続数が増えた場合、内部的なmutex競合などでスループットが落ちるケースも頻発します。現実問題としては、無尽蔵に接続を張るのではなく、コネクションプーリングやMaxClientsなどで接続数を限定することも必要になってきます。期待されるアクセス性能に対して、どの程度落ちた値になるか、という観点での性能分析も面白いところです。


4. どこのスループットやレスポンスタイムかを確認する(ローカルアクセスかリモートアクセスかを確認する)
 「ライブラリBはライブラリAよりも処理速度が100倍速い」という状況はよくあります。これを見て、ライブラリAをBに入れ替えたら、アプリケーションのレスポンスタイムやスループットが100倍速くなると考える人はいないでしょう。スループットやレスポンスタイムという観点では、Webブラウザ→Web/APサーバ→DBサーバというアクセス経路全体を見る必要があり、特定のサーバの特定のロジックだけを改善しても、それが全体から見て無視できる範囲であれば大きな効果にはなりません。経験的には、DBサーバがボトルネックになることが多いため、DBサーバだけをベンチマークしてX倍速くなった、と言うことが多いですが、RDBMSと比較して別の製品(キャッシュサーバ等)を評価するような場合は、単体だけではなく、全体的なスループットの面でどれだけ効果があるのかも確認したいところです。
 多くのベンチマークでは、負荷をかけるクライアント、DBサーバともに同一マシン上に同居させた状態で行われています。しかし、現実的な用途ではアプリケーションサーバとDBサーバは別のマシン上に置かれるので、リモートアクセスになります。
 これは、「同一プロセス空間内でアクセスできる製品」のベンチマークに顕著な影響を及ぼします。同一プロセス内でローカルアクセスする場合と、リモートアクセスになる場合とでは、ネットワークのオーバーヘッドだけでなく、オブジェクト・シリアライゼーションのオーバーヘッドも加わるので極端な差(条件次第では100倍を超える差)が生まれます。逆の言い方をすれば、可能ならローカルアクセスで完結させるのは良い設計だと言えます(EHCacheやTimesTenなどをAPサーバ上でローカルで使うというアプローチ)。
 「秒間100万アクセスができました」などと宣伝している製品も見かけますが、リモートアクセスをさせた場合は、GbEのネットワークアクセス性能に引きずられるので、1台あたりではせいぜい10数万かそれ以下のスループットに落ち着くのではないでしょうか。


■ベンチマーク用アプリケーションに関する4つのポイント

5. 処理の内容に気を配る
 ベンチマークプログラムが処理する内容と、実際のアプリケーションで処理する内容が大きくかけ離れているケースがあります。例えばDBサーバではI/Oが主なボトルネックになりますが、そこでI/O性能を測定するベンチマークをしようとしても簡単ではありません。以下のように性能に影響を与える要因が数多く存在し、どれを変えても結果が変わるためです。

・扱うデータ量
・メモリの搭載量
・ディスク本数、ディスクの種類(SSD/HDDなど)
・RAID構成
・ランダムI/OかシーケンシャルI/Oか
・ダイレクトI/Oかbuffered I/Oか
・ライトキャッシュの有無
・I/O単位(1KBか10MBか)
・同時I/Oスレッド本数
・扱うファイル数
・上書きか追記か
・ファイルシステムおよびマウントオプションの違い

 ddコマンドなどによってシーケンシャルなファイル書き込みを行ない、毎秒100MBの転送量が出たとします。いざDB サーバを動かすと毎秒数MB程度しか転送量が出ず、「このデータベースはおかしい」などと言い出す人がたまにいますが、それは行なっている処理が違うためです。
 多くのパフォーマンスの変動要因があるので、簡易なツールを用いてI/O の負荷測定を行なうのは簡単ではありません。やはり、DB サーバに対して直接負荷をかけたほうが確実性が高いでしょう。


6. データサイズに注意する
 バージョンも同じ、テーブル構成も同じ条件でベンチマークをしても、本番環境とかけ離れた結果になることはよくあります。最も大きな要因がデータサイズではないでしょうか。データサイズが実メモリに十分おさまる場合、すべてがメモリアクセスになるため高速になります。MySQL(InnoDB)でも、すべてがインメモリであれば、以下のように主キー検索がローカルアクセスで14万クエリ/秒、リモートアクセスで9万クエリ/秒などといった値をたたき出すことができます(Nehalem 2.8GHz x 16コアでmysqlslapを使用)。
ローカル50接続:
mysqlslap --concurrency=50 --iterations=1 --number-int-cols=2
--number-char-cols=3 --auto-generate-sql --engine=innodb
--auto-generate-sql-add-autoincrement --auto-generate-sql-load-type=key
--auto-generate-sql-write-number=10000 --number-of-queries=1000000
--host=127.0.0.1 --port=3306
Benchmark
Running for engine innodb
Average number of seconds to run all queries: 6.866 seconds
Minimum number of seconds to run all queries: 6.866 seconds
Maximum number of seconds to run all queries: 6.866 seconds
Number of clients running queries: 50
Average number of queries per client: 20000

リモート50接続:
mysqlslap --concurrency=50 --iterations=1 --number-int-cols=2
--number-char-cols=3 --auto-generate-sql --engine=innodb
--auto-generate-sql-add-autoincrement --auto-generate-sql-load-type=key
--auto-generate-sql-write-number=10000 --number-of-queries=1000000
--host=remote --port=3306
Benchmark
Running for engine innodb
Average number of seconds to run all queries: 11.200 seconds
Minimum number of seconds to run all queries: 11.200 seconds
Maximum number of seconds to run all queries: 11.200 seconds
Number of clients running queries: 50
Average number of queries per client: 20000

 しかし、データがメモリにおさまらなくなればディスクアクセスの割合が多くなるため、こんな値を出すことは不可能になります。範囲検索の場合はランダムアクセスの割合も増えるため、ディスクI/Oが多くなりがちで、性能はさらに落ちる傾向にあります。負荷テストをするときに、簡単のためデータ量は1GBで、などとやっていると現実的な結果を得ることは困難です。


7. データのアクセス範囲に注意する
 データサイズを十分に揃えたとしても、それだけでは十分ではありません。アクセス範囲をカバーすることも大切です。例えば、ユーザ数が10万人、同時にアクセスするユーザ数が100人と見積もったとします。ここでよくやってしまうのが、負荷テストに使うユーザ数をあらかじめ決めた100ユーザで固定してしまうことです。これは実際のアクセスパターンとは異なります。実際のアクセスパターンは100ユーザ固定ではなく、さまざまなユーザにまたがることでしょう。
 トータルで10万人のユーザがアクセスしてきた場合、DBのデータはキャッシュに収まりきらず、ディスクアクセスの頻発が予想されます。一方で100ユーザに固定した場合、100ユーザ分のデータしかアクセスされないので、すべてのアクセスがインメモリで行なわれることになるでしょう。つまり、実際に行なわれるであろう処理よりもずっと高速に処理されてしまい、実際よりも良い結果が得られてしまうのです。


8. データの内容に注意する
 多くのWeb アプリケーションでは、傾向の違いはあるとはいえ「頻繁にアクセスしてくるヘビーユーザ」と「ほどほどにアクセスするユーザ」「めったにアクセスしないライトユーザ」がいます。各ユーザによってDB サーバ内でのアクセス傾向は変わってきます。当然ながら、ライトユーザよりもヘビーユーザのほうがアクセス頻度は高くなります。このため、負荷テストを厳密に行なうなら、ユーザIDを均等に割り振っていくのではなく、ヘビーユーザのIDに対して重み付けをすることにも意味があります。
 ヘビーユーザはDBサーバに対する負荷の与え方も変わってきます。例えば、書いた日記の一覧を取り出すために「SELECT 日記ID FROM 日記テーブルWHERE ユーザID=?」というSQL 文を実行したとします。この場合、マッチするレコード数はヘビーユーザほど多くなるでしょう。ここで取得した日記ID を用いて日記の本文を取り出すためには、日記ID の数だけランダムアクセスが必要になります。このため、ヘビーユーザほどランダムアクセスの回数が増える、つまりSQL 文の実行負荷が増えることになります。一方でヘビーユーザに関しては、アクセス回数が多いことからよりキャッシュされやすく、ディスクアクセスの頻度は高くなりにくいというプラス材料も挙げられます。


■データベース設定に関する2つのポイント

9. OS/ミドルウェアのバージョンと設定に注意する
 OSの種類やバージョン、RDBMSのバージョン選定、パラメータ設定なども注意が必要です。これも説明は不要でしょう。
 RDBMSの場合、耐障害性の高い設定にするか、低い設定にするかによっても大きくスループットが変わるので注意が必要です。ベンチマーク結果を見るときは、自分がどういう設定にしようとしているかを踏まえた上で見るようにすべきでしょう。


10. 統計コマンドの使い方と見方を知る
 ボトルネックを特定する上で統計分析ツールは必須です。Linuxならvmstat、iostat、mpstatの読み方は最低限おさえておきたいところです。MySQLではスロークエリログ分析(mysqldumpslow等)、ステータス変数の推移分析(mysqladmin extended-status等)、実行中のクエリ解析(show full processlist等)の使い方と読み方くらいは知っておきたいところです。


 これらのポイントをおさえておくと、DBサーバのベンチマークを見たときに、その妥当性がある程度判断できるようになります。例題として「RDBMSは1レコードのアクセスに数ms程度かかる。一方でTimesTenのようなインメモリデータベースは数マイクロ秒でアクセスできる。だからインメモリデータベースはRDBMSより1000倍速い。これはインメモリデータベースの内部構造がメモリアクセスに最適化されているからだ」という説明があったときに、それをどう解釈するかを考えてみましょう。
 1レコードのアクセスに数msということは、1秒あたり数百回のアクセスしかできないことを意味します。数百回というのはHDD1本レベルの値なので、まったくキャッシュされていない条件での値だと判断できます。一方で数マイクロ秒という値は、リモートアクセス(GbE)では不可能なので、ローカルアクセスをした上での値だと考えられます。つまり、リモートアクセスな上にまったくキャッシュされていない状態のRDBMSと、ローカルアクセスでインメモリなデータベースの性能を比較して、1000倍速いと言っているのだということが分かります。ディスクアクセスとネットワークアクセスが発生する環境とまったく発生しない環境では、1000倍近い差は発生し得ます。決して、データベースの内部構造の違いだけで1000倍の差が発生しているわけではありません。上記のような説明を受けたときに、そのまま信じ込んでしまう人は多いのですが、DBスペシャリストが近くにいれば、説明を受けた瞬間に矛盾に気づくことができるでしょう。
 現実的なアプリケーションでは、それなりの量のメモリを搭載するはずなので、RDBMSはここまで不利な条件にはなりません。そこで、より正しい評価をするためには「メモリにキャッシュされている状態でのRDBMSとの性能比較」であったり、「同一メモリ容量でどれだけ多くのレコードをキャッシュできるか(T-TreeはB+-Treeよりも空間効率に優れる)」といった観点でのベンチマークをするのが妥当と考えられます。リモートアクセス前提であれば、前述のmysqlslapが示すように、InnoDBでも秒間9万クエリ級をさばくことが可能(すべてメモリにキャッシュされている状態での主キー検索の場合)で、これはリモートアクセスした状態のインメモリデータベースと比べても大きく見劣りしないでしょう。
 ベンチマークは悪意を持って行われることはむしろ少数派で、ほとんどの場合は得られた結果をそのまま発信しているだけなので、結果を発信した人を責めるのは良くないと考えています(私自身も間違えることは良くあります)。むしろ積極的な情報交換があった方が良いでしょう。ただ結果の変動要因が非常に多いので、それを読む側の人にもスキルが求められます。ベンチマークは簡単ではないのです。

2009年8月18日火曜日

InnoDBのAUTO_INCREMENTが遅い問題は5.1でどう改善されたのか

 MySQL5.1のGA版が出てから8ヶ月余りが経過しましたが、まだ5.0(あるいはそれ以前)をメインで使っている方も多いのではないでしょうか。5.1の何が良いのかいまいち分からないという方も多いかもしれません。そんな方にとって分かりやすい例の1つが、「5.1でInnoDBのAUTO_INCREMENT性能が大幅に改善された」という点です。私は仕事柄Web系の技術者の方と話をする機会もよくありますが、意外と知られていない改善なので(まさにトラフィックと同時接続数の多いWeb系システムのための改善なのに…)この機会に取り上げることにします。
 簡単に言えば、AUTO_INCREMENTを持つテーブルに対してINSERTをするクライアント数が数十、数百と増えていった時に、従来はスループットが指数関数的に落ちてしまっていたのが、5.1では高速かつ安定するようになりました。以下にmysqlslapのINSERTベンチマークの結果を示します。
mysqlslap  --concurrency=1,5,10,20,30,50,100,200,300,500 \
--iterations=1 --engine=innodb \
--auto-generate-sql --auto-generate-sql-add-autoincrement \
--auto-generate-sql-load-type=write \
--number-of-queries=100000

 MySQLの比較対象バージョンは5.0.83と5.1.37。my.cnfパラメータはWebアプリ向けにしました。log-binでバイナリログを有効化。innodb_flush_log_at_trx_commitは2にしてコミット時同期書き込みではなく「コミット時ファイルシステムキャッシュに書き込み」に変更(1にしていない理由は先週取り上げたグループコミットの性能問題を緩和するため)。max_connectionsを大きめにとってあります。
[mysqld]
innodb_flush_log_at_trx_commit=2
innodb_log_files_in_group=2
innodb_buffer_pool_size=11G
innodb_flush_method=O_DIRECT
innodb_log_file_size=256M
innodb_data_file_path=ibdata1:100M:autoextend
innodb_file_per_table
log-bin
max_connections=1000
table_cache=8192
server-id=1


 グラフの縦軸は計10万件をINSERTするのに要した時間です。見てのとおり、MySQL5.0では接続数の増加に対してINSERT性能が加速度的に低下しています。200接続のときは10万件のINSERTに150秒を要しており、秒間660件程度しかINSERTできていません。ディスクI/Oがボトルネックになっているわけでもないのに、660というのは低すぎる値です。さらに上図のグラフは同時接続数200の時点でグラフが切れていますが、それ以上のときは以下のようなエラーで止まってしまいました。
mysqlslap: Cannot run query INSERT INTO t1 VALUES
(NULL,100669,'qnMdipW5KkXdTjGCh2PNzLoeR0527frpQDQ8uw67Ydk1K06uuNHtkxYBxT5w8plb2Bbp
zhwYBgPNYX9RmICWGkZD6fAESvhMzH3yqzMtXoH4BQNylbK1CmEIPGYlC6')
ERROR : Deadlock found when trying to get lock; try restarting transaction

 メッセージを見るとデッドロックのように見えますが、実際にはもちろん(2つのトランザクションが互いにロック待ちになる)デッドロックではありません。同じロックを待つクライアント数が一定ライン(ソース上の定数LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK:200固定)を超えると、デッドロック扱いにして強制的にロールバックさせる、というInnoDBの実装に起因します。
 一方MySQL5.1では接続数の増加に対してINSERT性能が安定しています。500接続まで試しましたが、どれも10万件のINSERTに5秒前後で終了しており、平均INSERT数は秒間2万前後になっています。現実的な用途ではテーブル/インデックスサイズが大きいことによるディスクI/Oがボトルネックになりがちなので、ここまで速くはなりませんが、それでも5.0に比べれば十分に優れた値と言えるでしょう。
 AUTO_INCREMENTな主キーはInnoDBのクラスタ索引と相性が良い(理由は今後解説)ので、性能問題が改善されたことには大きな意味があります。このAUTO_INCREMENTの動作の変化については、オンラインマニュアルに記載があります。ここでも紹介します。

●AUTO-INCテーブルロック
 AUTO_INCREMENTを持つテーブルに対してINSERTをする場合、そのINSERT文が完了するまでの間、「AUTO-INC」と呼ばれるテーブルロックが確保されるという特徴がありました。テーブルロックを持つ理由は、マスターに対して行った更新結果と、バイナリログに書かれているSQL文を実行した結果(スレーブに反映される)を確実に一致させるためです。例えば、空のテーブルtに対して以下の処理をするケースを考えてみます。
マスター:
1) INSERT INTO t (val) SELECT val FROM t2; (t2は1000レコードあると仮定)
2) INSERT INTO t (id, val) VALUES (null, 'abc'); (1001が割り当てられる)

 この場合、2)のAUTO_INCREMENTに何の値が入るかは、1)で何レコードINSERTされるかが確定しないと分かりません。そのため、1)の完了を待つ必要があります。バイナリログには以下のように記録されます。
SET INSERT_ID=1;
INSERT INTO t (val) SELECT val FROM t2;
SET INSERT_ID=1001;
INSERT INTO t (id, val) VALUES (null, 'abc');

 1)の完了を待ってから2)を実行することで、2)で入るAUTO_INCREMENT値が1001であることが特定できるので、バイナリログには「SET INSERT_ID=1001;」を埋めることができます。この結果、どのスレーブで処理をしても1001がINSERTされ、マスターとスレーブで値を一致させることができます。
 通常のトランザクションのロックは、トランザクションが終了(コミットまたはロールバック)するまで保持されますが、AUTO-INCテーブルロックはINSERT文の終了時点で解放されます。したがってINSERTをした後にロールバックをしても、増えたAUTO_INCREMENT値は元に戻りません。またロックの期間も短くなります。
 5.0までは、AUTO_INCREMENTを持つテーブルに対するINSERT文すべてがAUTO-INCテーブルロックをかけます。AUTO_INCREMENT列に対してNULLを入れると新しい番号が採番されるというのがMySQLの仕様ですが、NULLではなく明示的に値をセットしたときにも、AUTO-INCテーブルロックは確保されます。
 テーブルロックを持つことの最大のデメリットは、同時実行性能が極端に低下してしまうことです。ロックの期間が短いといっても、INSERTが集中すれば大量の待ち行列ができるため上図のようにとんでもなく性能が悪化します。



●5.1ではAUTO-INCテーブルロックではなくmutexになった
 しかし、本来はINSERT文の実行が終わるまで常にテーブルロックを保持しなければならないわけではありません。例えば、以下の2つのINSERTを考えてみます。
3) INSERT INTO t (id, val) VALUES (null, 'abc'); # 3が入った
4) INSERT INTO t (id, val) VALUES (null, 'def'); # 4が入った

 別々のクライアントから3)→4)の順に実行した場合、テーブルロックをINSERT完了まで保持しないと、AUTO_INCREMENT値の採番順序とコミット順序(バイナリログへの記録順序)がずれる可能性が高くなります。しかし、実はずれてしまっても問題は起きません。バイナリログには以下のように、次にどのAUTO_INCREMENT値をセットするかの情報も一緒に記録されるからです。例えば以下のような感じで記録されます。
SET INSERT_ID=4;
INSERT INTO t (id, val) VALUES (null, 'def');
SET INSERT_ID=3;
INSERT INTO t (id, val) VALUES (null, 'abc');

 このため、バイナリログへの記録の順番が逆転しても最終結果は同じになります。そもそも、テーブルロック自体がINSERTの終了時点で解放されてしまうので、以下のような処理をすればMySQL5.0でも逆転現象は起きます。
T1:
BEGIN;
INSERT INTO t VALUES(null, 1);

T2:
BEGIN;
INSERT INTO t VALUES(null, 2);
COMMIT;

T1:
COMMIT;

 この場合、AUTO_INCREMENTの採番はT1の方が先に行われますが、コミットおよびバイナリログへの記録はT2の方が先に行われます。それでも「SET INSERT_ID=N」がバイナリログに一緒に記録されるため、矛盾した結果にはなりません。

 このようなINSERT文では、文の完了までテーブルロックを持たなくても矛盾が無いように処理できます。同じAUTO_INCREMENT値が採番されてはまずいので、採番の部分はテーブルロックをかける必要がありますが、それ以外は不要です。
 つまり、次にどのAUTO_INCREMENT値を割り当てれば良いかが分かれば、INSERT文が終わるまでテーブルロックを保持する必要はありません。別の言い方をすれば、そのINSERT文によって何件INSERTされるかが事前に判断できる場合には、INSERT文が終わるまでテーブルロックを保持する必要は無いということです。3)も4)も、実際にINSERTされるのは多くても1件だということは、INSERT構文から(実際に処理をしなくても)判断できます。そして、世の中で実行される大半のINSERT文はこの形です。そこで5.1では、「何件INSERTされるかをSQL構文から判断できる場合」のAUTO_INCREMENTロックのかけ方が以下のように変更されました。
mutex(table->autoinc_mutex)を取る
AUTO_INCREMENT値を採番する
mutexを解放する

 番号が重複してはいけないので、採番処理自体はクリティカルセクションの中に置く必要がありますが、それ以外は並列で動作できます。INSERT文が完了するまでがクリティカルセクションであった従来と比べると、同時実行性が大幅に高まっています。これが上図のような大きな差が生じた原因です。現在MySQL5.0以下を使っていて、AUTO_INCREMENT性能の遅さに悩まされている方にこそ、ぜひMySQL5.1を試してみてほしいです。

 ただし、いつでもこのような高速な動作ができるわけではありません。これができるのは、事前に何レコード入れるのかが分かっている場合だけです。最初に例として挙げたINSERT ... SELECTやLOAD DATAのように、何レコードがINSERTされるかをSQL文の実行前に判断できない処理については、いくつまでAUTO_INCREMENT番号を付与すれば良いかを特定することができません。こうしたSQL文を実行するときは、AUTO-INCテーブルロックが引き続き確保されます。AUTO-INCテーブルロックが確保されている間は、通常のINSERT文であっても(mutexではなく)AUTO-INCテーブルロックを確保しようとします。このため同時実行性が低下します。
 これを防ぐ手立てとしては、innodb_autoinc_lock_modeというパラメータを2にして(デフォルトは1)、なおかつバイナリログのフォーマットをSQLステートメントベースから行ベースに変える、という方法があります。この場合にはAUTO-INCテーブルロックが確保されなくなります。ただし、SQL文ベースのバイナリログでは整合性を保証できないので行ベースにする必要があること、行ベースにすることでの性能低下があるなどの副作用があることに注意が必要です。ユースケースによっては検討の余地があるでしょう。innodb_autoinc_lock_mode=0は従来型の動作をしますが、これは互換性を保証するためのもの(1や2では、処理内容によってAUTO_INCREMENT番号が飛ぶ場合がある。詳細はマニュアルを参照)で、よほどの理由が無い限り設定する必要はありません。
 もしこのあたりの実装の詳細を知りたい方がいたら、ha_innodb.ccのha_innobase::innobase_lock_autoinc(void)あたりを見てみてください。innodb_autoinc_lock_modeが1のときは条件付でAUTO-INCテーブルロックをかけることなどが分かるでしょう。

2009年8月13日木曜日

DBチューニングではディスクI/O性能を注視する

 DBチューニングにおいて、気を配るべきところは数多くありますが、中でも真っ先に見るべきところはディスクI/Oでしょう。なぜかというと、メモリアクセスに比べてHDDの方が圧倒的に遅く、最もパフォーマンス阻害要因になりやすいためです。ディスクI/Oネックの解決方法を探っていくと、「テーブル/インデックス設計やSQL文の見直し」に行き着くこともまた多いです。これらが不適切だと、結果として大量のレコードをアクセスすることになり、ディスクI/Oが多く発生してしまうためです。根本的な原因はディスクI/Oにあります(CPUネックになることもありますが、その例は別の機会に取り上げます)。
 ディスクI/Oには大きく分けてシーケンシャルアクセスとランダムアクセスの2種類のアクセスパターンがありますが、RDBMSではインデックスアクセスが主体となるため、ルート→ブランチ→リーフ→実レコードという経路でのランダムアクセスが発生します。
 秒間に処理できるI/O数のことをIOPSと呼びます。HDDへのランダムアクセスはシーク待ち時間や回転待ち時間のコストが極めて高く、1回あたり5ms級の時間を要すため、HDD1本あたりのランダムIOPSはせいぜい数百程度にしかなりません。メモリアクセスが数十ナノ秒単位ですから、HDDと比べて数十万倍クラスのアクセス性能差があります。1個のSQL文を処理するという観点からは、1回のメモリアクセス vs 1回のディスクアクセスにはならず、構文解析/実行計画生成/文字列コピーなどさまざまな処理が行なわれるので、格差という意味では数十万倍ほどにはなりません。それでも100倍くらいの差は簡単に発生してしまいます。すべてがインメモリの状態であれば、単純な主キー検索について、秒間数万クエリくらいは処理できます。一方で大半がディスクに向かう場合は、4本くらいのRAID1+0構成であっても毎秒数百クエリ程度にとどまってしまうことは少なくありません。
 仮に、インメモリで完結するSQL文の処理時間が1、ディスクアクセスを伴う場合の処理時間が100としましょう。バッファプールのヒット率が100%の場合は、100個のリクエストを処理するのにかかる延べ時間は1×100で100です。一方ヒット率95%の場合は、1×95+100×5=595となり、6倍くらいかかってしまいます。わずかなヒット率のダウンで、スループットが1/6にまで低下してしまう計算です。ヒット率が80%の場合は、1×80+100×20=2080で、最初の状態の約1/21まで落ちてしまいます。バッファプールにおさまるようにアクセスさせることが重要なことが分かります。実際のRDBMSのパフォーマンスも、これと似たような傾向を示します。データ量の増加に対してスループットがいかに低下していくかという例は、SH2さんという方が過去に行なったInnoDBのベンチマークが分かりやすいです。
グラフ:http://f.hatena.ne.jp/sh2/20090704095053
エントリ:http://d.hatena.ne.jp/sh2/20090705

 テーブルやインデックスのサイズに対して実メモリが十分に大きい場合、あるいはテーブル/インデックスサイズが大きくてもアクセス範囲が限定されているような場合は、すべての処理がキャッシュされるためディスクI/Oはほとんど発生しません。DBチューニングでは、この状態を目指していきます。DBサーバはメモリを増設すればそれでOK、と言う人が多いのはこのためです。それ以外にも、データ型の見直し(文字列→数値など)などによってデータサイズを小さくすることで、同じメモリサイズでもより多くのレコードをキャッシュできるようにすることも非常に効果的です。1個の巨大なテーブルあたり、20-40%程度のデータサイズ縮小ができることは珍しくありません。またIOPSを減らすという観点では、大量レコードをスキャンしないと返せない処理(件数取得とか)において、サマリーテーブルを用意してインデックスからのルックアップ一発で済ましたり、memcachedなどにキャッシュしておいてそもそもDBサーバにアクセスさせない、といった手も有効です。すでにさまざまな手立てを行なっている方が多いと思います。
 インデックス戦略も性能に決定的な影響を与えます。現実的な用途では、ルートとブランチはキャッシュされやすいので、多くてもリーフと実レコードの2回のランダムI/Oが発生すると見て良いでしょう。ただしこれはインデックス経由でレコードを1個だけ取る場合の話です。範囲検索によって10レコードを取る場合は、リーフ1回に対して実レコード10回の、計11回のランダムI/Oが発生し得ます。SQLはシンプルなのになんで遅いの?という質問を受けることがよくありますが、SQLがシンプルかどうかは関係なく、どれだけのI/Oが発生するかがポイントになります。ディスクI/O性能問題を解決する上で、インデックス戦略の見直しは大きなインパクトがあります。このあたりの詳細なテクニックは、今後さまざまな機会で取り上げていきたいと思います。

2009年8月12日水曜日

InnoDB Plugin 1.0.4 - InnoDB史上極めて重要なリリース

 日本時間の今日、InnoDB Pluginの新バージョン1.0.4がリリースされました。このバージョンでは、「バイナリログを有効にするとグループコミットが効かなくなる問題」が修正されています。ほとんどの環境にとって極めて効果の高い修正です。ほかにもI/Oスレッドの多重化(同様のものがMySQL5.4にも搭載)など効果的な修正が行なわれています。
 InnoDB PluginはまだGA(安定版)ではないので、品質面では標準搭載されているInnoDBよりも落ちます。ただしMySQL Enterpriseサブスクリプションを買っている方であれば追加費用無しでInnoDB Pluginのサポートを受けることができるので、お気軽に試してみて頂ければと思います。
 グループコミット問題修復の効果のほどは、一目瞭然なので図を見た方が分かりやすいでしょう。下図は、mysqlslapで、複数のコネクションから並列でINSERTを行なったときの結果です。
mysqlslap  --concurrency=30 --iterations=1 --engine=innodb \
--auto-generate-sql --auto-generate-sql-load-type=write \
--number-of-queries=100000


my.cnfのパラメータは以下のとおり。H/WはIntel Xeon X5560 Nehalem 2.80GHz * 16cores, 12GB RAM, SAS HDDです。innodb_flush_log_at_trx_commit=1でコミット時同期書き込みを有効にして、バイナリログを有効にして、ストレージではバッテリーバックアップつきライトキャッシュを有効にする、というオーソドックスな設定です。
[mysqld]
basedir=/usr/mysql5137
datadir=/data/mysql5137/data
ignore_builtin_innodb
plugin-load=innodb=ha_innodb.so;innodb_trx=ha_innodb.so;
innodb_locks=ha_innodb.so;innodb_lock_waits=ha_innodb.so;
innodb_cmp=ha_innodb.so;innodb_cmp_reset=ha_innodb.so;
innodb_cmpmem=ha_innodb.so;innodb_cmpmem_reset=ha_innodb.so

innodb_log_files_in_group=2
innodb_buffer_pool_size=2G
innodb_flush_method=O_DIRECT
innodb_log_file_size=512M
innodb_data_file_path=ibdata1:500M:autoextend
innodb_file_per_table
log-bin
table_cache=8192

明らかにInnoDB Plugin 1.0.4の方が高速化しています。通常版のInnoDBでは接続数の増加に対してスケールしていませんが、InnoDB Plugin 1.0.4ではスケールしており、30接続では6.1倍もの性能差が出ていることが分かります(innodb_support_xa=1の場合)。innodb_support_xaを0にすることもできますが、後述の理由で一般的には推奨されません。
以下では、このような性能差が出た理由について述べます。

●グループコミットの修正とは何か
 耐障害性を高めるために、トランザクションがコミットされると、その更新情報がREDOログファイルに同期書き込みされます。このことをコミット時同期書き込みと呼びます。ディスクへの同期書き込みはコストの高い処理で、バッテリーバックアップつきライトキャッシュが搭載されていてもせいぜい毎秒10000回程度しかできません。高速なSELECTなら毎秒数万回くらいは実行できますから、fsyncはパフォーマンスの阻害要因になります。
 そこでInnoDBでは、複数のスレッドから同期書き込みが行なわれた場合は、まとめ上げて少数回数の同期書き込みを行なう機能を搭載しています。これをグループコミットと呼びます。グループコミットは、ほとんどのRDBMSが機能として持っています。
 一方MySQL5.0から、2相コミットの機能が搭載されました。2相コミットは、異なるサーバー間のトランザクションの整合性を保つという(あまり使われない)機能がありますが、MySQLでは異なる(トランザクション対応の)ストレージエンジンおよびバイナリログ間でのトランザクションの整合性を保つという上で、インスタンス1個の環境でも意味があります。ほとんどのケースで「バイナリログとInnoDB」の整合性を保つために使われます。
 たとえば、「バイナリログに書き込んだ後、InnoDBログファイルに書き込む前にクラッシュした」という状況が起きたとします。2相コミットでない場合、リカバリするとバイナリログにだけ書かれていてInnoDBには書かれていないデータが存在することになります。それはスレーブにも転送されるので、「スレーブには存在するけどマスターには無いデータがある」という状態になってしまいます。2相コミットを使うと、「InnoDBログファイルにprepareする→バイナリログに書き込む→InnoDBログファイルにcommitする」という動作になります(2相とはprepare、commitの2フェーズという意味)。「バイナリログに書き込んだ後、InnoDBログファイルに書き込む前にクラッシュした」場合には、事前にprepareで書き込んだデータを用いてリカバリできます。また「バイナリログに書き込む前にクラッシュした」場合には、prepareしたものを無かったことにできます。いずれの場合でも、バイナリログとInnoDB間を整合性のある状態にできます。
 InnoDBでの2相コミットの実装ですが、従来はこうなっていました。
mutex(prepare_commit_mutex)確保
prepareのためにInnoDBログに書き、同期書き込みする
バイナリログに書く
commitのためにInnoDBログに書き、同期書き込みする
mutex解放

 mutex内は同時に1個のスレッドしか動くことができないので、従来型のアプローチだと、ファイルへの同期書き込みがシリアライズされます。つまり100個のスレッドが同時にコミットしたら、prepareのために100回のfsync、commitのために100回のfsyncの計200回が呼ばれます。つまりグループコミットが崩れてしまっていたのです。
 一方、InnoDB Plugin 1.0.4ではこうなりました。
prepareのためにInnoDBログに書き、同期書き込みする
mutex確保
バイナリログに書く
commitのためにInnoDBログに書く
mutex解放
InnoDBログを同期書き込みする

 InnoDB Plugin1.0.4のアプローチでは、同期書き込みをクリティカルセクションの外に出しています。そのため、複数のトランザクションからの同期書き込み命令をまとめあげるグループコミットが機能します。これによって、fsyncの実行回数を大幅に削減できます。以下は、fsyncの実行された回数を示す、Innodb_data_fsyncsの推移を示したものです。

 5.1.37+Builtin(support_xa=1)では、同時接続数に関係なく、1トランザクションあたり2回のfsyncが発生しています。innodb_support_xa=0の場合は1トランザクションあたり1回です。いずれも、グループコミットが効いていないことが分かります。最初のグラフと重ね合わせると、どちらも1秒あたりのfsync回数が10,000前後になっていますが、これは一般的なHDD(+ライトキャッシュ)の限界に近い値で、fsyncがボトルネックになっていると考えられる結果です。一方InnoDB Plugin 1.0.4の場合は、接続数の増加に対して劇的にInnodb_data_fsyncsの増加量が減っています。たとえば30接続(innodb_support_xa=1)のときは、10万トランザクションに対して増加量が200251から26092と、87%も減っています。よってグループコミットが効いていることが分かります。
 また、クリティカルセクションの中でバイナリログとInnoDBログの両方に書いている(同期書き込みはしていない)ので、「InnoDBログファイルへの書き込み順と、バイナリログへの書き込み順が一致しなくなる」という問題が起きることはありません。
 Prepareの順序は保証されないではないか、と言われるかもしれませんが、これは問題ありません。障害のタイミングによっては、prepareされたけれどもcommitされていないトランザクションが存在しえますが、これはそのままではアプリケーションからは見えません。MySQLでは、リカバリ時にはバイナリログをまず読んで、トランザクションID(xid)をピックアップします。次にストレージエンジン(InnoDB)を読んで、prepareされているけれどcommitされていないxidを特定します。そうして特定されたxidを、バイナリログへのコミット順にリカバリします。この結果、最終的にはバイナリログの書き込み順序とInnoDBログファイルへの確定(コミット)順序が一致します。
 というわけで、データ整合性の問題を引き起こさずに、スケーラビリティを上げることができました。バイナリログを同期書き込みする(sync-binlog=1)場合は依然として遅くなってしまいますが、デフォルトの0であれば上図のように非常に高速になります。興味のある方は試してみてください。

2009年8月11日火曜日

全テーブルの統計情報をサイズ順に一覧表示する

 MySQLにおいて、テーブルサイズやインデックスサイズ、レコード数、平均レコード長などの統計情報を知る上でshow table statusは定番です。ただ雑多な表示項目も多いので、たくさんのテーブルの統計を見る場合、必要な情報だけを返したいことは多いです。また全テーブルのうち、どのテーブルが一番大きいのかを知りたいとか、サイズが多い順に一覧表示したいとか、一目で分かるような情報がほしいことも多いです。
 こういうときはinformation_schema.tablesを使うと便利です。以下の例では、appデータベースの全テーブルについて、「テーブルサイズ+インデックスサイズ」の大きい順に、ストレージエンジン、レコード数、平均レコード長、テーブルサイズ(MB)、インデックスサイズ(MB)などを返しています。
use app;
select
table_name, engine, table_rows as tbl_rows, avg_row_length as rlen,
floor((data_length+index_length)/1024/1024) as allMB,
floor((data_length)/1024/1024) as dMB,
floor((index_length)/1024/1024) as iMB
from information_schema.tables
where table_schema=database()
order by (data_length+index_length) desc;

+------------+--------+----------+------+-------+------+------+
| table_name | engine | tbl_rows | rlen | allMB | dMB | iMB |
+------------+--------+----------+------+-------+------+------+
| stock | InnoDB | 9999831 | 381 | 3639 | 3639 | 0 |
| order_line | InnoDB | 28493701 | 95 | 2593 | 2593 | 0 |
| customer | InnoDB | 2972004 | 673 | 2130 | 1909 | 221 |
| orders | InnoDB | 3000309 | 60 | 301 | 171 | 129 |
| history | InnoDB | 2997455 | 82 | 236 | 236 | 0 |
| new_order | InnoDB | 905600 | 37 | 48 | 32 | 15 |
| item | InnoDB | 100160 | 110 | 10 | 10 | 0 |
| district | InnoDB | 917 | 178 | 0 | 0 | 0 |
| warehouse | InnoDB | 100 | 163 | 0 | 0 | 0 |
+------------+--------+----------+------+-------+------+------+
9 rows in set (0.84 sec)


 自分は、ここをスタートラインにして、怪しいテーブルに焦点をあててチューニングをすることが多いです。ただしshow table statusと同様、数値項目は概算値なので、毎回微妙に値が変わることに注意してください。

マスターInnoDB、スレーブMyISAMが勧められない理由

 MySQLにおいて、マスターをInnoDBにして、スレーブをMyISAMにすると幸せになれるという主張をよく聞くことがあります。マスターは耐障害性の高いInnoDBにする一方で、スレーブは耐障害性が低くても大丈夫なので、InnoDBのかわりに高速とされるMyISAMを使えば、可用性と性能の両方をバランス良く実現できる、という考えです。
 しかし、多くの場合これで幸せになることはできません。マスターとスレーブでストレージエンジンを合わせた方が無難です。その理由を以下に示します。

●MyISAMはテーブルロックになる
 マスターへの更新結果はバイナリログに更新系SQL文として書かれ、スレーブのI/Oスレッドによってリレーログとして同じフォーマットで記録され、スレーブのSQLスレッドによってその更新系SQL文がそのまま実行されます。この更新系SQL文は、当然ながらスレーブに対して発行されるSELECT文と競合します(5.1のオプションである、行ベースのバイナリログでも同様)。MyISAMにはconcurrent insertという、INSERTと並行でSELECTを処理できる機能がありますが、テーブルが虫食い状態になっていると並行できないなどの制限もあります。またUPDATEとSELECTはばっちり競合してしまいます。
 集計系の処理のために数十分クラスのバッチ系のSELECT文を実行すると、多くの場合レプリケーションも数十分止まってしまいます。InnoDBは行ロックで、かつMVCCの恩恵でSELECTが更新系SQL文によってロックされないため、こうした問題を防げます。

●SQLスレッド停止時の対処が面倒になる
 InnoDBはトランザクション対応で、MyISAMは非対応です。トランザクションのACID特性の1つA特性である、ロールバックができるという機能をMyISAMは持っていません。InnoDBは持っています。この違いがあるため、レプリケーションにおいてSQLスレッドが止まったときにちょっと面倒なことになります。
 以下のようなSQL文をマスターで実行し、バイナリログに記録されたものとします。

1. START TRANSACTION;
2. INSERT
3. INSERT
4. INSERT
5. COMMIT;

 スレーブでも同じSQL文が実行されますが、ここで4でなんらかのエラー(一意制約違反など)が起きてスレーブ(SQLスレッド)が止まったとします。この場合、エラーを修正してSQLスレッドを再開することになりますが、このとき4からではなく、1から再スタートするのです。バイナリログはトランザクションの存在を認識していて、トランザクションの開始時点から再スタートするためです。しかしMyISAMはトランザクション対応ではないので、SQLスレッド停止前の時点ですでに2、3のINSERT結果が確定しています。1から再開すると、2、3はもう一度実行されるため、重複したINSERTが行なわれてしまうことになります。そのため、SET GLOBAL SQL_SLAVE_SKIP_COUNTERによって、4の前(あるいは後)まで明示的に飛ばしてあげる必要があります。

●スレーブ→マスターの昇格が難しくなる
 マスターを1台で運用している場合、マスターの障害時にはスレーブを昇格させる運用を取ることになります。しかしスレーブはMyISAMなので、InnoDBだった場合とは耐障害性や性能面で大きく傾向が変わる(遅くなる)可能性があります。MyISAMはテーブルロック、InnoDBは行ロックということで、多数のクライアントから並列で更新が行なわれるマスターでは、多くの場合InnoDBの方が良い性能が得られます。またInnoDBはクラスタ索引という特徴があるため、更新性能だけでなく、主キー検索についてもInnoDBの方が速くなる傾向にあります。InnoDBは検索性能も含めてすでに十分速いということも付け加えておきます。MySQL5.0以降はInnoDBのCPUスケーラビリティが大きく改善されていますし、MySQL5.4(beta)ではCPUスケーラビリティに加えてI/O性能も大きく改善されています。

 スレーブのストレージエンジンをマスターと分けるとしたら、これらのデメリットを差し置いてもメリットが上回る場合でしょう。例えばマスターをBlackholeにして、マスターの更新性能を上げるという形は良く取られます。またKickfireのようなDWH系に特化したストレージエンジンをスレーブに置いて、分析専用に使うことなども考えられます。

2009年8月8日土曜日

勉強会「MySQL Hackingの手引き」を終えて

 昨日は、グリー勉強会にて「MySQLハッキングの手引き」というテーマで発表をしました。資料とデモに使用したソースコードやビルドスクリプト等はこちらに公開しています(サンプルプログラムのコンパイルにはソースからビルドしたMySQL5.1以降が必要)。声をかけてくださったグリーの一井さんや、会場準備など諸手続きを行なってくださったグリーのスタッフの方々、参加された皆さまありがとうございました。

●参加者数の意外な多さ
 無料の勉強会とはいえ、このようなマニアックなテーマで、60名定員のところに150名を超える応募が来たというのは驚きました。相当数の方が抽選落ちしてしまったのは残念でしたが、評判が良ければ似たようなテーマでのセミナーをまたどこかで行ないたいと考えています。
 自分はMySQLのコンサルティングという、MySQLの使い手としての専門職(パフォーマンスチューニングとか運用管理とか)に従事しています。過去に発信した記事/書籍やセミナー等はほぼ例外なく使い手としての知識と経験をベースにしています。ただ、たまにですが客先等で挙がった要望リストをもとにパッチを作ってコントリビュートしたり、コンサルティング案件としてMySQL本体を拡張(あるいは拡張したコードをレビューしたり)といった開発系の仕事をすることがあります。そうした開発系の話をしたいと思っていたところに、いい機会で声をかけてもらった、というのが経緯です。こうしたテーマでのプレゼンはほとんど経験がないので、デモとスライドは手探り状態で準備したところがありました。60分予定のところが90分かかるなど時間管理もなってない状況でしたが、フィードバックを得て今後の改善につなげていきたいと思います。懇親会での反応を見ると、興味を持っている方が少なからずいるのだろうと感じました。また初対面の方が多く刺激的でした。
 使う側の立場でMySQLを見ると、簡単に使える方が良いに決まっています。ハッキングなどという手間のかかることはやらないに越したことはありません。しかし今回のデモを通じて、「個人的な興味」という枠を超えて、普通に効果的な場面が出てくる可能性があることも感じていただけたのではないかと思います。MySQLはプラグイン化の流れを推し進めており、本体の拡張に比べるとずっと手軽にプラグインを作ることができます。今回来ていた方の中にはC/C++を非常に得意としている方も少なからずいたので、kazuhookuさんのQ4M斯波さんのSpiderのようなゼロからストレージエンジンを作る、とはいわないまでも、実用的なUDFなどは結構出てくるかもしれないな、と期待しています。


●KVSとしてのRDBMSの可能性
 今回の勉強会で、1つの可能性として「Key Value StoreのAPIのような感覚でダイレクトにテーブルにアクセスする」という形を提示しました。1年ほど前にkazuhookuさんがポストした「MySQL (InnoDB) に直接アクセスしてタイムライン処理を高速化する話」をより簡易化したものです。一般化すると、1回のHTTPリクエストの処理にあたり、多数回の(効率の良い)SELECT文を実行しなければ結果を返すことができない、というタイプの処理を、高速化するための実装、ということになるでしょうか。この手の処理は、ストアドプロシージャを使うと、ネットワークアクセス回数を減らせるため高速化が可能です。しかしMySQLはオープンソースな上に、汎用的なストレージエンジンAPIが存在するため、C/C++でストレージエンジンAPIを直接呼び出すようなコードを書いて、それをUDFなどのプラグインから呼ぶ、といったことができます。本来、ストレージエンジンAPIは、それを実装する側(Q4M/Spiderのように、APIを実装する側)のために用意されているのですが、APIを呼ぶロジックの初期化/解放ロジックをきちんと書けば、呼ぶ側のコードを書くことも不可能ではありません。デモで示したパフォーマンス測定の数字を掲載しておきます(レコードはすべてInnoDBバッファプールにキャッシュされている状態。再現手順は資料の方に含まれています)。CPUネックの処理で、Intel Xeon X5560 Nehalem 2.80GHz*16コアのマシンでの結果です。

SQL文:
$ super-smack smack1.smack 20 1
Query Barrel Report for client c1
connect: max=15ms min=3ms avg= 5ms from 20 clients
q_per_s
113701.80
100回で1リクエスト→1137リクエスト/秒

ストアド:
$ super-smack smack1.smack 20 1
Query Barrel Report for client c1
connect: max=9ms min=3ms avg= 4ms from 20 clients
q_per_s
1771.54
1回で1リクエスト→1772リクエスト/秒

UDF:
$ super-smack smack1.smack 20 1
Query Barrel Report for client c1
connect: max=10ms min=3ms avg= 5ms from 20 clients
q_per_s
14983.40
1回で1リクエスト→14983リクエスト/秒


 ストアドよりも8.5倍も高速化したことに驚いた方も多かったのではないでしょうか。MySQLはRDBMSの中でも十分に速いとされていますが、(CPUネックの状況になれば)KVSに比べると圧倒的に遅くなってしまうことをご存知の方も多いでしょう。これは、MySQLが長いSQL文字列の構文解析はもちろんのこと、毎回の実行計画の作成(MySQLは実行計画のキャッシュ機構が無い)や文字列コピーの多用といった、CPUコストの高い処理を結構行なっているということにも大きな要因があります。こうした処理をことごとくすっ飛ばしてダイレクトにストレージエンジンにアクセスすれば、KVSほどではないにしても十分に高速なスループットを出すことができます。もちろん副作用も多く、あまり想定されていない使われ方なので前例がまるで無い(ドキュメントも無いに等しい)とか、ちょっとバグを残してしまったらmysqldごと落ちてしまうかもしれないとか、バージョン間の互換性が低いとか、実際にやってみると苦労が絶えないだろうと思いますが。。
 「RDBMSはSQLがあるから遅い」のであれば、「内部的にはSQLの実行経路を迂回した高速アクセスをすれば良い」というような裏技的な思考は個人的には好きです。今回のデモのように、クライアントから見てアクセス手段が透過的であれば、背後で何が行なわれていようとクライアント側は普通にSQL文(UDF)を呼べばいいだけなので影響がありません。RDBMSにはB+Treeインデックスがありますから、KVSによっては難しいソート済みの結果を返すことも簡単にできます。データ型の機構が強力な点(値チェックができる等)も魅力になるケースがあるでしょう。SQLを標準インターフェイスとしつつ、ごく一部の「非常に高速に処理する必要のあるクエリ」を処理するために使いやすい高速APIを用意し、しかもアプリケーションからはこれらを特別な意識をせずに使い分けることができる(UDFを明示的に呼ぶ程度でOK)、というアーキテクチャは、次の世代のRDBMSの1形態になりうるだろうと考えています。トップページなど大量のアクセスが来るページにおいて、複雑なクエリを実行しないと結果を返せない項目がある、といったケースでは面白いのではないでしょうか。
 あまり見慣れない手法でもあるので、いろいろな方と意見交換したいと思っています。ごく最近Twitterも始めましたので、気軽にフォローしてくださればと思います。

2009年8月6日木曜日

日本語でのBlogを開始します

日本語でのブログを始めることにしました。

主に、書籍や雑誌連載などのサポート情報(補完的な情報や、質問への回答など)の発信や、セミナーや勉強会での発表内容の振り返りなどをしていきたいと考えています。職務上海外出張が多いので、そうした話もしていきたいと思います。

私は、ソフトウェア業界でのキャリアの多くをデータベース技術者として過ごしてきました。オープンソースの世界(MySQL/PostgreSQL)に足を踏み入れたのは2004年頃からで、その前はOracleを主に使っていました。とはいえ、自分がこれまで出してきた書籍等はすべてオープンソース(MySQL)絡みのものなので、MySQL技術者としての印象が強いかもしれません。オープンソースの世界は、技術志向の人を魅了するものがあると思います。自分もその1人でした。その影響でMySQLの開発元であるMySQL AB、Sun Microsystemsに移ってからは、利用者側であるユーザー企業の立場とはまた違う観点からビジネス面のことなどを検討してきました。オープンソース・ソフトウェアは浸透してきているとはいえ、そのビジネスモデルなど、まだ十分に理解されているとは言いがたい状況だと思います(ひとりでにソフトウェアができあがってくると考えている方はいまだに多いのではないでしょうか)。5年余りにおいて、ユーザー企業とベンダー企業の両方でオープンソース・ソフトウェアを使ってきた自分の知見のいくつかを、このブログで適当に書き留めていきたいと考えています。