サーバーのCPUもメモリも余裕があるのにサービスだけが詰まっている理由がDBにあると気づき、データベースの接続状況を落ち着いて調べはじめた一人運用の保守担当者

DBコネクション枯渇・デッドロックの見つけ方と一次対処

昼過ぎ、問い合わせが立て続けに入る。「ページが開かない」「保存を押しても返ってこない」。あわてて監視を見ても、サーバーの CPU もメモリもディスクも、いつもと変わらず余裕がある。なのに、サービスだけが詰まっている——。 アプリのログをたどると、Too many connectionsLock wait timeout といった、DB まわりのエラーが並んでいた。

そんな経験、ありませんか。 サーバー本体には余裕があるのに詰まるとき、原因がデータベースの側にあることがあります。よくあるのが2つ。ひとつは、DB への接続(コネクション)が上限まで埋まって、新しい処理がつなげなくなるコネクション枯渇。もうひとつは、複数の処理が互いの更新を待ち合って進めなくなるデッドロックです。どちらも、サーバーの負荷グラフを見ているだけでは気づきにくいのが、追いにくさの正体です。

この記事では、まず「枯渇なのか、デッドロックなのか」を切り分ける手順、いま DB に何がつながっているかを見る読み方、そして一次対処までを、一人運用の目線で順番に整理します。全部を一度に直そうとしなくて大丈夫です。まずは「詰まりの原因が、接続の埋まりか、待ち合いか」を、はっきりさせるところから。

結論:DB が詰まっていそうなら、①いまの接続を見るSHOW PROCESSLIST / pg_stat_activity で、何本つながって・何を待っているか)②枯渇かデッドロックか分ける(接続数が上限に張り付いていれば枯渇、SHOW ENGINE INNODB STATUS にデッドロック記録があれば待ち合い)③一次対処(詰まりの元になっている長時間クエリを止める・アプリを再起動して接続を解放)④再発を止める(接続数の監視と、長時間クエリ・接続リークの見直し)——の順で落ち着いて進めます。まずは①の「いま何がつながっているか」から。

コマンドは MySQL(8.0系)と PostgreSQL を前提にしています。バージョンやディストリビューションで出力やパスが少し異なります。ここを出発点に、本番へ反映する前に検証環境と公式情報で確認してください。

コネクション枯渇とデッドロックは、何が違うのか

同じ「DB が詰まる」でも、この2つは起きていることが違います。打ち手も変わるので、まずここを分けます。

コネクション枯渇は、DB が同時に受けられる接続数(MySQL なら max_connections)の上限まで埋まってしまい、新しい接続を張れなくなる状態です。アプリからは「つなごうとしたのに拒否された」形でエラーになります(MySQL なら Too many connections)。原因は、遅いクエリで接続が長く占有される、接続を使い終わっても閉じずに溜め込む(接続リーク)、アクセス集中で一気に接続が増える、などです。DB という「受付窓口」の席が全部埋まって、新しいお客さんが入れない状態、とイメージすると近いです。

デッドロックは、すでにつながっている複数の処理が、お互いが持っているロック(更新中の行などをつかんでいる状態)を待ち合って、どちらも進めなくなる状態です。A の処理が行1を、B の処理が行2をつかんだまま、A が行2を、B が行1を要求すると、永遠に待ち合いになります。多くの DB はこれを検知すると、片方を自動でエラーにして巻き戻し(ロールバック)ますが、そのぶんアプリ側には Deadlock foundLock wait timeout のエラーが返ります。窓口は空いているのに、2人が「お先にどうぞ」を譲り合って前に進めない、という詰まり方です。

見分けの入口はシンプルです。接続数が上限に張り付いているなら枯渇接続数には余裕があるのに特定の更新処理だけ固まる・エラーになるならデッドロック(ロック待ち)を疑います。次に、それぞれを実際のコマンドで確かめます。

いまの接続を見る(まず現場を確認)

DBが詰まったとき、いまの接続を見て、枯渇かデッドロックかを分け、一次対処へ進む流れを示した図
「接続を確認 → 枯渇かロック待ちか分ける → 一次対処」の順で進めると迷わない

まずは、DB にいま何本つながっていて、それぞれが何をしているかを見ます。これが分かると、枯渇なのかロック待ちなのかの見当がつきます。

MySQL の場合:SHOW PROCESSLIST

-- いまの接続と、各接続が実行中の処理を一覧で見る
SHOW FULL PROCESSLIST;

-- 上限(max_connections)と、いまの接続数を確認する
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';

SHOW FULL PROCESSLIST の各行で見たいのは、次のところです。

Threads_connectedmax_connections に張り付いていれば、枯渇とほぼ判断できます。

PostgreSQL の場合:pg_stat_activity

-- いまの接続と、各接続の状態・実行中クエリを見る
SELECT pid, state, wait_event_type, now() - query_start AS duration, query
FROM pg_stat_activity
ORDER BY duration DESC NULLS LAST;

-- 上限(max_connections)を確認する
SHOW max_connections;

stateactive のまま duration(実行時間)が長い行、wait_event_typeLock の行が、詰まりの手がかりです。接続数そのものは SELECT count(*) FROM pg_stat_activity; で数えられます。

ここで、接続が上限近くまで埋まっているのか接続には余裕があるのに特定の処理だけ固まっているのかが見えれば、原因を枯渇とデッドロックに切り分ける準備ができます。

枯渇かデッドロックかを分ける

接続一覧を見たうえで、もう一歩踏み込んで確かめます。

枯渇の確認Threads_connected(MySQL)や接続数(PostgreSQL)が上限に張り付いていれば枯渇です。このとき、SHOW FULL PROCESSLIST の中身を見て、「長時間の重いクエリで席が埋まっている」のか「Sleep の接続が溜まっている(リーク)」のかを分けます。前者はクエリやインデックスの問題、後者はアプリ側の接続の閉じ忘れ・コネクションプール設定を疑う方向になります。原因の当たりどころが変わるので、ここは丁寧に。

デッドロックの確認(MySQL):接続数には余裕があるのに更新処理が固まる・Deadlock found が出るなら、InnoDB のデッドロック記録を見ます。

-- 直近のデッドロックの詳細(LATEST DETECTED DEADLOCK の項)を確認する
SHOW ENGINE INNODB STATUS\G

出力の LATEST DETECTED DEADLOCK の項に、どのトランザクションが・どのテーブルの・どの行のロックを待ち合ってデッドロックになったかが記録されています。ここを読むと、「どの2つの処理がぶつかっているか」が分かり、アプリ側で更新の順番をそろえる・トランザクションを短くする、といった再発対策の入口になります。

ロック待ちの確認:デッドロック(相互待ち)まで至らなくても、片方が長くロックを握っていて他方が待たされる「ロック競合」でも詰まります。MySQL 8.0 なら performance_schema.data_lock_waits、PostgreSQL なら pg_lockspg_stat_activity を突き合わせると、「誰が誰を待たせているか」が見えます。まずは「待たせている側の長時間クエリ」を見つけるのが先決です。

補足:ここまで来て「接続にも余裕がある・ロック待ちも見当たらない」なら、詰まりの原因は DB ではなく、アプリ側の処理やネットワークかもしれません。切り分けの向きを、そちらへ切り替えます。犯人が違えば、打ち手も変わります。

一次対処:まず詰まりをほどく

DBの詰まりの一次対処として、長時間クエリを止める・接続を解放する・アプリを再起動する・原因を記録する流れを並べた図
いったん詰まりをほどいてから、再発を止める見直しへ。順番に一つずつで大丈夫

原因の当たりがついたら、まずはサービスを動く状態に戻します。急いでいるときほど、この順で。

  1. 詰まりの元になっている長時間クエリを止めるPROCESSLIST / pg_stat_activity で見つけた「明らかに居座っている重いクエリ」があれば、その接続だけを狙って止めます(MySQL は KILL <id>;、PostgreSQL は SELECT pg_terminate_backend(<pid>);)。止めるのはピンポイントで。関係ない接続まで巻き込むと、別の処理が中断されて被害が広がります。どの id/pid を止めるのか、Info/query の中身を必ず確認してから実行します。
  2. 溜まった接続を解放する(リークが疑わしいとき)Sleep の接続が大量に溜まっているなら、それを掴んでいるアプリ側を再起動すると、接続がまとめて解放されて枯渇がほどけることがあります。ただし、再起動しても原因(閉じ忘れ・プール設定)が残っていれば、また同じところで溜まります。応急処置と割り切ります。
  3. アプリを再起動して復旧させる(最終手段):どうにも身動きが取れないときは、アプリ(Web/API)側を再起動して接続をリセットします。DB 本体の再起動は、走っているトランザクションを巻き込むため、より慎重に。根本原因は消えていないので、再発監視とセットにします。
  4. いつ・何が詰まっていたかを記録に残す:止めたクエリ、そのときの接続数、INNODB STATUS のデッドロック記録などを、後から見返せる形で残します。「再起動で直った」で終わらせないことが、次に同じことが起きたときの近道になります。

一次対処の目的は、原因の完全解決ではなく、まずサービスの詰まりをほどいて、腰を据えて調べられる状態を作ることです。ここまで来れば、いちばん怖い「鳴り続けている最中の対応」からは抜けられます。

影響:仕組みで気づけると、何が変わるか

放置すると、DB の詰まりは「アクセスが増えた日に、また突然」戻ってきます。しかも枯渇すると新しい接続が一切張れなくなるため、サービス全体が一気に止まる形になりやすいのがつらいところです。逆に言えば、一度「見る場所」と「監視」を用意しておけば、同じ詰まり方でうろたえることは、ぐっと減ります。

明日やること:まず「いまの接続」を見る1手から

  1. DB が詰まった記憶のあるサーバーで、SHOW FULL PROCESSLIST(MySQL)または pg_stat_activity(PostgreSQL)を一度たたいて、いま何本つながって・何が長く居座っているかを見てみる。
  2. max_connections と現在の接続数を並べて、上限までどれくらい余裕があるかを把握する。張り付いていれば枯渇、余裕があってもロック待ちがあればデッドロックを疑う。
  3. いちばん止まると困る DB だけ、接続数の監視(上限の◯割を超えたら通知)を1本入れる。まずは1台でいい。
  4. 余力があれば、slow query log(時間のかかったクエリの記録)を有効にして、詰まりの元になりやすい重いクエリを平時から拾えるようにする。

この流れなら、30分で「いまの接続の把握」と「枯渇かロック待ちかの見当づけ」までは終わります。接続数の監視の1本は、次に同じことが起きたとき、あなたが問い合わせで気づく前に、DB が教えてくれる備えになります。

DBコネクション枯渇・デッドロック対応チェックリスト

最低ライン(優先順位つき:これだけで回る) 1) 現場を見る:SHOW FULL PROCESSLIST / pg_stat_activity で接続と長時間クエリを確認する 2) 原因を分ける:接続数が上限に張り付き=枯渇、余裕ありでロック待ち=デッドロックと切り分ける 3) 気づく仕組み:重要な DB に「接続数が上限の◯割で通知」の監視を1本入れる

余力が出たら拡張

免除条件(省略可)

確認項目

よければ、こちらも

DB の詰まりは、「どこかが遅い・詰まっている」サインのひとつです。同じ“重い・返ってこない”を切り分ける手順や、逼迫を前もって拾う監視の考え方も、あわせて型にしておくと落ち着けます。

DBの接続数を監視で見守れるようにして、詰まりの原因がはっきりしたことに静かに安心している保守運用の担当者

サーバーは余裕なのにサービスだけ詰まる、というのは、原因の入口が見えにくくて心細いものです。でも、「まず DB の接続を見る」と決めておくだけで、そのもやもやは、ずいぶん小さくなります。 今日は、気になっていたあの DB で SHOW FULL PROCESSLIST を一度たたいて、いま何がつながっているかを眺めるだけで十分です。現場が見えれば、次の一手は自然と見えてきます。原因の入口にたどり着けた時点で、もう対応は前に進んでいます。

ほかの実務ヒントは記事一覧からどうぞ。保守運用の小さな備えを、メールでも少しずつお届けしています。

関連用語