Spring SessionとRedisで簡単セッション管理

こんにちは、CAT開発チームの斎藤です。

チームでは主にテスト実行機能やRedmine, JIRAをはじめとしたBTS連携機能を担当しています。
どうぞよろしくお願いします。

さて前回からだいぶ間が空いてしまいましたが、、
今回は将来的にCATに導入しようと考えている、
Spring Sessionというライブラリについて紹介したいと思います。

Spring Sessionとは?

Spring Sessionは、JavaのWebアプリケーションフレームワークとして有名な
Spring Frameworkを発端にして生まれたSpring関連プロダクトの一つです。
Spring SessionはFilterを経由してHttpSessionの機能を上書きし、
HttpSessionのAPIを通じてRedisなどの外部のストレージとセッション情報をやり取りします。

主な機能

公式ページからの転載になりますが、主な機能は以下になります。

  • ユーザセッションを管理するAPIと実装を提供
  • HttpSession TomcatなどのアプリケーションコンテナのHttpSessionを上書きする
    • 一元化されたセッション アプリケーションコンテナに依存しない、セッションの一元管理方法を提供する
    • 複数セッション シングルブラウザで複数セッションを管理する方法を提供する
    • RESTful API RESTful APIに対応したセッション機能の提供
  • WebSocket メッセージ受信時にHttpSessionを維持する

導入のメリット

Spring Session導入のメリットとして最も分かりやすいのが、
複数台のアプリケーションサーバを運用している環境でセッション情報を一元管理できる点です。
通常のSpring Webアプリケーションでは、アプリケーションコンテナのメモリ内にユーザセッションを保持するため、
ユーザがアクセスする可能性がある場合はアプリケーションサーバを停止できません。
しかし、Spring Sessionを使ってユーザセッションを外部ストレージ化することで、
従来のスティッキーセッションから解放され、アプリケーション全体としての無停止デプロイやメンテナンス性の向上が期待できます。

導入方法

Spring Sessionを導入するのは簡単で、Springプロジェクトのpom.xmlやbuild.gradleにdependencyを追加するだけです。

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session</artifactId>
        <version>1.2.0.RELEASE</version>
    </dependency>
</dependencies>

検証環境

以下のような構成でSpringを使ったWebアプリケーションにSpring Sessionを組み込み、
実際の使用感や既存機能への影響などを検証しました。
セッション保存先のRedisには、本番環境でも利用する予定のAWSのElastiCacheを選択しました。
自前でRedisサーバを立てるのも検討したのですが、
チームの規模が小さくなるべく管理コストを削減しようということで
マネージド型サービスであるElastiCacheを使うことにしました。

env

検証環境のその他情報は以下の通りです。

OS CentOS 6.4
Java 1.8.0_66
Tomcat 8.0.28
Spring 4.1.5
Spring Session 1.1.1
Redis 2.8.24

実際に使ってみて

今回既存のSpring Webアプリケーションに導入したので、
若干コードの修正が必要だったのですがそれでもかなり簡単に導入できました。
新規開発で使う場合はほとんどSpring Sessionの存在を意識することなく使えると思います。
期待通りユーザセッションをアプリケーションコンテナから分離することができました。

パフォーマンスへの影響

セッションの保存先がアプリケーションコンテナからネットワーク越しのRedisに変更になるため、
アプリケーション全体の速度低下が心配だったのですが、
実際にパフォーマンスを測定してみると許容できる範囲の低下で収まりそう、という印象でした。
検証環境では+5%ほどの速度低下で収まりました。
しかし、大きめのファイルをセッションに保存したときなどは、
やはり少しもっさりした印象を受けたので何らかの処置は必要と感じました。

導入時の注意点

先に既存のコードで修正が必要だったと述べましたが、
下記でいくつか具体例を紹介したいと思います。

1. 全リクエストでRedisと通信する

Redisのmonitorコマンドで履歴を見ていると、画像やcss、JavaScriptなどのresourceファイル取得時もRedisにアクセスしていることが判明。
あまりRedisへのアクセスが多いとページの表示速度に影響が出るので、
以下のようなresourceファイルをSpring SessionのフィルターspringSessionRepositoryFilterから除外するフィルターを作成しました。

// ResourceExcludingFilter.java
public class ResourceExcludingFilter implements Filter {
    private static final String RESOURCE_PATH = "/resources/";
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String path = ((HttpServletRequest) request).getRequestURI();
        if (path.startsWith(RESOURCE_PATH)) {
            request.getRequestDispatcher(path).forward(request, response);
        } else {
            chain.doFilter(request, response);
        }
    }
    @Override
    public void destroy() {
    }
}

上記のresourceExcludingFilterフィルターをspringSessionRepositoryFilterの前にかませます。

<!-- web.xml -->
<filter>
  <filter-name>resourceExcludingFilter</filter-name>
  <filter-class>com.shift.tcm.util.ResourceExcludingFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>resourceExcludingFilter</filter-name>
  <url-pattern>/resources/*</url-pattern>
</filter-mapping>
<filter>
  <filter-name>springSessionRepositoryFilter</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
  <filter-name>springSessionRepositoryFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
2. セッションの更新タイミング

例えば以下のような、何か時間のかかる登録処理と、
セッションを介してその進捗を確認する2つのリクエストを持つControllerがあるとします。

// HogeController.java
@Controller
@RequestMapping("/hoge")
@SessionAttributes({ "status" })
public class HogeController {
    @Autowired
    SomeService someService;
 
    // 重い登録処理を行う
    @RequestMapping(value = "/registerSomething", method = RequestMethod.POST)
    @ResponseBody
    public String registerSomething(ModelMap modelMap, HttpServletRequest request, final Locale locale) {
        modelMap.addAttribute("status", 0);
 
        // 登録処理を行いつつ、中でstatusをインクリメントしていく
        someService.registerSomething(modelMap);
 
        return "complete";
    }
    // 登録処理の進捗を取得する
    @RequestMapping(value = "/checkProgress", method = RequestMethod.GET)
    @ResponseBody
    public String checkProgress(ModelMap modelMap, HttpServletRequest request, final Locale locale) {
        return String.valueOf(modelMap.get("status"));
    }
}

一見正しく動きそうですがSpring Sessionを使うとcheckProgressでnullが返ってきてしまいます。
これは、セッションの書き込みがリクエストの終わりにまとめて行われるからで、
上の例だとregisterSomethingの完了後にセッションが更新されます。
回避策ですが、セッションを使ってリクエスト間で進捗を共有するのをやめ、
DBを使った実装に変更するなどの方法があります。

3. 【ElastiCache限定】Redisに繋がらない

AWSのElastiCacheに限った話になりますが、デフォルトだとSpring SessionからElastiCacheのRedisに繋ぐことができません。
Spring SessionはRedisのconfigコマンドを使って初期化時にRedisの再設定を行うのですが、ElastiCacheのようなマネージドサービスではconfigコマンドが無効化されていてエラーになってしまいます。
この問題はConfigクラスにConfigureRedisAction.NO_OPをBeanとして登録することで回避できます。

// HttpSessionConfig.java
@EnableRedisHttpSession
public class HttpSessionConfig {
 
    // ...
 
    @Bean
    public static ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }
}

おわりに

以上、Spring Sessionと実際に使ってみて分かったTipsの紹介でした。
多少修正が必要でしたが、簡単にセッションを外部のストレージに移行できました。
Spring Sessionは今まさに開発されているプロダクトであり、
まだ日本語の情報が少ないためこれから使ってみようと考えている方はぜひ参考にしてください。


2016-05-18 | Posted in SpringNo Comments » 
Comment





Comment