まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:tomcat

今まで Tomcat を使う場合は使い慣れた(理由はそれだけじゃないけど) Tomcat6 を使っていました。試しに Tomcat7 を導入する機会があったので、その導入手順を紹介します。環境はいつもの CentOS 6 です。


まずは Java/JDK 環境が必要です。今回は OpenJava JDK 1.7 を導入することにしました:
# yum install java-1.7.0-openjdk-devel

さて Tomcat7 です。実は CentOS 6 の標準リポジトリには Tomcat7 は含まれていません。標準で含まれているのは Tomcat6 ですが、yum でインストールする場合は
# yum install tomcat6

のように、わざわざ「6」を指定して導入する必要があります。この辺りの導入手順の詳細はこちらを参照してください:
CentOS に Apache Tomcat を導入する

さて Tomcat7 です。Tomcat6 は「6」を指定する必要がありました。Tomcat7 も「7」を指定する必要があるかというと、これがありません(笑)。コマンドとしては
# yum install tomcat

で導入されるのですが、普通にそのまま実行しても「見つからない」というエラーになるだけだと思います。Tomcat7 を yum でインストールするには事前に EPEL リポジトリの登録が必要です。つまり、まずはこのコマンドを実行します:
# rpm -Uvh http://ftp.riken.jp/Linux/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm

その後に yum で install です。tomcat-admin-webapps パッケージを指定して、管理コンソール画面ごとインストールします:
# yum install tomcat tomcat-admin-webapps

管理コンソール画面にアクセスするための設定を追加します。/etc/tomcat/tomcat-users.xml をテキストエディタで編集し、以下の3行を追加します:
<role rolename="admin-gui"/>
<role rolename="manager-gui"/>
<user username="admin" password="P@ssw0rd" roles="admin-gui,manager-gui"/>
   ↑この例だとユーザー名: admin、パスワード: P@ssw0rd でアクセスするユーザーを作成


インストール後は "tomcat" というサービス名で起動したり、再起動したり、自動実行指定ができます:
# /etc/init.d/tomcat start
# chkconfig tomcat on

管理コンソール画面ごとインストールした場合は、http://(IPアドレス:8080)/manager/html で管理画面にアクセスできます。Basic 認証でユーザー名とパスワード(上記設定の場合であれば admin と P@ssw0rd)を指定してログインできます:
2015122801


CentOS6 で Tomcat7 を使う場合は EPEL リポジトリの登録、が肝です。


ウェブサービスを開発・公開する上で、URL のリライトの問題があります。

例えば、アプリケーションとしては
 http://xxx.myserver.com/abc.php?country=Japan&pref=Chiba&city=Funabashi
というパラメータを受け取って処理するようなページやサービスがあったとします。

単に動かすだけなら、このパラメータ処理でいいのですが、SEO 対策や、単に長くて見難いという理由から、これを
 http://xxx.myserver.com/abc.php/Japan/Chiba/Funabashi
のような URL パターンで受け取りたいことが出てきます。

この例であれば、
 http://xxx.myserver.com/abc.php/AAA/BBB/CCC
という URL パターンを有効な URL として認識し、実際に処理する際には
 http://xxx.myserver.com/abc.php?country=AAA&pref=BBB&city=CCC
と内部的に書き直し(ReWrite)してから処理をする、ということになります。これが URL リライトです。

HTTP サーバーに Apache HTTPD を使っている場合は、この URL リライト機能が標準モジュールになっていて、mod_rewrite モジュールを有効にして、httpd.conf や .htaccess に変換ルールを記述するだけで URL リライトが実現できます。 ではウェブサーバーに Apache Tomcat など Apache HTTPD を経由しない Java アプリケーションサーバーを使っている場合のリライト処理はどうすればいいでしょう??


その答の1つが UrlRewriteFilter です。jar ファイル1つをプロジェクトに組み込み、専用の XML ファイルに変換ルールを記述することで、Java アプリケーションサーバーでも mod_rewrite のようなリライト処理を実現できるようになります:
urlrewritefilter00


実際に使ってみたサンプルを紹介します。まずは公式ページから urlrewritefilter-4.0.3.jar ファイルと、サンプルの urlrewrite.xml ファイルをダウンロードして保存します。
urlrewritefilter01


Java アプリケーションのプロジェクト内にこれらのファイルを配置します。まず urlrewritefilter-4.0.3.jar は WEB-INF/lib/ に、urlrewrite.xml は WEB-INF にそれぞれ保存します。次に web.xml を編集します:
urlrewritefilter02

WEB-INF/web.xml の </web-app> の直前に次の内容を追加して、このウェブアプリケーション内の全ての URL パターン(/*)に UrlRewriteFilter フィルターを適用することを宣言します:
  :
  :
<filter>
  <filter-name>UrlRewriteFilter</filter-name>
  <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>UrlRewriteFilter</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>REQUEST</dispatcher>
  <dispatcher>FORWARD</dispatcher>
</filter-mapping>

</webapp>

実際の変換パターンは WEB-INF/urlrewrite.xml ファイルの <urlrewrite> 要素内に記述します。以下の例では2つの変換パターンを記載しています:
<urlrewrite>
  :
  :
  <rule>
    <from>/urlrewrite/(.*)/(.*)/(.*)</from>
    <to>/urlrewrite?x=$1&y=$2&z=$3</to>
  </rule>
  <rule>
    <from>/urlrewrite/(.*)/(.*)</from>
    <to>/urlrewrite?a=$1&b=$2</to>
  </rule>
</urlrewrite>

前者では /urlrewrite/ 以下に / をセパレータとして3つのパラメータを受け取る、つまり
 /urlrewrite/XXX/YYY/ZZZ 
のような URL 指定があった場合に、内部的に
 /urlrewrite?x=XXX&y=YYY&z=ZZZ
のようにリライトして処理する、という変換パターンを記載しています。

後者では同様に2つのパラメータを受け取った時、つまり
 /urlrewrite/AAA/BBB
のような URL 指定があった場合に、内部的に 
 /urlrewrite?a=AAA&b=BBB
のようにリライトして処理する、という変換パターンを記載しています。

#上記例ではパラメータが2つの時と3つの時とで、異なるパラメータ変数に渡すようにしています。
 もちろんこの部分は定義次第です。


後はこれらのパラメータ(リライト後のパラメータ)に対応して処理するような /urlrewrite を(サーブレットなどで)実装すればよい、ということになりますね。




 

リアルタイムに商品の最安値を調べるサービス「ねっぴ」を開発・運営しています:
ねっぴ(http://neppi.co/) 


サービス内のカテゴリを展開したり、検索窓から製品を検索して商品を絞込むと、その商品の販売を取り扱っていそうなウェブサービスを探し、取り扱っていた場合はそのサービス内での最安値を探し、最終的には価格順にソートして「どこで買うと安いか」を表示してくれる(クリックすればそのまま購入ページにいける)、というサービスです:
2015030500


有名な価格比較サイトと同様のサービスと理解いただくのが分かりやすいと思います。ただ上記のようにそのサイトもねっぴでは比較対象の1つと考えていて、必ずしもそのサイトが最安値を提供しているわけではないことがわかります。ねっぴは「その場で」&「リアルタイムに」価格を検索して比較する点が異なります。


サービスの使い方はこちらに簡単なオンラインヘルプを用意しています。ユーザートラッキングなどを目的とはしていないため、面倒なサインアップなど不要で、誰でも利用できます:
「ねっぴ」の使い方


本ブログエントリでは、「ねっぴ」の環境や技術的な内容について紹介します。
参考までに、こちらは以前に某LTでねっぴと試作段階の TweetsMapper について紹介した資料です:
ねっぴ on IDCF


上記資料内でも触れていますが、元々ねっぴは自宅サーバー環境で開発・動作確認等を行い、テスト運用を経て何度かの引越を経験し、現在は IDC フロンティア 様の IDCF クラウドを利用して運用しています。


基本構成としては一般的(?)な Linux + Tomcat + MySQL で、高速化のために一部 memcached を併用しています。 データベースの日次バックアップには Amazon S3 互換の Object Storage を使っています(参考)。 IDCF クラウドのサーバーインスタンスにはスワップメモリ領域が用意されていなくて不安(というか不安定)だったので、Linux の起動時にスワップファイルを作成して利用するようにしました(参考)。 更に IDCF を通じて利用できる Mackerel 監視サーバーを有効化するため、エージェントを導入しています:
2015030502


ここまでの環境を IDCF クラウドの light.S1 サーバー1台(!)でまかなえています。ディスクは ROOT の 15GB のみで追加なし、Mackerel は IDCF を通じて使う場合の特別プランが用意されていて、追加料金なしでもそれなりに使えます。Object Storage とネットワーク I/O は無料枠の範囲内で収まっています。

・・・というわけで、この「ねっぴ」の環境はバックアップや監視まで含めて light.S1 サーバーの月額最低料金である 500 円で実現している(!)、ということになります:
2015030501


うーん、我ながら俺って優秀やりくり上手でいい主夫になれるかもw まあやりくり上手というよりは「無茶」なだけかもしれませんけど。。。

ただこの環境を IDCF の light.S1 サーバー1台で提供している背景についても触れておく必要があります。
もともと IDCF クラウドに移る前は別業者の同価格帯のクラウド(というか VPS)を使っていました。サービスとして公開できていましたが、開発環境をそっくりそのまま公開した感じで、パフォーマンスレベルなどは二の次、という感じでした。開発者視点で考えると、そもそもどのレベルを求めるのか?という話にもなってしまい、現在もそこまでしっかり考えて公開しているわけではないのですが、当時は単に「動いたものをそのまま公開した」というレベルでした。その時点ではバックアップはしていません。監視もシェルスクリプトレベルでした。

このサービスは「リアルタイム価格検索」を行うことと、商品マスターをクローリングするという特性上、ネットワークのバックボーン性能がサービス性能に影響する度合いが大きいという特徴があります。実はここだけで見ると、自宅ネットワーク内のサーバーで運用する、という選択肢もありました。

その頃、当時仕事上で携わっていた業務で IDCF クラウドを使うことになり、light.S1 インスタンスが500円/月で提供されていること、ネットワークバックボーンに関しては非常に高いコストパフォーマンスを発揮してくれることが魅力でした。 


とはいえ移行を考えると、上記のように IDCF クラウドにはスワップメモリがないという問題があったり、当時使っていたクラウドの方がディスク容量を多く提供してくれたり、という比較要素もありました。一方で IDCF クラウド側にも無料枠で提供されている Object Storage や Mackerel といったプラス要素があり、結局は何をどう比較するか、という判断になりました。まあスワップメモリに関してはディスク上にファイルとして用意するという回避手段があること、ディスクは多いほど嬉しいけどサービスを展開する上で困るほど少なかったわけではないことが分かり、そして何よりもサービスの肝になるネットワーク性能が自宅ネットワークをも上回っていた、という違いが決め手でした。 これがディスク容量が問題になるようなケースだと結局月額料金の差になってしまうため違う判断になる可能性もありましたが、このねっぴに関してはほぼ同額の比較になりました。

結局、現時点でこのサービスを運営する先としてコストパフォーマンスでのベストは何か、という最終判断として IDCF クラウドを選んだ、という結論です。


おまけですが FAQ の1つをここに書いておきます。サービス名の「ねっぴ」は「較」の「値比」を無理やり発音した結果です。



 

Tomcat や Jetty などの Java ウェブアプリケーションでエラーが発生した時に表示されるエラーページを独自のものにカスタマイズする方法の紹介です。もちろんエラーが発生しないことが望ましいのですが、万が一エラーが発生してしまった場合に、標準のエラーメッセージが出てしまうと、どのアプリケーションサーバーを使っているのかが分かってしまいます。その結果、そのサーバーのセキュリティホールを狙われる可能性もないわけではありません。あとエラーページにユーモアを交えるような目的でも有用だと思います。

ではその手順の紹介です。 まずアプリケーションの web.xml の最後に以下の青字の情報を追加します:
  :
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

  <error-page>
    <error-code>404</error-code>
    <location>/error/404.html</location>
  </error-page>
  <error-page>
    <error-code>500</error-code>
    <location>/error/500.jsp</location>
  </error-page>
</web-app>

この例ではステータスコードが 404(ページが見つからないエラー)の時と、500(サーバー内部エラー)の時用のページをカスタマイズする前提で、その2つのページに関する情報を追加しています。他のエラー(認証など)についてもカスタマイズする場合は同様に追加してください。この例ではページが見つからない場合は /error/404.html に、Java コード等での内部エラー発生時には /error/500.jsp にそれぞれ飛ばすような指定をしています。もちろん静的な HTML ページでもいいのですが、今回は 500 エラーの時には JSP にして動的に作成してみます。

そしてこれらのページを作っていきます。/error/404.html は 404 エラー、つまり URL が間違っていることになるので、こんな感じの内容で:
<html>
<head>
<title>404</title>
</head>
<body>
<h1>見つからないよ~</h1>
</body>
</html>

一方、/error/500.jsp は 500 エラー、つまり Java の内部エラーなので、スタックトレースも表示するような内容にしてみます。試しにこんな感じにしました。また isErrorPage を true にしています:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="javax.servlet.*" %>
<% request.setCharacterEncoding("utf-8"); %>
<%@ page isErrorPage="true"%>

<html>
<head>
<title>500</title>
</head>
<body>
<h1>Internal な Server の Error だよ~</h1>

<% exception.printStackTrace(new java.io.PrintWriter(out)); %>


</body>
</html>

最後に、わざと 500 エラーを発生させるための JSP ページを作成します:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="javax.servlet.*" %>
<% request.setCharacterEncoding("utf-8"); %>

<%
String[] a = null;
for( int i = 0; i < a.length; i ++ ){ //. ここでぬるぽエラー
}
%>

<html>
<head>
<title>index</title>
</head>
<body>
<h1>Index</h1>

<% exception.printStackTrace(new java.io.PrintWriter(out)); %>


</body>
</html>

この状態で動かしてみます。まず存在しないページの URL を叩くとこんな感じのエラーになります:
2015012706


次に上記で作成したわざと NullPointer エラーがでるページにアクセスするとこんな感じに:
2015012707


できた!
 

以前にこの記事を書きました:
CentOS に Apache Tomcat を導入する


上記ページの下の方で、Apache Tomcat を(一般的な HTTP のポートである)80 番ポートで実行するための指定方法も書き加えています。簡単に言うと、Apache Tomcat 自体は 8080 番ポートで動かしながら 80 番ポートへのリクエストを 8080 番に転送することで、外部からは 80 番ポートで動いているように見せる、というものでした。

もちろんこの方法は今でも有効で、比較的簡単に Apache Tomcat を 80 番ポートで動かすことができるようになります。ただ1つ、「事実上 80 番ポートを専有してしまう」という難点もあります。つまり実際には 8080 番ポートでしか動いていないのに、80 番ポートも含めて専有してしまうということです。

この結果、どんな不都合があるでしょう? 例えば Apache Tomcat が 8080 番ポートのままであれば、同じシステムに Apache HTTPD を導入して 80 番ポートで平行して動かすことができます。例えば PHP と MySQL と WordPress まで導入した上で、80 番ポートでは Apache HTTPD で WordPress を動かし、8080 番ポートでは Apache Tomcat で Java アプリケーションサーバーを同時に動かす、ということが可能でした。 上記の 80 番ポートを 8080 番ポートに転送するという方法を取ってしまうと、80 番ポートで Apache HTTPD を動かすことができなくなってしまいます。つまり2つの HTTP サーバーを同時に動かすことはできなくなってしまいます。

とはいえ、Apache Tomcat が 8080 番ポートのままですと、Java アプリケーションサーバーにアクセスさせるには
  http://xxx.xxx.xxx:8080/hello
という、一般的とは言えないちょっと冗長な URL をユーザーに指定させる必要が出てきます。普通の HTTP(80番)リクエストが許可されていない環境は珍しいかもしれませんが、8080 番ポートとなると会社レベルのファイアウォールが許可していない可能性も出てきます。


これを解決する方法もあります。例えば WordPress が
 http://xxx.xxx.xxx/wordpress
という URL で動いていて、同時に Java アプリケーションが
 http://xxx.xxx.xxx:8080/hello
という URL で動いている場合であれば、以下の様なルールを作ります:

・原則的には、全てのリクエストを Apache HTTPD (80番ポート)で処理する
・但し /hello という URL パターンだった場合のみ、Apache Tomcat に渡して処理してもらう

これにより、http://xxx.xxx.xxx/hello というリクエストがあった場合には内部的に http://xxx.xxx.xxx:8080/hello に転送※して Apache Tomcat 側で処理をする、という作業分担が可能になります。 また同時に全てのリクエストをパフォーマンスのいい Apache HTTPD がいったん処理した上で分担処理することになるとか、サーバーのファイアウォールは 80 番ポートのみ開けておけばよくなる(Apache Tomcat の 8080 番ポートと停止することもできる)、という副次的なメリットも生まれます。

※実際には 8080 番ではなく、内部的な処理をするために 8009 番ポートを使います


この作業分担を実現するのが Apache Tomcat に付属している AJP(Apache Java Protocol) です。AJP は 8009 番ポートを使います。

具体的な作業の前に Apache Tomcat の設定を元に戻しておきます。つまり上記リンク先のような 80 番ポートへのリクエストを 8080 番ポートに転送するような指定がされている場合は無効にして、普通に Apache Tomcat が 8080 番ポートで動いているような状態にします。

続いて /etc/httpd/conf.d/proxy_ajp.conf というファイルを、以下の内容で作成します:
ProxyPass /hello/ ajp://localhost:8009/hello/

これで Apache HTTPD を再起動(# /etc/init.d/httpd restart)します。すると 80 番ポートへのリクエストは Apache HTTPD が受けるのですが、この AJP の設定により /hello/ というパスへのリクエストに関しては 8009 番ポートへの /hello/ に変換されます。そして内部的な 8009 番ポートで待ち構えている Apache Tomcat の AJP によって処理されるようになります。

結果的に、ユーザーからは WordPress へは
  http://xxx.xxx.xxx/wordpress/
Java アプリケーションへは
  http://xxx.xxx.xxx/hello/
それぞれの URL で利用できるようになる、ということです。

なお、この記述をすることで Apache Tomcat を 8080 番ポートで動かす必要はなくなります。消してしまうのであれば Apache Tomcat の server.xml から該当箇所を削除してください。


注意点としては、Java アプリケーションへの転送を proxy_ajp.conf で記述することになるため、複数の Java アプリケーションがあって、その全てを転送したいのであれば、全てを proxy_ajp.conf に(複数行で)記述する必要が出てきます。

もう1点、実は私自身はこの AJP をあまり信用していないというか、AJP コンテナ自身がそれほどでもない負荷を捌ききれなかった経験をしています。原因調査についてはこちらの記事を参考にしていますが、AJP を使う前提での根本解決はできていないので、ちとヤな感じではあります:
http://m97087yh.seesaa.net/article/212767022.html









 

このページのトップヘ