MySQL | SQLite経験者向け、30日で習得するMySQL:実務応用 - Day28 障害対応

SQL MySQL
スポンサーリンク

Day28 後半のゴール

「“起きたときにどう動くか・起きにくくするにはどう設計するか”を持つ」

前半で、データ破損とロック問題の「正体」はだいぶ見えてきました。
後半では、それを一歩進めて「実際にどう考え、どう動くか」「そもそも起きにくくするにはどう設計するか」を、初心者向けにかみ砕いていきます。

ここでのゴールは次のイメージです。
ロックが怪しいときに、まず確認するべきポイントを言葉で説明できる。
デッドロックを減らすための「設計・クエリの書き方」の基本パターンを理解する。
データ破損が疑われたときに、「絶対にやってはいけないこと」と「まず考えるべきこと」を区別できる。


ロックが怪しいときの考え方

「“本当にロックなのか”を切り分ける」

アプリから見ると、「なんか遅い」「タイムアウトする」という現象は、ロック以外でも起きます。
ネットワークが遅い、CPUが詰まっている、インデックスがなくて単にクエリが重い、などです。

ロックが怪しいかどうかを考えるとき、まず見るべきポイントは次のようなものです。

特定のテーブルや特定の操作(例:注文更新)だけが遅いか。
ピーク時間帯だけ発生するか、それとも常に発生するか。
たまに「Deadlock found」や「Lock wait timeout exceeded」のようなエラーがアプリログに出ていないか。

ここで「デッドロック」「ロックタイムアウト」のエラーが出ているなら、ロック絡みの可能性はかなり高いです。
逆に、そういったエラーが一切なく、単に「いつも遅い」だけなら、まずはインデックスやクエリの見直しを疑う方が自然です。

「長時間トランザクション」がいないかを疑う

ロック問題の典型的な原因の一つが、「長時間トランザクション」です。
トランザクションを開始したまま、なかなか COMMIT / ROLLBACK されないと、その間ずっとロックが残り続けます。

アプリ側のコードで、次のようなパターンがないかを意識してみてください。

トランザクションを開始してから、ユーザー入力待ちの処理を挟んでいる。
トランザクション内で、外部API呼び出しなど時間のかかる処理をしている。
例外が起きたときに、ROLLBACK を呼ばずに接続を放置している。

トランザクションの中には、「DBに関係ない時間のかかる処理」を入れない。
エラー時には必ず ROLLBACK する。
この二つだけでも、ロック問題のリスクはかなり下がります。


デッドロックを減らす設計・クエリの書き方

「“同じ順番で同じものをロックする”を徹底する」

デッドロックの本質は、「Aは1→2の順でロック、Bは2→1の順でロック」というように、ロックの順番がバラバラなことです。
これを避けるための基本ルールは、とてもシンプルです。

同じ種類のリソースを複数ロックするなら、必ず同じ順番でロックする。

例えば、「ユーザーIDの小さい方から先にロックする」と決めてしまうイメージです。

ユーザーID a と b を同時に更新する必要があるとき、
常に「min(a, b) → max(a, b)」の順で SELECT … FOR UPDATE する。

こうしておけば、AもBも同じ順番でロックを取りに行くので、「お互い逆順で待ち合う」状況が起きにくくなります。

「必要以上に広い範囲をロックしない」

もう一つのポイントは、「ロックの範囲を必要最小限にする」ことです。

WHERE 条件が曖昧で、インデックスもない UPDATE を投げると、
大量の行にロックがかかり、他のトランザクションを巻き込んでしまいます。

例えば、次のようなクエリは危険です。

UPDATE orders
SET status = 'canceled'
WHERE updated_at < NOW() - INTERVAL 30 DAY;
SQL

インデックスがなければ、全件をなめながらロックしていきます。
これをピーク時間帯に実行すると、他の処理が巻き込まれてロック待ち地獄になります。

対策としては、次のような考え方があります。

バッチ処理は小さな単位に分割して実行する(例:1回に1000件ずつ)。
WHERE 条件にインデックスを効かせる(updated_at にインデックスを張るなど)。
ピーク時間帯を避けて実行する。

ロックは「安全装置」ですが、範囲が広すぎると「通行止め」になってしまう、というイメージを持っておくとよいです。


ロックタイムアウトが起きたときのアプリ側の振る舞い

「“即エラー終了”か“リトライ”かを決めておく」

ロックタイムアウトが発生した場合、アプリ側はどうするかをあらかじめ決めておく必要があります。

ユーザーの操作として「もう一度やり直してもらえばいい」ものなら、
エラーメッセージを返して「時間をおいて再度お試しください」と伝える、という選択もあります。

一方で、「どうしても完了させたいバッチ処理」などでは、
アプリ側で自動的にリトライする戦略も考えられます。

ただし、リトライ戦略を取るときは、次の点に注意が必要です。

同じトランザクションをそのまま再実行しても問題ないように、処理を「冪等(べきとう)」にしておく。
無限リトライにならないように、回数や時間に上限を設ける。

「ロックタイムアウトが起きること自体は、完全には避けられない。起きたときにどう振る舞うかも設計の一部」という感覚を持っておくと、現場で慌てにくくなります。


データ破損が疑われたときの基本姿勢

「“その場で直そうとしない”が最重要」

データ破損は、ロック問題よりも一段重い障害です。
ここで一番大事なのは、「その場で直そうとしない」ことです。

具体的には、次のような行動は危険です。

壊れているテーブルに対して、よく分からないまま REPAIR TABLE を実行する。
バックアップも取らずに、テーブルを DROP して作り直そうとする。
本番環境で直接 UPDATE / DELETE を打ちながら「様子を見る」。

こういった操作は、「状況をさらに悪化させる」リスクが高いです。

まずやるべきことは、次の二つです。

現状のバックアップを必ず取る(たとえ壊れていても、その状態を保存する)。
ログやエラーメッセージを記録し、「いつから・どのテーブルで・どんなエラーが出ているか」を整理する。

そのうえで、テスト環境や別のサーバーにバックアップを復元し、
そこで REPAIR やテーブル再構築などの操作を試す、という流れが安全です。

「バックアップから戻せるかどうか」が最終ライン

データ破損の最終的な対処は、「バックアップからの復旧」です。
だからこそ、Day22 でやったような定期バックアップと、
「実際に復元してみるテスト」が重要になります。

もし「バックアップがない」「バックアップも壊れている」という状況だと、
できることはかなり限られてきます。

障害対応の観点から言うと、

バックアップがあるかどうか
バックアップから戻せるかどうか

が、「どこまで戦えるか」のラインになります。
これは、技術というより「運用の覚悟」の話でもあります。


「障害対応のための設計」を普段から仕込んでおく

「ログ・監査・権限・バックアップは全部つながっている」

Day26〜Day28でやってきた内容は、実は全部つながっています。

ログ設計
→ 障害が起きたときに「何が起きたか」を追うための証拠。

監査ログ
→ 不正操作や誤操作が原因だった場合に、「誰が何をしたか」を特定するための情報。

権限管理
→ そもそも「危険な操作」をできる人を最小限に絞ることで、障害の発生確率を下げる。

バックアップ
→ データ破損や誤削除が起きたときに、「戻れる場所」を用意しておく。

障害対応は、「何かあったときに頑張る」だけでは足りません。
「何かあったときに頑張れるように、普段から仕込んでおく」設計と運用がセットで必要です。

SQLite から MySQL にステップアップするということは、
「高機能になる代わりに、守るべき範囲も広がる」ということでもあります。
Day28 まで来たあなたなら、その重さもちゃんと受け止められるはずです。


Day28 後半のまとめ

ロック問題に対しては、まず「本当にロックが原因か」をアプリログや現象から切り分けたうえで、長時間トランザクション(トランザクション内にユーザー待ちや外部API呼び出しを入れない、エラー時は必ずROLLBACKする)を避け、複数行を更新するときは「常に同じ順番でロックする」「インデックスを効かせて必要最小限の行だけロックする」「重いバッチは小分けにしてピークを避ける」といった設計・クエリの書き方でデッドロックやロックタイムアウトの発生を減らし、発生した場合は「即エラー終了」か「制限付きリトライ」のどちらで振る舞うかをあらかじめ決めておく。
データ破損が疑われる場合は、その場でREPAIRやDROP/CREATEを試すのではなく、「まず現状のバックアップを取る」「いつからどのテーブルでどんなエラーが出ているかを整理する」「別環境で復旧手順を試す」という順番を守り、最終的には「バックアップから戻せるかどうか」が勝負になることを理解しつつ、平常時からログ・監査・権限・バックアップを設計しておくことで、「障害が起きても慌てずに戦えるMySQL運用」に近づいていく。

タイトルとURLをコピーしました