HOT deploy が遅くなるとき

実際に利用してみての注意点としては,再起動せずに変更を反映するホット・デプロイ機能は,クラス数が多くなると遅くなることを挙げた。開発が進みクラスが増えてくると,最終的はホット・デプロイを使わずにアプリケーション・サーバーを再起動した方が早くなったという。

Seasar Conference 2009 White でもそんな話があったようだし,最近 ML でも問い合わせがあったし,某巨大掲示板でも話題になってたので,少し (?) 書いておきます.


まず,HOT deploy では必要となったコンポーネントだけがロードされます.
例えば Web アプリであれば SAStrutsTeeda などのフレームワークがリクエストに応じて fooAction とか fooPage という名前のコンポーネントSeasar2 に要求するところから始まります.
その際,Seasar2ファイルシステムや Jar ファイルを走査するのではなく,必要なコンポーネントのクラスファイルをピンポイントで探します.
例えばルートパッケージが example で hoge_fooAction が要求されたなら,

  • example/web/hoge/FooAction.class

というクラスファイルを直接探します.
なので,クラス数が多くなってもこの部分が遅くなるということはほとんどないはずです.


次に,FooAction のコンストラクタやプロパティに DI するコンポーネントがあればそれを探します.
例えば FooAction が FooService 型のプロパティを持っていれば,それを探します.
さらに,FooService のコンストラクタやプロパティに DI するコンポーネントがあればそれを探します.
例えば FooService が FooDao 型のプロパティを持っていれば,それを探します.
というように,依存するコンポーネントがあるだけ連鎖的にコンポーネントインスタンス化され DI されます.
その際も,クラスはピンポイントで探します.
アプリケーション全体を走査するようなことはありません.


なので,単純なアプリケーション,例えば 1 つの Action には 1 つの Service が DI され,1 つの Service には 1 つの Dao が DI されるようなアプリケーションでは,その組の数がどんなに増えても HOT deploy が遅くなることはほとんどないはずです.
つまりこういうこと (矢印は DI される方向,依存の向きとは逆).

+-----------+    +------------+    +--------+
| FooAction |<---| FooService |<---| FooDao |
+-----------+    +------------+    +--------+

+-----------+    +------------+    +--------+
| BarAction |<---| BarService |<---| BarDao |
+-----------+    +------------+    +--------+

+-----------+    +------------+    +--------+
| BazAction |<---| BazService |<---| BazDao |
+-----------+    +------------+    +--------+

・・・

このようなアプリケーションは,どんなに大規模であっても HOT deploy でのレスポンスは一定のはずです.


インスタンス化されるコンポーネントの数が増えても,それが数百くらいなら極端に遅くなることもありません.
例えばうちの環境では,ClassLoader が 100 クラスロードするのにだいたい 40ms くらい,Seasar2 がそれをコンポーネントとして自動登録し,DI し終わるまでに 10ms くらい,合わせて 50ms くらいです.
おそらく 1000 クラスでも 1 秒かからないんじゃないかな.


これがもし 10 秒以上かかるなら,10000 クラスがコンポーネントとして自動登録されるということでしょうか?
もし 30 秒以上かかるなら,30000 クラスがコンポーネントとして自動登録されるということでしょうか?


確かに大規模なアプリであれば,万単位でクラスが存在することは普通にあるでしょう.
しかし,一回のリクエストでインスタンス化されるコンポーネントの連鎖が数万単位になるというのは,モジュール化の原則である低結合・高凝集に照らせば問題がある可能性が高いのではないでしょうか?
もちろん,一回のリクエストの中で数万単位のクラスが実際に使われるのなら,設計としては問題ないかもしれません.
それは HOT deploy が想定している範囲を超えた,複雑なアプリケーションということであり,遅くなるのはやむを得ないと思います.


しかし,インスタンス化され DI される数万単位のクラスが本当に全部使われれるのでしょうか?
実際は,適切なモジュール化がされていないために,芋づる式に DI されてしまうだけの使われないクラスがほとんどではないでしょうか?
例えばこんなケースが考えられます.

+-----------+    +------------+    +--------+
| FooAction |<---|            |<---| FooDao |
+-----------+    |            |    +--------+
                 |            |
+-----------+    |            |    +--------+
| BarAction |<---| FooService |<---| BarDao |
+-----------+    |            |    +--------+
                 |            |
+-----------+    |            |    +--------+
| BazAction |<---|            |<---| BazDao |
+-----------+    +------------+    +--------+

この例では,どの Action も一つの Service (FooService) に依存し,それは全ての Dao に依存しています.
これだと,FooService が FooAction から呼び出されたときは FooDao しか使わなかったとしても,BarDao も BazDao もインスタンス化されてしまいます.
FooService が BarAction から呼び出されたときは BarDao しか使わなかったとしても,FooDao も BazDao もインスタンス化されてしまいます.
このように,不必要なコンポーネントまでがインスタンス化され DI されるような構造になっていないでしょうか?


これは比較的簡単に測定できます.
Seasar2コンポーネントを自動登録する際,

クラス(example.web.hoge.FooService[hoge_FooService])のコンポーネント定義を登録します

といったログを出力します.
また,TraceInterceptor を適用すれば,コンポーネントのメソッドが呼ばれた際に

BEGIN example.web.hoge.FooService#hogeHoge()

といったログが出力されます.
このログを読み込んで,自動登録されたクラスの数と,自動登録されたけれどもメソッドが呼び出されていないクラスの数,およびその割合を求めるツールを作るのはたやすいでしょう.


その割合が 7〜8 割を越えているならどうということはないのですが,2〜3 割を下回るようなら,そしてそのような構造で HOT deploy が遅くなっているとしたら,それは HOT deploy の問題ではなく,モジュール設計に問題があるというサイン (悪臭ってやつ) と考えた方がいいでしょう.
そのような構造は,HOT deploy による開発時のみならず,COOL deploy による運用環境でも問題になる可能性があります.
Service や Dao など,多くのコンポーネントはデフォルトが prototype であり,リクエストの度に必要のないコンポーネントが大量にインスタンス化されている可能性があるからです.
それはレスポンスを劣化させ,GC の負担を増すことによりスループットを低下させているかもしれません.


このような事態を避けるには,原則通り低結合・高凝集となるようにクラスを分割することです.
多数のコンポーネントが DI されるクラスは多数のコンポーネントに依存しているということであり,おそらくは凝集度の低いクラスになってしまっている (様々なことをやろうとしすぎている) のではないでしょうか.
Seasar2 の public フィールドはあまりにも簡単に他のコンポーネントに依存することができてしまうため,注意しないと凝集度の低いクラスを作ってしまいやすいという面があるのかもしれませんが.


実際のアプリでは上記のような極端なケースより,一つの Action は 10 個の Service に依存し,各 Service は 10 個の Logic に依存し,各 Logic は10 個の Dao に依存する... という構造の方がありがちかもしれません.
少数の巨大なクラスが存在するのではなく,単独で見ればほどほどのサイズのクラスばかりなのに,全体として巨大なツリーを作ってしまうケース.
Teeda の Page クラスにせよ,SAStruts の Action クラスにせよ,複数のリクエストにまたがって使われるクラスなので,それぞれのリクエストで必要な Service や Dao をプロパティとして持つことになりやすく,結果として特定のリクエストでは不要なコンポーネントが大量にインスタンス化されるような作りになってしまっているケースは多いかもしれません.
一つの画面でいろいろなことができるようになっているとそうなりやすいかも.


こういうケースでは,本当にそのコンポーネントが必要になるまで DI を遅延することができると効果的だと思われます.
Seasar2 ではそのような機能は提供していませんが,AOP を使えばできないことはありません.
例えば

public class FooAction
  ...
  @DependencyLookup
  public FooService getFooService() {
    return new FooService();
  }
  @DependencyLookup
  public FooService getBarService() {
    return new BarService();
  }
  ...
}

なんてクラスがあるとして,@DependencyLookup が指定された getter メソッドに対して,コンテナからコンポーネントをルックアップして返すようなインターセプタを適用すれば遅延 DI 的な動きになります.
さすがに public フィールド使ってるクラスにそのまま適用とはいかないし,getter/setter 使ってる場合も getter メソッドを削除してもらわないといけなかったりしますが.
HOT deploy が遅くなったというケースをこれで救えるのなら,そのようなインターセプタを作って Seasar2 に含めるのもありだと思いますが,どうでしょうか?
需要があれば考えます.


最後に S2DaoOracle の組み合わせについて.
Oracle の場合,DatabaseMetaData を取得するのが途方もなく遅い (秒単位だっけ?) ので,Dao が数十個でも極端に遅くなる場合があり得ます.
そのようなケースでは,メタデータを使わないで初期化する設定にするのが効果的です.


以上,HOT deploy が遅くなるケースについて考察してみました.
他に「こんなケースで遅くなる」というのがあればお知らせください.
HOT depoy で対処可能であれば検討します.