Webアプリのセッション管理 / Azure Plugin for Eclipseのセッション アフィニティ

Posted: 2011/12/25 カテゴリー: Uncategorized
タグ:, , , , , , , ,

Windows Azure Advent Calendarに参加します。24日目の予定でしたが、訳あって25日目になりました…

 

 

Webアプリのセッション

Azure使いの皆さんはよくご存知の通り、Windows Azure コンピューティングのロール インスタンス (仮想サーバー) の手前に配置されるロード バランサーのアルゴリズムは、非常にシンプルなラウンド ロビンのみです。

ステートレスなWebアプリの方がスケールアウトが容易で、クラウド向きだとは分かっていても、既存環境からの移行や開発生産性などの理由で、Webアプリでステートフルなセッションを使っている場合は少なくないと思います。

ここではまず、Webアプリのセッションについて、復習も兼ねてまとめてみたいと思います。よくご存知の方は、読み飛ばしてくださいませ。

Webアプリにおけるセッションとは、特定のクライアント (主にWebブラウザ) と特定のWebアプリの間の一連のHTTPリクエスト群を関連付けることで、本質的にステートレスなHTTPプロトコルの上で、仮想的にステートフルな通信を実現するものです。これによって、複数のHTTPリクエストにわたって、セッション状態を維持することが可能となります。

Webアプリ開発言語でのセッションのサポートに関するリンクを、いくつか挙げておきます。

 

 

 

どの言語でも、セッションの基本的な概念は類似しています。セッションの新規作成、削除、タイムアウト (クライアントからのアクセスがしばらくないセッションの無効化)、セッション データ (名前/値ペア) の設定/取得がサポートされています。

クライアントとサーバー プロセスとの間でセッションを識別するために、Cookieに格納された、またはURL内に埋め込まれたセッションIDを使います。Cookieが使われるケースが、ほとんどでしょう。

既定では、特定のクライアントに対応するセッション情報は、単一のサーバー プロセスのメモリ上だけに保持されています。ですので、複数のサーバー プロセス群が同じWebアプリをホストするクラスタ環境では、特定のクライアントの一連のリクエストが同じサーバー プロセスにルーティングされ続けない限り、セッション情報にアクセスできずに、正常に動作しなくなるわけです。

クラスタ環境でセッションを使うWebアプリをで動作させる対応策は、2つあります。1つはセッション アフィニティ/スティッキー セッション、もう1つはセッションの永続化/レプリケーションです。

セッション アフィニティ/スティッキー セッション

セッション アフィニティ/スティッキー セッションとは、サーバー プロセス群の手前に位置し、ルーティング/負荷分散機能を提供するものが、背後のサーバー プロセス群のうち、リクエスト元のクライアントのセッションに属する処理を以前していたサーバー プロセスに対してルーティングする機能のことです。

「サーバー プロセス群の手前に位置するもの」とは、具体的にはロード バランサーや、(サーバー プロセスとWebサーバーが分離している場合は) ApacheやIISなどのWebサーバーが考えられます。

これらは、サーバー プロセスが返すセッションID Cookieに、どのサーバー プロセスかを示す追加情報を追加したり、独自のCookieを別途設定したり、セッションIDとサーバー プロセスのマッピング情報を保持したりすることで、適切なサーバー プロセスへのルーティングを実現します。

セッションに属さないリクエストや、セッション内の初回のリクエストに対しては、セッション アフィニティのルーティングは行われず、ラウンド ロビンなどの指定されたアルゴリズムで、負荷分散を行います。

セッションの永続化/レプリケーション

複数のサーバー群を配置するクラスタ環境を使う主な理由は、スケーラビリティと可用性の向上です。ですが、クラスタ内の「いずれか1台」に障害が発生する確率は、サーバー台数が多いほど増加します。1サーバーに障害が発生することでエラーが発生してしまうと、(可用性の定義にもよりますが) 可用性の低下、あるいは (セッション情報の喪失やエラーによるリトライなど) ユーザーの満足度低下を引き起こしてしまうでしょう。

セッションに話を戻しましょう。セッション アフィニティが実現されていても、セッション情報を保持しているサーバー プロセスに障害が発生し、リクエストを処理できなくなると、セッション情報が失われます (その結果起こる問題は、Webアプリの作り次第)。

こういったサーバー プロセス障害に対応するためには、セッション情報を保持しているサーバー プロセス (プライマリー サーバー) が、自身が保持するセッション情報を、他のサーバー プロセス (セカンダリー サーバー)が取得できるようにする必要があります。

主なアプローチは、2つあります。1つは、プライマリー サーバーが、自身のメモリ上に加えて、共有可能な外部ストア (外部のリレーショナル データベース、外部のストレージ、共有ファイル システム/共有ストレージ、インメモリー キャッシュ サーバーなど) に、セッション情報を随時格納しておく方法です。セカンダリー サーバーでセッション情報が必要となった場合には、これらの外部ストアからセッション情報を読み込みます。

別のアプローチとしては、プライマリー サーバーが、1つ以上のセカンダリー サーバーにセッション情報を随時送信することです。セカンダリー サーバーの数や選択などにバリエーションがありますが、基本的でセカンダリー サーバーがセッションが必要となった際には、すでにメモリ上にセッション情報がある、ということになります。

 

 

 

 

 

 

ここまでは、セッション アフィニティのルーティングがある状態でのセッションの永続化/レプリケーションを考えてきました。が、前述の通り、Windows Azureのロード バランサーは、セッション アフィニティのルーティングをしてくれません… この場合、(頻度の少ない、サーバー障害後のフェイルオーバー時ではなく) 定常的に頻繁にセカンダリー サーバーへのルーティングが発生してしまうので、セッションの永続化/レプリケーションはますます重要になります。

Windows Azure Plugin for Eclipse with Java

前置きはここまでにして、本題に入りましょう。

Windows Azure Plugin for Eclipse with Javaは、Tomcat、JettyなどのJavaアプリケーション サーバーをWindows Azureのワーカー ロールで実行するためのAzureパッケージ作成を支援する、Eclipseプラグインです。スタートアップ タスクなどを活用して、JDK、Javaアプリケーション サーバー、Java Webアプリのインストールやサーバー プロセス起動を自動化します。

今月 (2011年12月) リリースされたWindows Azure Plugin for Eclipse with JavaのCTPでは、「1つのチェック ボックスだけで、ステートフルなクラスタリングされたJavaアプリケーションへの対応が可能になる、セッション アフィニティ (スティッキー セッション) サポート」なる新機能が提供されています。具体的に何をしてくれるのか、気になりますよね?

 

Windows Azure Plugin for Eclipse with Java – December 2011 CTP has released. New features include:

Session affinity (“sticky sessions”) support: Helping enable stateful, clustered Java applications with just a single checkbox. For more information, see Session Affinity.

 

 

Java Webアプリのサンプル

Windows Azure Plugin for Eclipse with Javaは、Java WebアプリのAzure向けのパッケージングを行うものなので、Java Webアプリ自体の開発には必ずしもEclipseを使う必要はありません。とはいえ、ついでなので、Eclipse WTP (Web Tools Project) でセッションを使うJSPページ1つだけからなるWebアプリを作ってみましょう。

詳細な手順は割愛しますが、「Dynamic Web Project」を新規作成し、次のような「JSP File」を新規作成したら、ローカルのTomcat環境でテストしてみましょう。

<%@page import="java.net.InetAddress"%>
<%@page import="java.util.Date"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>HTTP Session</title>
</head>
<body>
<%
String counterAttribute = "counter";
Integer counter = 0;

if (session.getAttribute(counterAttribute) != null) {
	counter = (Integer)(session.getAttribute(counterAttribute));
}

session.setAttribute(counterAttribute, ++counter);
%>

<ul>
<li>Session ID: <%= session.getId() %></li>
<li>Counter: <%= counter %></li>
<li>Host Address: <%= InetAddress.getLocalHost().getHostAddress() %></li>
<li>Session Creation: <%= new Date(session.getCreationTime()) %></li>
</ul>

</body>
</html>

このシンプルなJSPは、セッションID、(セッション内のリクエストごとに増加する) カウンター、Javaアプリケーション サーバー (Tomcat) のIPアドレス、セッション作成日時を表示します。ローカル環境では、リロードするごとにカウンターが増加し、それ以外の情報には変化がないはずです。

Tomcatを含む、Java Servlet仕様に準拠したアプリケーション サーバーでは、セッションIDの送受信のために「JSESSIONID」という名前のCookieが使われます。

TomcatのAzure向けパッケージング

Windows Azure Plugin for Eclipse with Javaによって、「Windows Azure Project」という新しいプロジェクト テンプレートが追加されているので、このプロジェクトを新規作成します。インスタンス数は2以上に増やしておきましょう (プロジェクト作成後に変更も可能)。

「samples/startupApacheTomcat7.txt」を「<ロール名>/approot/startup.cmd」にコピーし、環境や好みに合わせて適宜編集します。

今回は、Azureパッケージ サイズの削減のため、JDKとTomcatを、事前にアップロードしておいたWindows Azure BLOBストレージから、ロール インスタンス起動時にダウンロードするようにしました。

:: *** Sample startup script containing the steps for starting Apache Tomcat and deploying a WAR file.
:: *** (Last tested with Apache Tomcat 7.0.22)

:: To use the sample, follow these steps:
:: 1) Copy all this content into approot/startup.cmd in the role folder, close this file, and edit the copy
:: 2) Place a JDK distribution as jdk.zip under approot
:: 3) Place an Apache Tomcat 7.x distribution as tomcat7.zip under approot in your project
::    3.1) If you want to download the server into Azure directly from a URL instead, then
::         uncomment the next line and modify the URL as appropriate:
:: cscript /NoLogo "util\download.vbs" "http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.22/bin/apache-tomcat-7.0.22.zip" "tomcat7.zip"

cscript /NoLogo "util\download.vbs" "https://********.blob.core.windows.net/java/jdk.zip" "jdk.zip"
cscript /NoLogo "util\download.vbs" "https://********.blob.core.windows.net/java/tomcat7.zip" "tomcat7.zip"

:: 4) Update SERVER_DIR_NAME below as appropriate:
::    (IMPORTANT: There must be no trailing nor leading whitespace around the setting)
:: SET SERVER_DIR_NAME=apache-tomcat-7.0.22

SET SERVER_DIR_NAME=apache-tomcat-7.0.23

:: 5) To deploy your own WAR file, place it in approot and update WAR_NAME below:
::   (IMPORTANT: There must be no trailing nor leading whitespace around the setting)

:: SET WAR_NAME=HelloWorld.war
SET WAR_NAME=SessionApp.war

:: *****************************************************************
:: *** Deployment and startup logic
:: *** (Do not make changes below unless you know what you're doing.

rd "\%ROLENAME%"
mklink /D "\%ROLENAME%" "%ROLEROOT%\approot"
cd /d "\%ROLENAME%"
cscript /NoLogo util\unzip.vbs jdk.zip "%CD%"
cscript /NoLogo util\unzip.vbs tomcat7.zip "%CD%"

copy %WAR_NAME% "%SERVER_DIR_NAME%\webapps\%WAR_NAME%"

cd "%SERVER_DIR_NAME%\bin"
set JAVA_HOME=\%ROLENAME%\jdk
set PATH=%PATH%;%JAVA_HOME%\bin
cmd /c startup.bat

@ECHO OFF
if %ERRORLEVEL%==0 exit %ERRORLEVEL%
choice /d y /t 5 /c Y /N /M "*** Windows Azure startup failed - exiting..."
exit %ERRORLEVEL%

Webアプリは「<ロール名>/approot/」に置くことにしたので、JSPを含んでいる「Dynamic Web Project」を右クリックし、「<ロール名>/approot/」に「WAR file」を「Export」します。

リモート デスクトップやデバッグの設定も簡単にできるので、興味のある方はドキュメントを参考にしてお試しください。

さて、この状態でクラウド向けにビルドしてcspkg/cscfgファイルを生成し、Windows Azure管理ポータルからデプロイしましょう。

JSPにアクセスし、リロードを繰り返すと、ランダムに異なるロール インスタンス (IPアドレス) にルーティングされ、その度にセッション (カウンター、作成日時) が初期化されることが分かります。

これは、Windows Azureのロードバランサーでセッション アフィニティのルーティングが行われず、複数のTomcat間でセッションの永続化/レプリケーションも行われていないため、アプリが正しく動作していないことを示しています。

セッション アフィニティ機能の有効化

それでは、セッション アフィニティ機能を有効化してみましょう。ロールのプロパティで、チェック ボックスをチェックするだけです。

再度、ビルドとデプロイを行い、JSPをテストしてみましょう。今回は、同じブラウザからリロードを繰り返しても、同じロール インスタンス (IPアドレス) にルーティングされ続け、セッションの初期化が起こらないはずです。

また、ブラウザを再起動するか、あるいは別のブラウザを使うと、別のロール インスタンス (IPアドレス) にルーティングされることがあることも分かります。

つまり、外部からの観察では、セッション アフィニティのルーティングが行われているように見えています。

セッション アフィニティ機能の内部実装

では、内部実装はどうなっているでしょうか?

セッション アフィニティ機能を有効化すると、サービス定義ファイル ServiceDefinition.csdef で、ConfigureARR.cmd がスタートアップ タスクとして定義されます。

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<ServiceDefinition xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" name="WindowsAzureProject">
  <WorkerRole name="TomcatRole" vmsize="ExtraSmall">
    <Startup>
      <!--Do not delete this startup task or insert tasks before it. It was created by Windows Azure Plugin for Eclipse with Java to enable session affinity. -->
      <Task commandLine=".sessionaffinity\ConfigureARR.cmd http http_SESSION_AFFINITY" executionContext="elevated" taskType="simple">
        <Environment>
          <Variable name="EMULATED">
            <RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated"/>
          </Variable>
        </Environment>
      </Task>
      <!-- Sample startup task calling startup.cmd from the role's approot folder -->
      <Task commandLine="startup.cmd" executionContext="elevated" taskType="simple"/>
    </Startup>
    <Runtime executionContext="elevated">
    	<EntryPoint>
	  		<!-- Sample entry point calling run.cmd from the role's approot folder -->
    		<ProgramEntryPoint commandLine="run.cmd" setReadyOnProcessStart="true"/>
    	</EntryPoint>
    </Runtime>
    <Imports>
  	  <Import moduleName="RemoteAccess"/>
  	  <Import moduleName="RemoteForwarder"/>
    </Imports>
    <Endpoints>
      <InputEndpoint localPort="31221" name="http" port="80" protocol="tcp"/>
    <InternalEndpoint name="http_SESSION_AFFINITY" protocol="tcp">
        <FixedPort port="8080"/>
      </InternalEndpoint>
    </Endpoints>
  </WorkerRole>
</ServiceDefinition>

「<ロール名>/approot」の下に「.sessionaffinity」というフォルダーが生成されますが、Eclipseからは見えないので、エクスプローラなどから確認しましょう。このフォルダーには、次の内容を含むスタートアップ タスクの ConfigureARR.cmd と、SessionAffinityAgent.exe が存在します。

@rem check if running in emulator , if yes then ignore session affinity settings silently
if "%EMULATED%"=="true" goto Exit

@rem creating path variable for session affinity folder to refer in multiple places
set  WEBPI_PATH="%ROLEROOT%\approot\webpi"
rmdir /S /Q %WEBPI_PATH%
mkdir %ROLEROOT%\approot\webpi

@rem Downloading webpicmdline
cscript "%ROLEROOT%\approot\util\download.vbs" "http://go.microsoft.com/?linkid=9752821" "%WEBPI_PATH%\webpicmd.zip"

@rem unzipping the contents of zip file
cscript %ROLEROOT%\approot\util\unzip.vbs "%WEBPI_PATH%\webpicmd.zip" "%WEBPI_PATH%\"

@rem Installing ARR
%WEBPI_PATH%\webpicmdline /accepteula /Products:ARR

@rem removing wepi folder
rmdir /S /Q %WEBPI_PATH%

@rem calling the
start %ROLEROOT%\approot\.sessionaffinity\SessionAffinityAgent.exe %1 %2
%ROLEROOT%\approot\.sessionaffinity\SessionAffinityAgent.exe -blockstartup

:Exit
exit 0

このConfigureARR.cmd では、WebPI コマンドライン ツール (webpicmdline) のダウンロード、webpicmdlineを使ったARR (Application Request Routing) のインストールSessionAffinityAgent.exe の起動を行っています。

 

 

 

 

SessionAffinityAgent.exeのソースは公開されていませんが、不明瞭化 (obfuscation) されていないので XXXXXX するか (一応自粛)、ロール インスタンスにリモート デスクトップ接続し、IIS ManagerでSessionAffinityAgent.exeが行った設定を確認することができます。

IIS Managerで確認すると、「http_SESSION_AFFINITY」というサーバー ファームに、自身を含むすべてのロール インスタンスが登録されていることが分かります。Windows Azure管理ポータルなどからロール インスタンス数を増減させると、サーバー ファームもそれに追随することも確認できます。

また、IIS (ARR) が受信したリクエストを、このサーバー ファーム内のサーバーにルーティングするURLリライトが定義されています。

サーバー ファームでは、(アフィニティのないリクエストに対して) ラウンドロビンで均等に負荷分散すること、「ARRAffinity」という名前のCookieを使ってアフィニティを管理すること (ARRのクライアント アフィニティ) が定義されています。

ARRのクライアント アフィニティは、ARRのレイヤーで、ルーティング先を識別するための独自のCookieをレスポンスに追加するものです。

これを踏まえると、Windows Azure Plugin for Eclipse with Javaのセッション アフィニティ機能は、次のようなアーキテクチャとなります。ロード バランサーは相変わらずアフィニティを無視しますが、ARRがアフィニティのルーティングを行うので、最終的に適切なTomcatにルーティングされることが分かります。

実際のHTTPリクエスト ヘッダーでも、確かに、2種類のCookieが送信されていることを確認できます。

2つの異なるCookieのライフサイクルの違い (Java WebアプリのHTTPセッションを削除しても、ARRAffinity Cookieには影響を与えないこと、異なる可能性のあるタイムアウト時間など) が、微妙に問題になるケースもあり得ますが、基本的には問題なく動作しそうです。

今回はカバーしていませんが、Tomcatなどのアプリケーション サーバーでセッションの永続化 (DBなどへの保存) も併用することで、さらに信頼性の高いセッション管理が可能になるでしょう。

この情報がどれだけ参考になるのか分かりませんが、ご参考まで!

コメント
  1. […] Webアプリのセッション管理 / Azure Plugin for Eclipseのセッション アフィニティ […]

  2. .NET初心者というより全て初心者 より:

    とても参考になります!
    でもこのセッションの仕組みってロードバランサーの負荷分散が意味なくなるような・・・
    僕の勘違いでしょうか??

    • SATO Naoki より:

      Azure自体のロード バランサーは、ワーカーロールの死活監視をした上で、正常稼働しているワーカー ロール インスタンスの1つにルーティングしてくれるので、意味がないわけではないと思います。

  3. SATO Naoki より:

    このブログ ポストをでは「SessionAffinityAgent.exeのソースは公開されていませんが」と書きましたが、実はここでソースが公開されています。

    http://wastarterkit4java.codeplex.com/SourceControl/changeset/view/16876

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中