VOYAGE GROUP エンジニアブログ

voyagegroup_techのブログ
VOYAGE GROUPエンジニアブログです。

2010年06月

MySQL InnoDBでのネクストキーロックの落とし穴

はじめまして、株式会社ECナビ システム本部 情報システムグループの三浦と申します。

私は主にデータベースの運用、管理を行っています。

ECナビでは様々なサービスを展開しています。そしてそれと同じ数と言っても良い程のデータベースが稼動しています。

リレーショナルデータベースがメインでサービスを支えていますが、それを補う形でキーバリューストア的なデータベースも多数存在しています。

メインで活躍しているリレーショナルデータベースは用途によりOracle、MySQL、Netezza等と多岐に渡っています。

今回はMySQL InnoDBで実装されているネクストキーロックの落とし穴をデッドロックと絡めて説明したいと思います。

評価環境のMySQLのバージョンは5.1.39、トランザクション分離レベルはデフォルトのREPEATABLE READ、InnoDB Pluginは未導入にて今回は行いました。

下記のテーブル、データにて実施します。

mysql> SHOW CREATE TABLE HOGE\G;
*************************** 1. row ***************************
       Table: HOGE
Create Table: CREATE TABLE `HOGE` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `COL_INT` int(11) NOT NULL,
  `COL_CHAR` char(1) NOT NULL,
  PRIMARY KEY (`ID`),
  KEY `COL_INT` (`COL_INT`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> SELECT * FROM HOGE;
+----+---------+----------+
| ID | COL_INT | COL_CHAR |
+----+---------+----------+
|  1 |      35 | A        |
|  2 |      40 | B        |
|  3 |      20 | C        |
|  4 |      25 | D        |
|  5 |      50 | A        |
|  6 |      10 | B        |
|  7 |      45 | C        |
|  8 |      15 | D        |
|  9 |      30 | A        |
+----+---------+----------+
9 rows in set (0.00 sec)

テストケース1

テーブル「HOGE」に存在するレコードをDELETEした後に新規レコードをテーブル「HOGE」に INSERT

トランザクションAトランザクションB
mysql> DELETE FROM HOGE WHERE COL_INT = 25;
Query OK, 1 row affected (0.00 sec)
 
 mysql> DELETE FROM HOGE WHERE COL_INT = 45;
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO HOGE (COL_INT,COL_CHAR) VALUES ( 44,'B');
WAIT状態
 
 mysql> INSERT INTO HOGE (COL_INT,COL_CHAR) VALUES ( 29,'C');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
トランザクションBがエラーとなったことを受け
Query OK, 1 row affected (7.51 sec)
 

テストケース2

テーブル「HOGE」に存在しないレコードをDELETEした後に新規レコードをテーブル「HOGE」に INSERT

トランザクションAトランザクションB
mysql> DELETE FROM HOGE WHERE COL_INT = 28;
Query OK, 1 row affected (0.00 sec)
 
 mysql> DELETE FROM HOGE WHERE COL_INT = 43;
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO HOGE (COL_INT,COL_CHAR) VALUES ( 44,'B');
WAIT状態
 
 mysql> INSERT INTO HOGE (COL_INT,COL_CHAR) VALUES ( 29,'C');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
トランザクションBがエラーとなったことを受け
Query OK, 1 row affected (11.76 sec)
 

テストケース3

テストケース1と同じくテーブル「HOGE」に存在するレコードをDELETEした後に新規レコードをテーブル「HOGE」に INSERT

但し、COL_INTを非UNIQUEからUNIQUEに変更

mysql> SHOW CREATE TABLE HOGE\G;
*************************** 1. row ***************************
       Table: HOGE
Create Table: CREATE TABLE `HOGE` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `COL_INT` int(11) NOT NULL,
  `COL_CHAR` char(1) NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `COL_INT` (`COL_INT`) ★UNIQUEに変更
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
トランザクションAトランザクションB
mysql> DELETE FROM HOGE WHERE COL_INT = 25;
Query OK, 1 row affected (0.00 sec)
 
 mysql> DELETE FROM HOGE WHERE COL_INT = 45;
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO HOGE (COL_INT,COL_CHAR) VALUES ( 44,'B');
Query OK, 1 row affected (0.00 sec)
 
 mysql> INSERT INTO HOGE (COL_INT,COL_CHAR) VALUES ( 29,'C');
Query OK, 1 row affected (0.00 sec)
エラーとならず正常終了エラーとならず正常終了

まとめ

非UNIQUE、UNIQUE INDEXの違いだけでも動きが違いました。

また、トランザクション分離レベルがREAD COMMITTEDの場合ですとテストケース1、2共にトランザクション間で干渉は起こらず正常終了します。

このようにINDEXの種類やトランザクション分離レベル、今回は実施していませんがデータの並び等のその他の要素によっても動きは違ってきます。

今回のような単一行単位でのDELETE&INSERTのようなアプリケーションはどこにでもあると思います。

まずは複数のDML(Data Manipulation Language)でトランザクションを構成する場合にこういう動きがあることを念頭に置くことが大事だと思います。

重たい処理を華麗にスルーして、アプリケーションの体感速度をぐっと向上させる方法

こんにちは。adingoでプログラマをしている真幡です。

アプリケーションの評価指標の一つにレスポンス性能があります。どれほど素晴らしいアプリケーションでも、レスポンスを返すまでに時間がかかるシステム(=重いシステム)は敬遠されがちです。今回はGearmanというジョブキュー管理ソフトウェアを使い、ウェブアプリケーションのレスポンス性能を向上させる方法を紹介します。

ジョブキューとは何か

ジョブキューとはジョブをキューで管理するものです。これでは説明になっていませんね。キューとはFIFO(First In First Out)を実現するデータ構造です。キューに登録されたモノは、キューに登録した順に処理されます。ジョブキューにおいては、キューに登録するモノはジョブなので、キューに登録した順にジョブが処理されることになります。ジョブキューに登録するジョブの粒度は大小を問いません。したがって、ユーザから重たいジョブを処理するようにリクエストされたときに、ジョブを逐次的に処理せず、ジョブキューに登録することで「ジョブの受付をしました」とユーザにレスポンスを返すことができます。「リクエストを受け付ける=>リクエストを処理する=>レスポンスを返す」という従来の流れから「リクエストを受け付ける=>(とりあえず)レスポンスを返す=>リクエストを処理する」という流れを実現することができます。レスポンスの早さはユーザ体験の向上につながるので、これはうまいやり方であると言えます。

次の図はジョブキューの全体像です。

ジョブキュー

ジョブサーバーが持つキューに注目してください。ジョブサーバーから出ている矢印を見ると、ジョブをキューの上側から詰めていることが分かります。一方、ジョブキューから出ている矢印を見ると、ジョブをキューの下側から取っていることが分かります。先ほどの「キューに登録した順にジョブが処理される」という説明を図で表現すると、このようになります。

より具体的なケースとして、ブログサービスにおける「ブログ記事の投稿」について考えてみましょう。このブログサービスでは、記事が投稿されると次のような処理を行うとします。

  • 記事内容をデータベースに追加
  • 添付画像を(モバイル用の)サムネイル画像に変換
  • RSSファイルの更新
  • 記事投稿の完了通知をメールで送信
この一連の処理において、添付画像の処理が重たい処理であると仮定します(例えば5秒程度の処理時間がかかるとします)。この場合、とりあえず記事内容をデータベースに追加し、それから先の処理をジョブキューに登録し、先にユーザにレスポンスを返すという実装方法が考えられます。こうすることで、ユーザのブラウザを5秒程度フリーズさせる部分を解消することができます。この代償として、添付画像の反映、RSSファイルの更新、メールの送信が5秒程度遅れることになりますが、サービスの性質を考えると、それほど大きな問題ではないと思います。

ところで、同じようなことをcronを使って実現することも可能です。短い間隔でcronを実行し、その中で添付画像の処理、RSSファイルの更新、メールの送信をすれば同様の結果を得られます。では、cronではなくジョブキューを使うメリットは何でしょうか。後述しますが、ジョブキューではワーカーと呼ばれるプロセスが常駐することで、プロセス起動のコストを削減することができます。cronが起動するプロセスがライブラリなどを大量に読み込む必要がある場合、プロセス起動のコストは高くつきます。そのような場合は、cronで都度プロセスを立ち上げるより、ジョブキューの常駐プロセス(ワーカー)を使う方が適切でしょう。

以上のことをまとめると、次のような処理を行うケースではジョブキューを使うメリットがあると言えます。
  • 即時性を求められない処理
  • プロセス起動のコストが高い処理
ジョブキューを効果的に使うことによって、アプリケーションの体感速度を向上し、システムの負荷を下げることも可能であると納得できたでしょうか。以下では、Gearmanというジョブキュー管理システムの使い方について説明します。

Gearmanのインストール

Gearmanのインストール方法は次の通りです。インストール環境は「MacOSX Snow Leopard」を想定しています。

$ sudo port selfupdate
$ sudo port install gearmand
$ sudo pecl install gearman channel://pecl.php.net/gearman-0.7.0

最後に、php.iniに"extension=gearman.so"を追記してください。

Gearmanの構成要素

Gearmanの構成要素には次の3種類があります。

  • ワーカー(またはコンシューマー): 要求に応答する役割
  • クライアント(またはプロデューサー): 要求を生成する役割
  • ジョブサーバー(またはエージェント): クライアントとワーカーの仲介を行う役割

Gearmanデーモンの起動

gearmand(Gearmanデーモン)を起動します。次のようにコマンドを打ちます(前述のようにGearmanをインストールした場合)。

$ /opt/local/sbin/gearmand -d

"-d"オプションはバックグラウンドで起動するためのオプションです。次のようにgearmandプロセスが残れば問題ありません。

$ ps xu | grep gearmand | grep -v "grep"
y-mahata 1314 0.0 0.0 2435700 320 ?? Ss 1:35PM 0:00.00 /opt/local/sbin/gearmand -d

クライアントとワーカーを動かす

ワーカーとしてworker.phpを用意します。

<?php
// worker.php
// ワーカー(コンシューマー)
$worker = new GearmanWorker();
$worker->addServer();
$worker->addFunction('log', 'my_log');
while ($worker->work());

function my_log($job)
{
sleep(60);
file_put_contents('/tmp/foo/' . $job->workload(), $job->workload());
}

ワーカーをバックグラウンドで動作させるため、実行するときには"&"を付けます。

$ php worker.php &

worker.phpについて説明します。これは、GearmanWorkerオブジェクトを生成し、addServer関数でジョブサーバーを追加します。引数なしでaddServer関数を呼ぶと127.0.0.1がジョブサーバーとして追加されます。その後、ワーカーとして提供する関数をaddFunction関数で追加します。この例では、60秒のスリープの後に/tmp/foo以下にログを吐き出すmy_log関数を"log"という名前で(クライアントに)提供します。

次に、クライアントとしてclient.phpを用意します。

<?php
// client.php
// クライアント(プロデューサー)
$client = new GearmanClient();
$client->addServer();
for ($i = 0; $i < 10; $i++) {
// $client->do('log', $i);
$client->doBackground('log', $i);
}

こちらは、何も考えずに実行することができます。

$ php client.php

client.phpについて説明します。これは、GearmanClientオブジェクトを生成し、addServer関数でジョブサーバーを追加します。GearmanWorkerオブジェクトと同様に、引数なしでaddServer関数を呼ぶと127.0.0.1がジョブサーバーとして追加されます。GearmanClientオブジェクトのdoBackground関数は、ジョブキューにジョブを追加し、すぐに終了します。この関数は第一引数にワーカーの関数名を、第二引数にワークロードを取ります。ワークロードとは、ワーカー側のworkload関数で取り出すことのできる値です。do関数はdoBackground関数と同様の引数を取りますが、結果を受け取るまで待ちに入る点が異なります。この他にも、GearmanClientオブジェクトには、ジョブを追加し最後にまとめて実行する関数なども存在します。詳細についてはPHPマニュアルのGearmanの項を参照してください。

client.phpを実行した後、/tmp/fooディレクトリを見ると次のようになります。

$ ls -l /tmp/foo/
total 80
drwxr-xr-x 12 y-mahata wheel 408 6 7 17:05 ./
drwxrwxrwt 22 root wheel 748 6 7 17:05 ../
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:56 0
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:57 1
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:58 2
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:59 3
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:00 4
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:01 5
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:02 6
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:03 7
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:04 8
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:05 9

client.phpはすぐに終了したはずですが、ファイル生成はきっかり60秒間隔で行なわれたことが確認できます。

注意するべきポイント

doBackground関数やdo関数の第二引数であるワークロードは文字列を受け取ります。したがって、文字列以外のデータ(例えば配列など)をワーカーに渡すときにはシリアライズをする必要があります。また、doBackground関数やdo関数は第三引数としてID(となる文字列)を渡すことができます。この値を指定すると、同一のIDで登録したジョブは一度しか実行されないので、注意してください。

一歩進んだトピック

Gearmanはデフォルトでメモリ上でキューを管理します。もしキューの管理をMySQLやDrizzleで行いたければlibdrizzleを使うことができます。また、ジョブキュー管理ソフトウェアはGearmanの他にも存在します。TheSchwartzQ4Mなどと比較することで、Gearmanの長所と短所をより理解することができるかもしれません。

参考資料

カンファレンスに行こう(その1)

こんにちは、ECナビの開発・運営に携わっております小芝と言います。
昨今、特にIT業界では勉強会ブームです。私も勉強会に出たり、お話したり、開催したりと活動しています。今回は勉強会のでかい版ともいえるカンファレンスの参加について、これまでに得たノウハウをまとめました。

カンファレンスに参加していますか?

ECナビのメンバー(社内ではクルーと言います)は結構頻繁にカンファレンスに参加しています。参加だけではなく、出展、スポンサード、講演、主催など様々な関わり方をしております。
私も、東京Ruby会議01東京Ruby会議03DevLOVE2009FUSION日本Ruby会議2009日本Ruby会議2010などのカンファレンスに参加したり、講演やLTをしたり、実行委員をしたりと、結構カンファレンス充させてもらってます。

しかし社外の知人らを見るに見るにまだまだ参加する習慣が無い方が多いのではないでしょうか。

これは大変勿体無い!

日本でのカンファレンス参加の金銭的負担は比較的軽いというメリットがあります。(開催側は大変ですが)
プログラミング言語Rubyやアジャイルな開発手法に関する活動で有名なプログラマーの方が講演の中で「日本では水と安全とカンファ レンスはタダだと思われている」(DevLOVE2009FUSION講演ビデオの2/6の4:35あたり)という発言が、端的に表された現状かと思います。

カンファレンスに参加することによって、知識や事例、ノウハウなどを集中して知ることができることはもちろん、普段の仕事ではなかなか得られない新鮮な刺激を得ることができ、中長期的なメリットを享受することができます。
より多くの人がより多くの人がカンファレンスに行けるようになればと思うので、カンファレンス参加のコツをまとめました。

じゃあ、どうすればカンファレンスに参加できるのか

以下の4つを行うことで、カンファレンスに参加することができます。
  1. 調べる
  2. 申し込む
  3. 選ぶ
  4. 交渉する

1.調べる

そもそもどのようなカンファレンスがあるのかを調べましょう。カンファレンスによく行く人に尋ねてみる、Googleで検索するなどで簡単に調べられます。
IT勉強会カレンダーにも載っているものが色々あるので、参照してみてください。

2.申し込む

たいていはネット申し込みが可能です。
これは! と思うカンファレンスを見つけたらポチっと参加申し込みをしましょう。
申し込みが始まっていないなら、申し込み開始日を忘れずにスケジュール登録しておきましょう。人気カンファレンスは数日で埋まってし まうことも多々あります。

3.選ぶ

カンファレンスで行われるセッションの中でどれを行こうか予め考えておきましょう。
ここでよくあるアンチパターンは、「受講セッションの詰め混み過ぎ」です。朝から晩まですべてのセッションの枠を埋めてしまうと、ただただしんどいばかりで効果的な参加とはなりません。
自分の中でテーマを絞って、是が非でも出たいセッションとそうではないセッションを区別しておきましょう。

更には、是非ともセッションに出ずに廊下やロビーで過ごす時間を意図的に設定しておきましょう。そういう場所で偶然出会う 突発的な催し物や他の参加者との交流も、カンファレンスの大きな楽しみです。

4.交渉する

会社勤めの方は、努めて業務の一環としてカンファレンスに参加できるように上司と交渉しましょう。
こうすると、会社へのカンファレンス参加レポートが必要となるので、自分は何を得て何を持ち帰りたいかを明確にすることができます。
また、業務でのカンファレンス参加の習慣が定着し、他の勉強会や研修にも参加しやすくなる環境を作ることができます。

なお、ECナビではキャリアアップ支援制度という、カンファレンスや研修など自分のスキルアップのための参加費や研修費を全額支給する枠組みがあり、積極的に活用することが推奨されています。
またそれとは別に、海外カンファレンス参加制度があり、国内カンファレンスのみならず海外カンファレンスへ参加についても支援を行う枠組みがあります。

もちろん、カンファレンスに参加する分の業務の調整も忘れずに行いましょう。とはいえ、リリース直前など何かの締め切り間際でなければ、カンファレンスで1日や2日抜けたところで体勢に影響はありません。
先の業務の予定が決まっていないのであればあればなお好都合です。自分でスケジュールを立てて決定事項としてカンファレンスの予定を入れておきましょう。

次回予告

以上4つを行うことでもうカンファレンスに参加する準備は整いました!
次回はカンファレンスに出てからどういう風に立ち回ればいいかをまとめます。 

お知らせ

ECナビグループのPeXが、世界最大級のプログラム言語Rubyのカンファレンスである日本Ruby会議2010ゴールドスポンサーとして支援しております。
2010-06-15 19:00(JST)からチケットの一般販売が開始されますので、Rubyを使っている方はもとより、ちょっとでもRubyにご興味のある方は是非ご参加下さい。

理系インターン募集!!

こんちには、 システム本部の吉村と申します。
ECナビでは理系インターンを毎年夏に開催しており、 今回で5回目となります。
開催期間は、2010/8/23(月)から2010/9/17(金)までです。

詳細は、ECナビ/理系インターンシップ-Treasure をご覧下さい。

ECナビ エンジニアブログの執筆者たちも、 もちろんインターンのサポートを行ないます。
参加者はカリキュラムに沿った講義・実習を行なうだけではなく、
現場の技術者に話を聞いたり質問できます(カリキュラムと関係ないことでもOKです)。
成長したいあなたのエントリーをお待ちしております。

blktraceによるI/Oトレース

こんにちは. adingoで検索周りの仕事をしているHと申します. 検索エンジンやデータベースなどのディスクI/Oが多いアプリケーションでは, いかにI/Oを抑えるかがパフォーマンスの決め手になります. 今回はアプリケーションが出したI/Oリクエスト一つ一つがどのようにデバイスドライバに渡るかをトレースするblktraceというツールを簡単に紹介します. blktraceはlinux kernel 2.6におけるツールとなっており, 2.6.17-rc1以降ではカーネルにパッチを当てることなく利用可能です. (ツール自体はインストールする必要があります.)

何がトレースされる?

linuxカーネルではI/Oリクエストは多数のレイヤーを通じて実際にデバイスに要求が出されます. ざっくり書くと, アプリケーションから発行されたI/Oリクエストは以下のようなパスを通りデバイスへと辿りつきます. ([...] はカーネルの範囲を表しています.)

アプリケーション -> [ファイルシステム -> ページキャッシュ -> ブロックI/Oレイヤ -> デバイスドライバ] -> デバイス
           

blktraceがトレースするのは, "ブロックI/Oレイヤ"の部分になります. ブロックI/Oレイヤでは, I/OスケジューラによりI/Oリクエストの並び換えや連接ブロックへのI/Oリクエストのマージ等が行われているため, ブロックI/Oレイヤの入口と出口ではI/Oリクエストの順番が異なります. つまり, blktraceはブロックI/Oレイヤの入口と出口, そして内部でのI/Oリクエストの状態をトレースすることができるツールとなります.

インストール

通常は, ディストリビューションのパッケージ管理システムからインストールが可能だと思います. 私が試した範囲では, CentOS(yum), Debian(apt), openSUSE(yast) では可能でした. 一応, CentOS(yum)での例を載せておきます.

% sudo yum install blktrace
            

使い方

私も使い方が詳しい訳ではないので, 一番基本的な使い方のみ紹介します. とりあえずこれだけでも何とかなると思います.

% blktrace -d /dev/sda -o - | blkparse -i > trace.out
            

blktraceは実際にトレースを行うプログラムで, blkparseが見やすく整形してくれるツールとなります. 上のコマンドでは監視するデバイス(上では/dev/sda)とトレースログの出力先(上ではtrace.out)を指定すれば良いことになります. これを実行し, 別のシェルなどでI/Oを発行するプログラムを実行すると, そのプログラムのI/Oをトレースすることができます. 実際には, カーネルやその他の動作中のアプリケーションすべてのI/Oが捕捉されてしまうため, 注意が必要です. (また, ファイルにトレース結果を書き込んでしまうため, その分のI/Oも捕捉されてしまいます. blktraceのオプションで別のマシンにネットワーク経由で転送するモードもあるので, それにより回避できます.)

以下に, 自分のsortした時の出力の一部を載せておきます.

8,0    5        1     0.073660305 21678  A   R 280968474 + 32 <- (8,3) 263971704
8,1    5        2     0.073660507 21678  Q   R 280968474 + 32 [zsh]
8,1    5        3     0.073662783 21678  G   R 280968474 + 32 [zsh]
8,1    5        4     0.073664230 21678  P   N [zsh]
8,1    5        5     0.073665153 21678  I   R 280968474 + 32 [zsh]
8,1    5        6     0.073666419 21678  U   N [zsh] 1
8,1    5        7     0.073667496 21678  D   R 280968474 + 32 [zsh]
8,0    5        8     0.078350794 21678  A   R 280968506 + 8 <- (8,3) 263971736
8,1    5        9     0.078350962 21678  Q   R 280968506 + 8 [zsh]
8,1    5       10     0.078351973 21678  G   R 280968506 + 8 [zsh]
8,1    5       11     0.078352571 21678  P   N [zsh]
8,1    5       12     0.078353001 21678  I   R 280968506 + 8 [zsh]
            

結果は左から, 1:デバイス番号, 2:CPU, 3:シーケンス番号, 4:タイムスタンプ, 5:PID, 6:イベント名, 7:リクエストタイプ, 8:開始ブロック(セクタ番号(LBA)) + ブロック(セクタ)数, 9:プロセス名 となります. どのプロセス(5)のどのブロック(8)のI/Oリクエストが, どの状態であるか(6)を見ることができます. イベント名には, I(I/Oスケジューラのキューに挿入された), D(ブロックI/Oレイヤからデバイスドライバに送られた), C(I/Oが完了した)などがあります. 詳しくはマニュアルをご確認ください.

まとめ

linux kernel 2.6から導入されたI/Oトレースのツールを紹介しました. このツールの出力結果を解析して, カーネルのI/Oスケジューリングアルゴリズムを修正する人は少ないと思いますが, 既存スケジューリングアルゴリズムやカーネルのパラメータを調整する場合などは一つの指標になると思います. また, 内部の動作を見てみるのは単純に面白いものです. blktrace自体ができることや, 詳しい使い方は以下のマニュアルやプレゼン資料を見てください.

記事検索
QRコード
QRコード