2013年9月26日木曜日

Undertow Design Document 和訳

このエントリは WildFly(旧名 JBoss AS)の Web サブシステムとしても利用される Undertow の以下設計ドキュメントの和訳です。実際に存在するクラス名も出てきますので、ソースコードもお手元にあるとよいかと思います。

Undertow Design Document

原本は https://github.com/undertow-io/undertow-io-site の a839203 時点のものを利用しています。更新が確認され次第、随時反映します。

なお、自分が理解が足りていないところが多いため、変な日本語になっています。。ちょくちょく直していく予定です。

Undertow 設計ドキュメント

これは Web サーバ Undertow の設計ドキュメントです。アーキテクチャ全般と設計思想をカバーしています。要求仕様書ではありません。

概要


Undertow のアーキテクチャのコアは軽量非同期ハンドラという考えに基づいています。これらハンドラ群は完全な HTTP サーバをなすためにチェーンされます。ハンドラはまた、1 つのスレッドプールによって戻されたブロッキングハンドラへ自身を渡します。

このアーキテクチャはエンドユーザがサーバ設定の変更を行う際に完全な柔軟性を持たせるために設計されています。例えばユーザが単にサーバへ静的ファイルをアップしたい場合、そのタスクを必要とするハンドラだけでサーバを設定できます。

ハンドラチェーンの例を以下に挙げます。



















図1. ハンドラチェーンの例

Servlet の機能は非同期サーバのコアの最上位に構築されます。Servlet モジュールはServlet の特定の機能に対するハンドラを提供することでコアと統合されます。Serlvet 実装では可能な限り非同期ハンドラを利用し、どうしても必要な場合だけブロッキングハンドラへと変更します。これは、ある Servlet 中にパッケージされた静的なリソースが、非同期 IO を通して提供されることを意味します。

コアサーバ


リクエストの受け取り


標準的な HTTP リクエストはサーバへ HTTPOpenListener 経由で送られます。この HTTPOpenListener は 1 つの PushBackStreamChannel 中のチャネルをラップし、 HTTPReadListener へ渡します。HTTPReadListener はリクエストを受け取るとパースを行い、いったん全てのヘッダを読込み、HTTPServerExchange インスタンスを作成し、ルートハンドラを実行します。このリスナに読まれるどのメッセージボディや次に来るリクエストも、ストリームへと先延ばしにされます。

HTTP パース処理はバイトコード生成ステートマシンにより行われ、一般的なヘッダや HTTP メソッドが認識されます。これは一般的なヘッダに対するパース処理はより高速かつ省メモリで実行されうることを意味し、あたかもヘッダの値がステートマシン、つまり Srting や StringBuilder インスタンスに割り当てられる必要なしに直接返却される内部に持っている文字列として認識されているようにふるまいます。

注意: このことが実際の運用においてパフォーマンスの向上に大きく寄与するかはまだ示されていません。もしそうでないのであれば、我々はこれ以上の複雑さを避けるため、よりシンプルなパーサへと移行する可能性もあります。

HTTPS や AJP、SPDY といった他のプロトコルのサポートはチャネルの実装を通して提供され、ハンドラに対して可能な限りプロトコルの詳細を切り離された抽象的なものとして実装されます。

ハンドラ


ベーシックなハンドラインターフェースは以下のようなものです。
public interface HttpHandler {
  /**
   * Handle the request.
   *
   * @param exchange the HTTP request/response exchange
   */
   void handleRequest(HttpServerExchange exchange) throws Exception;
}

HttpServerExchange はリクエストとレスポンス、ヘッダ、レスポンスコード、チャネルなど、現在の全ての状態を保持します。任意のアタッチメントも付与でき、ハンドラチェーンの後でハンドラによって読込まれるオブジェクトのアタッチもハンドラで行えます。例で言えば、認証ハンドラでは認証されたアイデンティティのように、後で承認ハンドラによってあるユーザがリソースにアクセス可能かどうかを判断するために利用されるオブジェクトです。

HttpCompletionHandler はリクエストが完了した時に実行されます。どのハンドラも、このインスタンスをラップするいくつかのソートマップのクリーンナップ処理を次のハンドラへ渡す前に実施することが必要です。これらは非同期ハンドラであることから、コールチェーンはリクエストが行われている間に返却する可能性があり、そのため finally ブロック中でのクリーンナップ処理はできません(実際には、ハンドラは一般的には次のハンドラの実行後にいかなるコードも実行しません)。

最初ハンドラは XNIO の読込みスレッドで実行されます。これは、ハンドラは、サーバが書込みスレッドが返されるまで他のリクエストを処理できないようなブロッキング処理を行ってはいけないことを意味します。そのかわりに、ハンドラはコールバック可能な非同期処理の実施や、タスクの(XNIO ワーカープールのような)スレッドプールへの委譲を行います。

リクエストとレスポンスのストリームはハンドラや HttpServerExchange のChannelWrapper の登録によりラップされます。このラップは一般的に、transfer や content encoding を実装するハンドラによってのみ利用されます。例えば圧縮を実装するために、ハンドラは データを圧縮し、圧縮データを下層のチャネルへ書き出す ChannelWrapper を登録します。これらラッパは、レスポンスのボディへ書き出すのみに利用され、ステータスラインやヘッダ内容の変更のためには利用できないことに注意してください。

単一のハンドラのみでリクエストの読込みやボディへの書き出しが可能です。ハンドラが別のハンドラがすでにチャネルを取得した後にそのチャネルを取得しようとした場合、null が返却されます。

コネクションの永続化


コネクションの永続化は、チャンク形式か固定長のリクエストとレスポンスのチャネルをラップすることにより実装されます。一度リクエストが完全に読込まれると、次のリクエストが即座に開始され、次のレスポンスが提供され、ゲート化したストリームは現在のレスポンスが完了するまでレスポンス処理が始まる事を許可しません。

セッションのハンドリング


セッションはセッションハンドラにより実装されます。リクエストが処理される際、このハンドラがセッションクッキーの存在を調べ、もし存在するのであればセッションマネージャからセッションを受け取り、HttpServerExchange に付与します。また、セッションマネージャも HttpServerExchange に付与します。セッションの取得は非同期に行われる可能性があります(例えばセッションのデータベースへの格納や、クラスタにおける別のノードへの配置など)。

一度セッションとセッションマネージャが HttpServerExchange へ付与されると、その後のハンドラはセッションへデータを格納できます。また、セッションが存在しない場合、新しいセッションを生成するためにこのセッションマネージャを利用できます。

設定


Undertow の Core 部では、プログラム上でハンドラーチェーンをくみ上げる事で設定するかわりになるような、設定用の API 提供しません。XML による設定は AS7 サブシステムにより提供されます。これはサーバが XML で全ての設定をすることなしに組み込みモードで利用できるということです。Tomcat や Jetty に対抗するスタンドアロンのサーブレットコンテナを提供するために、縮小版の AS7 インスタンスを利用します。このインスタンスは web サブシステムのみを提供します。完全な AS7 インスタンスよりもダウンロード量が少なく、軽量であるコンテナを利用する事で、モジュール機構やマネジメント機能といった AS7 のメリットの全てをユーザが得られます。

エラーハンドリング


エラーページの生成は HttpCompletionHandler にラップされ実施されます。このラッパはレスポンスがすでにコミットされたかどうかを確認でき、されていない場合はエラーページを書き出します。チェーン中で後に存在する HttpCompletionHandler は優先され、 HttpCompletionHandler のラッパが最初に実行されます。

Servlet


Undetow Servlet のコア


サーブレットのコードは、Undertow リポジトリに存在し、AS7 のJava EE インテグレーションコード中にもあります。サーブレットのコードは大まかに以下に分類されます。

  • ライフサイクルマネジメント
  • 全てのリクエストハンドリング機能を含むサーブレットハンドラ
  • セッションマネジメント
AS7 の役割は以下です。
  • XML のパースとアノテーションの処理
  • インスタンスの注入、生成及び削除
  • クラスタリング
TODO: 分類の完全な定義

Undertow のサーブレットコンポーネントは AS7 の柔軟な API により設定され、別のインテグレータにも利用されます。この API は XML やアノテーションの代りとなり、コンテナはでぷろコンテナ自身のライフサイクルの制御にこの API を利用します。

Servlet ハンドラチェーン


サーブレット実行時用のハンドラーチェーンの役割は一般的にはとても簡単なものであり、サーブレットハンドラの前のノンブロッキングハンドラ層により提供される機能がほとんどです。サーブレットコアは以下のようなハンドラを提供します。
  • 全サーブレットとフィルタのパスに対するマッチルールを考量した上で、リクエストを適切なハンドラチェーンへディスパッチするハンドラ
  • リクエスト/レスポンスラッパオブジェクトが要求する仕様を生成し、そのラッパオブジェクトを HttpServerExchange へ付与するハンドラ
  • フィルタを実行するハンドラ
  • サーブレットを実行するハンドラ
各機能は可能な限りノンブロッキングなハンドラによってハンドリングされます。例えば、どのフィルタやサーブレットにも該当しないパスへのリクエストにはブロッキングなハンドラは利用されず、代替として静的なリソースが非同期ハンドラにより返されます。

設定とブートスラップ


基本的な設定は、以下の例のように柔軟なビルダー API により行われます。
final PathHandler root = new PathHandler();
final ServletContainer container = new ServletContainer(root);

ServletInfo.ServletInfoBuilder s = ServletInfo.builder()
        .setName("servlet")
        .setServletClass(SimpleServlet.class)
        .addMapping("/aa");

DeploymentInfo.DeploymentInfoBuilder builder = DeploymentInfo.builder()
        .setClassLoader(SimpleServletServerTestCase.class.getClassLoader())
        .setContextName("/servletContext")
        .setDeploymentName("servletContext.war")
        .setResourceLoader(TestResourceLoader.INSTANCE)
        .addServlet(s);

DeploymentManager manager = container.addDeployment(builder);
manager.deploy();
manager.start();

これらビルダーのディープコピーは、deploy() フェーズの間、ServletContainerInitializers により変更されます。ビルダーをクローンする意図ですが、MSC サービスに問題があった場合に備えて、元々の設定を常に保持しておきます。

deploy() フェーズが完了すると、ビルダーはすぐに Deploymentinfo のイミュータブルなコピーをビルドします。Deploymentinfo はデプロイ設定を全て保持しています。start() が呼ばれたとき、このメタデータは適切なハンドラチェーンを構築するのに利用されます。

JBoss アプリケーションサーバとのインテグレーション


このインテグレーションは Undertow チームでメンテナンスされる分割モジュールによって提供されます。このことによって AS7 サブシステムと Torquebox や Immutant のようなインストーラが提供され、既存の AS7 インスタンスへサブシステムが追加されます。AS7 リポジトリ外で開発するにはいくつか理由があります。
  • 衝突のリスクを減らすこと。AS7 のブランチ上で開発した場合、AS7 の開発ツリーと差分を解消するために、マージコミットか頻繁なリベースを実施する必要があり、どちらも望ましいものではありません。
  • ビルド実行時間を短くすること。AS7 のビルドと全テストを実施すると、約1時間程度かかります。異なるリポジトリで開発することでビルド/テスト実行時間をかなり短縮できます。
  • ユーザに選択しやすくすること。このアプローチによって簡単に AS7 ユーザが Undertow を既存のインスタンスにインストールして試すことができます。

セキュリティ


Undertow 中のセキュリティ機構は非同期ハンドラのセットと認証メカニズムのセットそれぞれを組み合わせて実装されます。
  • 認証機能を可能な限り早く呼び出すことができます。
  • 様々なシナリオ中で認証メカニズムの利用が可能であり、サーブレット以外でも適用できます。
ハンドラチェーンの初期段階で呼ばれるハンドラが SecurityInitialHandler であり、空の SecurityContext を現在の HttpServerExchange にセットし既存の SecurityContext を破棄することを保証します。この呼び出しは後でセットされた SecurityContext を破棄する役割をもつこのハンドラを返します。

SecurityContext は現在の認証ユーザに関する状態の保持と設定された AuthenticationMechanism の保持及び、これらに対してはたらきかけるメソッドを提供します。この SecurityContext は交換可能かつ復元可能であるため、一般的な設定をサーバに対して適用してから、後の呼び出しにおいて詳細な設定を行うことが可能です。

SecurityContext を確立した後で後続のハンドラが AuthenticationMechanism を SecurityContext へ追加することが可能であり、簡単に追加するために Undertow は AuthenticationMechanismsHandler というハンドラを用意しています。AuthenticationMechanism を追加するもう1つの方法として、カスタムハンドラを利用できます。

認証プロセスにおける次のハンドラは AuthenticationConstraintHandler です。このハンドラの役割は、現在のリクエストをチェックし、そのリクエストが認証が必要かどうかを特定することです。デフォルトの実装では、認証は全てのリクエストで必要なものとします。このハンドラーは拡張することができ、isAuthenticationRequired メソッドをオーバーライドすることでより詳細なチェックを行うことができます。

チェーン最後のハンドラは AuthenticationCallHandler であり、これにより SecurityContext の呼び出しによる認証処理の実行が保証されます。このハンドラが認証処理を委譲するか、設定されたメカニズムが適切な場合に認証処理を実施するかは、制約の内容に依存します。

これらのハンドラは連続して実行される必要はありませんが、最初に SecurityContext が確立された後は、AuthenticationMechanism と制約チェックはどの順番でもよく、最後に AuthenticationCallHandler が呼ばれる必要があります。しかし、それも制限されたリソースの処理が行われる前です。
図2. セキュリティチェーンの例

セキュリティメカニズムは以下のインタフェースを実装する必要があります。

public interface AuthenticationMechanism {
    IoFuture<authenticationresult> authenticate(final HttpServerExchange exchange);
    void handleComplete(final HttpServerExchange exchange, final HttpCompletionHandler completionHandler);
}

AuthenticationResult は試行された認証の状態を指定するためにメカニズムによって利用される他、認証後に認証されたアイデンティティのプリンシパルを返すメカニズムとしても利用されます。

public class AuthenticationResult {
    private final Principal principle;
    private final AuthenticationOutcome outcome;

    public AuthenticationResult(final Principal principle, final AuthenticationOutcome outcome) { ... }
}

認証処理は INBOUND フェーズと OUTBOUND フェーズの2つのフェーズに分かれます。INBOUND フェーズでは、クライアントから認証データが送られてきたかの確認を行い、データが送られてきている場合のみ、認証の試行に利用します。INBOUD フェーズの間はメカニズムは連続的に呼ばれます。複数のメカニズムの同時実行も1リクエストごとに複数スレッドを利用することで可能です。

AuthenticationMechanism の authenticate メソッドが呼ばれた時、その結果は以下の  AuthenticationOutcome とともに AuthenticationResult を利用して表します。

  • AUTHENTICATED - メカニズムはリモートユーザを認証した
  • NOT_ATTEMPTED - メカニズムは適切なセキュリティトークンを受け取っていないため、ユーザに対して認証を行わなかった
  • NOT_AUTHENTICATED - メカニズムは認証を行ったが失敗した、またはクライアントへ追加情報の要求を行っている
AuthenticationOutcome が AUTHENTICATED である場合、AuthenticationResult 中にプリンシパルが返されなければならず、残りの AuthenticationOutcome はプリンシパルを保持しません。

認証処理全体として以下のようにメカニズムが利用されます。

認証処理が要求されたかどうかに関わらず、リクエストが AuthenticationCallHandler に達した際、SecurityContext が処理を開始するために呼ばれます。認証処理の必要有無に限らず呼ばれる理由は、第1に、クライアントが追加で認証トークンを送る可能性があり、このことを考慮に入れたレスポンスが期待されるからです。第2に、追加のやりとりなしにリモートユーザを認証できる場合があります。特に認証がすでに行われているような場合です。第3に、認証メカニズムは更新途中の状態をクライアントへ渡す必要がある場合などを考えると、送られてきた全てのトークンが正当であることを保証する必要があります。

認証において設定された各 AuthenticationMechanism の authenticate メソッドが順番に実行され、以下の状態のいずれかになるなるまで実行され続けます。
  1. メカニズムがリクエストに対して認証し、AUTHENTICATED を返す
  2. メカニズムが認証を試行したが完了せず、NOT_AUTHENTICATED を返す
  3. リスト中のメカニズムを使い果たした
現時点では、レスポンスが AUTHENTICATED であった場合、そのリクエストは次のハンドラをそのまま通過できます。

リクエストが NOT_AUTHENTICATED である場合、認証が失敗したか、メカニズムが追加情報をクライアントに要求し、どちらの場合でも各 AuthenticationMethod で定義された handleComplete メソッドが順番に呼ばれ、クライアントにレスポンスが返されます。たとえ1つのメカニズムが中間認証であり、クライアントがこのメカニズムを中止して他のメカニズムに切り替えることができるとしても、全てのメカニズムが呼ばれるため、全てのチャレンジに再送が必要です。

リスト中のメカニズムを使い果たした場合、前に設定した認証制約のチェックが必要です。認証が必要でない場合はリクエストはチェーン中の次のハンドラへ処理を続行でき、このリクエストは認証されたものとします(後続のハンドラで認証処理が委譲されたり、陸リクエストが認証を再試行する場合を除く)。認証が必要である場合は NOT_AUTHENTICATED のレスポンスとともに各メカニズムの handleComplete メソッドが順番に呼ばれクライアントに対して認証チャレンジを送ります。

メカニズムを呼んだ後のリクエスト処理が後続のハンドラに許可される場合や、リクエストを認証するメカニズムがない場合、メカニズムは OUTBOUND フェーズの中で呼ばれません。しかし AuthenticationMechanism がクライアントを確証した場合、認証の必要有無に関わらず、メカニズムの handleComplete メソッドが呼ばれます。これはメカニズムがさらにメカニズム特有のトークンをクライアントに送る必要がある場合を想定しています。

メカニズムが一般的なチャレンジの送信、または任意の更新を行うために、以下のように handleComplete メソッド内でチェックすることができます。
if (Util.shouldChallenge(exchange)) { ... }

2013年8月31日土曜日

WildFly のログを Fluentd で扱う

WildFly(旧JBoss AS) のログ解析を、Fulentd を使って実施してみます。

今回の対象ログは、WildFly が出力する access.log とします。Apache httpd の access_log 相当のログです。

なお、本ブログは以下の記事を参考にしています。それぞれの設定の意味合いなどは、そちらのブログを参照いただければと思います。

ステップバイステップで学ぶ Fluentd + GrowthForecast でグラフ作成まとめ

目標

access.log ログを収集・解析し、レスポンスタイムの統計情報(最小値・最大値・平均値など)をグラフ化する。

大まかな流れ

  1. WildFly/JBoss AS7 の設定
  2. Fluentd の設定
    1. ログ送信サーバ(fluent-agent-lite) の設定
    2. ログ受信サーバ(td-agent)の設定
    3. グラフ化サーバ(GlowthForecast) の設定
  3. 動作確認

環境

  • CentOS 6.4 64bit
  • Maven 3.1.0
  • WildFly...と言いたいところですが、JBoss AS7.2.0 Final※
  • Fluentd(td-agent-1.1.14-0)
  • fluent-agent-lite-0.9
  • GlowthForecast
※ 2013/08/31 現在、WilfFly のバージョンは 8.0.0 Alpha4 ですが、access.log を出力する機能は次リリース版である Beta1 です。また、現在レスポンスタイムの出力機能が Undertow(WIldFly の Web 部)に入っていないため、Beta1 のリリースに含まれるか微妙なところです。WildFly においてレスポンスタイムの出力が確認され次第、ブログも反映いたします。

Access log support - WildFly JIRA

JBoss AS7 の設定

インストール


JBoss AS7.2.0.Final のバイナリはコミュニティにて配布されていませんので、自分でビルドする必要があります。またビルドスクリプト(build.sh)の実行に Maven が必要ですので、あらかじめインストールください。なお、ここでは時間短縮のため、テストを省略してビルドしています。
$ git clone https://github.com/wildfly/wildfly.git
$ cd wildfly
$ git checkout refs/tags/7.2.0.Final
$ ./build.sh -DskipTests=true

wildfly/build/target/jboss-as-7.2.0.Final が作成されたバイナリです。適当なところにインストールください。このインストールパスを $JBOSS_HOME と表記します。

ログ設定

$ cd $JBOSS_HOME/bin
$ ./standalone.sh
$ ./jboss-cli.sh -c --command='/subsystem=web/virtual-server=default-host/access-log=configuration:add(pattern="%t,%a,%m,%U,%s,%S,%D",prefix="access.log.")'
念のため JBoss を再起動し、適当に Web アクセスします(wget localhost:8080 など)。$JBOSS_HOME/standalone/log/default-host に、access.log.YYYY-mm-dd とアクセスログが出力されていれば成功です。
cat $JBOSS_HOME/standalone/log/default-host/access.log.2013-08-31
[31/Aug/2013:13:06:59 +0000],127.0.0.1,GET,/,200,-,8

Fluentd の設定

実際には以下のような構成をイメージしていますが、今回は1サーバで全ての役割を持たせます。


ログ送信サーバ


WildFly/JBoss AS7 の access_log が出力されているサーバです。fluent-agent-light を利用して、ログ受信サーバへログを送信します。

インストール用のシェルもありますが、せっかくなので rpm を作成してインストールしてみます。

fluentd-agent-light のインストール

# yum install rpm-build
# useradd rpmbuilder
# su - rpmbuilder
$ mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
$ wget http://tagomoris.github.io/tarballs/fluent-agent-lite.v0.9.tar.gz
$ tar zxvf fluent-agent-lite.v0.9.tar.gz && mv fluent-agent-lite* ~/rpmbuild/SOURCES
$ cp ~/rpmbuild/SOURCES/fluent-agent-lite/package/fluent-agent-lite.conf ~/rpmbuild/SOURCES
$ cp ~/rpmbuild/SOURCES/fluent-agent-lite/SPECS/fluent-agent-lite.spec ~/rpmbuild/SPECS
$ rpmbuild
$ exit
# rpm -ivh /home/rpmbuilder/rpmbuild/RPMS/x86_64/fluent-agent-lite-0.9-original.x86_64.rpm

fluentd-agent-lightの設定


以下の内容を /etc/fluentd-agent-lite.conf に記述します。なお、ログのパスは絶対パスに変更してください。
うーん、JBoss AS7 の場合、現在日時に利用されるファイルにすでに日付が入ってしまうため、パス名を日付の切り替えとともに書き換える処理が必要ですね。。

ログ受信サーバ


Fluentd の安定かつパッケージ版である、td-agent をインストールします。また、各種プラグインもインストールしておきます(このエントリ中では利用しないものも含まれています)。

td-agent のインストール

# echo "[treasuredata]\nname=TreasureData\nbaseurl=http://packages.treasure-data.com/redhat/\$basearch\nenabled=0\ngpgcheck=0\n" >> /etc/yum.repos.d/td.repo
# yum install -y td-agent --enablerepo=treasuredata
# /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-file-alternative
# /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-parser
# /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-forest
# /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-amplifier-filter
# /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-numeric-monitor
# /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-growthforecast

以下の内容を /etc/td-agent/td-agent.conf に記述します。

グラフ表示サーバ


グラフ化には GlowthForecat を利用します。
インストールから起動確認まで一気にやってみます。インストールする Perl のバージョンは、5.<偶数> のもので一番数字が大きいものにしました。5.<奇数>は開発版とのことです。
# yum -y groupinstall "Development Tools"
# yum -y install pkgconfig glib2-devel gettext libxml2-devel pango-devel cairo-devel
# useradd growthforecast
# su - growthforecast
$ curl -kL http://install.perlbrew.pl | bash
$ echo '[[ -s "$HOME/perl5/perlbrew/etc/bashrc" ]] && source "$HOME/perl5/perlbrew/etc/bashrc"' >> ~/.bash_profile
$ source "$HOME/perl5/perlbrew/etc/bashrc"
$ perlbrew available
  perl-5.19.3
  perl-5.18.1
  perl-5.16.3
  perl-5.14.4
  perl-5.12.5
  perl-5.10.1
  perl-5.8.9
  perl-5.6.2
  perl5.005_04
  perl5.004_05
  perl5.003_07
$ perlbrew install perl-5.18.1
$ perlbrew switch perl-5.18.1
$ perlbrew install-cpanm
$ cpanm -n GrowthForecast
$ mkdir /home/growthforecast/data
$ mkdir /home/growthforecast/log
$ growthforecast.pl --data-dir=/home/growthforecast/data > /home/growthforecast/log/growthforecast.log 2> /home/growthforecast/log/growthforecast.err &
$ curl -F number=10 http://<ip-addr>:5125/api/socialgame/member/register

動作確認

ここまでくれば完成です。WildFly/JBoss AS7 に適当に Web アクセスをして、/var/log/td-agent に以下のログが出力されている事をご確認ください。

  • access.log の生ログ(wildfly-access.log.*)
  • パースされたログ(parsed-wildfly-access.log.*)
  • GrowthForecat に送るレスポンスタイムの統計ログ(wildfly-response-time.*)

なにか問題が出ている場合は、同ディレクトリの td-agent.log にログが残っています。

GrowthForecat にレスポンスタイムのグラフが作成されているか、http://<ip-addr>:5125 にアクセスしてみましょう。以下のようなグラフが表示されていれば成功です。


参考リンク

2013年7月24日水曜日

Java におけるプロキシ経由のリモートIP 取得あれこれ

Servlet プログラミングで、リモートホストの IP の取得に HttpServletRequest#getRemoteAddr をしたところ、プロキシ経由からのアクセスだったためにプロキシサーバの IP が取得されてしまうという、さみしい事態になることがあります。

この対策として 3 パターンほど挙げます。
  1. HttpServletRequest#getHeader("x-forwaded-for") を利用
  2. (Tomcat6以降のみ)RemoteIpValve を利用
  3. Byteman を利用

プロキシの想定

今回はリバースプロキシとして、CentOS 6.4 に yum でインストールした Nginx を利用していると想定します。

インストールしていない場合は以下を実施ください。
# rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
# yum install nginx

Nginx がインストールできたら /etc/nginx/conf.d/proxy.conf にプロキシ設定を記載します。
※ proxy-host, app-host の部分は環境に合わせて読み替えてください。
# cat /etc/nginx/conf.d/proxy.conf
server {
  listen 80;
  server_name proxy-host;

  location / {
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://app-host:8080;
  }
}

次節からリモートホストの IP 取得方法を記述していきます。

HttpServletRequest#getHeader("x-forwaded-for") を利用

Web で一番多く見かける方法です。HttpServletRequest#getRemoteAddr の代りに、 HttpServletRequest#getHeader("x-forwaded-for") の値を利用します。実際は以下のブログのようにフィルタにすると楽ですね(こちらは X-Real-IP の場合ですが)。

Keeping real user IP in Java web apps behind Nginx proxy

(Tomcat6以降のみ)RemoteIpValve を利用

@msfm さんに教えていただきました。ありがとうございます!

Tomcat 6 以降、または JBoss AS7/EAP6のみとなりますが、
org.apache.catalina.valves.RemoteIpValve を利用する方法です。

Tomcat であれば上記 API リファレンスを参考に、server.xml に Valve 要素を追加します。

JBoss AS7/EAP6 では、残念ながらCLI で設定できなさそうです。war アプリケーションの WEB-INF 配下に以下のような jboss-web.mxl を追加し、再ビルドします。

<jboss-web>
  <valve>
    <class-name>org.apache.catalina.valves.RemoteIpValve</class-name>
    <param>
      <param-name>remoteIpHeader</param-name>
      <param-value>x-forwarded-for</param-value>
    </param>
  </valve>
</jboss-web>

どちらの方法でも、ソースコードを変更することなく実施できます。

なお、WildFly の Web/Servlet コンテナであるところの Undertow では、まだこの機構はないようです。

Byteman を利用

Byteman を使って、アプリケーションのソースコードを変更せずに、
HttpServletRequest#getRemoteAddr の実装を書き換える方法です。
ザ・黒魔術という感じがしますね。

以下、手順です。

1. Byteman のインストール


2013/07/24 時点での最新版である 2.1.3 をインストールします。
# cd /opt
# wget -qO- -O tmp.zip http://downloads.jboss.org/byteman/2.1.3/byteman-download-2.1.3-bin.zip && unzip tmp.zip && rm -f tmp.zip && ln -s byteman-download-2.1.3 byteman

2. Byteman のルールを作成


x-forwarded-for.btm という名前で以下の内容で作成します。

今回作成した Byteman のルールですが、発想としては一番最初の HttpServletRequest#getHeader("x-forwaded-for")  を利用する方法と同じです。

リクエストヘッダに x-forwarded-for が存在する場合は、HttpServletRequest#getRemoteAddr の値を HttpServletRequest#getHeader("x-forwarded-for") の値に書き換えます。存在しない場合は何もしません。

3. Tomcat/JBoss 起動時に Byteman がアタッチされるように設定


Tomcat: $CATALINA_HOME/bin/setenv.sh
JBoss AS7/EAP6: $JBOSS_HOME/bin/standalone.conf

に以下を新規作成(または追記)します。
BYTEMAN_RULE 変数に代入する x-forwaded-for.btm のパスは適宜変更ください。

BYTEMAN_HOME=/opt/byteman
BYTEMAN_RULE=/path/to/x-forwarded-for.btm
BYTEMAN_OPTS="-javaagent:$BYTEMAN_HOME/lib/byteman.jar=listener:true,boot:$BYTEMAN_HOME/lib/byteman.jar"
if [ "x$BYTEMAN_RULE" != "x" ]; then
BYTEMAN_OPTS="${BYTEMAN_OPTS},script:$BYTEMAN_RULE"
fi
BYTEMAN_OPTS="$BYTEMAN_OPTS -Dorg.jboss.byteman.transform.all
-Dorg.jboss.byteman.debug"
JAVA_OPTS="$BYTEMAN_OPTS $JAVA_OPTS"

※ Tomcat の場合は、JAVA_OPTS を CATALINA_OPTS に変更してください。

4. Tomcat/JBoss AS7/EAP6 の起動


起動してみて、getRemoteAddr の返り値がクライアントのリモートホストの IP であれば成功です。

参考


課題

  • 多段プロキシに対応できない
  • 現状の Byteman ルールでは、X-Forwarded-For ヘッダを送ってくるプロキシが信頼できるプロキシかどうかチェック(IP アドレスの確認)を行っていない

2013年6月28日金曜日

Vagrant で CentOS 6.4 を利用する

発作的にまっさらな CentOS が欲しくて、でもはなからインストールするのはめんどくさい。
ということで Vagrant を試してみました。ちなみに、「ゔぇいぐらんと」と読むみたいですね。スクウェアにそんな感じのゲームがありましたね。

利用手順

VirtualBox のインストール


2013/06/27 現在、VirtualBox 4.2.14 だと動作しません。4.2.12 をご利用ください。

バックエンドの仮想化環境として、VirtualBox をインストールします。
なお、バックエンド環境は VirtualBox でなくてもよいようです。

Vagrant のインストール


http://downloads.vagrantup.com/ から最新版を選択し、各 OS に合った形式でインストール。

CentOS 6.4 32bit 版のゲストOSを作成する


今回はお試しなので、サイズの小さい 32bit 版にしました(といっても 600 MB くらいしか違わないようですが)。なお、公式から配布されているイメージは以下から確認できます。


上記でお望みのイメージが見つからない場合は自作するか、有志で公開しているものを利用させてもらうといった手があります。

Box の追加


適当な作業用ディレクトリで行います。以下、このディレクトリをプロジェクトディレクトリと呼びます。
$ mkdir -p ~/vagrant/centos64_32 && cd ~/vagrant/centos64_32
centos64_32 という名前で Box (仮想サーバ、ゲストOS)を追加します。第4引数の URL は、http://www.vagrantbox.es/ で確認した URL です。
$ vagrant box add centos64_32 http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-i386-v20130427.box

Box のセットアップ

$ vagrant init centos64_32
VagrantFile という設定ファイルが作成されます。

Box の起動

$ vagrant up
Virtual Box 側にもゲストOSが追加されたことが確認できます。

Box の利用

$ vagrant ssh
[vagrant@localhost ~]$
vagrant ユーザとしてログインできました。

基本的な操作

root になるには sudo -s または su - で


sudo -s であればノンパスで、su - であればパスワード vagrant で root ユーザになれます。

/vagrant は共有ディレクトリ


/vagrant で作られたファイルはプロジェクトディレクトリと共有されます。
$ ls
Vagrantfile
$ vagrant ssh
[vagrant@localhost ~]$ touch /vagrant/foo
[vagrant@localhost ~]$ ls /vagrant
Vagrantfile  foo
[vagrant@localhost ~]$ exit
$ ls
Vagrantfile foo

ssh の設定


現状ですと vagrant ssh を利用してログインする場合、カレントディレクトリはVagrantFile が存在するディレクトリである必要があります。これでは不便という事で、~/.ssh/config にホスト情報を追記して、どのディレクトリにいてもログインできるようにしておきます。
$ vagrant ssh-config --host centos >> ~/.ssh/config
$ cd /path/to # 適当なところへ移動
$ ssh centos
[vagrant@localhost ~]$

ネットワーク


VagrantFile を編集する事で、Box に任意の IP アドレスを設定する事ができます。以下の設定では 192.168.33.10 でホスト OS からアクセスすることができます。
# Create a private network, which allows host-only access to the machine
# using a specific IP.
config.vm.network :private_network, ip: "192.168.33.10" # コメントアウトを外す

プロビジョニング


Box を起動(vagrant up)する際に、
  1. yum upgrade  して
  2. epel レポジトリを追加して
  3. Apache httpd と nkf をインストール
するようにしてみたいと思います。サーバ環境構築の自動化の第一歩ですね。
以下のようなスクリプトを用意します。ファイル名は bootstrap.sh としておきます。
#!/usr/bin/env bash
yum upgrade -y
wget http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
rpm -ivh epel-release-6-8.noarch.rpm
yum install -y httpd
yum install -y nkf

次に Vagrantfile に Box 起動時にこのシェルを実行するように記述します。
# Every Vagrant virtual environment requires a box to build off of.
  config.vm.box = "centos64_32"
  config.vm.provision :shell, :path => "bootstrap.sh"

Box を起動します。
$ vagrant destroy // すでにBoxを作成している場合
$ vagrant up

Box の起動とともに、bootstrap.sh に記述した内容が実行されていきます。
完了後、ログインします。
$ vagrant ssh
[vagrant@localhost ~]$ httpd -v
Server version: Apache/2.2.15 (Unix)
Server built:   May 13 2013 22:08:57
[vagrant@localhost ~]$ nkf --version
Network Kanji Filter Version 2.0.8 (2007-07-20)
[vagrant@localhost ~]$ sudo -s
[root@localhost vagrant]# yum upgrade
...
No Packages marked for Update
うまくいってるみたいですね。すごい。

参考

2013年6月23日日曜日

「JUnit実践入門」を読む会 第4回 参加報告

前回から Java 読書会 BOF に参加させていただいております。
現在は「JUnit 実践入門」が題材で、今回は第4回です。

Twitter ハッシュタグ
#javareading

参加者は(自分のテリトリの) Java EE 層は少なめで、地図ソフトウェアや組み込みのお話が聞けて非常に刺激的です。話題によっては高度な話まで突っ込まれることがあり、理解できなくて悔しい思いをすることも多いですが。。

読書会のスタイル

参加者各々が順番に、時間を区切って音読していくのが基本になります。
そしてここが肝ですが、途中で疑問に思ったところやメンバに聞いてみたいことなどを、遠慮なくぽろりと聞きます。すると議論が始まります。これが非常に楽しい。自分が口を挟むもよし、色々な意見を傾聴するもよし、です。

これは私だけかもしれませんが、自分ひとりで本を読んでいると、特に難しい内容になると顕著ですが、「まああとでわかるだろう」と読み飛ばしてしまいがちです。そして、だいたいきちんと理解せずに読了してしまいます。この読書会のやり方では、疑問に思ったところを相談できます。疑問がその場で解決することもありますし、もちろん書籍の内容だけでは判断できないものは保留になることもあります。そういった場合でも、あとでちゃんと調べてみようと指針を立てたり、議論のうちに自分が何を理解していないのか整理できることがあります。非常によいスタイルだと思います。

実は社内でも、Java 仮想マシン仕様 の読書会をこのやり方で実施しています。この仕様書は自分にとって相当難解で、気楽に周りに相談できるという場は非常にありがたいです。

第4回 所感

※書籍の内容とは関係ない内容も含みます。

初めて知ったこと

  • Java EE 7 の WebSocket はもう使われているシステムあり。LB や SSL アクセラレータをかましての動作実績もありそう。AP サーバは何使ってるか聞きそびれた。。Jetty あたりかなあ。次回聞いてみたい。
  • 匿名 Inner クラスでメソッドを実装する際に、実装しようとするメソッドの外側にあるローカル変数は final つきでないと読めない。例えば boolean をチェックしたい場合は Boolean bool = false; などと宣言してしまうと、Inner クラス中で bool = true; などとできなくなるので、AtomicBoolean などを使って凌ぐしかない。

困り事と提案

  • インターネット前提の Maven は使いづらい。
    • たしかにその通りだと思う。プログラマがインターネットの利用を制限されている環境は少なくないかもしれない。ただ、Sonatype などを使って、社内のプライベートリポジトリを用意することで、毎回だれか(インターネット接続許可のある一部のひと)がライブラリをダウンロードするのに走り回る必要はなくなるんじゃないかと思う。
  • システム依存や異常系のテストはどこまでやるか
    • 線引きが難しい。ただ、参加者の方も言ってらしたけど、結合試験で実施した方が早いものなんかは割り切りも大事かなと。

情報共有

Arquillian による DB 接続を伴うユニットテスト

書籍中では DBUnit を利用したコードが紹介されています(接続コードだけでなく、DB 起動・停止、テスト実行における前処理・後処理、テストデータの設定含む)。

JBoss などの Java EE AP サーバを利用して開発をする際に、DB 接続部分と開発用 DB の起動・停止に関して簡略化する方法をご紹介します。

Arquillian という、JUnit と協調するテスティングフレームワークがあります。
これは Java EE AP サーバを実際に起動させることで、Java EE の本物のコンテナ(EJB や CDI など)をテスト中で使えるようにするものです。

これが ユニットテストにおける DB接続をどう簡略化させるかというと、JPA(Java EE 標準の ORM 仕様) + インメモリデータベースの組み合わせが鍵になります。
JBoss や GlassFish であれば 、内部にインメモリデータベース(H2 / Derby)を持っており、サーバの起動とともにこれら DB が立ち上がります。これで起動・停止は自動化されます。あとは通常のデータソース設定として DB 接続定義を記載しておけば、プロダクションコードと同様の仕組みで DB 接続ができます。Maven の設定次第では テスト用と開発用の DB を分けておく事もできます。

テストコード中の DB 接続コードについては、JPA では EntityManager というインタフェースを利用してエンティティの操作を行いますが、このインスタンスは @PersistenceContext とアノテーションを付与することでインジェクションされます(参考)。JPA は Java SE でも利用できますが、こういったインジェクションが可能なのは AP サーバのコンテナがあってこそのものです。

JPA を使いたくない場合でも、@Inject(CDI) や @Resource といったインジェクション用アノテーションを利用して、javax.sql.Datasource を取得できますので、すぐに DB 処理に関するコードを書き始められます。

前処理・後処理に関しては、通常通り @Before・@After を利用します。

調べる事

  • Mockito の実装をちょっと読んでみたい。

2013年6月13日木曜日

PukiWiki Adv. を nginx で動かす

以下環境で PukiWiki Adv. を動かすためのメモ。
  • CentOS 6.4(さくらの VPS)
  • nginx 1.4.1
  • PHP 5.4.16
  • PukiWiki Adv. 1.0.3

なぜ Apache でなく nginx か

最初は PukiWiki Adv. を Apache httpd で動かそうと考えていましたが、.htaccess の設定がうまく読込めず、mod_rewrite を利用しての静的URL表現ができなかったため、nginx を試してみました。特にトラフィックが〜などの動機ではありませんので、性能面に関しての言及はありません。すんません。

nginx のインストール

今回の PukiWIki Adv. のために追加で必要そうなモジュールがいまいちよくわからなかったので、yum を使ってインストールしました。

# rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
# vim /etc/yum.repos.d/nginx.repo
enabled の値を 0 に変更
# yum -y install nginx --enablerepo=nginx

チューニングなどはよしなに。

[参考]
CentOS-6へ最新版のnginxを3分でインストールする方法

PHP関連パッケージのインストール

現在、以下のパッケージがインストールされています。
remi や rpmforge、epel などのレポジトリを追加しておくと良いと思います。

# rpm -qa | grep php
php-common-5.4.16-1.el6.remi.x86_64
php-gd-5.4.16-1.el6.remi.x86_64
php-cli-5.4.16-1.el6.remi.x86_64
php-devel-5.4.16-1.el6.remi.x86_64
php-mbstring-5.4.16-1.el6.remi.x86_64
php-fpm-5.4.16-1.el6.remi.x86_64
php-5.4.16-1.el6.remi.x86_64

PHP-FPM のチューニング、がんばりましょう。

PukiWiki Adv. のインストール

# cd /var/www/html
# tar Jxvf /path/to/PukiWiki\ Adv.\ 1.0.3.tar.xz
# mv webroot pukiwiki
# mkdir misc
# mv COPYING.txt README.txt doc tools misc
# ls -l | awk '{ print $10 }'
misc
pukiwiki
wiki-common
wiki-data

PHP-FPM にプロキシさせる nginx の設定

/etc/nginx/conf.d/pukiwiki.conf を以下の内容で新規作成しました。
yourdomain は皆様のドメイン名に変更してください。

server {
    listen              80;
    server_name  yourdomain;
    root                /var/www/html/pukiwiki;
    index               index.php;

    location / {
        error_page      404 = @pukiwiki;
        log_not_found   off;
    }

    location @pukiwiki {
        rewrite ^/(.+)$         /index.php?$1   last;
        break;
    }

    location ~ \.php$ {
        include         fastcgi_params;

        fastcgi_pass    127.0.0.1:9000;
        fastcgi_index   index.php;
        fastcgi_param   SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    }
}

[参考]
Rewriteを用いたサーチエンジン最適化 - nginx

nginx と PHP-FPM の起動

# /etc/init.d/php-fpm start
# /etc/init.d/nginx start

感想

自分は nginx を今日初めて触りましたが、設定書式がプログラムチックなためか非常に直感的で、ほとんど悩まずに設定することができました。Apache も別に詳しくなく、PHP に至ってはほとんどよくわかってないのですが、とりあえず公開する所までの敷居は低いと思います。

参考URL・書籍

感想でえらそうにいってましたが、スムーズに構築できたのは下記ページや書籍のおかげです。ありがとうございます。

  • nginx
    • nginx 本家
  • WEB+DB PRESS Vol.72
    • nginx の設定内容から PHP アプリケーションとの連携までわかりやすく解説されています。