この記事の対象読者
- 本番環境でDB障害に遭遇して焦った経験がある人
- 「ロック待ちでタイムアウト」のエラーを見たことがある人
- 「Too many connections」で冷や汗をかいたことがある人
- 誤ってDELETE文を実行してしまったことがある(または怖い)人
この記事では、ロック関連の障害調査、コネクション枯渇の対応、データ誤削除からの復旧まで、実務で遭遇するデータベース障害の対応方法を体系的に解説します。
障害対応の心構え
まず落ち着く
障害発生時、最初にやることは 深呼吸 です。
❌ やってはいけないこと
- 焦ってよく分からないコマンドを打つ
- 原因特定前に再起動する
- ログを見ずに「たぶんこれだろう」で対応する
✅ やるべきこと
- 現状を把握する(何が起きているか)
- 影響範囲を確認する(どこまで影響しているか)
- 記録を残す(後で振り返れるように)
障害対応の基本フロー
flowchart TD
Start([🚨 障害発生])
Detect[1️⃣ 検知<br/>アラート・ユーザー報告]
Check[2️⃣ 状況確認<br/>ログ・メトリクス・エラー]
Impact[3️⃣ 影響範囲<br/>サービス・ユーザー数]
Emergency{深刻度は?}
Critical[🔴 Critical<br/>即座に応急処置]
High[🟡 High<br/>計画的に対応]
FirstAid[4️⃣ 応急処置<br/>被害拡大を防ぐ]
Investigate[5️⃣ 原因特定<br/>根本原因を調査]
Recovery[6️⃣ 復旧<br/>サービスを正常化]
Verify{正常化確認}
PostMortem[7️⃣ 振り返り<br/>再発防止策検討]
Document[📝 ドキュメント化<br/>ナレッジ蓄積]
End([✅ 完了])
Start --> Detect
Detect --> Check
Check --> Impact
Impact --> Emergency
Emergency -->|Critical<br/>サービス停止| Critical
Emergency -->|High<br/>一部影響| High
Critical --> FirstAid
High --> Investigate
FirstAid --> Investigate
Investigate --> Recovery
Recovery --> Verify
Verify -->|まだ異常| Investigate
Verify -->|正常化| PostMortem
PostMortem --> Document
Document --> End
style Start fill:#f44336,color:#fff
style Critical fill:#ff9800,color:#fff
style Recovery fill:#4caf50,color:#fff
style End fill:#2196f3,color:#fff
style Document fill:#9c27b0,color:#fff
重要ポイント:
- 深呼吸してから行動 — 焦りは禁物
- 記録を残す — 後で振り返れるように
- 影響範囲を把握 — 優先順位を決める
第1部:ロック関連の障害
ロックとは何か
基本概念
ロック は、複数のトランザクションが同時に同じデータを変更することを防ぐ仕組みです。
-- トランザクションA
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- ここでロックを取得(id=1の行)
-- ...処理中...
COMMIT;
-- ロック解放
-- トランザクションB(同時実行)
BEGIN;
UPDATE accounts SET balance = balance + 1000 WHERE id = 1;
-- Aがロックを持っているので待機...
-- Aがコミットするまで待つ
ロックの種類(MySQL InnoDB)
graph TB
subgraph Locks["ロックの種類と用途"]
S["共有ロック(S)<br/>SELECT ... FOR SHARE<br/>🔵 読み取り専用"]
X["排他ロック(X)<br/>UPDATE/DELETE/INSERT<br/>🔴 書き込み専用"]
IS["インテンション共有(IS)<br/>テーブルレベル<br/>行ロックの予告"]
IX["インテンション排他(IX)<br/>テーブルレベル<br/>行ロックの予告"]
Gap["ギャップロック<br/>範囲の隙間<br/>🚫 INSERT防止"]
NK["ネクストキーロック<br/>行+ギャップ<br/>ファントムリード防止"]
end
subgraph Compat["ロック競合マトリックス"]
direction LR
M1["🟢 互換性あり<br/>(両方取得可)"]
M2["🔴 競合<br/>(待機が発生)"]
end
subgraph Matrix["主要な競合パターン"]
C1["S ⇔ S: 🟢 OK<br/>複数読み取り可能"]
C2["S ⇔ X: 🔴 競合<br/>読み書き排他"]
C3["X ⇔ X: 🔴 競合<br/>書き込み排他"]
C4["IS ⇔ IX: 🟢 OK<br/>意思表示のみ"]
end
style S fill:#2196f3,color:#fff
style X fill:#f44336,color:#fff
style IS fill:#e3f2fd
style IX fill:#ffebee
style Gap fill:#fff3e0
style NK fill:#e8f5e9
style M1 fill:#4caf50,color:#fff
style M2 fill:#ff9800,color:#fff
ロック競合の完全マトリックス:
| S(共有) | X(排他) | IS | IX | |
|---|---|---|---|---|
| S | 🟢 OK | 🔴 競合 | 🟢 OK | 🔴 競合 |
| X | 🔴 競合 | 🔴 競合 | 🔴 競合 | 🔴 競合 |
| IS | 🟢 OK | 🔴 競合 | 🟢 OK | 🟢 OK |
| IX | 🔴 競合 | 🔴 競合 | 🟢 OK | 🟢 OK |
実務での使い分け:
- SELECT … FOR SHARE → 共有ロック(複数読み可)
- SELECT … FOR UPDATE → 排他ロック(完全排他)
- UPDATE/DELETE → 自動で排他ロック
- REPEATABLE READ → ギャップロック・ネクストキーロック自動適用
デッドロック
デッドロックとは
デッドロック は、2つ以上のトランザクションが互いのロック解放を待ち合う状態です。
graph TB
subgraph T0["時刻 T0: 両方のトランザクション開始"]
A1["Transaction A<br/>行1をロック"]
B1["Transaction B<br/>行2をロック"]
end
subgraph T1["時刻 T1: 次の行をロックしようとする"]
A2["Transaction A<br/>行2をロック要求<br/>(Bが保持中)"]
B2["Transaction B<br/>行1をロック要求<br/>(Aが保持中)"]
end
subgraph Deadlock["デッドロック発生"]
D["互いのロック解放を<br/>待ち合う状態"]
end
subgraph Resolution["MySQL自動検出と解決"]
R1["Transaction Aを<br/>ロールバック"]
R2["Transaction Bが<br/>行1のロックを取得"]
R3["Transaction B<br/>処理完了"]
end
A1 --> A2
B1 --> B2
A2 --> D
B2 --> D
D --> R1
R1 --> R2
R2 --> R3
style A1 fill:#e3f2fd
style A2 fill:#ffebee
style B1 fill:#e8f5e9
style B2 fill:#ffebee
style D fill:#f44336,color:#fff
style R1 fill:#ff9800,color:#fff
style R2 fill:#4caf50,color:#fff
style R3 fill:#4caf50,color:#fff
デッドロック発生の条件(4つ全て満たす):
- 相互排除 — リソースが排他的に使われる
- 保持と待機 — ロックを保持しながら別のロックを待つ
- 非プリエンプション — ロックは自発的にしか解放されない
- 循環待機 — ロック待ちが循環する(A→B→A)
デッドロックの検出
MySQLはデッドロックを自動検出し、片方のトランザクションをロールバック します。
-- エラーメッセージ
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction
デッドロックの調査(MySQL)
-- 最新のデッドロック情報を表示
SHOW ENGINE INNODB STATUS\G
出力の LATEST DETECTED DEADLOCK セクションを確認:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-12-13 10:30:45 0x7f1234567890
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 100, OS thread handle 123456, query id 5000 localhost app
UPDATE accounts SET balance = balance - 1000 WHERE id = 1
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 50 page no 3 n bits 72 index PRIMARY of table `mydb`.`accounts`
trx id 12345 lock_mode X locks rec but not gap
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 4 n bits 72 index PRIMARY of table `mydb`.`accounts`
trx id 12345 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 3 sec starting index read
...
*** WE ROLL BACK TRANSACTION (2)
デッドロックの分析ポイント
flowchart TD
Start([🚨 デッドロック発生<br/>ERROR 1213])
Check1[📊 SHOW ENGINE<br/>INNODB STATUS]
Section[LATEST DETECTED<br/>DEADLOCK セクション確認]
Q1{デッドロックの<br/>詳細を確認}
Thread["1️⃣ トランザクション特定<br/>MySQL thread id<br/>query内容"]
Holds["2️⃣ 保持ロック確認<br/>HOLDS THE LOCK(S)<br/>どのテーブル・行"]
Waiting["3️⃣ 待機ロック確認<br/>WAITING FOR THIS LOCK<br/>何を待っているか"]
Rollback["4️⃣ ロールバック確認<br/>WE ROLL BACK TRANSACTION<br/>どちらが犠牲に"]
Analyze{原因分析}
Cause1["🔍 ロック順序不一致<br/>A: 行1→行2<br/>B: 行2→行1"]
Cause2["🔍 長時間トランザクション<br/>ロック保持時間が長い"]
Cause3["🔍 インデックス欠如<br/>不要な範囲ロック"]
Solution["💡 解決策適用"]
Fix1["✅ ロック順序統一<br/>常に小さいID順"]
Fix2["✅ トランザクション短縮<br/>外部APIは外で"]
Fix3["✅ インデックス追加<br/>必要な行だけロック"]
Fix4["✅ リトライ機構<br/>exponential backoff"]
Monitor["📈 監視設定<br/>デッドロック検知"]
Start --> Check1
Check1 --> Section
Section --> Q1
Q1 --> Thread
Q1 --> Holds
Q1 --> Waiting
Q1 --> Rollback
Thread --> Analyze
Holds --> Analyze
Waiting --> Analyze
Rollback --> Analyze
Analyze --> Cause1
Analyze --> Cause2
Analyze --> Cause3
Cause1 --> Fix1
Cause2 --> Fix2
Cause3 --> Fix3
Fix1 --> Fix4
Fix2 --> Fix4
Fix3 --> Fix4
Fix4 --> Solution
Solution --> Monitor
style Start fill:#f44336,color:#fff
style Check1 fill:#2196f3,color:#fff
style Solution fill:#4caf50,color:#fff
style Monitor fill:#9c27b0,color:#fff
style Cause1 fill:#ff9800,color:#fff
style Cause2 fill:#ff9800,color:#fff
style Cause3 fill:#ff9800,color:#fff
分析の4つのステップ:
-
どのトランザクションが関与しているか → MySQL thread id, query を確認
-
どのロックを持っているか → HOLDS THE LOCK(S) を確認
-
どのロックを待っているか → WAITING FOR THIS LOCK を確認
-
どちらがロールバックされたか → WE ROLL BACK TRANSACTION を確認
デッドロックの解決策
1. ロック順序を統一する
# 悪い例:順序がバラバラ
def transfer_bad(from_id, to_id, amount):
# トランザクションAは1→2の順
# トランザクションBは2→1の順
# → デッドロックの可能性
update_account(from_id, -amount)
update_account(to_id, +amount)
# 良い例:常に小さいIDから処理
def transfer_good(from_id, to_id, amount):
ids = sorted([from_id, to_id])
if from_id == ids[0]:
update_account(ids[0], -amount)
update_account(ids[1], +amount)
else:
update_account(ids[0], +amount)
update_account(ids[1], -amount)
2. トランザクションを短くする
# 悪い例:トランザクション内で外部API呼び出し
def process_order_bad():
with db.transaction():
order = create_order()
payment = call_payment_api() # ← 遅い!ロック長時間保持
update_order(order, payment)
# 良い例:外部呼び出しはトランザクション外
def process_order_good():
order = create_order_draft()
payment = call_payment_api() # トランザクション外
with db.transaction():
finalize_order(order, payment) # 最小限の処理
3. リトライ機構を実装する
import time
from mysql.connector import errors
def execute_with_retry(func, max_retries=3):
for attempt in range(max_retries):
try:
return func()
except errors.DatabaseError as e:
if e.errno == 1213: # Deadlock
if attempt < max_retries - 1:
time.sleep(0.1 * (2 ** attempt)) # 指数バックオフ
continue
raise
ロック待ちタイムアウト
症状
ERROR 1205 (HY000): Lock wait timeout exceeded;
try restarting transaction
デッドロックではないが、ロック待ちが長すぎてタイムアウトした状態です。
ロック待ちの調査(MySQL)
-- 現在のロック待ち状況を確認
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM
information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
MySQL 8.0以降:
-- performance_schemaを使用
SELECT
waiting.THREAD_ID AS waiting_thread,
waiting.EVENT_NAME AS waiting_event,
waiting.OBJECT_NAME AS waiting_table,
blocking.THREAD_ID AS blocking_thread,
blocking.EVENT_NAME AS blocking_event
FROM
performance_schema.data_lock_waits w
JOIN performance_schema.data_locks waiting
ON w.REQUESTING_ENGINE_LOCK_ID = waiting.ENGINE_LOCK_ID
JOIN performance_schema.data_locks blocking
ON w.BLOCKING_ENGINE_LOCK_ID = blocking.ENGINE_LOCK_ID;
実行中のトランザクション一覧
-- 全トランザクションを表示
SELECT
trx_id,
trx_state,
trx_started,
trx_mysql_thread_id,
trx_query,
trx_rows_locked,
trx_rows_modified
FROM
information_schema.innodb_trx
ORDER BY
trx_started;
プロセスリストの確認
-- 全接続を表示
SHOW FULL PROCESSLIST;
-- または
SELECT
id,
user,
host,
db,
command,
time,
state,
info
FROM
information_schema.processlist
WHERE
command != 'Sleep'
ORDER BY
time DESC;
長時間ロックを持つトランザクションを特定
-- 60秒以上実行中のトランザクション
SELECT
trx_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
trx_mysql_thread_id,
trx_query
FROM
information_schema.innodb_trx
WHERE
TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY
trx_started;
ブロッキングトランザクションの強制終了
-- 問題のスレッドを特定したら
KILL [thread_id];
-- 例
KILL 12345;
注意: KILLはトランザクションをロールバックします。影響を理解してから実行してください。
ロック関連の調査(PostgreSQL)
現在のロック状況
-- ロック待ちを確認
SELECT
blocked.pid AS blocked_pid,
blocked.usename AS blocked_user,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.usename AS blocking_user,
blocking.query AS blocking_query
FROM
pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked
ON blocked.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking
ON blocking.pid = blocking_locks.pid
WHERE
NOT blocked_locks.granted;
長時間実行中のクエリ
SELECT
pid,
usename,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM
pg_stat_activity
WHERE
(now() - pg_stat_activity.query_start) > interval '5 minutes'
AND state != 'idle'
ORDER BY
duration DESC;
トランザクションの強制終了
-- 穏やかに終了(SIGTERM)
SELECT pg_cancel_backend(pid);
-- 強制終了(SIGKILL)
SELECT pg_terminate_backend(pid);
ロック問題の予防
1. 適切なインデックス
-- インデックスがないと全行スキャン→全行ロック
-- 悪い例
UPDATE orders SET status = 'shipped' WHERE customer_id = 123;
-- customer_idにインデックスがないと全行ロック
-- 良い例:インデックスを作成
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
2. SELECT … FOR UPDATE の使い方
-- 明示的にロックを取得
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- NOWAIT:ロックが取れなければ即エラー
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- SKIP LOCKED:ロック中の行をスキップ(キュー処理に便利)
SELECT * FROM tasks WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1;
3. トランザクション分離レベル
-- 現在の分離レベルを確認
SELECT @@transaction_isolation; -- MySQL
SHOW transaction_isolation; -- PostgreSQL
-- 分離レベルを設定
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
| 分離レベル | ロックの強さ | 用途 |
|---|---|---|
| READ UNCOMMITTED | 最も弱い | ほぼ使わない |
| READ COMMITTED | 弱い | 一般的なWebアプリ |
| REPEATABLE READ | 中程度 | MySQLデフォルト |
| SERIALIZABLE | 最も強い | 厳密な整合性が必要 |
第2部:コネクション枯渇
症状
# MySQL
ERROR 1040 (HY000): Too many connections
# PostgreSQL
FATAL: sorry, too many clients already
現状確認(MySQL)
-- 最大接続数
SHOW VARIABLES LIKE 'max_connections';
-- 現在の接続数
SHOW STATUS LIKE 'Threads_connected';
-- 接続の詳細
SELECT
user,
host,
db,
command,
time,
state
FROM
information_schema.processlist
ORDER BY
time DESC;
-- ユーザーごとの接続数
SELECT
user,
COUNT(*) AS connections
FROM
information_schema.processlist
GROUP BY
user
ORDER BY
connections DESC;
現状確認(PostgreSQL)
-- 最大接続数
SHOW max_connections;
-- 現在の接続数
SELECT count(*) FROM pg_stat_activity;
-- 接続の詳細
SELECT
pid,
usename,
application_name,
client_addr,
state,
query_start,
query
FROM
pg_stat_activity
ORDER BY
query_start;
-- 状態ごとの接続数
SELECT
state,
COUNT(*)
FROM
pg_stat_activity
GROUP BY
state;
コネクション枯渇の原因と対策
graph TB
subgraph Problem["❌ コネクション枯渇の3大原因"]
P1["1️⃣ コネクションリーク<br/>conn.close() を忘れる<br/>with文を使わない"]
P2["2️⃣ プール設定ミス<br/>pool_size が小さすぎる<br/>max_overflow が0"]
P3["3️⃣ 長時間保持<br/>Sleep状態が多い<br/>wait_timeout が長すぎる"]
end
subgraph Symptom["🚨 症状"]
S1["ERROR 1040<br/>Too many connections"]
S2["Threads_connected<br/>= max_connections"]
S3["アプリケーション<br/>タイムアウト"]
end
subgraph Diagnosis["🔍 診断コマンド"]
D1["SHOW STATUS<br/>Threads_connected"]
D2["SHOW PROCESSLIST<br/>Sleep多数を確認"]
D3["ユーザーごと集計<br/>GROUP BY user"]
end
subgraph Emergency["🚑 緊急対応"]
E1["KILL [thread_id]<br/>不要なSleepを切断"]
E2["SET GLOBAL<br/>max_connections増加"]
E3["SET GLOBAL<br/>wait_timeout短縮"]
end
subgraph Solution["✅ 恒久対策"]
Sol1["with文で自動close<br/>try-finally必須"]
Sol2["コネクションプール<br/>適切なサイズ設定"]
Sol3["PgBouncer/ProxySQL<br/>コネクションプーリング"]
Sol4["監視アラート<br/>80%で警告"]
end
P1 --> S1
P2 --> S2
P3 --> S3
S1 --> D1
S2 --> D2
S3 --> D3
D1 --> Emergency
D2 --> Emergency
D3 --> Emergency
E1 --> Solution
E2 --> Solution
E3 --> Solution
Sol1 -.->|防止| P1
Sol2 -.->|防止| P2
Sol3 -.->|防止| P3
Sol4 -.->|早期検知| S1
style P1 fill:#ffebee
style P2 fill:#ffebee
style P3 fill:#ffebee
style S1 fill:#f44336,color:#fff
style S2 fill:#f44336,color:#fff
style S3 fill:#f44336,color:#fff
style Emergency fill:#ff9800,color:#fff
style Solution fill:#4caf50,color:#fff
style Sol4 fill:#2196f3,color:#fff
1. コネクションリーク
# 悪い例:コネクションを閉じていない
def get_user_bad(user_id):
conn = mysql.connector.connect(...)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return cursor.fetchone()
# conn.close() がない!
# 良い例:with文で自動クローズ
def get_user_good(user_id):
with mysql.connector.connect(...) as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return cursor.fetchone()
# withを抜けると自動でclose
2. コネクションプールの設定ミス
# SQLAlchemyの例
from sqlalchemy import create_engine
# 悪い例:プールサイズが小さすぎる
engine = create_engine(
"mysql://...",
pool_size=5, # 本番で5は少なすぎる
max_overflow=0 # オーバーフロー禁止
)
# 良い例:適切なサイズ
engine = create_engine(
"mysql://...",
pool_size=20,
max_overflow=10, # 一時的に30まで増加可能
pool_timeout=30, # プール取得のタイムアウト
pool_recycle=3600, # 1時間でコネクションを再作成
pool_pre_ping=True # 使用前に接続確認
)
3. 長時間接続の保持
-- Sleepが多い場合は接続が解放されていない
SELECT
command,
COUNT(*) AS count,
AVG(time) AS avg_time
FROM
information_schema.processlist
GROUP BY
command;
+-------------+-------+----------+
| command | count | avg_time |
+-------------+-------+----------+
| Sleep | 150 | 300.5 | ← Sleep が多い = コネクションリーク
| Query | 5 | 0.1 |
+-------------+-------+----------+
緊急対応
1. 不要なコネクションを切断
-- MySQL: Sleepが長いコネクションを特定
SELECT
id,
user,
host,
time
FROM
information_schema.processlist
WHERE
command = 'Sleep'
AND time > 300
ORDER BY
time DESC;
-- 切断
KILL [id];
2. 最大接続数を一時的に増やす
-- MySQL(動的変更可能)
SET GLOBAL max_connections = 500;
-- PostgreSQL(再起動が必要)
-- postgresql.conf を編集
max_connections = 500
3. wait_timeout を短くする
-- MySQL: アイドルコネクションのタイムアウト
SET GLOBAL wait_timeout = 300; -- 5分(デフォルト8時間)
SET GLOBAL interactive_timeout = 300;
コネクション管理のベストプラクティス
1. コネクションプール必須
# Pythonの例(各フレームワーク)
# Django
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'CONN_MAX_AGE': 300, # 5分間コネクションを再利用
'CONN_HEALTH_CHECKS': True, # Django 4.1+
}
}
# FastAPI + SQLAlchemy
from sqlalchemy.pool import QueuePool
engine = create_engine(
DATABASE_URL,
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
)
2. PgBouncer(PostgreSQL用プロキシ)
graph LR
subgraph Clients["アプリケーション層"]
C1[App Instance 1<br/>250 connections]
C2[App Instance 2<br/>250 connections]
C3[App Instance 3<br/>250 connections]
C4[App Instance 4<br/>250 connections]
end
subgraph PgBouncer["PgBouncer<br/>コネクションプール"]
PB[PgBouncer<br/>:6432<br/><br/>max_client_conn: 1000<br/>pool_mode: transaction<br/>default_pool_size: 20]
end
subgraph Database["PostgreSQL"]
DB[(PostgreSQL<br/>:5432<br/><br/>実際の接続: 20<br/>max_connections: 100)]
end
C1 -->|接続| PB
C2 -->|接続| PB
C3 -->|接続| PB
C4 -->|接続| PB
PB -->|プール済み<br/>20接続のみ| DB
Note1["💡 1000接続を20接続に集約<br/>50倍の効率化"]
style Clients fill:#e3f2fd
style PgBouncer fill:#4caf50,color:#fff
style Database fill:#2196f3,color:#fff
style Note1 fill:#fff3e0
設定例:
# pgbouncer.ini
[databases]
mydb = host=localhost dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# プールモード
pool_mode = transaction # トランザクション単位でプール
# サイズ設定
max_client_conn = 1000 # クライアントからの最大接続
default_pool_size = 20 # DBへの実際の接続数
効果:
- Before: 1000アプリ接続 → 1000 DB接続 → 枯渇
- After: 1000アプリ接続 → 20 DB接続 → 余裕
3. ProxySQL(MySQL用プロキシ)
-- ProxySQLの設定
INSERT INTO mysql_servers (hostgroup_id, hostname, port)
VALUES (1, 'mysql-master', 3306);
-- コネクションプール設定
UPDATE global_variables
SET variable_value = '100'
WHERE variable_name = 'mysql-max_connections';
-- 読み取りクエリの分散
INSERT INTO mysql_query_rules (rule_id, match_pattern, destination_hostgroup)
VALUES (1, '^SELECT', 2); -- SELECTはレプリカへ
第3部:データ誤削除からの復旧
最悪のシナリオ
-- やってしまった...
DELETE FROM users; -- WHERE句を忘れた!
-- または
DROP TABLE orders; -- 間違えた!
まず落ち着いてください。 復旧方法はあります。
復旧方法の概要
| 方法 | 条件 | 復旧可能範囲 |
|---|---|---|
| バイナリログ | binlog有効 | 任意の時点 |
| バックアップ+PITR | バックアップ+binlog | 任意の時点 |
| スナップショット | クラウド/LVM | スナップショット時点 |
| レプリカから | レプリカが遅延 | レプリカの時点 |
| 論理バックアップ | mysqldump等 | バックアップ時点 |
バイナリログからの復旧(MySQL)
前提条件
-- バイナリログが有効か確認
SHOW VARIABLES LIKE 'log_bin';
-- ON であれば使用可能
-- バイナリログの一覧
SHOW BINARY LOGS;
手順1:該当のバイナリログを特定
# 現在のバイナリログファイル
mysql -e "SHOW MASTER STATUS"
# バイナリログの内容を確認(人間が読める形式)
mysqlbinlog --base64-output=DECODE-ROWS -v \
/var/lib/mysql/mysql-bin.000123 | less
手順2:誤操作の位置を特定
# 特定の時間範囲で検索
mysqlbinlog --base64-output=DECODE-ROWS -v \
--start-datetime="2025-12-13 10:00:00" \
--stop-datetime="2025-12-13 10:30:00" \
/var/lib/mysql/mysql-bin.000123 | grep -A 20 "DELETE"
出力例:
# at 12345
#251213 10:15:30 server id 1 end_log_pos 12500
# DELETE FROM users WHERE id = 100
### DELETE FROM `mydb`.`users`
### WHERE
### @1=100
### @2='john@example.com'
### @3='John Doe'
手順3:復旧SQLを生成
# mysqlbinlog でDELETEをINSERTに変換
# --flashback オプション(MariaDB)または手動で変換
# 誤操作の直前までのログを抽出
mysqlbinlog \
--start-position=1000 \
--stop-position=12345 \
/var/lib/mysql/mysql-bin.000123 > recovery.sql
# 適用
mysql < recovery.sql
Flashback(MariaDB)
MariaDBには --flashback オプションがあり、DELETEをINSERTに自動変換できます:
# DELETEをINSERTに変換
mysqlbinlog --flashback \
--start-position=12345 \
--stop-position=12500 \
/var/lib/mysql/mariadb-bin.000123 > flashback.sql
# 実行
mysql < flashback.sql
ポイントインタイムリカバリ(PITR)
概念
gantt
title ポイントインタイムリカバリ(PITR)のタイムライン
dateFormat HH:mm
axisFormat %H:%M
section データ状態
正常稼働 :done, normal1, 00:00, 10:00
誤操作発生 :crit, error, 10:15, 10:16
異常状態 :active, abnormal, 10:16, 12:00
section 復旧作業
バックアップ時点 :milestone, backup, 00:00, 0m
誤操作発生時刻 :milestone, mistake, 10:15, 0m
復旧目標時点 :milestone, target, 10:14, 0m
flowchart LR
subgraph Timeline["データベースのタイムライン"]
T0["00:00<br/>📦 フルバックアップ<br/>backup.sql"]
T1["00:00-10:14<br/>📝 バイナリログ<br/>mysql-bin.000123"]
T2["10:15<br/>💥 誤操作発生<br/>DELETE FROM users"]
T3["10:16-12:00<br/>❌ 異常状態<br/>データ欠損"]
end
subgraph Recovery["復旧プロセス"]
R1["1️⃣ バックアップリストア<br/>mysql < backup.sql<br/>→ 00:00の状態に戻る"]
R2["2️⃣ バイナリログ適用<br/>--stop-position=12345<br/>→ 10:14の状態まで進める"]
R3["✅ 復旧完了<br/>誤操作直前の状態"]
end
T0 --> T1
T1 --> T2
T2 --> T3
T0 -.->|リストア| R1
T1 -.->|部分適用| R2
R1 --> R2
R2 --> R3
style T0 fill:#4caf50,color:#fff
style T1 fill:#2196f3,color:#fff
style T2 fill:#f44336,color:#fff
style T3 fill:#ffebee
style R1 fill:#ff9800,color:#fff
style R2 fill:#ff9800,color:#fff
style R3 fill:#4caf50,color:#fff
復旧の流れ:
- 00:00のバックアップ をリストア → データベースが00:00の状態に戻る
- 00:00〜10:14のバイナリログ を適用 → 誤操作直前まで進める
- 10:15の誤操作 はスキップ
- 復旧完了 — 10:14時点のデータが復元される
手順(MySQL)
# 1. バックアップをリストア
mysql -u root -p mydb < backup_20251213.sql
# 2. 必要なバイナリログを特定
ls -la /var/lib/mysql/mysql-bin.*
# 3. 誤操作の位置を特定
mysqlbinlog --base64-output=DECODE-ROWS -v \
/var/lib/mysql/mysql-bin.000123 | less
# 4. 誤操作の直前までバイナリログを適用
mysqlbinlog \
--stop-position=12345 \
/var/lib/mysql/mysql-bin.000120 \
/var/lib/mysql/mysql-bin.000121 \
/var/lib/mysql/mysql-bin.000122 \
/var/lib/mysql/mysql-bin.000123 | mysql -u root -p mydb
WALからの復旧(PostgreSQL)
前提条件
-- アーカイブモードが有効か確認
SHOW archive_mode;
-- on であれば使用可能
-- WALの場所
SHOW archive_command;
手順
# 1. PostgreSQLを停止
systemctl stop postgresql
# 2. データディレクトリをバックアップから復元
rm -rf /var/lib/postgresql/14/main/*
tar xzf backup_20251213.tar.gz -C /var/lib/postgresql/14/main/
# 3. recovery.signal を作成(PostgreSQL 12+)
touch /var/lib/postgresql/14/main/recovery.signal
# 4. postgresql.conf に復旧設定を追加
cat >> /var/lib/postgresql/14/main/postgresql.conf << EOF
restore_command = 'cp /var/lib/postgresql/wal_archive/%f %p'
recovery_target_time = '2025-12-13 10:15:00'
recovery_target_action = 'promote'
EOF
# 5. 起動
systemctl start postgresql
pg_basebackup でのバックアップ
# フルバックアップ
pg_basebackup -h localhost -U postgres \
-D /backup/base_20251213 \
-Ft -z -P
# WALアーカイブ設定(postgresql.conf)
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'
AWS RDSでの復旧
スナップショットからの復旧
# 利用可能なスナップショットを確認
aws rds describe-db-snapshots \
--db-instance-identifier mydb
# スナップショットから新しいインスタンスを作成
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier mydb-restored \
--db-snapshot-identifier mydb-snapshot-20251213
ポイントインタイムリカバリ
# 特定の時点に復旧
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier mydb \
--target-db-instance-identifier mydb-pitr \
--restore-time "2025-12-13T10:15:00Z"
# 最新の復旧可能時刻を確認
aws rds describe-db-instances \
--db-instance-identifier mydb \
--query 'DBInstances[0].LatestRestorableTime'
削除防止策
1. トランザクションで確認
-- 常にトランザクションで実行
BEGIN;
DELETE FROM users WHERE id = 100;
-- 結果を確認
SELECT * FROM users WHERE id = 100;
-- 問題なければ
COMMIT;
-- 問題があれば
ROLLBACK;
2. 論理削除(Soft Delete)
-- 物理削除の代わりに論理削除
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL;
-- DELETE の代わりに UPDATE
UPDATE users SET deleted_at = NOW() WHERE id = 100;
-- アプリケーション側で除外
SELECT * FROM users WHERE deleted_at IS NULL;
3. トリガーでバックアップ
-- 削除前にバックアップテーブルに保存
CREATE TABLE users_deleted_backup (
backup_id INT AUTO_INCREMENT PRIMARY KEY,
deleted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
original_id INT,
email VARCHAR(255),
name VARCHAR(255)
-- ... 他のカラム
);
DELIMITER //
CREATE TRIGGER backup_before_delete
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
INSERT INTO users_deleted_backup (original_id, email, name)
VALUES (OLD.id, OLD.email, OLD.name);
END//
DELIMITER ;
4. 安全なDELETEスクリプト
#!/bin/bash
# safe_delete.sh
TABLE=$1
WHERE_CLAUSE=$2
# 削除対象を表示
echo "以下のレコードが削除されます:"
mysql -e "SELECT * FROM $TABLE WHERE $WHERE_CLAUSE LIMIT 10"
# 件数を表示
COUNT=$(mysql -N -e "SELECT COUNT(*) FROM $TABLE WHERE $WHERE_CLAUSE")
echo "合計: $COUNT 件"
# 確認
read -p "本当に削除しますか? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "中止しました"
exit 1
fi
# 削除実行
mysql -e "DELETE FROM $TABLE WHERE $WHERE_CLAUSE"
echo "削除完了"
5. MySQLのsql_safe_updates
-- WHERE句なしのUPDATE/DELETEを禁止
SET sql_safe_updates = 1;
-- これはエラーになる
DELETE FROM users;
-- ERROR 1175: You are using safe update mode...
-- WHEREが必要
DELETE FROM users WHERE id = 100; -- OK
障害対応チェックリスト
ロック問題
□ SHOW ENGINE INNODB STATUS でデッドロック確認
□ information_schema.innodb_trx で長時間トランザクション確認
□ information_schema.innodb_lock_waits でロック待ち確認
□ 問題のトランザクションを特定
□ 必要に応じて KILL で強制終了
□ アプリケーションにリトライ機構を追加
コネクション枯渇
□ SHOW STATUS LIKE 'Threads_connected' で現状確認
□ SHOW PROCESSLIST で接続の内訳を確認
□ Sleep が多い場合はコネクションリークを疑う
□ 不要なコネクションを KILL
□ 必要に応じて max_connections を増加
□ コネクションプールの設定を見直し
データ復旧
□ まず追加の変更を止める(READ ONLY化)
□ バイナリログ/WALが有効か確認
□ 誤操作の時刻・位置を特定
□ バックアップからのリストアを計画
□ バイナリログ/WALを誤操作直前まで適用
□ データを確認
□ サービス復旧
監視と予防
監視すべきメトリクス
# Prometheus + MySQL Exporter の例
groups:
- name: mysql_alerts
rules:
# コネクション使用率
- alert: MySQLTooManyConnections
expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL connections > 80%"
# デッドロック検知
- alert: MySQLDeadlocks
expr: rate(mysql_global_status_innodb_deadlocks[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "Deadlock detected"
# 長時間クエリ
- alert: MySQLSlowQueries
expr: rate(mysql_global_status_slow_queries[5m]) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "Slow queries increasing"
定期バックアップ
#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backup/mysql"
# フルバックアップ(週次)
if [ "$(date +%u)" = "1" ]; then
mysqldump --all-databases \
--single-transaction \
--routines \
--triggers \
--events \
> "${BACKUP_DIR}/full_${DATE}.sql"
fi
# 差分バックアップ用にバイナリログをローテート
mysql -e "FLUSH BINARY LOGS"
# バイナリログをアーカイブ
cp /var/lib/mysql/mysql-bin.* "${BACKUP_DIR}/binlogs/"
# 古いバックアップを削除(30日以上前)
find "${BACKUP_DIR}" -name "*.sql" -mtime +30 -delete
まとめ
データベース障害対応の全体マップ
mindmap
root((DB障害対応))
ロック問題
デッドロック
SHOW ENGINE INNODB STATUS
ロック順序統一
トランザクション短縮
リトライ機構
ロック待ちタイムアウト
innodb_lock_waits確認
長時間トランザクション特定
KILL実行
インデックス追加
コネクション枯渇
症状確認
Threads_connected
SHOW PROCESSLIST
Sleep多数
緊急対応
不要なKILL
max_connections増加
wait_timeout短縮
恒久対策
コネクションプール
PgBouncer/ProxySQL
監視アラート
データ復旧
バイナリログ
mysqlbinlog
位置特定
flashback
PITR
バックアップリストア
binlog部分適用
誤操作スキップ
予防策
トランザクション確認
論理削除
sql_safe_updates
定期バックアップ
ロック問題
| 症状 | 調査 | 対応 |
|---|---|---|
| デッドロック | SHOW ENGINE INNODB STATUS | リトライ機構、ロック順序統一 |
| ロック待ち | innodb_lock_waits | 長時間トランザクションをKILL |
| 遅いクエリ | SHOW PROCESSLIST | インデックス追加、クエリ最適化 |
コネクション問題
| 症状 | 調査 | 対応 |
|---|---|---|
| Too many connections | Threads_connected確認 | max_connections増加、プール設定見直し |
| コネクションリーク | Sleepが多い | アプリケーション修正、wait_timeout短縮 |
データ復旧
| 状況 | 方法 | 条件 |
|---|---|---|
| 直後に気づいた | ROLLBACK | トランザクション内 |
| 数分後 | バイナリログ | binlog有効 |
| 数時間後 | PITR | バックアップ+binlog |
| 翌日 | バックアップ復元 | バックアップあり |
心がけ
- バックアップは命綱 — 定期的に取得、復旧テストも実施
- 監視は早期発見の鍵 — 異常を検知できる体制を
- DELETE/DROPは慎重に — トランザクション、確認、論理削除
- ドキュメントを残す — 障害対応の手順を整備