Azureの小ネタ (改)

~Azureネタを中心に、色々とその他の技術的なことなどを~

Windows Azure Plugin for Eclipse with Java を使ってWindows AzureでJettyを複数インタンス起動しつつ、セッションの永続化を試してみる

メリークリスマス!この記事は、Windows Azure Advent Calendar 2013 - Qiita [キータ] の25日目、最後の記事となります。

はじめに

どちらかというと、まだまだC#などよりJava歴の方が長いわけなんですが、IDEなんかも、Visual StudioよりEclipseが使いやすいと感じてしまいます。というわけで、今回のアドベントカレンダーは、AzureでもマイナーなJavaの記事を書くことにします。

Java on Azure

Azure上でJavaを動かすというのは、Tomcatなどのアプリケーションサーバー(サーブレットコンテナ)を動かすことを指しているのですが、現在のところ以下の2種類の方法が選択できるでしょう。

  1. 仮想マシン(IaaS)で動かす
  2. クラウドサービスのWorkerロールで動かす

まぁ、1.なんかは、IaaSなのでどうぞご自由にって感じなため、今回は触れません。今回は、2.のWorkerロールでがんばるほうです。

さて、クラウドサービスであるのWorkerロールでTomcatなどを動かす場合、MS OpenTech謹製のEclipse Pluginツールがリリースされているので、これを利用することになります。EXEへの依存などがあって、Windows上でしか動かないことに注意してください。

Azure Eclipse Pluginの特徴

Azure Eclipse Plugin には、ARRによるセッションアフィニティを構成する機能が組み込まれており、特定のインスタンスに通信を割り振ることが出来るようになっています。詳しくは、[ Webアプリのセッション管理 / Azure Plugin for Eclipseのセッション アフィニティ | S/N Ratio (by SATO Naoki)]で解説されているので、熟読してみてください。

これにより、複数インスタンス起動時の問題が微妙に解決されてしまうわけですが、完璧ではありません。片方のインスタンスが落ちたときにセッション情報が失われてしまいます。

というわけで、ようやく本題なのですが、Windows Azure Eclipse Pluginとアプリケーションサーバーを使ってセッションの永続化をしてみたいと思います。たぶん、私の技術的興味からスタートしたので、ほとんどの人にとっては役立たずなネタになることでしょう。

サーバーですが、Tomcatは色々と重たいので、最近組み込みの用途などで重宝されている軽量なJettyを使います。またお手軽なWTPプラグイン(Web Tool Platform Plugin)を利用する都合上、最新版のJetty 9ではなくJetty 8で試してみます。ちなみにEclipseは、4.3 Keplerです。

アプリ作成

まずはEclipseでDynamic Web Projectを作成し、以下のJSPを用意します。ロールの情報とセッションデータを入出力できるように適当に作ってあります。

<%@page import="java.util.Map" %>
<%@page import="java.util.Date" %>
<%@page import="java.util.Enumeration" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!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=ISO-8859-1">
<title>Hello from Windows Azure</title>
<style>
.label { font-weight: bold;}
</style>
</head>
<body style="font-family: 'segoe UI', 'tahoma'">
<h1 style="color: navy; font-weight: lighter">Hello from Windows Azure!</h1>

<%    
    if(request.getMethod().equals("POST")) 
    {
        session.setAttribute(request.getParameter("key"),
                             request.getParameter("value"));
    }
%>

<table>
<tr><td class="label">Role name:</td><td><%=System.getenv("RoleName") %></td></tr>
<tr><td class="label">Role instance ID:</td><td><%=System.getenv("RdRoleID") %></td></tr>
<tr><td class="label">Role root directory:</td><td><%=System.getenv("RoleRoot") %></td></tr>
<tr><td class="label">Role type:</td><td><%=System.getenv("WaRoleType") %></td></tr>
<tr><td class="label">JAVA_HOME:</td><td><%=System.getenv("JAVA_HOME") %></td></tr>
<tr><td class="label">Current time:</td><td><%=new java.util.Date() %></td>
<%
    Enumeration<String> attrNames = session.getAttributeNames();
    while(attrNames.hasMoreElements())
    {
        String key = attrNames.nextElement();
        String value = session.getAttribute(key).toString();
%>
<tr><td class="label"><%=key %></td><td><%=value %></td>
<%
    }
%>
</table>
<div class="label">
<form id="the-form" action="/HelloAzure/index2.jsp" method="POST">
<p>Input session data.</p>
    Key : <input type="text" name="key"/><br/>
    Value : <input type="text" name="value"/><br/>
    <button>Submit</button></form>
</div>
</body>
</html>

なにも設定せずローカルに(Azureエミュレータでもない)実行すると、以下の感じになります。まだこの段階では、セッション情報はメモリにあります。

f:id:StateMachine:20131224190843p:plain

セッションストアを構成する

目的は、Windows Azure上での実行ですので、セッションストアにはSQL Database(SQL Azure)を選択します。SQL Database用のJDBCドライバは以下にあります。解凍してsqldcbc4.jarをクラスパスに通してください。あとで気づいたのですが、Windows Azure Eclipse Plugin中に、プラグイン化(OSGi Bundle化)されたJDBCドライバーが同梱されており、Add Libraryからお手軽にクラスパスに追加できそうでした。

jetty.xml

まずはjetty.xmlの修正です。WTPプラグインだと、インストールしたJettyのetc/jetty.xmlを修正しなくても、Eclipseで定義したサーバーのローカルファイルを修正して試すことができます。接続文字列はAzureの管理ポータルからコピペして持ってきてください。呪文めいたXMLですが、じっと見ているとインスタンスをNewして、Setして、Callしてみたいな感じで読み解けてくるでしょう。

    ...
    ...

    <Set name="sessionIdManager">
      <New id="jdbcidmgr" class="org.eclipse.jetty.server.session.JDBCSessionIdManager">
        <Arg>
          <Ref id="Server"/>
        </Arg>
        <Set name="workerName">azure</Set>
        <Call name="setDriverInfo">
          <Arg>com.microsoft.sqlserver.jdbc.SQLServerDriver</Arg>
          <Arg>jdbc:sqlserver://normalian.database.windows.net:1433;database=testdb;user=mukuri@hentai;password=hogehoge!;encrypt=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;</Arg>
        </Call>
        <Set name="scavengeInterval">60</Set>
      </New>
    </Set>
    <Call name="setAttribute">
      <Arg>jdbcIdMgr</Arg>
      <Arg>
        <Ref id="jdbcidmgr"/>
      </Arg>
    </Call>

WEB-INF/jetty-web.xml

次にWEB-INF/jetty-web.xmlを作成して、以下を定義します。アプリのコンテキストでセッションマネージャを構成します。

<?xml version="1.0" encoding="UTF-8"?>
<Configure class="org.eclipse.jetty.webapp.WebAppContext">

    <Get name="server">
    <Get id="jdbcIdMgr" name="sessionIdManager" />
    </Get>
    <Set name="sessionHandler">
        <New class="org.eclipse.jetty.server.session.SessionHandler">
            <Arg>
                <New class="org.eclipse.jetty.server.session.JDBCSessionManager">
                    <Set name="sessionIdManager">
                        <Ref id="jdbcIdMgr" />
                    </Set>
                </New>
            </Arg>
        </New>
    </Set>
</Configure>

ローカル実行

さて、WTPプラグイン上で必要なサーバーの構成をして、実行してみますが、予想を裏切って例外で落ちてしまいます。SQL Server Exceptionが発生し、どうやらblob typeが見つからないと言っています。某ムッシュ氏ならば、この段階でというか、実行する前から原因が分かってしまうのでしょうが、ど素人の私にはさっぱり分からないので、ソースを追ってデバッグしてみることにします。

2013-12-24 16:42:56.623:INFO:oejs.Server:jetty-8.1.14.v20131031
2013-12-24 16:43:01.275:WARN:oejuc.AbstractLifeCycle:FAILED org.eclipse.jetty.server.session.JDBCSessionIdManager@107d91: com.microsoft.sqlserver.jdbc.SQLServerException: Column, parameter, or variable #12: Cannot find data type blob.
com.microsoft.sqlserver.jdbc.SQLServerException: Column, parameter, or variable #12: Cannot find data type blob.
    at com.microsoft.sqlserver.jdbc.SQLServerException.makeFromDatabaseError(SQLServerException.java:216)
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.getNextResult(SQLServerStatement.java:1515)
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.doExecuteStatement(SQLServerStatement.java:792)
    at com.microsoft.sqlserver.jdbc.SQLServerStatement$StmtExecCmd.doExecute(SQLServerStatement.java:689)

Eclipseだと後からソースをアタッチしてデバッグできるので楽チンです(VSでも出来るんでしょうけど、やり方を知らない)。追っかけていくと、CREATE TABLEの引数にサポートしてない型を指定しているようです。さらに調べると、結局は、JDBCSessionIdManager#getBlobType メドッドで、SQL Databaseに対応したBLOBタイプが定義されていないのが原因でした。

        public String getBlobType ()
        {
            if (_blobType != null)
                return _blobType;
            
            if (_dbName.startsWith("postgres"))
                return "bytea";
            
            return "blob";
        }

Googleにて「sql server blob data type」などと検索してみますと、SQL Databaseには、blob型はありませんでした、かわりに varbinary(MAX) などと指定すればいいことが分かりました。

さて、ソースを修正したいところですが、今回は少し面倒なので、あらかじめデータベースに手動でテーブルを作成します。テーブルが作成されていれば、該当ルートは通らないので例外で落ちることはありません。テーブルは2つ作られるのですが、失敗しているのは、以下のテーブルです。

create table JettySessions (rowId varchar(120), sessionId varchar(120),  
       contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), 
       accessTime bigint,  lastAccessTime bigint, createTime bigint, 
       cookieTime bigint,  lastSavedTime bigint, expiryTime bigint, 
       map varbinary(MAX), primary key(rowId))

SQL Management Studioで接続してテーブルを作成して、再実行し動作確認をします。以下のようになにかデータが入っていればよいでしょう。

f:id:StateMachine:20131224191840p:plain

Windowa Azure プロジェクト作成

ようやくアプリが作成できたので、Windows Azure デプロイメントプロジェクトを作成します。プロジェクト名は適当に設定して、Next。

f:id:StateMachine:20131224191940p:plain

JDKの設定をします。JDKに設定は以下の3種類から選べます。2.が現実的な選択肢なんでしょうが、OpenJDKをあまり信用していないので、ローカルのJDKをZIP化してストレージからダウンロードしてもらいます。これらは自動でやってくれます。

  1. ローカルのJDKをZIPして、ストレージにアップロード。
  2. 3rd PartyのJDKをダウンロード。ただし、Open JDKとなる。
  3. カスタムダウンロード

f:id:StateMachine:20131224192056p:plain

次にアプリケーションサーバーです。Jetty 8のロケーションを設定すると、勝手にTypeが設定されます。これもローカルをZIP化、デプロイ時に ストレージ経由でダウンロードされます。

f:id:StateMachine:20131224192507p:plain

次にアプリの設定しますが画像は割愛。最後にセッションアフィニティ、キャッシュ、リモートデバッグなどの構成が行えますが、ここではすべてオフにしておきます。

f:id:StateMachine:20131224192946p:plain

エミュレータ上での実行

先ほどはWTP上での実行であったため、エミュレータで実行する前には、いくつか設定を変更する必要があります。

  • %jetty.home%\lib\ext\ にJDBCのJARをコピー。追加のJARはここに入れておけば勝手にクラスパスに通ります。
  • %jetty.home%etc\jetty.xml に先ほどと同じセッション構成の修正を行います。

エミュレータで起動すると、以下のような感じです。

f:id:StateMachine:20131224193808p:plain

インスタンスIDや、ロール名にも値が入るようになりました。

f:id:StateMachine:20131224194004p:plain

実際に、エミュレータや作りの上で気になる点として以下。

  • エミュ上で複数インスタンス起動できない(複数ロールは大丈夫そうだが、未検証)
  • C:\WorkerRole1 とかいう、シンボリックリンクを勝手に作る。
  • 起動に失敗したときに、エラーが追っかけられない(ような気がする、どこかにログある?)
  • UNZIPがVBScriptで書いてあって遅い(たぶん、昔から変わらず)

デプロイ & 実行

ようやくデプロイの準備が整ったので、デプロイして実行確認してみましょう。リモートデスクトップも簡単に構成しておきます。起動に失敗する可能性もあるので、デバッグのため接続可能にしておくと便利です。

f:id:StateMachine:20131224231538p:plain

いちおうコンソールに状況を表示してくれます。Runningになれば、デプロイ完了ですが、結構時間がかかる気がします。下の経過時間を見ると、6分弱でしょうか。

Deploying to Windows Azure...
12/24/2013 07:42:46 - Creating cloud service - hogehoge
12/24/2013 07:42:53 - Uploading certificate...
12/24/2013 07:43:00 - Configuring remote desktop
12/24/2013 07:43:01 - Uploading deployment package
12/24/2013 07:43:04 - Creating deployment
12/24/2013 07:43:40 - Waiting for instance
12/24/2013 07:48:37 - Running

この時点では、まだインスタンスが1つであるためスケールしてませんが、シングルインスタンスで実行確認ができます。

f:id:StateMachine:20131224224008p:plain

管理ポータルからスケールアウトしみましょう。リッチにSサイズを3インスタンスです。

f:id:StateMachine:20131224225010p:plain

ブラウザを複数開いて確認しましょう。ARRを使わずに、異なるインスタンスからセッション情報が共有できていることが確認できました。

f:id:StateMachine:20131224225435p:plain

まとめ

ここまで、結構細かい点でハマったり、失敗したりで結構時間がかかりました。ツールに習熟していないので、手番が最適化されてなく、試行錯誤した結果ではありますが、記録として残しておきます。 仮想マシンならもっと簡単に終わっていたと思いますが、Workerロールを使うメリットは、Immutable Infra よろしく使い捨てできるところがメリットかなと思います。

ということで、Windows Azure Advent Calendar 2013は、これにておしまいです。みさなまご苦労様でした。来年には、Japan D.C.もやってきますし、もっとWindows Azure界が盛り上がるとよいですね!それでは、メリークリスマス!