午前2時、アプリがエラーを投げている
監視アラートが発火した。ログは Error: MySQL server has gone away で埋め尽くされている。一日中問題なく動いていたクエリが、今は失敗している。データベース自体は起動している——手動で接続もできる——では、何が起きているのか?
MySQLがアプリの使用中に接続を切断した。クライアントがクエリを送信したとき、サーバーはもう待ち受けていなかった。クラッシュではない。単なる切断だ。
素早い診断:まず本当の原因を特定する
設定を変える前に、接続が切れた理由を把握しよう。95%のケースは3つの原因に絞られる:
- 接続タイムアウト — アプリが再利用する前に、MySQLがアイドル接続を閉じた
- パケットサイズ超過 — クエリまたはblobが
max_allowed_packetを超えた - MySQLのクラッシュまたは再起動 — セッション途中でサーバー自体が落ちた
MySQLエラーログを確認する
# Ubuntu/Debian
tail -100 /var/log/mysql/error.log
# CentOS/RHEL
tail -100 /var/log/mysqld.log
# MySQLに直接聞く
SHOW VARIABLES LIKE 'log_error';
現在のタイムアウト設定を確認する
SHOW VARIABLES LIKE '%timeout%';
SHOW VARIABLES LIKE 'max_allowed_packet';
wait_timeout と interactive_timeout に注目しよう。MySQLのデフォルトは28800秒(8時間)だ。クラウドプロバイダーはこれを大幅に短く設定していることが多い——AWS RDSはデフォルトで600秒、マネージドデータベースによっては60秒まで下げているものもある。
サーバーが再起動したか確認する
-- 最終再起動からの経過時間
SHOW STATUS LIKE 'Uptime';
# Linuxでシステムログを確認
journalctl -u mysql --since "1 hour ago"
修正1:接続タイムアウト(最も多い原因)
アプリが接続を開いたまま放置し、MySQLがそれを強制終了した。アプリは接続がまだ生きていると思っているため、次のクエリが MySQL server has gone away で失敗する。
方法A — MySQLのタイムアウトを延長する(サーバー側)
/etc/mysql/mysql.conf.d/mysqld.cnf(または /etc/my.cnf)を編集する:
[mysqld]
wait_timeout = 3600
interactive_timeout = 3600
再起動なしで即時適用する:
SET GLOBAL wait_timeout = 3600;
SET GLOBAL interactive_timeout = 3600;
方法B — アプリ側の接続プールを修正する(根本的な解決策)
タイムアウトを延ばすだけでは問題を先送りにしているに過ぎない。いずれ接続は古くなる。本質的な修正は、アプリが死んだ接続を検出して自動的に再接続できるようにすることだ。
Python(SQLAlchemy):
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # 使用前に接続をテストする
pool_recycle=1800, # 30分ごとに接続をリサイクルする
pool_timeout=30,
pool_size=10
)
Node.js(mysql2):
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'mydb',
waitForConnections: true,
connectionLimit: 10,
enableKeepAlive: true, // キープアライブパケットを送信する
keepAliveInitialDelay: 10000
});
// 単一の永続接続ではなく、常にpool.execute()を使うこと
const [rows] = await pool.execute('SELECT 1');
PHP(PDO):
// PDO::ATTR_PERSISTENT は避けること — 接続が生きているか確認せずにリクエスト間で再利用する
// 代わりにエラーをキャッチして再接続する:
try {
$stmt = $pdo->query($sql);
} catch (PDOException $e) {
if (str_contains($e->getMessage(), 'server has gone away')) {
$pdo = new PDO($dsn, $user, $pass, $options); // 再接続
$stmt = $pdo->query($sql);
} else {
throw $e;
}
}
修正2:max_allowed_packetが小さすぎる
アイドル接続ではなく、大きなテキスト・画像・JSONブロブを挿入するときにエラーが発生する?原因はタイムアウトではなく、パケットサイズだ。
-- 現在の制限を確認
SHOW VARIABLES LIKE 'max_allowed_packet';
-- デフォルトはMySQL 5.7で4MB(4194304)、MySQL 8.0で64MB
my.cnf で値を増やす:
[mysqld]
max_allowed_packet = 64M
またはライブで適用する(新規接続からのみ有効):
SET GLOBAL max_allowed_packet = 67108864;
再起動後も設定を維持するためにMySQLを再起動する:
sudo systemctl restart mysql
修正3:MySQLがクラッシュまたはOOMキルされた
Uptime が低い、またはエラーログに再起動の記録がある場合、サーバー自体が落ちている——通常はメモリ不足が原因だ。OSが接続を正常にクローズする前にMySQLを強制終了した。
# OOMキラーがMySQLを強制終了したか確認
dmesg | grep -i 'killed process'
grep -i 'oom' /var/log/syslog | tail -20
MySQLがOOMキルされた場合、innodb_buffer_pool_size を利用可能なRAMの70〜75%に制限しよう。4GBサーバーなら約2〜3GBが目安だ:
[mysqld]
# 4GB RAMのサーバーの場合:
innodb_buffer_pool_size = 2G
修正を確認する
変更を適用した後、新しい値が実際に有効になっているか確認しよう:
-- 新しいタイムアウト値を確認
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'max_allowed_packet';
-- 接続エラーをリアルタイムで監視
SHOW STATUS LIKE 'Aborted_clients';
SHOW STATUS LIKE 'Aborted_connects';
Aborted_clients が増加し続けているなら、クライアントが接続を正常にクローズせずに切断している——接続プールの設定を見直す必要がある。代わりに Aborted_connects が増えている場合は、認証またはネットワークの問題を示している。
アプリ側の再接続ロジックをストレステストしたい? wait_timeout を10秒に下げ、15秒待ってからアプリ経由でクエリを実行してみよう:
SET GLOBAL wait_timeout = 10;
-- 15秒待ってから、アプリ経由でクエリを実行する
-- エラーなし = pool_pre_ping / 再接続ロジックが機能している
まとめ
- アイドル状態のDB接続を保持し続けない — 常に
pre_pingまたはキープアライブを備えた接続プールを使うこと。MySQLのタイムアウトが短くても、プールが再接続を透過的に処理してくれる。 - リリース前にmax_allowed_packetを64Mに設定する — アプリがファイルアップロードや大きなJSONペイロードを扱う場合、デフォルトの4MBは本番環境で思わぬ落とし穴になる。
- Aborted_clientsを監視する — 継続的な増加は、接続を開いたままにしているか、リークしているかの早期警告サインだ。
- マネージドデータベースはタイムアウトが積極的に短い — RDS、PlanetScale、Cloud SQLはデフォルトで60〜600秒に設定されていることが多い。安全を保つために
pool_recycleをその半分の値に設定しよう。

