Google Apps Engine/Javaのセッション管理
Google Apps Engine(GAE)では、Java6.0の実行環境とServlet API 2.5に準ずるサーバー環境が提供されている。Servlet APIバージョンはスタブとして作成されるweb.xmlから見て取れる。
仕様の中で気になることの1つが「セッション管理」である。
Webアプリケーション・サーバーをスケールアウトした構成にする際に必ず出てくるのが、「セッション・レプリケーション」という技術。これは、複数のサーバー間で、セッションIDやセッションオブジェクトを、active-activeな関係でコピーし合うものである。これができないと、アプリケーションサーバーをスケールアウトし、完全な形でロードをバランスさせることができない。
(とはいえ、ロードバランシングの方法論にも色々あって、クライアントのIPアドレスでスティックするような方法を取れば、セッション・レプリケーションを行う必要はない。)
GAEはクラウドであるから、実行環境は「雲の中のどこか」。このような大規模なスケールアウトの環境では、セッション管理をいかに行うかには、相当なオリジナリティーが必要なのだろうと思う。
- javax.servlet.http.HttpSessionインターフェイスによるセッション管理機能を実装して提供している(なぜか、Servlet API 2.2へのリンクが貼ってある)。
- この実装では、App Engine のデータストアと memcache を使用してセッション データを保管する。
- appengine-web.xmlに以下の記述をすることで、この実装を利用することができる。
このうち、2番目が肝なのかなと思う。要するに、セッションはデータストアか、(その前段の)メモリーキャッシュに永続化される(シリアライズされる)という意味。実際に、セッションスコープで管理されるオブジェクトは、java.io.Serializableをimplementsして、シリアライズ可能でなければならない。
Googleでは、「HWは壊れるものだから、永続化されたデータをロストしないために、数回のレプリカの作成をしている」と聞く。このコンセプト(というか「リスク管理」)はGFS上で実装されているが、セッション管理もこの機能を前提に設計されていると考えられる。
ここでは、ローカル環境にあるTomcat6.0(Servlet API2.5の参照実装)と、クラウド上のGAEに、HttpSessionを利用する同じサーブレットをデプロイして、以下のことをテストしてみた。
<テスト内容>
- HttpSessionインターフェイスから得られる結果の比較
- HttpSessionインターフェイスを用いて、JavaBeanを格納し、その取り出し(復元)、更新、保存を行う。JavaBeanの状態が、同一セッションの中で「保存」されることの確認
- web.xmlの設定(セッションのインターバルタイム)が反映されるか?
以下の作業は、GAEプラグインを導入したEclipse(3.4;Ganymede)で行った。
Web Application Projectの作成
Eclipseで、「New」->「Web Application Project」で、新規プロジェクト(GaeSessionTest)とGAEアプリのスタブを作成する。このとき、GWTは利用しないので、チェックをはずす。
GaeSessionTest.javaの改造
スタブとしてできたサーブレット(GaeSessionTest.java)を改造する。まず、その前に、SessionオブジェクトにシリアライズするJavaBean(TestBean.java)を作成しておく。メンバー変数は、全てシリアライズ可能であること(メンバー変数の実装クラスがjava.io.Serializableをimplementsしていること)に注意する。以下の例では、Map
(注記)serialVersionUIDは、永続化されたJavaBeanのバージョンの整合をとるものであるが、これを変更してGAEに再デプロイをかけた際に、GAE上でエラーが発生してしまう事象がでた。アプリケーションを一度消すなり、変更しないなりの手続きを踏んだ方がいいかもしれない。
TestBean.java
package test; import java.io.Serializable; import java.util.Map; public class TestBean implements Serializable{ private static final long serialVersionUID = -9110143420906726458L; private Map<Integer,String> testMap; public TestBean() { } public Map<Integer,String> getTestMap() { return testMap; } public void setTestMap(Map<Integer,String> testMap) { this.testMap = testMap; } }
GaeSessionTest.java
JSPすら使わないサーブレットで、上記の<テスト内容>を検証するものとなっている。
Sessionオブジェクトに格納するのは、アクセスカウンター(Integer)とTestBeanで、このサーブレットが呼び出された時に、以下のようなロジックで、Sessionオブジェクトへのデータの保存と更新、復元をテストする。
- Sessionが新しければカウンターは1にし、HashMap
のインスタンスに5つの要素をputする。 - Sessionが新しくなければ、Sessionオブジェクトからカウンターを取り出して+1し、TestBeanインスタンスを復元する。
- HashMapインスタンスが空でなければ、1つ要素を減らした後にTestBeanを更新し、Sessionオブジェクトに保存する。
また、以下のHttpSessionから得られる情報を取得する。
- HttpSessionオブジェクトのAPIにある、isNew()、getId()、getCreationTime()、getLastAccessedTime()、getMaxInactiveInterval()を実行する。
package test; import java.io.IOException; import java.io.PrintWriter; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.servlet.http.*; @SuppressWarnings("serial") public class GaeSessionTestServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session = req.getSession(true); boolean ns = session.isNew(); // 同一セッション内でのアクセスカウンターの保管、取り出し(復元)。 Integer hit; if(ns){ hit = new Integer(1); }else{ hit = (Integer)session.getAttribute("hit.counter"); hit = new Integer(hit.intValue()+1); } session.setAttribute("hit.counter", hit); // Serialize可能なJava Beanをセッションに保管、取り出し(復元)をする。 TestBean tb; String flg; Map<Integer, String> mp; if(!ns){ tb = (TestBean)session.getAttribute("testBean"); // 呼び出されるごとに1つずつ減らしていく。 mp = tb.getTestMap(); Collection<String> cl = mp.values(); int sz = cl.size(); if(sz > 0){ mp.remove(sz); tb.setTestMap(mp); } flg = "sessionにTestBeanあり。サイズ: "+(new Integer(sz)).toString(); }else{ mp = new HashMap<Integer,String>(); mp.put(new Integer(1), "String1"); mp.put(new Integer(2), "String2"); mp.put(new Integer(3), "String3"); mp.put(new Integer(4), "String4"); mp.put(new Integer(5), "String5"); tb = new TestBean(); tb.setTestMap(mp); flg = "sessionにTestBeanなし"; } session.setAttribute("testBean", tb); Date lat = new Date(session.getLastAccessedTime()); Date ct = new Date(session.getCreationTime()); // HttpSessionインターフェイスのその他の情報を取得する。 resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>セッション情報</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>セッション情報</h2>"); out.println("セッションid = "+session.getId() + "<br>"); out.println("生成 = "+ ct + "<br>"); out.println("最終アクセス = "+ lat + "<br>"); out.println("インターバル(秒) = "+ session.getMaxInactiveInterval() + "<br>"); out.println("セッション内でのアクセス数 = "+hit.intValue() + "<br>"); // TestBeanの抽出状況 out.println(flg+"<br>"); Iterator<String> it = mp.values().iterator(); String tmpStr; while(it.hasNext()){ tmpStr = it.next(); out.println("TestBean: "+tmpStr+"<br>"); } out.println("</body></html>"); } }
web.xmlの変更
war/WEB-INF/web.xmlに以下を追加し、セッションのインターバル時間を設定する(GAEのデフォルトは1日になっていた)。これが反映されるかは、上のHttpSessionget.getMaxInactiveInterval()で確認できる。
<session-config> <session-timeout>30</session-timeout> </session-config>
appengine-web.xmlの変更
war/WEB-INF/appengine-web.xmlに以下を追加し、セッション管理を有効にする。
<sessions-enabled>true</sessions-enabled>
デプロイと実行
いつもの手順で、これをGAEにデプロイする。今回は比較のためにローカル環境のTomcat6.0にもデプロイしてみた。
以下が、GAEにデプロイしたものにアクセスした際の最初の画面。
同一セッションで、もう一度、アクセスした場合は次の画面になった。セッションオブジェクトに保管したオブジェクトの復元と更新、保管が、HttpSessionインターフェイスを使って(普通どおりに)行われることが分かる。
ただし、getCreationTime()は(クラウド上のどこかにある)サーバーのシステム時間を表し、getLastAccessedTime()は機能しないようだ。getMaxInactiveInterval()には、web.xmlの設定が反映されている。
以下は、Tomcatにデプロイした場合の最初の画面。
この場合には、(当たり前のことだが)getCreationTime()はローカルPCのシステム時間を表し、getLastAccessedTime()も機能している。セッションオブジェクトに保管したオブジェクトの復元と更新、保管については、GAEと同じ動きをすることが確認できた。