2018/09/27

TomcatのJMXにWebアプリの中からアクセスしたい

どうも、CTOの石田です。運用などはできればマネージドサービスに任せたいと切に願います。

プロダクション環境でのトラブルのうち出現頻度が高いものに、パフォーマンスが出ないというのがある。原因はいろいろとあるのだが、現象としてはリクエストの処理が滞り、それでも忖度せずにリクエストはバンバン来るので、処理中のリクエストの数が増加し、さらに状況が悪化するという経過をたどる。

JVM言語で書いたアプリケーションを動かすサーブレットコンテナの場合は、リクエスト処理スレッドのスレッドプールが処理中のスレッドで埋め尽くされて、新たなリクエストを受け取れなくなる。

こういうトラブルのきっかけはあるひとつの画面であったりするのだが、スレッドプールはコンテナ内で共用しているので、遅い画面へのリクエストのみでスレッドプールを埋めてしまうとサービス全体がダウンに至ってしまう。

画面ごとに選択的に流量制御する

特定の画面がスレッドプールを埋めているのであれば、サービス全体がダウンするよりも、その画面だけエラーを返して、特定のユーザーのみの影響で済ませたい。

できればあと少しで埋まってしまいそうな兆候が見られたタイミングでロードバランサーからマイルドに切り離し、正常に復帰するのを待ってまた復活させたい。

多くのロードバランサーのヘルスチェックはかなり乱暴だ。たいていのケースでは完全に埋まってしまって、ある程度時間が立たないと切り離されない。これは困る。

このとき、同時に何スレッドまでなら受けていいのか、いくつ以上なら 503 Service temporarily unavailable で返すべきなのか、ロードバランサーからマイルドに切り離すべきなのか。そのベストプラクティスはデプロイされる先のホストのCPU数、メモリサイズ、そしてコンテナのスレッドプールのサイズ設定に依存する。

自分のことは自分で調整したい。そういうわけで自分のWebアプリの中から、自分が動くコンテナのスレッドプールの状況を取得する必要が生じた。

Tomcatにおけるスレッドプールの状態取得

我々のアプリはTomcatで動いている。

稼働しているTomcatのスレッドプールの状態を取得する場合、JMXが利用できる。

JMXでTomcatの状態を取得するには、ソケット経由でJMXに接続するようにTomcatの起動時のオプションを設定する必要がある。
startup.shで起動するのであれば、CATALINA_OPTS環境変数に以下の値をセットすればよい。

ポート番号は空いているポートを選べばよいが、サーバーの外から接続できないようにファイヤウォールで守られていることを確認しなければならない。

この環境でJMXに接続するクライアントとして jmxterm を利用すると、以下のようなコマンドで

このような結果を得ることができる。

この結果は、200スレッドのプールに対して、現在34スレッドがビジー状態であることを現している。

JMXを用いると、このように currentThreadsBusy が特定のしきい値を超えたときに、アラートをあげたりロードバランサーから切り離したりといった対応が可能になる。

アプリ内から自分のTomcatを取得したい

上記の方法ではソケットを経由してプロセスの外から取得した。しかし、それだけのために新たにjavaプロセスを起動するのはエコではないし、画面ごとに流量制御をかけたいのでアプリ内からこの情報を取得する。

これはjava.lang.management.ManagementFactoryからTomcatの提供するMBeanを取得すれば簡単に実現できる。

自分が動くコンテナのJMXにアクセスするには、ManagementFactory.getPlatformMBeanServer() を利用するところがミソだ。

将来展望

とはいえ、アプリが自分の動く環境について気にしなければならないのは美しくない。

システム全体をマイクロサービス化して、kubernetesにデプロイするような世界であれば、サービスごとのサービスレベル制御ができるようになるはずだ。早くそういうモダンな世界で生活できるように可能なところから地道に取り組んでいく。