その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

Server::Starter + Java 環境下で JMX による監視を有効にしていると graceful restart 時に不具合が出ちゃって困るんですけど〜って時

割とニッチな話題ではありますが……


Server::Starter を使ってプロセスを立ち上げると graceful restart を簡単に実現できるなど便利な点が多く,LL 時代はこれでやっていっていたわけですが,残念なことに Java からその Server::Starter テクノロジを利用するのは長らく不可能なものと思われてきました.しかし昨年2015年の中頃に Java からでも Server::Starter の利用が可能であることが id:tokuhirom 氏により発見された (+ Server::Starter にパッチが送られた) ため,Java からでもお手軽に Server::Starter を用いた graceful restart が出来るようになりました.
Server::Starter については参考になる記事がインターネット上にたくさんありますから適宜検索してもらうとして,Java から Server::Starter を利用する術については以下を参照してください.


さて本題ですが,タイトルが長いのでわかりやすく分割して書きますと,

  • Server::Starter を使って Java プロセスを立ち上げていて
  • JMX による 監視を有効にしていて
  • Graceful Restart を行った時

に,JMX が port を食い合ってしまうために上手く restart できないという問題についての話です.

例えば,以下のようにして Server::Starter + Java + jmx を起動してから,

$ java \
    ...
    -Dcom.sun.management.jmxremote \
    -Dcom.sun.management.jmxremote.port=5555 \
    -Dcom.sun.management.jmxremote.rmi.port=5555 \
    Main

restart を行うと,瞬間的に2つの Java のプロセスが立ち上がるため,JMX の connector server が port を食い合って (新プロセスの方が "Address already in use" を吐く) 新しいプロセスを上手く立ち上げることがきなくなります.こうした場合,connector server は立ち上がりませんし,手法によっては古いプロセス・新しいプロセスの両方とも終了せずに無限に起動し続けるといった,言うなればデッドロックのような状況に陥るかもしれません.

というわけでどうするかというと,コマンドラインオプションを指定する代わりに手で connector server を立ち上げるコードを書いて,daemon の起動時に立ち上げてやります.

public void startJMXConnectorServer() throws IOException {
    final int port = 5555;
    LocateRegistry.createRegistry(port);
    final JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi");
    final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
    JMXConnectorServerFactory.newJMXConnectorServer(url, null, mBeanServer).start();
}

こんな感じのものを書いて,立ち上げのタイミングで startJMXConnectorServer() を呼び出してやると,コマンドラインオプションで指定した時と同様の効果が得られます.
が,しかしこれで上手くいくかと思いきや,このままだとコマンドラインオプションを使った手法と同様で上手く resstart することが出来ません (依然 "Address already in use" が出る).
ので,苦肉の策でこうしてやる.

public void startJMXConnectorServer() {
    final int port = 5555;
    boolean isJMXLaunched = false;
    for (int i = 0; i < 100; i++) {
        try {
            LocateRegistry.createRegistry(port);
            final JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi");
            final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
            JMXConnectorServerFactory.newJMXConnectorServer(url, null, mBeanServer).start();
        } catch (IOException e) {
            // Might be "address already in use" error, retry
            try {
                Thread.sleep(300);
            } catch (InterruptedException ie) {
                log.warn("Interrupted");
            }
            continue;
        }
        isJMXLaunched = true;
        break;
    }
    if (!isJMXLaunched) {
        throw new RuntimeException("Failed to start JMX connector server");
    }
    log.info("JMX connector server started");
}

スピンロックのような感じで port が空くまで待って,port が空いた,すなわち古いプロセスが終了したら connector server を立ち上げるという感じ.これでひとまず動くっちゃ動く.良かった良かった.

しかしこの方法だと起動のタイミングによっては「古いプロセスの破棄」と「新しいプロセスの完全な立ち上がり」との狭間に落ちて,リクエストを取りこぼしてしまう可能性があるので,実装に (つまり startJMXConnectorServer() を呼び出すタイミングに) 気を使う必要がありそうです.
あるいは Web Application のようなものであればこんな方法を使わずに,起動してから最初にリクエストが来た瞬間に connector server を立ち上げてやる,というような方法でも良いかもしれませんね.

こちらからは以上です.