FreeBSDでmod_jk/mod_jk2を使ってApache2.0とTomdcat4をつなげる

2004.5.17 付記

新たに、FreeBSD 4-STABLE で mod_jk2 を使うというのを書きました。

はじめに

FreeBSD 4.7-RELEASE 上で、mod_jk 及び mod_jk2 を使って Tomcat4 と Apache 2.0 をつなげようと、いろいろいじってみた。 その一部始終をまとめる(←「一部始終」の使い方間違い:-)。

ここに記述されていることは、プログラムのバージョンが違うとあてはまらない可能性がある。 特に mod_jk2 は(2003年1月現在)まだ開発途中であり、今後のバージョンアップによってここにかかれていることが obsolete になる可能性が高いので注意してほしい。

必要なプログラムのインストール

必要となるプログラムとそのインストール方法は、以下の通りである。

apache-2.0.43

ports からインストールする。

pthread つきでビルドされているかどうかで mod_jk/mod_jk2 のビルド方法が異なる。 ports のデフォルトでは、MPM に prefork が使用され、pthread なしでビルドされる。 MPM に prefork を使用したまま pthread つきでビルドするには、ports の make 時に WITH_THREADS=yes を指定する。 ports の make 時に WITH_MPM=worker を指定した時などで、MPM に prefork 以外を使用する場合はもちろん pthread つきでビルドされる。

jdk-1.3.1p7

ports からがんばってインストールする。

ユーザの環境変数 JAVA_HOME に /usr/local/jdk1.3.1 を設定し、PATH 変数に $JAVA_HOME/bin を追加しておく。

jakarta-tomcat-4.1.18

http://jakarta.apache.org/builds/jakarta-tomcat-4.0/release/v4.1.18/bin/ から jakarta-tomcat-4.1.18.tar.gz をダウンロードし、インストール先のディレクトリに展開する。 以降では、展開して生成されるディレクトリ jakarta-tomcat-4.1.18 を $CATAILNA_HOME と表す。

jakarta-ant-1.5.1

mod_jk/mod_jk2 のビルドに使用する。

http://www.apache.org/dist/ant/binaries/ から jakarta-ant-1.5.1-bin.tar.gz をダウンロードし、インストール先のディレクトリに展開する。 展開してできるディレクトリ jakarta-ant-1.5.1/bin をユーザの PATH 変数に追加しておく。

libtool-1.3.4_4

mod_jk/mod_jk2 のビルドに使用する。

ports からインストールするが、apache 2.0 を ports で make すると依存関係により自動的にインストールされる。

mod_jk/mod_jk2 のインストール

準備

http://jakarta.apache.org/builds/jakarta-tomcat-4.0/release/v4.1.18/src/ から jakarta-tomcat-connectors-4.1.18-src.tar.gz をダウンロードし、適当なディレクトリに展開する。 展開してできるディレクトリ jakarta-tomcat-connectors-4.1.18-src を以後、$CONNECTORS_SRC_HOME と呼ぶこととする。

$CONNECTORS_SRC_HOME/jk に以下の内容のファイル build.properties を作成する。

build.properties
apache2.home=/usr/local
apache2.include=/usr/local/include/apache2
apr.include=/usr/local/include/apache2
apr.lib=/usr/local/lib/apache2

ディレクトリ $CONNECTORS_SRC_HOME/jk で次のコマンドを実行し、ビルド用の ant task をコンパイルする。

% ant jkant

mod_jk のインストール

mod_jk.so の作成とインストール

$CONNECTORS_SRC_HOME/jk/native/build.xml に以下の変更を加える。

--- jk/native/build.xml.orig	Thu Dec 19 22:52:00 2002
+++ jk/native/build.xml	Tue Jan 28 23:08:23 2003
@@ -50,6 +50,9 @@
                file="${iplanet.home}" />
     <available property="HAVE_APR" 
                file="${apr.include}/apr.h" />
+    <condition property="freebsd">
+      <equals arg1="${os.name}" arg2="FreeBSD"/>
+    </condition>
     <mkdir dir="${jk.build}/jk" />
   </target>
 
@@ -165,6 +168,7 @@
         <include name="${java.home}/../include/linux"  if="linux"/>
 	<include name="${novelllibc.dir}/include" if="netware" />
 	<include name="${novelllibc.dir}/include/winsock" if="netware" />
+	<include name="${java.home}/../include/freebsd" if="freebsd" />
       </includes>
       <depends>
 	<fileset dir="${native.dir}/common" includes="*.h" />
@@ -208,6 +212,10 @@
       <linkOpt value="-flags AUTOUNLOAD, PSEUDOPREEMPTION" if="netware" />
       <linkOpt value="-entry _LibCPrelude" if="netware" />
       <linkOpt value="-exit _LibCPostlude" if="netware" />
+
+      <def name="FREEBSD" if="freebsd" />
+      <def name="_THREAD_SAFE" if="freebsd" />
+
     </so>
 
   </target>
上記は、apache 2.0 を pthread つきでビルドしたの場合のものである。 apache が pthread つきでビルドされていない場合は、 _REENTRANT_THREAD_SAFE の二つのマクロの定義を削除すればよい。

ディレクトリ $CONNECTORS_SRC_HOME/jk/native で以下のコマンドを実行して、mod_jk をビルドする。

% ant apache20

上記により、ディレクトリ $CONNECTORS_SRC_HOME/jk/build/jk/apache2 に mod_jk.so が生成されるので、これをディレクトリ /usr/local/libexec/apache2/ にコピーする。

workers.properties の作成

ディレクトリ /usr/local/etc/apache2 に、ファイル workers.properties を作成する。

workers.properties
worker.list=ajp13
worker.ajp13.port=8009
worker.ajp13.host=localhost
worker.ajp13.type=ajp13
worker.ajp13.cachesize=25
worker.ajp13.cache_timeout=0

上記の cachesize は、mod_jk と tomcat との間のコネクションのキャッシュであり、ここで指定した数のコネクションが再利用される。 リクエストを処理する時に、コネクションがキャッシュされていれば、それを再利用し、そうでなければ新しいコネクションが作成される。 リクエストの処理終了時にキャッシュされているコネクションの数が cachesize 未満であれば、それをキャッシュに入れ、そうでなければコネクションはクローズされる。

キャッシュはプロセス毎のものなので、MPM が prefork の場合は意味をなさない。デフォルトの 1 とすべきである。 MPM が prefork 以外の場合、後述するように、マルチスレッドで動作したときにソケットの接続処理に極端に時間がかかるという問題があるため、http.conf の MaxThreadsPerChild の値の範囲内である程度の数値を設定しておかないと、複数のクライアントから同時にクエストがなされたときに、動作が遅くなる。

キャッシュされたコネクションは、cache_timeout で設定された時間(秒)使用されなければ破棄される。 0 は、タイムアウトなしで、そのコネクションに対してエラーが発生しないかぎり、プロセスが生きている間、何度も再利用される。 キャッシュされたコネクションは、cache_timeout で設定された時間利用されなければすぐに破棄される、というわけではない。 リクエストの処理でコネクションを再利用しようとしたときに、そのコネクションが cache_timeout で設定された時間以上使われていなければ、それが破棄され新しいコネクションが生成される。 cache_timeout に 0 以外の値を設定することに、どのような意義があるのかはよくわからない。

httpd.conf の編集

/usr/local/etc/apache2/httpd.conf に以降に示す内容を追加する。

下記は、モジュール mod_jk.so をロードするためのものである。

LoadModule jk_module libexec/apache2/mod_jk.so

下記は、Tomcat の example を apache 経由で利用する場合の mod_jk の設定である。

<IfModule mod_jk.c>
    JkWorkersFile  /usr/local/etc/apache2/workers.properties
    JkLogFile      /var/log/mod_jk.log
    JkLogLevel     info
    JkMount        /examples/servlet/* ajp13
    JkMount        /examples/*.jsp ajp13
</IfModule>

下記は、 Tomcat の example にある html ファイルを、直接 Apache が参照できるようにするための設定である。

Alias /examples "/usr/local/jakarta-tomcat/webapps/examples"
<Location "/examples/">
    Options Indexes FollowSymLinks
</Location>

<Location "/examples/WEB-INF/">
AllowOverride None
deny from all
</Location>

Tomcat 側のコネクタの設定

Tomcat は、デフォルトで Coyote AJP 1.3 コネクタが有効になっているので、これを利用する。

mod_jk2 のインストール

mod_jk2.so の作成とインストール

$CONNECTORS_SRC_HOME/jk/native2/build.xml に以下の変更を加える。

--- jk/native2/build.xml.orig	Tue Jan 28 23:01:12 2003
+++ jk/native2/build.xml	Fri Jan 24 18:58:37 2003
@@ -86,6 +86,9 @@
     <condition property="netware">
       <available file="novellndk.home" />
     </condition>
+    <condition property="freebsd">
+       <equals arg1="${os.name}" arg2="FreeBSD"/>
+    </condition>
 
     <echo message="Linux:${linux} Win32:${win32} Netware:${netware} Solaris:${solaris} HPUX:${hpux}" />
   </target>
@@ -289,6 +292,8 @@
       <def name="_MBCS" if="win32" />
       <def name="_USRDLL" if="win32" />
       <def name="MOD_JK2_EXPORTS" if="win32" />
+      <def name="FREEBSD" if="freebsd" />
+      <def name="_THREAD_SAFE" if="freebsd" />
       <src dir=".">
 	<include name="server/apache2/*.c" />
 	<include name="common/*.c" />
@@ -306,6 +311,7 @@
         <include name="${java.home}/../include/win32" if="win32" />        
         <include name="${java.home}/../include/solaris" if="solaris" />        
         <include name="&quot;${mssdk.home}/include&quot;" if="win32"/>
+        <include name="${java.home}/../include/freebsd" if="freebsd"/>
       </includes>
       <depends>
 	<fileset dir="${native.dir}/common" includes="*.h" />
上記は、apache 2.0 を pthread つきでビルドしたの場合のものである。 apache が pthread つきでビルドされていない場合は、 _REENTRANT_THREAD_SAFE の二つのマクロの定義を削除すればよい。

ディレクトリ $CONNECTORS_SRC_HOME/jk/native2 で以下のコマンドを実行して、mod_jk2 をビルドする。

% ant apache20

上記により、ディレクトリ $CONNECTORS_SRC_HOME/jk/build/jk2/apache2 に mod_jk2.so が生成されるので、これをディレクトリ /usr/local/libexec/apache2/ にコピーする。

workers2.properties の作成

ディレクトリ /usr/local/etc/apache2 に、ファイル workers2.properties を作成する。 以下は、Tomcat の example を apache 経由で利用する場合の設定である。

workers2.properties
[logger]
level=INFO

[config:]
file=${serverRoot}/etc/apache2/workers2.properties

[uriMap:]

[shm:]
file=/var/log/jk2.shm
size=1000000
disabled=0

[workerEnv:]

[channel.socket:localhost:8009]
tomcatId=localhost:8009

[status:]

[uri:/jkstatus/*]
group=status:

[uri:/examples/servlet/*]

[uri:/examples/*.jsp]

mod_jk2 には、mod_jk にある cachesize の指定はなく、代りに max_connections という設定がある(上の例では設定していない)。 これは、mod_jk の cachesize とは異なり、文字通りコネクションの最大数であり、mod_jk2 の一つプロセスに対してこの数を越えるリクエストが同時に割り当てられた場合、オーバーしたリクエストがエラーとなる。 デフォルトは、無制限である。

mod_jk にあったコネクションのタイムアウトの設定は mod_jk2 にはなく、コネクションに対してエラーが発生しないかぎり、プロセスが生きている間、何度も再利用される。

httpd.conf の編集

/usr/local/etc/apache2/httpd.conf に以降に示す内容を追加する。

下記は、モジュール mod_jk2.so をロードするためのものである。

LoadModule jk2_module libexec/apache2/mod_jk2.so

下記は、 workers2.properties の位置を指定するためのものである。

<IfModule mod_jk2.c>
    JkSet  config:file  ${serverRoot}/etc/apache2/workers2.properties
</IfModule>

下記は、 Tomcat の example にある html ファイルを、直接 Apache が参照できるようにするための設定である。

Alias /examples "/usr/local/jakarta-tomcat/webapps/examples"
<Location "/examples/">
    Options Indexes FollowSymLinks
</Location>

<Location "/examples/WEB-INF/">
AllowOverride None
deny from all
</Location>

Tomcat 側のコネクタの設定

Tomcat は、デフォルトで Coyote AJP 1.3 コネクタが有効になっているので、これを利用する。

使ってみる

次の4通りの場合について、動作確認を行った。

mod_jk/mod_jk2 の設定については、インストール のところで記述したものを使用した。 Apache の設定は インストール のところで記述したものの他、ServerAdmin などの設定を行っているが、MaxClients など性能に関する設定はデフォルトのままである。 Tomcat についても、後述する setenv.sh の設定以外は、デフォルトである。

なお、テストに使用したマシンは、 Pentium II 400 MHz 、実メモリ 256 メガバイトである。 サーブレットを動作させるにはスペックが低い環境であるため、性能の数値に関しては参考程度に考えてほしい(速いマシンがほしい:-)。

動かし方

起動は、Tomcat を最初に実行し、次に Apache を実行する。

# $CATALINA_HOME/bin/startup.sh
# /usr/local/etc/rc.d/apache2.sh start
Tomcat や Apache が既に起動されている場合は、もちろん先に終了してから上記のコマンドを実行する。終了するには、次のコマンドを実行する。
# /usr/local/etc/rc.d/apache2.sh stop
# $CATALINA_HOME/bin/shutdown.sh

システムの立ち上げ時に Tomcat を起動するには、/usr/local/etc/rc.d/apache2.sh に Tomcat の起動と終了を行うように記述しておけばよい。 この場合、セキュリティを考慮して、特定のユーザで Tomcat を起動するようにすべきである(例えば、「su -m www -c "$CATALINA_HOME/bin/startup.sh"」として Tomcat を起動する)。 この時、$CATALINA_HOME 以下の work, log, temp など書き込みを行うディレクトリ及びファイルのアクセス権を適切に設定しておく必要がある。

Tomcat と Apache の起動後、ブラウザで http://hogehoge/examples/servlet/HelloWorldExample などにアクセスすると、サーブレットが起動され、ブラウザにサーブレットが作成したレスポンスが表示される。 また、mod_jk2 を使用した場合、 http://hogehoge/jkstatus/ にアクセスすると、mod_jk2 の状態がブラウザの画面に表示される。

mod_jk2 を使用した場合、Apache の起動時に /var/log/httpd-error.log に次のエラーが出力される。

[error] mod_jk child init 1 0
また、次のエラーが出力される場合もある。
[error] jk2_init() Can't find child 4421 in scoreboard
[error] mod_jk child init 1 -2
上記のエラーが出力されているものの、リクエストは正常に処理されており、特に障害が発生することはなかった。

安定性はどうか

Apache に付属の Apache Benchmark を使って、リクエストが頻発した場合の動作を確認してみた。 別のホストから次のコマンドを実行した。

ab -c 50 -n 10000 http://hogehoge/examples/servlet/HelloWorldExample
上記は、最大並列度 50 で合計1万回のリクエストを発行する。 このテストを行うと、デフォルトでは JVM のヒープサイズが不足するので $CATALINA_HOME/bin にファイル setenv.sh を作成し、最大ヒープサイズを指定しておく必要がある。 以下は、初期ヒープサイズと最大ヒープサイズを共に 128 メガバイトにする例である。
setenv.sh
JAVA_OPTS="-Xms128M -Xmx128M"

結果、MPM が prefork の場合は、mod_jk/mod_jk2 共に、全て正常に処理された。 しかし、MPM を worker にした場合、何度か以下のエラーが発生した。

[jk_connect.c (203)]: jk_open_socket, connect() failed errno = 60
[jk_ajp_common.c (626)]: Error connecting to tomcat.
Tomcat is probably not started or is listenning on the wrong port. Failed errno = 60
[jk_ajp_common.c (874)]: Error connecting to the Tomcat process.
[jk_ajp_common.c (1190)]: sending request to tomcat failed in send loop. err=0
上記は mod_jk の場合であるが、mod_jk2 でも同様のエラーが発生する。 エラーの内容は、見ての通り mod_jk から tomcat へソケットで接続しようとしたときにタイムアウトが発生した、というものである。 当初、 Tomcat 側のコネクタの最大プロセッサ数(デフォルトでは75) を越えてコネクションをはろうとしたためと推定したが、調査したところそうではない。 性能のところでも考察するが、マルチスレッドで動作しているときに、ソケットの connect() 関数の実行に極端に時間がかかる場合があるようである。

ab -c 100 として、最大並列度 100 で実行してみたところ、MPM が prefork の場合でも、httpd のプロセスが kill されるなど不安定な状態になった。 実際に運用する場合は、httpd.conf の MaxClients の設定でクライアント数を制限する必要がある。 なお、制限値はマシンの性能によって変ってくるはずである。

性能を測定する

別ホストから以下のスクリプトを実行し、性能を測定した。

#! /bin/sh

HOST=${1:-hogehoge}

AB=/usr/local/sbin/ab

do_test() {
    name=$1
    url=$2
    ${AB} ${url} > /dev/null
    for c in 5 10 15 20
      do
      ${AB} http://${HOST}/examples/servlet/GCServlet > /dev/null
      sleep 10
      ${AB} -c ${c} -n 1000 -g ${name}${c}.dat ${url} > ${name}${c}.txt
  done
}

do_test hello http://${HOST}/examples/servlet/HelloWorldExample
do_test foo http://${HOST}/examples/jsp/simpletag/foo.jsp
上記で、GCServlet は、System.gc() を呼び出して GC を行うための自作のサーブレットである。

スクリプトの動作は、以下の通りである。

  1. http://hogehoge/examples/servlet/HelloWorldExample に一度アクセスする。
  2. http://hogehoge/examples/servlet/HelloWorldExample に対して、最大並列度 5, 10, 15, 20 で、それぞれ 1000 回リクエストを発行し時間を計測する。 それぞれの時間の計測の前には、GC を行う。
  3. http://hogehoge/examples/jsp/simpletag/foo.jsp について、上記 1, 2 と同様のテストを行う。

各リクエストの処理時間をグラフにしたものが これ である。 グラフの横軸は、テスト開始時刻を 0 としたときのリクエスト発行時刻である。 なお、tomcat http とあるのは、Apache を経由させずに、Tomcat の HTTP コネクタにアクセスした場合である。

グラフを見ると、MPM が worker の場合、HelloWorldExample の各テストで、テスト開始直後のリクエストで極端に時間がかかっているのがわかる。 mod_jk/mod_jk2 から、から tomcat への接続に時間がかかっているものと思われる。 コネクションがキャッシュされた以後の simpletag 並列度 5, 10, 15 のテストでは、この現象は発生していない。 simpletag のテストの並列度 20 のテストの開始直後にやはり時間がかかっているリクエストがあるが、mod_jk/mod_jk2 の 2 つのプロセスに割り当てられる同時リクエスト数が完全に均等ではないため、ここで mod_jk/mod_jk2 から tomcat への接続処理が発生したためと思われる。

MPM が prefork の場合、ほぼ全てのテストについて何度か数秒の時間がかかっているリクエストが存在する。 この現象は全てテストを開始してから約4秒以内の前半の時間帯で発生している。

グラフの縦軸のスケールを拡大したものが これ である。 これを見ると、Apeche を経由した場合、極端に時間がかかっている部分を除き各コネクタでのリクエストの処理時間は大差ないようである。 Tomcat の HTTP コネクタへ直接アクセスした場合は、さすがに処理時間が短くばらつきも少ない。

各テストの Processing time の最大値、最小値、平均値及び標準偏差を次の表に示す。

Processing times (単位はミリ秒)
MPM コネクタ コンテンツ(並列度) 最小値 平均値 標準偏差 最大値
prefork mod_jk HelloWorld(5) 9 52 324.3 6279
HelloWorld(10) 8 101 266.2 6317
HelloWorld(15) 17 156 259.8 3267
HelloWorld(20) 9 208 606.7 6565
simpletag(5) 22 75 7.1 161
simpletag(10) 12 150 134.6 3153
simpletag(15) 14 225 842.9 12793
simpletag(20) 31 303 462.7 9736
mod_jk2 HelloWorld(5) 9 50 257.1 6273
HelloWorld(10) 8 103 172.6 3161
HelloWorld(15) 9 159 540.0 6695
HelloWorld(20) 8 223 917.5 9469
simpletag(5) 13 76 9.1 143
simpletag(10) 13 153 167.5 3182
simpletag(15) 27 232 381.4 6519
simpletag(20) 14 308 578.8 6570
worker mod_jk HelloWorld(5) 9 49 370.3 9478
HelloWorld(10) 9 100 372.4 9558
HelloWorld(15) 9 159 743.6 15809
HelloWorld(20) 27 206 440.7 9620
simpletag(5) 17 73 7.9 157
simpletag(10) 21 150 16.5 237
simpletag(15) 14 226 24.3 436
simpletag(20) 31 302 110.0 3586
mod_jk2 HelloWorld(5) 9 49 370.4 9453
HelloWorld(10) 9 103 430.3 9526
HelloWorld(15) 9 156 541.6 12631
HelloWorld(20) 9 207 489.0 9629
simpletag(5) 15 74 7.6 156
simpletag(10) 17 151 14.3 236
simpletag(15) 17 228 21.4 323
simpletag(20) 14 304 299.4 6519
TomcatのHTTPコネクタ HelloWorld(5) 8 36 5.3 106
HelloWorld(10) 9 72 7.8 137
HelloWorld(15) 10 108 7.8 119
HelloWorld(20) 11 145 11.7 159
simpletag(5) 16 59 7.7 146
simpletag(10) 17 118 9.4 194
simpletag(15) 18 180 32.0 440
simpletag(20) 19 236 17.9 257

MPM が worker の場合のコネクション時間の問題を確認するため、mod_jk の workers.properties で cachesize と cache_timeout を無効に設定して同様のテストを行った。この場合、キャッシュサイズは1、タイムアウトは1秒になる。 MPM が prefork, worker の場合、及びコネクションキャッシュの設定有無の場合それぞれについて、結果をグラフにしたのが これ である。

MPM が prefork の場合は、コネクションキャッシュの設定がある時とない時の傾向は同じである。 コネクションキャッシュの設定がない場合、各テストの前に 10 秒スリープしているため、テスト開始時に前回のテストでキャッシュされた全ての接続が破棄され、新たなコネクションが生成される。 テスト結果からは、コネクションキャッシュの設定があるときと比較して、各テストの開始時に特に大きな性能の劣化はみられず、mod_jk と Tomcat との接続にさほど時間がかかっていないことがわかる。

MPM が worker の場合、キャッシュはプロセス毎にひとつだけなので、コネクションキャッシュの設定がないときは、大部分のリクエストで mod_jk と Tomcat とのコネクションが生成される。 グラフを見ると、コネクションキャッシュの設定がある時にテスト開始直後にだけ時間がかかっているリクエストが存在していたのが、キャッシュの設定がない時は全体を通して時間がかかっているリクエストが幾つか存在するのがわかる。 なお、このテストでコネクションタイムアウト(75秒)のエラーが何度か発生したが、mod_jk の中でリトライの処理が行われ、リクエストそのものはすべて正常に終了した。 グラフからは分からないが、MPM が worker でキャッシュの設定なしの場合、並列度が 5 の場合を除き、テスト開始直後のリクエストの処理時間が約 75 秒であり、テストが終了しているのは、テスト開始から 75 秒後である。 リクエストの発行はグラフの通りテスト開始から 12 〜 16 程で全て終了しているが、その後 CPU を使用していない時間が 1 分程あって、コネクションのタイムアウトが発生した後、リトライが成功してテストが終了している。 原因の詳細は不明であるが、FreeBSD の pthread 環境におけるソケットの処理に問題があるのではないかと睨んでいる。

グラフの縦軸のスケールを拡大したものが これ である。 これを見ると、一見、MPM が worker でキャッシュの設定なしの場合の各リクエストの処理時間が短く性能がよいように見えるが、これは、Tomcat との接続で止まっているリクエストが常に複数存在するため、結果として同時に処理しているリクエストの数が少ないためである。