MySQLのクラッシュバグにご用心

ちょっと間があいてしまいましたが、今回はMySQLのお話です。

現象

2カ月ほど調子よく動いていたMySQLがある日突然ダウンしてしまいました。
MySQLのエラーログを見ると、下記のアサーションエラーが記録されており、どうやらMySQLがクラッシュしてしまったようです。

110831 13:12:39
InnoDB: Assertion failure in thread 1043281332 in file row/row0sel.c line 4473
InnoDB: Failing assertion: trx->isolation_level == TRX_ISO_READ_UNCOMMITTED
InnoDB: We intentionally generate a memory trap.
InnoDB: Submit a detailed bug report to http://bugs.mysql.com.
InnoDB: If you get repeated assertion failures or crashes, even
InnoDB: immediately after the mysqld startup, there may be
InnoDB: corruption in the InnoDB tablespace. Please refer to
InnoDB: http://dev.mysql.com/doc/refman/5.1/en/forcing-innodb-recovery.html
InnoDB: about forcing recovery.
110831 13:12:39 - mysqld got signal 6 ;
This could be because you hit a bug. It is also possible that this binary
or one of the libraries it was linked against is corrupt, improperly built,
or misconfigured. This error can also be caused by malfunctioning hardware.
We will try our best to scrape up some info that will hopefully help diagnose
the problem, but since we have already crashed, something is definitely wrong
and this may fail.

ご丁寧にエラーログで"you hit a bug"とバグを踏んだことを教えてくれます:-)
上記エラーメッセージの内容でGoogle先生に問い合わせると、MySQLのバグ情報が引っかかります。
http://bugs.mysql.com/bug.php?id=62037
これを見ると、どうもBLOB型カラムへのINSERT (または UPDATE) とSELECT文の並行アクセス時のレースコンディションにバグがあるようです。なお、BLOB型以外にTEXT型でもこの問題は起きるようです(実際、私が遭遇した時のデータはTEXT型でした)
また、MySQLのバージョン5.1.52, 5.1.58, 5.5.14で影響がある問題のようです(おそらくこれより下位のバージョンでも問題があります)。結構新しいバージョンでも問題があるので、既に運用している方は要注意と思われます。

さらに恐ろしいことに、運が悪いとデータが破壊されその部分をSELECTすると必ずクラッシュする状態にもなります。バグ情報のコメントで下記のように記載されています。

Yes Mark, your table could be corrupted so that a BLOB pointer is full of zero bytes (sort
of a null pointer). This could be a transient or a permanent condition. The condition
should be transient when another transaction is trying to access an off-page column just
after an UPDATE transaction has done the first mtr_commit() and is about to x-latch the
clustered index page so that it can write the off-page columns.

If you get a crash and crash recovery at an unfortunate moment, you will have permanent
corruption.

今回、不運にもあるレコードがこの状態になり、アプリケーションからSELECTされると毎回クラッシュするという恐ろしい状態になってしまいました。

解決方法

そもそもの原因はMySQLのバグです。なのでMySQLのバージョンアップで解決することができます。5.1系の場合は5.1.59以上、5.5系の場合は5.5.15以上に変更すれば良いでしょう。

不運にもpermanent corruptionになった場合の復旧方法

もしこの状態に陥った場合は、バージョンアップだけでは解決できません。バージョンアップ後も問題のレコードをSELECTするとやはりクラッシュしてしまいます。DELETEも試してみましたが同様にクラッシュしてしまいました。

じゃあどうやって復旧するんだい?というところですが、これはもう力技で解決するしかありません。大まかな復旧手順としては以下になります。

  1. 破損レコードを頑張って特定する
  2. 破損レコードを除外してダンプを出力する
  3. テーブルを新規に作成する
  4. 新規作成したテーブルに出力したダンプを流し込む
  5. 古いテーブルと新しいテーブル名を入れ替える

これはかなり地道な作業になります。まず、破損レコードをどうやって探すかってところです。

  • 最初の取っ掛かりとしては、クラッシュ時のMySQLのエラーログに記載されているSQLを確認しましょう。そのSQLがもし主キーまたはユニークキーでアクセスしていればラッキーです。恐らくそのレコードが破損しているでしょう。
  • 最悪なのが全件とかレンジ検索をしている場合。これは残念ながら特定はできません。レンジの幅を変えたSQLを別途流して、クラッシュする/しないを確認して範囲を狭め、特定する必要があります。
  • また、他にも破損している可能性は否定できないので、BLOBやTEXT型を使用している全テーブルについて同様にクラッシュする/しないを確認すべきです。これは、例えば以下のように検査対象のテーブル全件をSELECTし、/dev/nullにでも流してやればOKです。なお、--quickオプションを付けたのは、特にBLOBデータがあるとmysqlクライアントがメモリを大量に使ってしまうためです。
mysql -e --quick "SELECT * FROM 検査テーブル" > /dev/null

ダンプして再投入するというところから、サービス停止は必要になるでしょう。既に運用中のシステムとしては、一番これが痛いところですね。

まとめ

  • MySQL 5.1.58、5.5.14以下を使用し、かつ、BLOBやTEXT型を使用している人はバージョンアップをオススメします。クラッシュ地獄は本当ツライですよ〜
  • 運悪くレコードが破損してしまった場合は、地道に破損レコードを特定することになります。復旧に時間がかかりますが頑張りましょう。