Tomcatを利用したJavaのWebアプリケーションをオートスケールするとき、セッション情報ってどうするの?ってなったので調べてみました。
Tomcatをクラスタリングする
基本に忠実に。
Tomcatにクラスタを設定しセッションレプリケーションするという方法。ただクラスタメンバシップを組むのにマルチキャストを利用するんですよね。今回はAWSの環境を利用しようとしていたため、ここで詰んでしまいました。VPC内ではマルチキャストが制限されているのです。
この辺、Oracle CloudやIBM BluemixはJavaフレンドリーに構築されてるんじゃないかなって気がするのですが、どうなんでしょう。
Amazon DynamoDB Session Manager for Apache Tomcat を使う
なら、AWSが提供しているライブラリを使えばいいのではー?
こちらはTomcatが標準で用意しているセッションマネージャのひとつであるパーシステントマネージャを拡張したもののようです。
パーシステントマネージャは外部ストレージにセッション情報を保存する機能を提供するもので、それを利用しセッションの保存場所をメモリからDynamoDBへとすることでステートレスなアプリにしてオートスケールを可能にするという感じでしょうか。
ただ残念なことにアーカイブされて、もはや更新されていないんですよね。さすがにそれを今から採用するのは避けたい、となったところでREADMEに「memcached-session-manager」を代わりにどうぞとあったのでそちらを利用することにしました。
memcached-session-manager を使う
READMEに載っていたSetupAndConfiguration wiki pageを読みながら設定を行い、最終的にContextの定義は以下のようになりました。
<Manager
className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="redis://redis" sticky="false"
sessionBackupAsync="false"
lockingMode="auto"
requestUriIgnorePattern=".*/uptime_check.html$" />
Exampleから変更した点
lockingModeをautoに変更
この属性は、Ajaxやタブブラウジングなどによりセッションが同時に変更され更新が失われたりすることを防ぐためのものだそうです。
その可能性のあるURLを正規表現で指定できるのですが、具体的にどこにがそうなのか今回対象のアプリで特定が難しかったため「auto」という設定にしました。
これは「readonly」とされるリクエストの場合はロックしない、とのことですが具体的にはセッションにまったくアクセスがなかった(HttpSession#getAttribute()
やHttpSession#setAttribute()
されなかった)場合が対象になるようです。
requestUriIgnorePatternの変更
リクエストのURLがこの属性で指定した正規表現と合致すればセッションをインメモリDBに保存する処理をスキップさせることが出来るそうです。
ロードバランサーがヘルスチェックを行うページ(uptime_check.html)についてはセッション情報も生成されないので余計な処理をさせないためスキップするURLとして設定しました。
また、Exampleにあるような静的ファイルへのアクセスもセッションは関係ないため最初は設定していたのですが、今回対象のアプリが想定外の動きを起こしたため変更することにしました。
その対象アプリは静的ファイルでもServletFilterを通るように設計されていたのですが、このライブラリではrequestUriIgnorePatternと合致するリクエスト内でHttpServletRequest#getSession()
が呼ばれた際は必ず新規セッションを生成するという実装になっており、それが原因で静的ファイルにアクセスする度にセッションIDが切り替わっていきアプリの動きが変になっていました。
sessionBackupTimeoutの設定
少し負荷をかけたらタイムアウトのエラーが発生するようになりました。とりあえず1.5秒程に延ばしたところ問題は解消しましたが、それでいいのかは疑問。
30-Oct-2018 07:04:51.146 WARNING [http-nio-8080-exec-1] de.javakaffee.web.msm.BackupSessionTask.handleException Could not store session 3EE38D05C62D287516AD504BFCBD572D in memcached.
Note that this session was relocated to this node because the original node was not available.
java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)
at de.javakaffee.web.msm.BackupSessionTask.storeSessionInMemcached(BackupSessionTask.java:235)
at de.javakaffee.web.msm.BackupSessionTask.doBackupSession(BackupSessionTask.java:198)
at de.javakaffee.web.msm.BackupSessionTask.call(BackupSessionTask.java:119)
at de.javakaffee.web.msm.BackupSessionTask.call(BackupSessionTask.java:50)
at de.javakaffee.web.msm.BackupSessionService$SynchronousExecutorService.submit(BackupSessionService.java:345)
at de.javakaffee.web.msm.BackupSessionService.backupSession(BackupSessionService.java:204)
at de.javakaffee.web.msm.MemcachedSessionService.backupSession(MemcachedSessionService.java:1098)
at de.javakaffee.web.msm.RequestTrackingHostValve.backupSession(RequestTrackingHostValve.java:232)
at de.javakaffee.web.msm.RequestTrackingHostValve.invoke(RequestTrackingHostValve.java:161)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:800)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1471)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
セッションIDの衝突
セッションID生成時、Tomcatはメモリ上を検索しセッションIDが衝突していたら再生成するようになっています。
このライブラリはセッション情報をメモリから消してインメモリDBに保存するようで、これだとセッションIDがユニークにならない可能性があるのでは?と気になってしまいました。
…作者的にはそうそう衝突しないから気にしすぎ。って言ってる? (GitHub Issues)