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)) { ... }

0 件のコメント:

コメントを投稿