Day28 前半のゴール
「“DBでトラブルが起きたときの見方”を持てるようになる」
ここからは、ちょっと怖いテーマです。
「障害対応」――データ破損やロック問題の話です。
前半のゴールはこうです。
データ破損とは何か、アプリのバグとどう違うのかをイメージできる。
ロック・デッドロック・タイムアウトの違いを言葉で説明できる。
「おかしいときに、どこを疑うか」の視点を持てる。
まだコマンドや詳細な手順には踏み込まず、「現象の正体」をかみ砕いていきます。
データ破損とは何か
「“アプリのバグ”ではなく“DBの中身そのものがおかしい”状態」
まず、「データ破損」という言葉を整理します。
アプリのバグで「間違った値がINSERTされた」「UPDATEの条件を間違えた」
これは“論理的な間違い”であって、厳密には「データ破損」とは別物です。
ここで言うデータ破損は、もっと低いレイヤーの話です。
テーブルやインデックスの内部構造が壊れていて、
本来あるはずの行が読めない、
あるいは、読もうとするとエラーになる。
たとえば、こんな状態です。
あるテーブルを SELECT しようとすると、毎回エラーになる。
インデックスを使うとエラーになるが、フルスキャンなら読める。
mysqld が「このテーブルは壊れている」とログに出してくる。
これは、アプリのSQLが悪いというより、
ストレージ・ファイル・ハードウェア・MySQL自体の問題に近い領域です。
SQLite だと「1ファイルが壊れるかどうか」という世界でしたが、
MySQL ではテーブルごと・インデックスごとに壊れる可能性があります。
データ破損が起きると何が怖いか
「“正しいはずのデータが信用できなくなる”という恐怖」
データ破損が厄介なのは、「どこまで信用していいか」が分からなくなることです。
一部の行だけ読めないのか。
インデックスだけ壊れていて、データ本体は無事なのか。
テーブル全体が壊れているのか。
さらに怖いのは、「壊れていることに気づかないまま運用してしまう」ケースです。
バックアップを取っているつもりでも、
すでに壊れた状態をそのままバックアップしているかもしれない。
だからこそ、Day22 でやった「定期バックアップ」とセットで、
「バックアップから復元できるかをたまに検証する」ことが大事になります。
前半では、「データ破損=アプリのバグとは別のレイヤーの問題で、発生したらバックアップ・復旧の話になる」という位置づけだけ、しっかり押さえておいてください。
ロックとは何か
「“同時に触ると壊れるから、順番待ちさせる仕組み”」
次に、ロックの話に移ります。
これは、SQLite経験者なら「トランザクション中は他の書き込みが待たされる」感覚があるはずです。
ロックは一言で言うと、
同時に書き換えられると困るから、
一時的に「ここ触っちゃダメ」と印を付ける仕組み
です。
MySQL(InnoDB)では、主に次のようなロックがあります。
テーブルロック(テーブル全体にかかる)
行ロック(特定の行にかかる)
例えば、あるユーザーの残高を更新する処理があったとします。
同じユーザーIDに対して、同時に2つのUPDATEが走ると、
どちらが先か・どの値が最終的に残るかが分からなくなります。
そこで、最初のUPDATEがその行にロックをかけ、
終わるまで他のUPDATEを待たせる、という動きになります。
これは「壊れないようにするための安全装置」なので、
ロックがあること自体は悪ではありません。
ロックが問題になるのはどんなときか
「“待ちが長すぎる”か“お互い待ち合って進まない”とき」
ロックが正常に働いているときは、
一瞬だけ待ってすぐ終わるので、ユーザーは気づきません。
問題になるのは、次のようなケースです。
ロック待ちが長く続き、アプリから見ると「応答が返ってこない」状態になる。
複数のトランザクションが互いにロックを取り合って、誰も先に進めない(デッドロック)。
前者は「ロック待ちが長すぎる問題」、
後者は「デッドロック」と呼ばれます。
どちらも、アプリ側から見ると、
クエリがやたら遅い
タイムアウトエラーになる
たまに「Deadlock found」といったエラーが返ってくる
といった形で現れます。
デッドロックとは何か
「“お互いに相手のロック解除を待っている”行き詰まり」
デッドロックは、ロック問題の中でも典型的なパターンです。
シンプルな例でイメージしてみましょう。
トランザクションA:ユーザー1の行をロック → 次にユーザー2の行をロックしようとする。
トランザクションB:ユーザー2の行をロック → 次にユーザー1の行をロックしようとする。
このとき、
Aは「ユーザー2のロックが空くのを待っている」。
Bは「ユーザー1のロックが空くのを待っている」。
つまり、お互いに相手が終わるのを待っていて、
どちらも先に進めない状態になります。
これがデッドロックです。
MySQLは、デッドロックを検出すると、
どちらか一方のトランザクションを強制的にロールバックして、
「Deadlock found; try restarting transaction」というエラーを返します。
アプリ側は、「デッドロックが起きたら、そのトランザクションをやり直す」という戦略を取るのが基本です。
ロックタイムアウトとは何か
「“一定時間待ってもロックが空かないので諦める”」
もう一つ、ロック周りでよく出てくるのが「ロックタイムアウト」です。
これは、
あるトランザクションが、別のトランザクションのロックが外れるのを待っている。
しかし、一定時間(設定されたタイムアウト値)待っても外れない。
「もう待てない」と判断してエラーにする。
という現象です。
デッドロックは「お互い待ち合っている」特殊なパターンですが、
ロックタイムアウトは「一方的に長く待たされている」パターンです。
アプリ側から見ると、
たまに UPDATE がタイムアウトエラーになる
ピーク時間帯だけ特定のクエリがよく失敗する
といった形で現れます。
原因としては、
長時間トランザクション(コミットされないまま放置されている)
重いバッチ処理が大量の行をロックしている
インデックスが悪くて、必要以上に多くの行をロックしている
などが考えられます。
SQLite と MySQL のロックの違いをざっくり押さえる
「“ファイル全体”から“行単位”へ」
SQLite経験者向けなので、ここは少し比較しておきます。
SQLite
基本的に「データベースファイル全体」に対してロックがかかるイメージ。
同時書き込みにはあまり強くない。
MySQL(InnoDB)
行ロックが基本で、「必要な行だけ」をロックできる。
同時書き込みに強く、トランザクションもリッチ。
この違いのおかげで、MySQLは高い同時実行性を持てますが、
その分、「どのクエリがどの行をロックしているか」を意識する必要が出てきます。
Day28 前半では、
ロックは“壊れないようにするための安全装置”
でも、設計やクエリの書き方が悪いと“詰まり”の原因になる
という感覚を持ってもらえれば十分です。
障害対応で一番大事なのは「慌てて触りまくらない」こと
「“まず現状を観察する”というマインドセット」
最後に、技術というより姿勢の話を一つだけ。
データ破損やロック問題が起きたとき、
人はどうしても「とりあえず何かしなきゃ」と焦ります。
しかし、障害対応で一番やってはいけないのは、
原因も分からないまま、
設定を変えたり、サービスを再起動したり、
本番データを直接いじり始めること
です。
前半の段階で覚えておいてほしいのは、
まず「現象」を正確に言葉にする
(どのクエリが遅いのか、いつからか、再現性はあるか)
ログやメトリクスを見て、「どのレイヤーの問題か」を切り分ける
(アプリか、DBか、ネットワークか、ストレージか)
という“観察と切り分け”の姿勢です。
後半では、
MySQL側でロック状況を確認するイメージ
デッドロックを減らすための設計・クエリの書き方
データ破損が疑われるときの「バックアップ前提」の考え方
など、もう一歩踏み込んだ話をしていきます。
Day28 前半のまとめ
データ破損は「アプリのバグで間違った値が入った」というレベルではなく、テーブルやインデックスの内部構造が壊れて「正しいはずのデータが読めない・読もうとするとエラーになる」といった、ストレージ寄りのトラブルであり、発生した場合はバックアップや復旧の話になる“重めの障害”だと理解しておく。
一方、ロックは「同時に書き換えられると壊れるので、一時的に順番待ちさせる安全装置」で、行ロックを基本とするMySQLでは同時実行性が高い代わりに、ロック待ちが長引く・デッドロック(互いに待ち合って進めない)・ロックタイムアウト(一定時間待っても空かないので諦める)といった問題が起きうるため、「どのクエリがどの行をロックしているか」を意識しつつ、障害時にはまず現象を言語化し、どのレイヤーの問題かを落ち着いて切り分ける姿勢が重要になる。
