MySQLのDBコネクション関連でトラブルが発生した際にやること

Linux

稼働中のシステムにおいて発生するトラブルのひとつに、MySQLのコネクションに関するトラブルがあります。
“アプリケーションからDBコネクションを取得できない!””想定以上にアプリケーションがコネクションを取得している”といったとトラブルになります。


今回は、MySQLのコネクションに関するトラブルが発生した場合に調査するポイントと、調査のために実行するコマンドについて紹介していきます。


現在のコネクション数を確認


MySQLに、現在接続しているコネクション数を確認するには、show status コマンドを実行します。
MySQLにログインしてコマンドを実行します。


mysql> show status like 'Threads_connected';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 29    |
+-------------------+-------+
1 row in set (0.00 sec)

上記コマンドで現在接続されているコネクション数を確認して、MySQLで許可している最大コネクション数を超えていないかの確認が可能です。


最大コネクション数を確認


MySQLが許可している最大コネクション数を確認します。
このコネクション数が接続される可能性がある数を超えてしまうので、MySQLに接続できない状態になってしまいます。


mysql> SHOW GLOBAL VARIABLES like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 512   |
+-----------------+-------+
1 row in set (0.01 sec)

接続元のIPを確認


現在のプロセスを一覧で表示して、発生しているコネクションが、どのサーバから接続されているかの確認も可能です。


mysql> SHOW processlist;
+------------+------------+------------------------------+
| Id | User | Host | db | Command | Time | State | Info  |
+------------+------------+---------------------+--------+
  ・
  ・
  ・

Hostカラムが接続元サーバのIPとPORTです。


MySQLを起動してからの最大接続数を確認


MySQLを起動してから接続された最大コネクション数も確認することができます。

トラブルが発生しても、調査を開始したタイミングではコネクション数は問題ない場合があります。

しかし、トラブル発生直後はコネクション数が規定値をオーバーしている可能性があるので、このコマンドを実行して過去の最大コネクション数を確認するとよいです。


mysql> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 513   |
+----------------------+-------+
1 row in set (0.00 sec)

DBサーバのKeepAliveを確認


TomcatなどのWEBアプリケーションがコネクションプールを取得している状態で、Tomcatが起動しているサーバが落ちてしまうと、MySQLのコネクションは残ってしまいます。
このコネクションは不要なコネクションとなるのですが、一定時間が過ぎると自動的にコネクションは解放されます。


この「一定時間」は、MySQLがインストールされているサーバのKeepAliveパケット送信の設定に依存します。

システム設定値となり、以下のコマンドで格納可能です。


以下の設定例では、60秒で不要なコネクションは解放されます。


$>sysctl -a
 ・
 ・
 ・
net.ipv4.tcp_keepalive_intvl=5
net.ipv4.tcp_keepalive_probes=6
net.ipv4.tcp_keepalive_time=30
 ・
 ・
 ・

30秒間隔でKeepAliveパケットを6回送信する。
その6回は5秒間隔、という設定になります。


各項目の説明を一応記載しておきます。


net.ipv4.tcp_keepalive_intvl

KeepAliveパケットを送信する間隔

net.ipv4.tcp_keepalive_probes

KeepAliveパケットを送信する回数

net.ipv4.tcp_keepalive_time

KeepAliveパケットを送信するまでの時間


JavaのSupplierを使って汎用的なリトライ処理を実装する

Java

アプリケーションの機能を実装していく中で、様々なリトライ処理が必要になってきます。
DBアクセスに失敗した場合のリトライ、HTTP通信(APIなど)に失敗した場合のリトライ、など様々かと思います。


Javaでこういったリトライ処理を実装しようとすると、普通にやるとfor文やwhile文を使ってしまいがちです。
それでもいいですが、Javaのユーティリティクラス群であるSupplierを使えば、もうちょっと汎用的に実装することができます。


今回は、Javaのユーティリティクラスである「java.util.function.Supplier」を使って、汎用的なリトライ処理を実装する方法を紹介していきます。


環境情報


  • OS:Windows10
  • Java:Java1.8

ユーティリティクラス群で使用されている関数型インターフェースは、Java8から導入された機能となります。
Java1.7以前は使用できないので注意してください。


java.util.functionは関数型インターフェース


java.util.function配下の機能は全て、関数型インターフェースで実装されています。
イメージとしては、実行するメソッドをパラメータとして渡す形です。


java.util.functionのAPI仕様をみると複雑なインターフェースが沢山あるようにみえるのですが、大きくわけて4つに分類されます。
今回のリトライ処理では「Supplier」を使うのですが、他3つのインターフェースは以下になります。


種類

実装するメソッド

概要

Function<T,R>

R apply(T t)

実装するメソッドは、引数としてTを受け取り、結果としてRを返すものになる

Consumer<T>

void accept(T t)

実装するメソッドは、引数としてTを受け取り、結果を返さず終了するものになる

Predicate<T>

boolean test(T t)

実装するメソッドは、引数としてTを受け取り、boolean値を結果として返すものになる

Supplier<T>

T get()

実装するメソッドは、何も引数として受け取らず、結果としてTを返すものになる


関数型インターフェースの簡単なサンプルを、以下に紹介します。
先ほど説明した通り、関数をパラメータとして渡して渡された側で関数を実行しています。


return new RetryUtil()
    .retryFunc(() -> {
        ※実行する機能
    );
public class RetryUtil {
    private <T> T retryFunc(Supplier<T> func) {
        return func.get();
    }
}

リトライ処理


リトライ処理は、java.util.function配下の「Supplier」を使用します。
「Supplier」を使用し、実施する処理自体は呼び出し元で定義し、リトライの仕組み自体は共通クラス(ユーティリティクラス)に実装しています。


まずはリトライ処理を実施するメイン処理です。

import java.util.function.Supplier;

/**
    リトライ処理サンプル
*/
public class RetrySample {

    /**
        リトライ処理メイン
    */
    public static void main(String[] args) 
        throws InterruptedException {
        System.out.print("start: RetrySample\r\n");

        // 処理結果を失敗で初期化
        boolean isDone = false;

        // リトライ処理
        try {
            isDone = retryExec();
        } catch (Exception e) {
            System.out.print("RetrySample: main is failer\r\n");
        } finally {
            System.out.print(
                "RetrySample: result is " + isDone + "\r\n");
        }

        System.out.print("end: RetrySample\r\n");
    }

    /**
        リトライ処理の実施
    */
    private static boolean retryExec () 
        throws InterruptedException {

        // 1.リトライ回数とスリープ時間を設定
        int retryNum = 3;
        long sleepMilSec = 1000;

        // 2.ユーティリティを使って〇〇〇処理を実施する
        return new RetryUtil(retryNum, sleepMilSec)
            .retryFunc(() -> {
                try {
                    System.out.print("retryExec\r\n");
                    boolean isSucess = 〇〇〇処理を実施
                    if (isSucess) {
                        return true;
                    } else {
                        throw new RetryException();
                    }
                } catch (Exception e) {
                    throw new RetryException();
                }
            }, "RetrySample");
    }
}

「1.リトライ回数とスリープ時間を設定」で、リトライユーティリティのオブジェクトに設定する初期値を定義しています。
今回はリトライ処理ということで、ありがちなリトライ回数とスリープ時間はユーティリティクラスのコンストラクタで設定できようにしています。
その他、初期値として設定したい値があるのであれば、同様にコンストラクタで定義するのがよいかと思います。


「2.ユーティリティを使って〇〇〇処理を実施する」の機能が、実際に関数型インフェースで定義しているリトライ処理する中身です。
サンプルなので「〇〇〇処理を実施」としていますが、実際にはここにリトライ制御したい機能を実装していくのがよいでしょう。
リトライする契機としては、独自例外クラスである「RetryException」が発生した場合になります。
失敗したら(isDoneがfalseだったら)「RetryException」をスローすることで、リトライ処理を実行するようにユーティリティクラスを準備しています。


次にリトライユーティリティクラスです。

import java.text.MessageFormat;
import java.util.function.Function;
import java.util.function.Supplier;

/**
    リトライユーティリティ
*/
public class RetryUtil {

    // リトライ回数
    private final int retryNum;

    // リトライ時のスリープ時間
    private final long retryWait;

    /**
     * コンストラクタ
     */
    public RetryUtil(int retryNum, long retryWait) {

        // 1.リトライ回数とスリープ時間を設定
        this.retryNum = retryNum;
        this.retryWait = retryWait;
    }

    /**
     * リトライ処理
     */
    public <T> T retryFunc(Supplier<T> func, String funcName)
            throws RetryException, InterruptedException {
        return retryFunc(func, funcName, 1, null);
    }

    /**
     * リトライユーティリティ
     */
    private <T> T retryFunc(Supplier<T> func, 
        String funcName, int count, Exception befEx)
        throws RetryException, InterruptedException {

        // 2.Supplierで提供されたfuncを実行
        try {
            return func.get();

        // 3.例外が発生した場合はリトライ回数に
        //     達するまで自身を再度呼び出す
        } catch (RetryException e) {
            System.out.print("RetryException\r\n");
            if (count > this.retryNum) {
                System.out.print("retry end\r\n");
                throw new RetryException(e);
            }
            count++;
            Thread.sleep(this.retryWait);
            System.out.print("next retry " + 
                Integer.toString(count) + "回目の処理開始\r\n");
            return retryFunc(func, funcName, count, null);
        }
    }
}

「1.リトライ回数とスリープ時間を設定」で、リトライ回数とスリープ時間をクラス変数として格納しています。
実際のリトライ処理時に、この値を参照しています。


「2.Supplierで提供されたfuncを実行」で、実際に関数型インターフェースで渡されたメソッドを実行しています。
実行方法は、パラメータ名.get() で実行されます。


「3.例外が発生した場合はリトライ回数に達するまで自身を再度呼び出す」がリトライ処理のメインとなる部分ですね。
個別例外である「RetryException」が発生するとリトライ処理を実施します。
リトライ回数が上限に達すると上位に例外をスローしてリトライ処理を中断します。
上限に達していない場合は再度自身を呼び出して同じ処理を実行しています。


最後に個別の例外クラスです。
リトライ処理を制御しやすくするために、個別例外を準備しています。


/**
    リトライユーティリティ固有例外
*/
public class RetryException extends RuntimeException {

    /**
     * コンストラクタ
     */
    public RetryException() {
        super();
    }

    /**
     * コンストラクタ(例外指定)
     * @param cause 例外
     */
    public RetryException(Throwable cause) {
        super(cause);
    }
}