🔁

Db2の「SQL30108N」をエラー扱いすると障害を自作する ― ACR(自動クライアントリルート)の正しい理解

に公開

HADRのテイクオーバー(フェイルオーバー)を試験したら、切り替わった直後にアプリが軒並みエラーで落ちた——。ログを見ると SQL30108N。「やっぱりフェイルオーバーは危ない」「HADRの設定が悪い」と結論づけて、切り替え運用そのものをためらうケースがあります。

しかしこの SQL30108N多くのケースでエラーではありません。「再接続に成功しました」という成功の通知です。これをエラーとして扱ったアプリが自分から処理を中断し、本来は続行できたはずの業務を止めてしまう——障害を自作している、というのが真相であることが少なくありません。

この記事では、SQL30108N の正体、ACR(自動クライアントリルート)が「何をして/何をしないのか」、アプリ開発チームと事前に合意しておくべきことを整理します。

まず結論:SQL30108N は「再接続できました」の通知

SQL30108N のSQLSTATEは 08506。メッセージは「接続は失敗したが、自動的に再確立された」という趣旨で、どのホスト/ポートへ再接続したかまで含まれています。

SQL30108N  A connection failed but has been re-established.
           The hostname or IP address is "db2node2" and the
           service name or port number is "50000".
           ...  SQLSTATE=08506

ここで押さえるべきは次の2点です。

  1. SQL30108N は異常系ではなく、ACRが再接続を成功させた通知である。
  2. ただし、切断時に走っていたトランザクションはロールバックされている。だから「再接続できたから続行」ではなく、そのトランザクションは再実行する必要がある。

この2点をアプリが正しく扱えば、テイクオーバーは「一瞬詰まって、再接続して、処理を投げ直す」だけで済みます。SQL30108N を通信エラーと同じ扱いにすると、再接続できているのにアプリが止まります。

ACR(自動クライアントリルート)とは何か

ACRは、接続先が応答しなくなったとき、クライアントをあらかじめ登録しておいた代替サーバーへ自動的に再接続させる機能です。HADRのテイクオーバーとセットで使うことで、「プライマリが切り替わってもアプリの接続文字列を書き換えずに追従させる」ことを狙います。

テイクオーバー前:
  アプリ → db2node1(プライマリ)

テイクオーバー後(ACRあり):
  アプリ → db2node1(応答なし)→ 自動的に db2node2 へ再接続

代替サーバーの登録は、サーバー側でDBに設定しておくのが基本です。ここで登録した情報がクライアントへ自動的に伝播します。

# サーバー側:DBに代替サーバー(新プライマリ候補)を登録する
db2 "UPDATE ALTERNATE SERVER FOR DATABASE SAMPLE
     USING HOSTNAME db2node2 PORT 50000"

# 登録内容の確認
db2 "LIST DATABASE DIRECTORY"   # 対象DBの "Alternate server" 欄を確認

クライアント側の接続プロパティ(ALTERNATE_SERVER_NAME / ALTERNATE_SERVER_PORT 等)で持たせる構成もありますが、いずれにせよ「元サーバーが落ちたら、この代替サーバーへ繋ぎ直す」という宛先を先に教えておく、という点は共通です。

SQL30108N が返る2つのシナリオ

SQL30108N は「再接続に成功したとき」に返ります。失敗したときは返りません。

ACRが成功した場合:
  アプリ → 接続断 → ACRが新プライマリへ自動再接続 → SQL30108N を返却
  ※ SQL30108N =「別サーバーへ繋ぎ直しました」という通知
  ※ ただし切断時のトランザクションはロールバック済み → 再実行が必要

ACRが失敗した場合:
  アプリ → 接続断 → 代替サーバーにも繋がらない → 元の通信障害エラーを返却
  (SQL30081N など。こちらが本当のエラー)

SQL30108N が返っているということは、少なくとも再接続自体は成立しているということです。本当に接続できないときは、SQL30108N ではなく通信系のエラー(SQL30081N など)が返ります。ここを取り違えると、「再接続できているのにエラー扱いして止める」か、「本当に落ちているのに再接続できたと誤認する」かの、どちらかの事故になります。

現場の失敗:SQLSTATEで一律エラー判定して握りつぶす

いちばん多い事故がこれです。

SQL30108N を握りつぶすと、再接続後に続行できたはずのトランザクションが投げ直されず、業務が止まります。「フェイルオーバーが危ない」のではなく、「フェイルオーバー後の1件をアプリが再実行しない」だけの問題であることが多い、ということです。

実機で再現:HADRフェイルオーバーで SQL30108N を観測する

実際に 2ノードHADR環境(Db2 12.1.4 / NEARSYNC) を組んで再現しました。まずクライアントに両系を教えてACRを有効化します。

# サーバー側:各DBに「相手ノード」を代替サーバーとして登録(★両系に設定する。理由は後述)
# db2node1(192.168.0.11) で
db2 "UPDATE ALTERNATE SERVER FOR DATABASE SAMPLE USING HOSTNAME 192.168.0.12 PORT 50000"
# db2node2(192.168.0.12) で
db2 "UPDATE ALTERNATE SERVER FOR DATABASE SAMPLE USING HOSTNAME 192.168.0.11 PORT 50000"

# クライアント側:プライマリをカタログ(代替サーバーは接続時にサーバーから受け取る)
db2 "CATALOG TCPIP NODE S1 REMOTE 192.168.0.11 SERVER 50000"
db2 "CATALOG DATABASE SAMPLE AS RSAMPLE AT NODE S1"

検証には、1本の接続を張りっぱなしにして2秒ごとに「今どの物理ホストのプライマリに繋がっているか」を問い合わせるアプリを bash + Db2 CLP で用意しました。第1引数で SQL30108N の扱い(naive=致命エラー / correct=再実行)を切り替えます。

#!/bin/bash
# ACR検証アプリ(bash + Db2 CLP): 1接続を保持し、接続先プライマリを2秒ごとに確認する
# ※ db2の出力は「ファイルにリダイレクト」する。パイプや $() はサブシェルを作り、
#    CLPの接続が各ループに引き継がれない(ここで一度ハマった)。
MODE="${1:-correct}"                       # naive | correct
export LANG=C; . ~/sqllib/db2profile
Q="SELECT PRIMARY_MEMBER_HOST FROM TABLE(MON_GET_HADR(NULL)) AS T"   # 接続先の可視化用
O=/tmp/acr_out.$$

db2 "CONNECT TO RSAMPLE USER db2inst1 USING ******" > "$O" 2>&1
grep -iE "Database server|SQL30108N" "$O"

for i in $(seq 1 12); do
  ts=$(date +%H:%M:%S)
  db2 -x "$Q" > "$O" 2>&1
  if grep -q "SQL30108N" "$O"; then
    if [ "$MODE" = "naive" ]; then
      echo "[$ts] #$i  SQL30108N -> 誤った版は致命エラー扱い -> ABORT(自作の障害)"
      break
    fi
    echo "[$ts] #$i  SQL30108N (SQLSTATE=08506) = ACR再接続 -> ロールバックして再実行"
    db2 -x "$Q" > "$O" 2>&1
    echo "[$ts] #$i  再実行OK -> 接続先プライマリ = $(tr -d ' \n' < "$O")"
  else
    echo "[$ts] #$i  OK  接続先プライマリ = $(tr -d ' \n' < "$O")"
  fi
  sleep 2
done
db2 connect reset >/dev/null 2>&1; db2 terminate >/dev/null 2>&1; rm -f "$O"

MON_GET_HADR は「今どちらのプライマリに繋がっているか」を可視化するための問い合わせです。実アプリでは通常の業務SQLに読み替えてください。)

アプリの実行中に、別ノードから TAKEOVER HADR ON DATABASE SAMPLE を打ってフェイルオーバーを起こします。

① 誤った版(SQL30108N を致命エラー扱い)

#1  OK  接続先プライマリ = 192.168.0.11
#2  OK  接続先プライマリ = 192.168.0.11
#3  OK  接続先プライマリ = 192.168.0.11
   >>> ここで db2node2 へ TAKEOVER(フェイルオーバー) <<<
#4  SQL30108N -> 誤った版は致命エラー扱い -> ABORT(自作の障害)

再接続自体は成功しているのに、SQL30108N をエラーと見なしたアプリが自分から止まりました。「障害を自作」している状態です。

② 正しい版(SQL30108N を検知して再実行)

#1  OK  接続先プライマリ = 192.168.0.11
#2  OK  接続先プライマリ = 192.168.0.11
#3  OK  接続先プライマリ = 192.168.0.11
   >>> ここで db2node2 へ TAKEOVER(フェイルオーバー) <<<
#4  SQL30108N (SQLSTATE=08506) = ACR再接続 -> ロールバックして再実行
#4  再実行OK -> 接続先プライマリ = 192.168.0.12
#5  OK  接続先プライマリ = 192.168.0.12
#6  OK  接続先プライマリ = 192.168.0.12
   ... 以降、新プライマリ(192.168.0.12)で処理継続 ...

SQL30108N を受けてそのトランザクションを再実行しただけで、接続先は 192.168.0.11 → 192.168.0.12 へ自動で移り、業務は止まらず継続しました。同じ SQL30108N でも、扱い方だけで「停止」と「無停止」に分かれます。

アプリ開発チームと事前に合意すべきこと

ACRはインフラだけで完結しません。SQL30108N を正しく扱うのはアプリ側の責務なので、設計段階での合意が必須です。最低限これを握っておきます。

合意すべき事項:
├── SQL30108N(SQLSTATE=08506)をエラーとして扱わない
├── SQL30108N を受けたら、そのトランザクションを再実行する
│    (再接続は済んでいるので、投げ直せば通る)
├── 再実行は冪等に設計する(二重実行を防ぐ/重複キーを吸収する)
└── 再接続後も同じエラーが続く場合は「本当の障害」として扱う

特に「再実行を冪等に」は重要です。切断のタイミングによっては、コミット直前だったのか直後だったのかがアプリから判別しづらいことがあります。そのまま素朴に再実行すると二重登録になりかねないので、業務キーでの重複チェックや MERGE 相当の考え方を入れておくと安全です。

本番アプリでの実装イメージ(Java / JDBC)

上の検証はCLPで動かしたものですが、実際のアプリでは例外処理で SQL30108N を判定します。ポイントは SQLState08506(=errorCode-30108)なら「エラー」ではなく「再接続完了」として扱い、そのトランザクションを再実行することです。

// ACR(自動クライアントリルート)を正しく扱うトランザクション実行
// SQLSTATE=08506 / errorCode=-30108 は SQL30108N =「別サーバーへ再接続した」通知。
// 致命エラーにせず、ロールバック済みのトランザクションを投げ直す。
private static final String ACR_RECONNECTED = "08506"; // SQL30108N

void runWithAcrRetry(DataSource ds, int maxRetry) throws SQLException {
    int attempt = 0;
    while (true) {
        try (Connection con = ds.getConnection()) {
            con.setAutoCommit(false);
            doBusinessTransaction(con);   // ← 冪等に設計しておく(二重実行を吸収)
            con.commit();
            return;                        // 成功
        } catch (SQLException e) {
            if (ACR_RECONNECTED.equals(e.getSQLState())) {   // = -30108
                // エラーではなく再接続完了の通知。接続は新プライマリへ張り替え済み。
                if (++attempt > maxRetry) throw e;
                log.warn("ACR reconnected (SQL30108N). retrying {}/{}", attempt, maxRetry);
                continue;                  // トランザクションを再実行
            }
            throw e;                        // それ以外は本当のエラー
        }
    }
}

「SQLSTATEが 00000 以外なら全部エラー」といった実装だと、この 08506 も巻き込んで停止します。08506 / -30108 だけは特別扱いして再実行する——たったこれだけで、前掲の「② 正しい版」と同じ無停止動作になります。なお maxRetry を設けているのは、再接続後も失敗が続く(=本当の障害)ケースで無限リトライに陥らないためです。

テイクオーバーと状態確認のコマンド

SQL30108N を「再接続の通知」と判断するには、DB側が本当に正しく切り替わっているかを確認できることが前提です。テイクオーバー前後で見るコマンドを並べておきます。

# HADRの状態とロールを確認(テイクオーバー後にロールが入れ替わっているか)
db2pd -db SAMPLE -hadr

# 計画的テイクオーバー(新プライマリにしたい側=現スタンバイで実行)
db2 "TAKEOVER HADR ON DATABASE SAMPLE"
-- ログギャップの確認(テイクオーバー前の健全性チェック)
SELECT HADR_LOG_GAP, LOG_HADR_WAIT_TIME
FROM TABLE(MON_GET_HADR(NULL)) AS T;

HADR_LOG_GAP はプライマリとスタンバイのログ差(バイト)、LOG_HADR_WAIT_TIME はログ転送の待機時間累計(ミリ秒)です。ギャップが大きい状態で緊急テイクオーバー(BY FORCE)を打つとデータロストの恐れがあるため、切り替え前のギャップ確認はセットで習慣化しておくのが安全です。

SQL30108N が出たときのチェックリスト

テイクオーバー後に SQL30108N を見たら、上から順に確認します。

  1. SQLSTATEを確認する08506 なら再接続の通知。通信エラー SQL30081N 等とは別物)
  2. db2pd -db SAMPLE -hadr で、DB側がちゃんと新プライマリへ切り替わっているか見る
  3. 切り替わっているなら、SQL30108N は正常系。該当トランザクションを再実行すればよい
  4. アプリが処理を中断していないか、例外処理が SQL30108N を握りつぶしていないか確認する
  5. 再接続後も同じエラーが続くなら、それは本当の障害。代替サーバーの登録(UPDATE ALTERNATE SERVER)と疎通を確認する
  6. 再実行が冪等かどうかを、切断タイミング(コミット前後)の観点で見直す

📕 もっと深く:Db2運用を12章で体系化した本を書きました

この記事で扱ったACR/SQL30108N の話は、HADR運用のほんの一部です。

Db2はRDBMS市場でシェア数%。情報が少なく、頼れるのは膨大な公式マニュアルだけ——そんな現状を変えたくて、「なぜこの値にするのか」「この障害のとき次に何を見るのか」という、マニュアルに載っていない現場の文脈を1冊にしました。実機で検証した内容と、Db2の設計上そうなる理由をまとめました。

アーキテクチャ/構成パラメーター選定/セキュリティ/バックアップ・リカバリ/HADR(本記事の深掘り版:構築・テイクオーバー・pureScale+HADRの落とし穴・ACR)/pureScale/日常点検の自動化/メモリ・ロック競合/SQLチューニング/RUNSTATS・REORG/MON_GET監視/現場の難題集まで、全12章+運用シェルスクリプト集です。

https://zenn.dev/firese/books/db2-practical-bible

HADRの切り替えで踏んだ地雷や、扱ってほしいテーマがあれば、ぜひコメントで教えてください。次の記事のネタにさせてもらいます。

Discussion